1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
use super::*;
impl ExecutionContext {
pub(crate) fn dispatch_exception(&mut self, message: String) -> Result<(), RuntimeError> {
self.uncaught_exception = Some(message.clone());
loop {
let Some(frame) = self.try_stack.last().cloned() else {
return Err(RuntimeError::ExecutionError { message });
};
// Exceptions thrown inside FINALLY blocks, or inside CATCH blocks without a FINALLY,
// are not handled by that frame and must propagate to outer frames.
if frame.state == TryFrameState::Finally
|| (frame.state == TryFrameState::Catch && frame.finally_target.is_none())
{
self.try_stack.pop();
continue;
}
// TRY state + catch handler => enter catch and clear the exception.
if frame.state == TryFrameState::Try {
if let Some(catch_target) = frame.catch_target {
// If the throw happened inside a callee (i.e. there are
// call_stack frames pushed after this TRY frame's owner),
// unwind them. Otherwise the catch body's RET would pop
// the callee's frame and resume the TRY body's post-call
// continuation — causing the catch's return value to leak
// into the success path's expression (batch31 H2b).
while self.call_stack.len() > frame.owner_call_depth {
if let Some(callee_frame) = self.call_stack.pop() {
// Restore the owner's locals and args. Don't push
// a return value — the stack unwound to the catch
// handler pushes the exception message itself.
self.stack.truncate(callee_frame.stack_base);
self.locals = callee_frame.saved_locals;
self.args = callee_frame.saved_args;
// S7 fix — if this frame crossed a contract-call
// boundary (handle_contract_call's self-offsets
// arm), restore the storage overlay to the snapshot
// taken at entry. Without this, the callee's dirty
// storage writes leak into the caller's overlay and
// get committed at top-level halt — diverging from
// Neo N3, which reverts storage on any inner fault.
if let Some(snapshot) = callee_frame.storage_snapshot {
self.restore_storage_snapshot(snapshot);
}
}
}
if let Some(top) = self.try_stack.last_mut() {
top.state = TryFrameState::Catch;
}
// Task #86 — the catch handler receives the raw revert
// payload so `catch (bytes memory data)` sees the
// EVM-canonical envelope (`selector || abi.encode(args)`)
// produced by THROW. Falling back to the UTF-8 rendering
// of the exception message (the pre-Task-#86 behavior)
// would leak the `"THROW: …"` prefix and the lossy U+FFFD
// replacements for non-UTF-8 payload bytes into the
// caller, breaking ABI decoding.
let payload = if !self.revert_payload.is_empty() {
self.revert_payload.clone()
} else {
message.as_bytes().to_vec()
};
self.push_stack(StackItem::byte_array(payload))?;
self.uncaught_exception = None;
self.revert_payload.clear();
self.instruction_pointer = catch_target;
return Ok(());
}
}
// Otherwise, route into FINALLY (must exist).
if let Some(finally_target) = frame.finally_target {
if let Some(top) = self.try_stack.last_mut() {
top.state = TryFrameState::Finally;
}
self.instruction_pointer = finally_target;
return Ok(());
}
// Defensive: malformed try frame (no catch or finally).
self.try_stack.pop();
}
}
pub(crate) fn execute_flow_try_frames(&mut self, opcode: u8) -> Result<bool, RuntimeError> {
match opcode {
0x3B => {
// TRY CatchOffset(sbyte) FinallyOffset(sbyte)
let catch_offset = self.read_i8_offset("TRY")? as i32;
let finally_offset = {
let idx = self.instruction_pointer as usize + 2;
if idx >= self.bytecode.len() {
return Err(RuntimeError::ExecutionError {
message: "TRY: insufficient bytecode for offset".to_string(),
});
}
(self.bytecode[idx] as i8) as i32
};
if catch_offset == 0 && finally_offset == 0 {
return Err(RuntimeError::ExecutionError {
message: "TRY: catchOffset and finallyOffset cannot both be 0".to_string(),
});
}
let catch_target = if catch_offset == 0 {
None
} else {
Some(self.compute_offset_target(
"TRY",
self.instruction_pointer,
catch_offset,
)?)
};
let finally_target = if finally_offset == 0 {
None
} else {
Some(self.compute_offset_target(
"TRY",
self.instruction_pointer,
finally_offset,
)?)
};
self.try_stack.push(TryFrame {
catch_target,
finally_target,
end_target: None,
state: TryFrameState::Try,
owner_call_depth: self.call_stack.len(),
});
self.instruction_pointer += 3;
}
0x3C => {
// TRY_L CatchOffset(int) FinallyOffset(int)
let catch_offset = self.read_i32_offset("TRY_L")?;
let finally_offset = {
let start = self.instruction_pointer as usize + 5;
let end = start + 4;
if end > self.bytecode.len() {
return Err(RuntimeError::ExecutionError {
message: "TRY_L: insufficient bytecode for offset".to_string(),
});
}
let mut buf = [0u8; 4];
buf.copy_from_slice(&self.bytecode[start..end]);
i32::from_le_bytes(buf)
};
if catch_offset == 0 && finally_offset == 0 {
return Err(RuntimeError::ExecutionError {
message: "TRY_L: catchOffset and finallyOffset cannot both be 0"
.to_string(),
});
}
let catch_target = if catch_offset == 0 {
None
} else {
Some(self.compute_offset_target(
"TRY_L",
self.instruction_pointer,
catch_offset,
)?)
};
let finally_target = if finally_offset == 0 {
None
} else {
Some(self.compute_offset_target(
"TRY_L",
self.instruction_pointer,
finally_offset,
)?)
};
self.try_stack.push(TryFrame {
catch_target,
finally_target,
end_target: None,
state: TryFrameState::Try,
owner_call_depth: self.call_stack.len(),
});
self.instruction_pointer += 9;
}
0x3D => {
// ENDTRY: endOffset(sbyte)
if self.try_stack.is_empty() {
return Err(RuntimeError::ExecutionError {
message: "ENDTRY: corresponding TRY block not found".to_string(),
});
}
if self
.try_stack
.last()
.is_some_and(|frame| frame.state == TryFrameState::Finally)
{
return Err(RuntimeError::ExecutionError {
message: "ENDTRY: cannot execute inside FINALLY".to_string(),
});
}
let end_offset = self.read_i8_offset("ENDTRY")? as i32;
let end_target =
self.compute_offset_target("ENDTRY", self.instruction_pointer, end_offset)?;
let finally_target = self.try_stack.last().and_then(|frame| frame.finally_target);
if let Some(finally_target) = finally_target {
if let Some(frame) = self.try_stack.last_mut() {
frame.state = TryFrameState::Finally;
frame.end_target = Some(end_target);
}
self.instruction_pointer = finally_target;
} else {
self.try_stack.pop();
self.instruction_pointer = end_target;
}
}
0x3E => {
// ENDTRY_L: endOffset(int)
if self.try_stack.is_empty() {
return Err(RuntimeError::ExecutionError {
message: "ENDTRY_L: corresponding TRY block not found".to_string(),
});
}
if self
.try_stack
.last()
.is_some_and(|frame| frame.state == TryFrameState::Finally)
{
return Err(RuntimeError::ExecutionError {
message: "ENDTRY_L: cannot execute inside FINALLY".to_string(),
});
}
let end_offset = self.read_i32_offset("ENDTRY_L")?;
let end_target =
self.compute_offset_target("ENDTRY_L", self.instruction_pointer, end_offset)?;
let finally_target = self.try_stack.last().and_then(|frame| frame.finally_target);
if let Some(finally_target) = finally_target {
if let Some(frame) = self.try_stack.last_mut() {
frame.state = TryFrameState::Finally;
frame.end_target = Some(end_target);
}
self.instruction_pointer = finally_target;
} else {
self.try_stack.pop();
self.instruction_pointer = end_target;
}
}
0x3F => {
// ENDFINALLY
let Some(frame) = self.try_stack.pop() else {
return Err(RuntimeError::ExecutionError {
message: "ENDFINALLY: corresponding TRY block not found".to_string(),
});
};
if self.uncaught_exception.is_none() {
let Some(end_target) = frame.end_target else {
return Err(RuntimeError::ExecutionError {
message: "ENDFINALLY: ENDTRY target not set".to_string(),
});
};
self.instruction_pointer = end_target;
} else {
let message = self
.uncaught_exception
.clone()
.unwrap_or_else(|| "Unhandled exception".to_string());
self.dispatch_exception(message)?;
}
}
_ => return Ok(false),
}
Ok(true)
}
}