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
fn try_lower_runtime_member_access(
inner: &Expression,
member: &Identifier,
ctx: &mut LoweringContext,
instructions: &mut Vec<Instruction>,
) -> Option<bool> {
let is_nep17_payment = ctx.function_name == "onNEP17Payment";
let is_nep11_payment = ctx.function_name == "onNEP11Payment";
let is_payment_callback = is_nep17_payment || is_nep11_payment;
match member.name.as_str() {
"sender" => {
if let Expression::Variable(base) = inner {
if base.name == "msg" {
if is_payment_callback {
instructions.push(Instruction::LoadParameter(0));
} else {
instructions.push(Instruction::LoadRuntimeValue(RuntimeValue::MsgSender));
}
return Some(true);
}
}
None
}
"value" => {
if let Expression::Variable(base) = inner {
if base.name == "msg" {
if is_payment_callback {
// Both onNEP17Payment and onNEP11Payment have amount at param 1
instructions.push(Instruction::LoadParameter(1));
} else {
// Neo N3 has no "attached value" for calls; msg.value is only
// meaningful inside payment callbacks. Outside that context we
// emit a warning (via the payable modifier check) and return 0
// via RuntimeValue::MsgValue for source compatibility.
instructions.push(Instruction::LoadRuntimeValue(RuntimeValue::MsgValue));
}
return Some(true);
}
}
None
}
"data" => {
if let Expression::Variable(base) = inner {
if base.name == "msg" {
if is_nep17_payment {
// onNEP17Payment(from, amount, data) — data is param 2
instructions.push(Instruction::LoadParameter(2));
} else if is_nep11_payment {
// onNEP11Payment(from, amount, tokenId, data) — data is param 3
instructions.push(Instruction::LoadParameter(3));
} else {
// msg.data lowers to the runtime's `input_data` (the raw calldata
// bytes passed to `execute`), exposed via the `Script` field of
// `System.Runtime.GetScriptContainer`. The bytecode emitter for
// `RuntimeValue::MsgData` fetches that field. See
// `src/cli/bytecode/bytecode_helpers/array_runtime.rs`.
//
// For entry-point invocations this is the exact calldata: fallback()
// observes `msg.data.length == injected_calldata.len()`, and
// external functions observe `selector || abi.encode(args)` as the
// runtime dispatches them from input_data.
//
// Across internal contract-to-contract calls Neo N3's script
// container still reflects the *transaction* script, so observers
// can see a mismatch vs. EVM (which repopulates calldata on each
// internal call). The surviving informational warning captures
// that residual difference.
ctx.record_warning_with_suggestion(
"msg.data is approximated on Neo N3 as `selector || abi.encode(current args)` for cross-contract calls where the Neo script container still reflects the entry-point transaction script rather than the per-call payload. Entry-point fallback/receive observe the exact injected calldata.",
"Pass the bytes payload explicitly (e.g. `function f(bytes calldata data)`) when cross-contract call input must be recovered bit-for-bit.",
);
instructions.push(Instruction::LoadRuntimeValue(RuntimeValue::MsgData));
}
return Some(true);
}
}
None
}
"sig" => {
if let Expression::Variable(base) = inner {
if base.name == "msg" {
ctx.record_warning_with_suggestion(
"msg.sig is approximated on Neo N3 using the current function selector. This differs from EVM semantics across internal calls, where msg.sig preserves the original external-call selector.",
"Use explicit method-name logic or interface IDs when you need cross-call-stable dispatch identity.",
);
instructions.push(Instruction::PushLiteral(LiteralValue::ByteArray(
ctx.current_function_selector().to_vec(),
)));
return Some(true);
}
}
None
}
"origin" => {
if let Expression::Variable(base) = inner {
if base.name == "tx" {
// Non-fatal warning: tx.origin compiles but has different semantics on Neo.
ctx.record_warning_with_suggestion(
"tx.origin has different semantics on Neo N3. Neo uses multi-signature witnesses instead of a single origin.",
"Use msg.sender or Runtime.checkWitness() for authorization instead.",
);
instructions.push(Instruction::LoadRuntimeValue(RuntimeValue::TxOrigin));
return Some(true);
}
}
None
}
"gasprice" => {
if let Expression::Variable(base) = inner {
if base.name == "tx" {
// Neo N3 auto-compat: tx.gasprice → Policy.getFeePerByte()
ctx.record_warning_with_suggestion(
"tx.gasprice auto-mapped to Policy.getFeePerByte() on Neo N3. Neo fees are determined by script size and syscall costs.",
"Use Policy.getFeePerByte() directly when targeting Neo.",
);
instructions.push(Instruction::CallBuiltin {
builtin: BuiltinCall::NativeCall {
contract: NativeContract::Policy,
method: "getFeePerByte".to_string(),
},
arg_count: 0,
});
return Some(true);
}
}
None
}
"hash" => {
if let Expression::Variable(base) = inner {
if base.name == "tx" {
// Neo N3 auto-compat: tx.hash → System.Runtime.GetScriptContainer
// Returns the transaction that triggered execution
ctx.record_warning_with_suggestion(
"tx.hash auto-mapped to System.Runtime.GetScriptContainer on Neo N3. This returns the current transaction as a ScriptContainer.",
"Use System.Runtime.GetScriptContainer directly if you need the current Neo transaction container.",
);
instructions.push(Instruction::CallBuiltin {
builtin: BuiltinCall::Syscall(
"System.Runtime.GetScriptContainer".to_string(),
),
arg_count: 0,
});
return Some(true);
}
}
None
}
"timestamp" => {
if let Expression::Variable(base) = inner {
if base.name == "block" {
instructions.push(Instruction::LoadRuntimeValue(RuntimeValue::BlockTimestamp));
return Some(true);
}
}
None
}
"number" => {
if let Expression::Variable(base) = inner {
if base.name == "block" {
instructions.push(Instruction::LoadRuntimeValue(RuntimeValue::BlockNumber));
return Some(true);
}
}
None
}
"chainid" => {
if let Expression::Variable(base) = inner {
if base.name == "block" {
// Solidity `block.chainid` is a uint256 chain identifier. Neo N3 exposes a
// network "magic" number via `System.Runtime.GetNetwork`; use that as the
// closest equivalent.
instructions.push(Instruction::CallBuiltin {
builtin: BuiltinCall::Syscall("System.Runtime.GetNetwork".to_string()),
arg_count: 0,
});
return Some(true);
}
}
None
}
"coinbase" => {
if let Expression::Variable(base) = inner {
if base.name == "block" {
// Neo N3 auto-compat: block.coinbase → address(0)
// dBFT has no PoW miner; there is no single "coinbase" address.
// Return address(0) to match EVM type semantics (address return).
ctx.record_warning_with_suggestion(
"block.coinbase auto-mapped to address(0) on Neo N3 because dBFT consensus has no block miner.",
"Use Neo.getNextBlockValidators() if you need the current validator set, or Runtime.checkWitness() for authorization.",
);
instructions.push(Instruction::PushLiteral(LiteralValue::ByteArray(vec![
0u8;
20
])));
return Some(true);
}
}
None
}
"difficulty" | "prevrandao" => {
if let Expression::Variable(base) = inner {
if base.name == "block" {
// Neo N3 auto-compat: block.difficulty/prevrandao → Runtime.getRandom()
ctx.record_warning_with_suggestion(
format!(
"block.{} auto-mapped to Runtime.getRandom() on Neo N3 because dBFT consensus has no PoW difficulty.",
member.name
),
"Review any randomness assumptions; Neo's Runtime.getRandom() is not equivalent to EVM difficulty/prevrandao.",
);
instructions.push(Instruction::CallBuiltin {
builtin: BuiltinCall::Syscall("System.Runtime.GetRandom".to_string()),
arg_count: 0,
});
return Some(true);
}
}
None
}
"gaslimit" => {
if let Expression::Variable(base) = inner {
if base.name == "block" {
// Neo N3 auto-compat: block.gaslimit → Policy.getExecFeeFactor()
ctx.record_warning_with_suggestion(
"block.gaslimit auto-mapped to Policy.getExecFeeFactor() on Neo N3. Neo uses GAS token fees, not per-block gas limits.",
"Avoid relying on EVM block gas-limit semantics on Neo.",
);
instructions.push(Instruction::CallBuiltin {
builtin: BuiltinCall::NativeCall {
contract: NativeContract::Policy,
method: "getExecFeeFactor".to_string(),
},
arg_count: 0,
});
return Some(true);
}
}
None
}
"basefee" => {
if let Expression::Variable(base) = inner {
if base.name == "block" {
// Neo N3 auto-compat: block.basefee → Policy.getFeePerByte()
ctx.record_warning_with_suggestion(
"block.basefee auto-mapped to Policy.getFeePerByte() on Neo N3. Neo does not use EIP-1559 base fees.",
"Review any fee-market logic before deploying on Neo.",
);
instructions.push(Instruction::CallBuiltin {
builtin: BuiltinCall::NativeCall {
contract: NativeContract::Policy,
method: "getFeePerByte".to_string(),
},
arg_count: 0,
});
return Some(true);
}
}
None
}
"parenthash" => {
if let Expression::Variable(base) = inner {
if base.name == "block" {
// Neo N3 auto-compat: block.parenthash → Ledger.currentHash
// EVM's blockhash(block.number - 1) returns the parent block hash.
// On Neo, Ledger.currentHash returns the current block's hash.
ctx.record_warning_with_suggestion(
"block.parenthash auto-mapped to Ledger.currentHash on Neo N3.",
"Use Ledger.getBlock(currentIndex - 1).hash if you need the actual parent block hash.",
);
instructions.push(Instruction::CallBuiltin {
builtin: BuiltinCall::NativeCall {
contract: NativeContract::Ledger,
method: "currentHash".to_string(),
},
arg_count: 0,
});
return Some(true);
}
}
None
}
"sha3" => {
if let Expression::Variable(base) = inner {
if base.name == "block" {
ctx.record_warning_with_suggestion(
"block.sha3 is deprecated in Solidity 0.8+ and not fully available on Neo N3. On EVM it returns keccak256 of the current block. On Neo, this is approximated as Ledger.currentHash (the current block's hash).",
"Use Ledger.currentHash() directly if you need the current block hash on Neo.",
);
instructions.push(Instruction::CallBuiltin {
builtin: BuiltinCall::NativeCall {
contract: NativeContract::Ledger,
method: "currentHash".to_string(),
},
arg_count: 0,
});
return Some(true);
}
}
None
}
_ => None,
}
}