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
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
fn try_lower_resolved_builtin_call(
func: &Expression,
args: &[Expression],
ctx: &mut LoweringContext,
instructions: &mut Vec<Instruction>,
) -> Option<bool> {
let builtin = resolve_builtin_call(func)?;
if matches!(&builtin, BuiltinCall::RuntimeNotify) {
validate_runtime_notify_call(args, ctx);
}
if matches!(&builtin, BuiltinCall::TypeOf) {
ctx.record_error(
"`type(...)` is only supported via members like `type(uint256).max`, `type(int256).min`, or `type(IInterface).interfaceId`",
);
return Some(false);
}
let (min_args, max_args) = match &builtin {
BuiltinCall::RuntimeNotify => (2, Some(2)),
BuiltinCall::RuntimeCheckWitness => (1, Some(1)),
// Solidity allows abi.encode()/encodePacked() with zero arguments.
BuiltinCall::AbiEncode | BuiltinCall::AbiEncodePacked | BuiltinCall::AbiEncodeCall => {
(0, None)
}
BuiltinCall::AbiEncodeWithSignature => (1, None),
BuiltinCall::AbiDecode => (2, None),
BuiltinCall::Keccak256 => (1, Some(1)),
BuiltinCall::Ecrecover => (4, Some(4)),
BuiltinCall::StorageFind => (1, None),
BuiltinCall::StoragePut => (2, Some(2)),
BuiltinCall::StorageGet | BuiltinCall::StorageDelete => (1, Some(1)),
BuiltinCall::ContractCall => (3, Some(3)),
BuiltinCall::ContractCallWithFlags => (4, Some(4)),
BuiltinCall::NotifySerialized => (1, Some(1)),
BuiltinCall::VerifySignature => (3, Some(3)),
BuiltinCall::DeployContract => (2, Some(3)),
BuiltinCall::GetContract | BuiltinCall::GetContractScript => (1, Some(1)),
BuiltinCall::GetNeoAccountState => (1, Some(1)),
BuiltinCall::NativeCall { .. } => (0, None),
BuiltinCall::Syscall(_) => (0, None),
BuiltinCall::TypeOf => (1, Some(1)),
BuiltinCall::BytesConcat => (0, None),
BuiltinCall::PrecompileEcrecover | BuiltinCall::PrecompileModexp => (1, Some(1)),
};
if args.len() < min_args || max_args.is_some_and(|max| args.len() > max) {
ctx.record_error(format!(
"builtin call requires between {} and {} argument(s), got {}",
min_args,
max_args
.map(|v| v.to_string())
.unwrap_or_else(|| "∞".to_string()),
args.len()
));
return Some(false);
}
if matches!(builtin, BuiltinCall::AbiEncode | BuiltinCall::AbiEncodeCall) {
if let Some(result) = lower_abi_encode_args_direct_from_slice(args, ctx, instructions) {
return Some(result);
}
}
if matches!(builtin, BuiltinCall::AbiEncodePacked) {
if let Some(result) =
lower_abi_encode_packed_args_direct_from_slice(args, ctx, instructions)
{
return Some(result);
}
}
if matches!(builtin, BuiltinCall::AbiDecode) {
if let Some(result) = lower_abi_decode_direct(args, ctx, instructions) {
return Some(result);
}
}
// Task #66 — `abi.encodePacked` width-aware packing for narrow integers.
//
// Per Solidity spec, `abi.encodePacked(uintN)` emits exactly N/8 bytes in
// big-endian form (not a 32-byte slot). The runtime `abiencodepacked`
// handler can't recover the Solidity type from the StackItem (it sees an
// `Integer(5)` whether the source was `uint8(5)` or `uint256(5)`). When
// ALL args have narrow integer types (bits in {8,16,32,64,128}) we
// side-step the runtime path and lower to a compile-time CAT chain of
// fixed-width BE byte arrays — `coerce_to_fixed_bytes(N, reverse=true)`
// converts the LE integer-bytes NeoVM emits into right-aligned BE of
// exactly N bytes. uint256/bytes/strings/address/bool args fall through
// to the existing runtime handler (preserves Task #44 semantics).
//
// `coerce_to_fixed_bytes` leaves the MEMCPY-returned destination buffer
// on the stack beneath the canonical ByteString result (its original
// callers `bytesN(..)`/`address(..)` are single-use so the leak is
// unobservable). Chaining N packed args would stack up N leaked items
// which the subsequent `BytesConcat` (only pops top 2) would then
// mis-concatenate. We follow each coercion with `Swap; Drop` to discard
// the leak and keep only the canonical result.
if matches!(builtin, BuiltinCall::AbiEncodePacked) && !args.is_empty() {
let narrow_widths: Option<Vec<usize>> = args
.iter()
.map(|arg| match infer_type_from_expression(arg, ctx) {
// Bug #23 (packed variant): only UNSIGNED widths may take the
// zero-filling `coerce_to_fixed_bytes` fast path. Negative
// signed values need 0xff sign-extension, which the direct
// path's `emit_abi_fixed_buffer_signed` handles.
Some(ValueType::Integer {
bits,
signed: false,
}) if matches!(bits, 8 | 16 | 32 | 64 | 128) => Some(bits as usize),
_ => None,
})
.collect();
if let Some(widths) = narrow_widths {
let mut success = true;
for (arg, bits) in args.iter().zip(widths.iter()) {
if !lower_expression(arg, ctx, instructions) {
success = false;
break;
}
// Convert the integer to its natural LE byte array (NeoVM
// stores integers little-endian); then coerce into a fixed
// N-byte buffer and reverse in place to get right-aligned BE.
instructions.push(Instruction::Convert {
target: ConvertTarget::ByteArray,
});
coerce_to_fixed_bytes(bits / 8, true, ctx, instructions);
// No Swap; Drop needed: real NeoVM MEMCPY pushes nothing.
}
if success {
instructions.push(Instruction::CallBuiltin {
builtin: BuiltinCall::BytesConcat,
arg_count: args.len(),
});
}
return Some(success);
}
}
let mut success = true;
// Task #124 — track flattened arg count for `AbiEncode` when struct args
// are expanded into per-field stack items. For non-AbiEncode builtins and
// the struct-less fast path this stays equal to `args.len()`.
let mut flat_arg_count: usize = args.len();
match &builtin {
BuiltinCall::AbiDecode => {
if let Some(first) = args.first() {
if !lower_expression(first, ctx, instructions) {
success = false;
}
}
// Task #84 — `abi.decode(buf, (T, U, …))` must PANIC when `buf`
// is too short to hold every declared slot. Emit a bytecode-level
// guard `DUP ; SIZE ; PUSH expected ; NE ; JMPIFNOT decode_ok ;
// emit_panic(0x41)` keyed on the second argument's tuple
// arity when every element is a static 32-byte type. Dynamic
// tuples (string/bytes, nested) keep the legacy no-guard path.
//
// Task #107 — canonical EVM Panic(uint256) envelope so
// `catch Panic(uint code)` can bind code = 0x41.
if success {
if let Some(expected_bytes) = abi_decode_expected_static_bytes(args) {
let decode_ok_label = ctx.next_label();
instructions.push(Instruction::Dup);
instructions.push(Instruction::GetSize);
instructions.push(Instruction::PushLiteral(LiteralValue::Integer(
BigInt::from(expected_bytes),
)));
// Solidity's `abi.decode` only requires the buffer to be LONG
// ENOUGH (it ignores trailing bytes); only an UNDER-length
// buffer reverts. Using `<` (not `!=`) accepts over-length
// input, matching ethers/foundry/solc decoding.
instructions.push(Instruction::BinaryOp(BinaryOperator::Lt));
instructions.push(Instruction::JumpIf {
target: decode_ok_label,
});
emit_panic(0x41, instructions);
instructions.push(Instruction::Label(decode_ok_label));
}
}
}
BuiltinCall::AbiEncode => {
// Task #109 — `bytesN(..)` / `address(..)` casts route through
// `coerce_to_fixed_bytes` which leaves the MEMCPY-returned dst
// buffer on the stack BENEATH the canonical ByteString result
// (MEMCPY pushes `dst` back per C-style memcpy semantics — see
// src/runtime/execution/execution_impl_part3_bytes.rs::memcpy_bytes).
// When such a cast appears as an argument to a multi-arg builtin
// like `abi.encode(uint256 x, bytes32 y)`, the subsequent PACK
// picks up the leaked buffer instead of the earlier scalar arg,
// corrupting the encoded payload.
//
// Task #124 — struct-typed args must be flattened into per-field
// stack items before the runtime `abiencode` handler sees them.
// Otherwise the struct's `StackItem::Array` shape hits the
// dynamic-type classifier and emits Task #121's offset+length+
// elements buffer instead of the EVM-canonical field-concat
// (e.g. `keccak(abi.encode(Voucher{a,b,c}))` diverges from the
// 96-byte-BE oracle). Detect all-static structs and emit
// `lower_expression(expr); push i; ArrayGet` per field so the
// AbiEncode builtin receives N scalar items (Integer / ByteArray
// / Boolean) that `abi_pad32_be` can slot-encode directly.
//
// Non-struct args follow the standard lower-then-leak-drop path.
flat_arg_count = 0;
for arg in args {
if let Some(field_count) =
try_flatten_struct_arg_for_abi_encode(arg, ctx, instructions, &mut success)
{
flat_arg_count += field_count;
continue;
}
if !lower_expression(arg, ctx, instructions) {
success = false;
continue;
}
// No Swap; Drop needed: real NeoVM MEMCPY pushes nothing.
flat_arg_count += 1;
}
}
_ => {
// Task #109 — `bytesN(..)` / `address(..)` casts route through
// `coerce_to_fixed_bytes` which leaves the MEMCPY-returned dst
// buffer on the stack BENEATH the canonical ByteString result
// (MEMCPY pushes `dst` back per C-style memcpy semantics — see
// src/runtime/execution/execution_impl_part3_bytes.rs::memcpy_bytes).
// When such a cast appears as an argument to a multi-arg builtin
// like `abi.encode(uint256 x, bytes32 y)`, the subsequent PACK
// picks up the leaked buffer instead of the earlier scalar arg,
// corrupting the encoded payload (e.g. abi.encode produces
// bytes32(y) || bytes32(y) instead of bytes32(x) || bytes32(y)).
// Mirror the Task #66/#89 packed-encoding fix in builtins.rs /
// member_access.rs: follow each leaky arg with `Swap; Drop` to
// discard the leaked buffer and keep only the canonical result.
for arg in args {
if !lower_expression(arg, ctx, instructions) {
success = false;
continue;
}
// No Swap; Drop needed: real NeoVM MEMCPY pushes nothing.
}
}
}
if success {
match builtin {
BuiltinCall::RuntimeNotify
| BuiltinCall::RuntimeCheckWitness
| BuiltinCall::AbiEncodePacked
| BuiltinCall::AbiEncodeCall
| BuiltinCall::AbiDecode
| BuiltinCall::Keccak256
| BuiltinCall::Ecrecover
| BuiltinCall::StorageFind
| BuiltinCall::StoragePut
| BuiltinCall::StorageGet
| BuiltinCall::StorageDelete
| BuiltinCall::ContractCallWithFlags
| BuiltinCall::NotifySerialized
| BuiltinCall::VerifySignature
| BuiltinCall::DeployContract
| BuiltinCall::GetContract
| BuiltinCall::GetContractScript
| BuiltinCall::GetNeoAccountState
| BuiltinCall::NativeCall { .. }
| BuiltinCall::Syscall(_)
| BuiltinCall::BytesConcat
| BuiltinCall::PrecompileEcrecover
| BuiltinCall::PrecompileModexp => {
instructions.push(Instruction::CallBuiltin {
builtin,
arg_count: args.len(),
});
}
BuiltinCall::AbiEncode => {
// Task #124 — use the flattened arg count (per-field for
// struct args) so the runtime `abiencode` handler PACKs the
// correct number of scalar slots. For struct-less calls this
// equals `args.len()` so the static/dynamic encoding paths
// (Tasks #44/#72/#73/#121) keep their existing semantics.
instructions.push(Instruction::CallBuiltin {
builtin,
arg_count: flat_arg_count,
});
}
BuiltinCall::ContractCall => {
if ctx.is_safe {
// In safe (view/pure) contexts, default contract calls to read-only.
instructions.push(Instruction::PushLiteral(LiteralValue::Integer(
BigInt::from(0x05u8),
)));
instructions.push(Instruction::CallBuiltin {
builtin: BuiltinCall::ContractCallWithFlags,
arg_count: 4,
});
} else {
instructions.push(Instruction::CallBuiltin {
builtin: BuiltinCall::ContractCall,
arg_count: args.len(),
});
}
}
BuiltinCall::AbiEncodeWithSignature => {
let mut selector = Vec::new();
if let Some(Expression::StringLiteral(parts)) = args.first() {
let bytes = string_literal_bytes(parts);
let mut hasher = Keccak256::new();
hasher.update(&bytes);
let digest = hasher.finalize();
selector.extend_from_slice(&digest[..4]);
}
for _ in args {
instructions.push(Instruction::Drop(ValueType::Any));
}
instructions.push(Instruction::PushLiteral(LiteralValue::ByteArray(selector)));
}
BuiltinCall::TypeOf => {}
}
}
Some(success)
}
/// Task #84 — return `arity * 32` when `abi.decode(buf, types)`'s `types`
/// argument is a tuple whose elements are all static 32-byte Solidity types
/// (`uintN`, `intN`, `address[ payable]`, `bool`, `bytesN`). Returns `None`
/// for dynamic or mixed tuples; callers then fall back to the legacy
/// no-guard path since a simple size comparison is insufficient.
fn abi_decode_expected_static_bytes(args: &[Expression]) -> Option<u32> {
let slot_count: u32 = match args.get(1)? {
Expression::List(_, params) => {
let mut count: u32 = 0;
for (_, param) in params {
if !is_static_abi_type(¶m.as_ref()?.ty) {
return None;
}
count = count.checked_add(1)?;
}
count
}
Expression::Parenthesis(_, inner) => {
if !is_static_abi_type(inner.as_ref()) {
return None;
}
1
}
other => {
if !is_static_abi_type(other) {
return None;
}
1
}
};
slot_count.checked_mul(32)
}
/// Task #84 — true iff `expr` names a Solidity type whose ABI encoding is
/// exactly one 32-byte slot (no offset/length tail).
fn is_static_abi_type(expr: &Expression) -> bool {
matches!(
expr,
Expression::Type(
_,
PtType::Uint(_)
| PtType::Int(_)
| PtType::Address
| PtType::AddressPayable
| PtType::Payable
| PtType::Bool
| PtType::Bytes(_)
)
)
}
/// Task #127 — true iff `expr` names a Solidity type whose ABI encoding
/// uses the dynamic-tail layout: `bytes`, `string`, or `T[]` (dynamic
/// array). In the ABI head these take a single 32-byte offset slot
/// that points to a length + payload tail section. Used by the mixed
/// static+dynamic tuple passthrough in `return_revert.rs` to recognise
/// `return abi.decode(buf, (uint, string, address))`-style shapes
/// whose canonical output IS the input buffer verbatim.
fn is_dynamic_abi_type(expr: &Expression) -> bool {
match expr {
Expression::Type(_, PtType::DynamicBytes | PtType::String) => true,
// `T[]` appears as an ArraySubscript with no length index.
Expression::ArraySubscript(_, _inner, None) => true,
_ => false,
}
}