neo_solidity/ir/expressions/calls/
low_level.rs

1fn resolve_signature_string(expr: &Expression, ctx: &LoweringContext) -> Option<String> {
2	match expr {
3		Expression::Parenthesis(_, inner) => resolve_signature_string(inner, ctx),
4		Expression::StringLiteral(parts) => {
5			Some(String::from_utf8_lossy(&string_literal_bytes(parts)).to_string())
6		}
7		Expression::Variable(identifier) => {
8			let state_index = ctx.state_index_map.get(&identifier.name).copied()?;
9			let meta = ctx.state_metadata(state_index)?;
10			if !meta.is_constant {
11				return None;
12			}
13			let initializer = meta.initializer.as_ref()?;
14			resolve_signature_string(initializer, ctx)
15		}
16		Expression::FunctionCall(_, func, args) => {
17			if args.len() == 1 {
18				match func.as_ref() {
19					Expression::Type(_, _) => resolve_signature_string(&args[0], ctx),
20					Expression::Variable(id) if id.name == "bytes" || id.name == "string" => {
21						resolve_signature_string(&args[0], ctx)
22					}
23					_ => None,
24				}
25			} else {
26				None
27			}
28		}
29		_ => None,
30	}
31}
32
33fn is_single_argument_bytes_or_type_wrapper(func: &Expression, args: &[Expression]) -> bool {
34	if args.len() != 1 {
35		return false;
36	}
37
38	match func {
39		Expression::Type(_, _) => true,
40		Expression::Variable(id) => id.name == "bytes" || id.name == "string",
41		_ => false,
42	}
43}
44
45fn is_contract_type_reference(expr: &Expression, ctx: &LoweringContext) -> bool {
46	match expr {
47		Expression::Variable(type_id) => ctx.is_contract_type_name(&type_id.name),
48		Expression::MemberAccess(_, namespace_expr, type_id) => {
49			matches!(
50				namespace_expr.as_ref(),
51				Expression::Variable(namespace_id)
52					if !ctx.param_index_map.contains_key(&namespace_id.name)
53						&& ctx.resolve_local(&namespace_id.name).is_none()
54						&& !ctx.state_index_map.contains_key(&namespace_id.name)
55						&& !ctx.is_contract_type_name(&namespace_id.name)
56			) && ctx.is_contract_type_name(&type_id.name)
57		}
58		_ => false,
59	}
60}
61
62fn resolve_encode_call_method_name(expr: &Expression, ctx: &LoweringContext) -> Option<String> {
63	if let Some(name) = resolve_selector_method_name(expr, ctx) {
64		if !name.trim().is_empty() {
65			return Some(name);
66		}
67	}
68
69	match expr {
70		Expression::Parenthesis(_, inner) => resolve_encode_call_method_name(inner, ctx),
71		Expression::FunctionCall(_, func, args)
72			if is_single_argument_bytes_or_type_wrapper(func.as_ref(), args.as_slice()) =>
73		{
74			resolve_encode_call_method_name(&args[0], ctx)
75		}
76		Expression::MemberAccess(_, inner, member) => {
77			if member.name == "selector" {
78				if let Expression::MemberAccess(_, type_expr, function_member) = inner.as_ref() {
79					if is_contract_type_reference(type_expr.as_ref(), ctx) {
80						let function_name = function_member.name.trim();
81						if !function_name.is_empty() {
82							return Some(function_name.to_string());
83						}
84					}
85				}
86				return None;
87			}
88
89			if !is_contract_type_reference(inner.as_ref(), ctx) {
90				return None;
91			}
92
93			let name = member.name.trim();
94			if name.is_empty() {
95				None
96			} else {
97				Some(name.to_string())
98			}
99		}
100		_ => None,
101	}
102}
103
104fn extract_encode_call_arguments(expr: &Expression) -> Option<Vec<&Expression>> {
105	match expr {
106		Expression::Parenthesis(_, inner) => extract_encode_call_arguments(inner),
107		Expression::FunctionCall(_, func, args)
108			if is_single_argument_bytes_or_type_wrapper(func.as_ref(), args.as_slice()) =>
109		{
110			extract_encode_call_arguments(&args[0])
111		}
112		Expression::List(_, params) => {
113			let mut arguments = Vec::with_capacity(params.len());
114			for (_, param) in params {
115				let param = param.as_ref()?;
116				arguments.push(&param.ty);
117			}
118			Some(arguments)
119		}
120		_ => Some(vec![expr]),
121	}
122}
123
124fn parse_low_level_call_data<'a>(
125	expr: &'a Expression,
126	ctx: &LoweringContext,
127) -> Result<Option<(String, Vec<&'a Expression>)>, String> {
128	match expr {
129		Expression::Parenthesis(_, inner) => parse_low_level_call_data(inner, ctx),
130		Expression::FunctionCall(_, func, args)
131			if is_single_argument_bytes_or_type_wrapper(func.as_ref(), args.as_slice()) =>
132		{
133			parse_low_level_call_data(&args[0], ctx)
134		}
135		Expression::FunctionCall(_, func, args) => {
136			let Expression::MemberAccess(_, inner, member) = func.as_ref() else {
137				return Ok(None);
138			};
139
140			if !matches!(inner.as_ref(), Expression::Variable(id) if id.name == "abi") {
141				return Ok(None);
142			}
143
144			match member.name.as_str() {
145				"encodeWithSignature" => {
146					let Some((first, rest)) = args.split_first() else {
147						return Err("abi.encodeWithSignature requires a signature argument".to_string());
148					};
149
150					let signature = resolve_signature_string(first, ctx).ok_or_else(|| {
151						"abi.encodeWithSignature signature must be a string literal or a constant string"
152							.to_string()
153					})?;
154
155					let name = signature
156						.split('(')
157						.next()
158						.unwrap_or(signature.as_str())
159						.trim()
160						.to_string();
161					if name.is_empty() {
162						return Err(
163							"abi.encodeWithSignature signature must include a function name".to_string(),
164						);
165					}
166					Ok(Some((name, rest.iter().collect())))
167				}
168				"encodeWithSelector" => {
169					let Some((first, rest)) = args.split_first() else {
170						return Err("abi.encodeWithSelector requires a selector argument".to_string());
171					};
172
173					let name = resolve_selector_method_name(first, ctx).ok_or_else(|| {
174						"abi.encodeWithSelector has an unsupported selector".to_string()
175					})?;
176					if name.trim().is_empty() {
177						return Err(
178							"abi.encodeWithSelector selector resolves to an empty name".to_string(),
179						);
180					}
181					Ok(Some((name, rest.iter().collect())))
182				}
183				"encodeCall" => {
184					if args.len() != 2 {
185						return Err(
186							"abi.encodeCall requires function selector and tuple argument list"
187								.to_string(),
188						);
189					}
190
191					let method_name = resolve_encode_call_method_name(&args[0], ctx)
192						.ok_or_else(|| "abi.encodeCall has an unsupported function reference".to_string())?;
193
194					let call_args = extract_encode_call_arguments(&args[1]).ok_or_else(|| {
195						"abi.encodeCall tuple argument list must contain positional expressions"
196							.to_string()
197					})?;
198
199					Ok(Some((method_name, call_args)))
200				}
201				_ => Ok(None),
202			}
203		}
204		_ => Ok(None),
205	}
206}
207
208fn resolve_call_data_local(expr: &Expression, ctx: &LoweringContext) -> Option<(usize, String)> {
209	match expr {
210		Expression::Parenthesis(_, inner) => resolve_call_data_local(inner, ctx),
211		Expression::FunctionCall(_, func, args)
212			if is_single_argument_bytes_or_type_wrapper(func.as_ref(), args.as_slice()) =>
213		{
214			resolve_call_data_local(&args[0], ctx)
215		}
216		Expression::Variable(identifier) => {
217			let slot = ctx.resolve_local(&identifier.name)?;
218			let method = ctx.call_data_method_for_local(slot)?.to_string();
219			Some((slot, method))
220		}
221		_ => None,
222	}
223}
224fn try_lower_low_level_address_call(
225	func: &Expression,
226	args: &[Expression],
227	ctx: &mut LoweringContext,
228	instructions: &mut Vec<Instruction>,
229) -> Option<bool> {
230	// Limited low-level call support:
231	// `address.call(abi.encodeWithSignature("foo(T1,T2)", a, b))`
232	// `address.staticcall(abi.encodeWithSignature("foo(T1,T2)", a, b))`
233	// `bytes data = abi.encodeWithSignature(...); address.call(data)`
234	// `bytes data = abi.encodeWithSelector(...); address.staticcall(data)`
235	//
236	// These are lowered into Neo `System.Contract.Call` invocations and return
237	// `(success, Serialize(ret))`, mirroring Solidity's `(bool, bytes)` low-level calls.
238	// We wrap only the contract call itself in NeoVM TRY/ENDTRY so that callee faults
239	// become `success=false` with empty return data, while local evaluation errors still
240	// abort execution.
241	if let Expression::MemberAccess(_, inner, member) = func {
242		let member_name = member.name.as_str();
243		let is_staticcall = member_name == "staticcall";
244		if (member_name == "call" || is_staticcall) && args.len() == 1 {
245			if ctx.is_safe && member_name == "call" {
246				ctx.record_error(
247					"address.call(...) is not allowed in view/pure functions; use address.staticcall(...) or an external view/pure interface call",
248				);
249				return Some(false);
250			}
251
252			match parse_low_level_call_data(&args[0], ctx) {
253				Ok(Some((method_name, encode_args))) => {
254					let data_local = ctx.allocate_local("__call_data".to_string(), None);
255
256					// Build tuple `(success, data)` as an array.
257					let tuple_local = ctx.allocate_local("__call_tuple".to_string(), None);
258					instructions.push(Instruction::PushLiteral(LiteralValue::Integer(BigInt::from(
259						2u8,
260					))));
261					instructions.push(Instruction::NewArray {
262						element_type: ValueType::Any,
263					});
264					instructions.push(Instruction::StoreLocal(tuple_local));
265
266					if !lower_expression(inner.as_ref(), ctx, instructions) {
267						return Some(false);
268					}
269
270					instructions.push(Instruction::PushLiteral(LiteralValue::String(
271						method_name.as_bytes().to_vec(),
272					)));
273
274					let mut lowered = true;
275					for call_arg in &encode_args {
276						if !lower_expression(call_arg, ctx, instructions) {
277							lowered = false;
278						}
279					}
280
281					if !lowered {
282						return Some(false);
283					}
284
285					instructions.push(Instruction::CallBuiltin {
286						builtin: BuiltinCall::AbiEncode,
287						arg_count: encode_args.len(),
288					});
289
290					let catch_label = ctx.next_label();
291					let end_label = ctx.next_label();
292					instructions.push(Instruction::Try {
293						catch_target: catch_label,
294					});
295
296					if is_staticcall {
297						// CallFlags.ReadOnly (ReadStates | AllowCall).
298						instructions.push(Instruction::PushLiteral(LiteralValue::Integer(
299							BigInt::from(0x05u8),
300						)));
301						instructions.push(Instruction::CallBuiltin {
302							builtin: BuiltinCall::ContractCallWithFlags,
303							arg_count: 4,
304						});
305					} else {
306						instructions.push(Instruction::CallBuiltin {
307							builtin: BuiltinCall::ContractCall,
308							arg_count: 3,
309						});
310					}
311
312					instructions.push(Instruction::StoreLocal(data_local));
313
314					// success at index 0
315					instructions.push(Instruction::LoadLocal(tuple_local));
316					instructions.push(Instruction::PushLiteral(LiteralValue::Integer(BigInt::zero())));
317					instructions.push(Instruction::PushLiteral(LiteralValue::Boolean(true)));
318					instructions.push(Instruction::ArraySet);
319
320					// data at index 1
321					instructions.push(Instruction::LoadLocal(tuple_local));
322					instructions.push(Instruction::PushLiteral(LiteralValue::Integer(BigInt::one())));
323					instructions.push(Instruction::LoadLocal(data_local));
324					instructions.push(Instruction::ArraySet);
325
326					instructions.push(Instruction::EndTry { target: end_label });
327
328					instructions.push(Instruction::Label(catch_label));
329					// NeoVM pushes the exception object onto the stack for the catch block.
330					// Preserve it by serializing the exception value into the returned `bytes`.
331					instructions.push(Instruction::CallBuiltin {
332						builtin: BuiltinCall::NativeCall {
333							contract: NativeContract::StdLib,
334							method: "serialize".to_string(),
335						},
336						arg_count: 1,
337					});
338					instructions.push(Instruction::StoreLocal(data_local));
339
340					// success=false
341					instructions.push(Instruction::LoadLocal(tuple_local));
342					instructions.push(Instruction::PushLiteral(LiteralValue::Integer(BigInt::zero())));
343					instructions.push(Instruction::PushLiteral(LiteralValue::Boolean(false)));
344					instructions.push(Instruction::ArraySet);
345
346					// data=serialized exception
347					instructions.push(Instruction::LoadLocal(tuple_local));
348					instructions.push(Instruction::PushLiteral(LiteralValue::Integer(BigInt::one())));
349					instructions.push(Instruction::LoadLocal(data_local));
350					instructions.push(Instruction::ArraySet);
351
352					instructions.push(Instruction::EndTry { target: end_label });
353					instructions.push(Instruction::Label(end_label));
354					instructions.push(Instruction::LoadLocal(tuple_local));
355					return Some(true);
356				}
357				Ok(None) => {}
358				Err(message) => {
359					ctx.record_error(message);
360					return Some(false);
361				}
362			}
363
364			if let Some((call_data_slot, method_name)) = resolve_call_data_local(&args[0], ctx) {
365				let data_local = ctx.allocate_local("__call_data".to_string(), None);
366
367				let tuple_local = ctx.allocate_local("__call_tuple".to_string(), None);
368				instructions.push(Instruction::PushLiteral(LiteralValue::Integer(BigInt::from(
369					2u8,
370				))));
371				instructions.push(Instruction::NewArray {
372					element_type: ValueType::Any,
373				});
374				instructions.push(Instruction::StoreLocal(tuple_local));
375
376				if !lower_expression(inner.as_ref(), ctx, instructions) {
377					return Some(false);
378				}
379
380				instructions.push(Instruction::PushLiteral(LiteralValue::String(
381					method_name.as_bytes().to_vec(),
382				)));
383				instructions.push(Instruction::LoadLocal(call_data_slot));
384
385				let catch_label = ctx.next_label();
386				let end_label = ctx.next_label();
387				instructions.push(Instruction::Try {
388					catch_target: catch_label,
389				});
390
391				if is_staticcall {
392					instructions.push(Instruction::PushLiteral(LiteralValue::Integer(BigInt::from(
393						0x05u8,
394					))));
395					instructions.push(Instruction::CallBuiltin {
396						builtin: BuiltinCall::ContractCallWithFlags,
397						arg_count: 4,
398					});
399				} else {
400					instructions.push(Instruction::CallBuiltin {
401						builtin: BuiltinCall::ContractCall,
402						arg_count: 3,
403					});
404				}
405
406				instructions.push(Instruction::StoreLocal(data_local));
407
408				instructions.push(Instruction::LoadLocal(tuple_local));
409				instructions.push(Instruction::PushLiteral(LiteralValue::Integer(BigInt::zero())));
410				instructions.push(Instruction::PushLiteral(LiteralValue::Boolean(true)));
411				instructions.push(Instruction::ArraySet);
412
413				instructions.push(Instruction::LoadLocal(tuple_local));
414				instructions.push(Instruction::PushLiteral(LiteralValue::Integer(BigInt::one())));
415				instructions.push(Instruction::LoadLocal(data_local));
416				instructions.push(Instruction::ArraySet);
417
418				instructions.push(Instruction::EndTry { target: end_label });
419
420				instructions.push(Instruction::Label(catch_label));
421				instructions.push(Instruction::CallBuiltin {
422					builtin: BuiltinCall::NativeCall {
423						contract: NativeContract::StdLib,
424						method: "serialize".to_string(),
425					},
426					arg_count: 1,
427				});
428				instructions.push(Instruction::StoreLocal(data_local));
429
430				instructions.push(Instruction::LoadLocal(tuple_local));
431				instructions.push(Instruction::PushLiteral(LiteralValue::Integer(BigInt::zero())));
432				instructions.push(Instruction::PushLiteral(LiteralValue::Boolean(false)));
433				instructions.push(Instruction::ArraySet);
434
435				instructions.push(Instruction::LoadLocal(tuple_local));
436				instructions.push(Instruction::PushLiteral(LiteralValue::Integer(BigInt::one())));
437				instructions.push(Instruction::LoadLocal(data_local));
438				instructions.push(Instruction::ArraySet);
439
440				instructions.push(Instruction::EndTry { target: end_label });
441				instructions.push(Instruction::Label(end_label));
442				instructions.push(Instruction::LoadLocal(tuple_local));
443				return Some(true);
444			}
445
446			ctx.record_error_with_suggestion(
447				format!("unsupported low-level EVM call '{}'", member_name),
448				"Neo N3 does not support low-level EVM calls; use NativeCalls.sol for contract-to-contract interactions",
449			);
450			return Some(false);
451		}
452	}
453
454	None
455}