diff --git a/src/codegen/mod.rs b/src/codegen/mod.rs index 1adb0070d..be993a770 100644 --- a/src/codegen/mod.rs +++ b/src/codegen/mod.rs @@ -302,6 +302,18 @@ fn contract(contract_no: usize, ns: &mut Namespace, opt: &Options) { if !ns.diagnostics.any_errors() && ns.contracts[contract_no].instantiable { layout(contract_no, ns); + if ns.target == Target::Soroban { + soroban::validate_accessor_abi_types(contract_no, ns); + if ns.diagnostics.any_errors() { + return; + } + + soroban::validate_event_abi_types(contract_no, ns); + if ns.diagnostics.any_errors() { + return; + } + } + let mut cfg_no = 0; let mut all_cfg = Vec::new(); @@ -359,6 +371,13 @@ fn contract(contract_no: usize, ns: &mut Namespace, opt: &Options) { ns.contracts[contract_no].default_constructor = Some((func, cfg_no)); } + if ns.target == Target::Soroban { + soroban::validate_abi_types(&all_cfg, ns); + if ns.diagnostics.any_errors() { + return; + } + } + for mut dispatch_cfg in function_dispatch(contract_no, &mut all_cfg, ns, opt) { optimize_and_check_cfg(&mut dispatch_cfg, ns, ASTFunction::None, opt); all_cfg.push(dispatch_cfg); diff --git a/src/codegen/soroban.rs b/src/codegen/soroban.rs index 62037c46a..0fb0a5a97 100644 --- a/src/codegen/soroban.rs +++ b/src/codegen/soroban.rs @@ -1,14 +1,377 @@ // SPDX-License-Identifier: Apache-2.0 -use super::cfg::{ControlFlowGraph, Instr, InternalCallTy}; +use super::cfg::{ASTFunction, ControlFlowGraph, Instr, InternalCallTy}; use super::encoding::soroban_encoding::soroban_encode_arg; use super::expression::{expression, load_storage}; use super::vartable::Vartable; use super::Options; -use crate::codegen::{Expression, HostFunctions}; +use crate::codegen::{Builtin, Expression, HostFunctions}; use crate::sema::ast; use crate::sema::ast::{Function, Namespace, RetrieveType, Type}; -use solang_parser::pt; +use crate::sema::Recurse; +use crate::Target; +use solang_parser::helpers::CodeLocation; +use solang_parser::{diagnostics::Diagnostic, pt}; +use std::collections::BTreeSet; + +pub(super) fn validate_accessor_abi_types(contract_no: usize, ns: &mut Namespace) { + if ns.target != Target::Soroban { + return; + } + + for variable in &ns.contracts[contract_no].variables { + if !matches!(variable.visibility, pt::Visibility::Public(_)) { + continue; + } + + if let Some(unsupported_type) = unsupported_accessor_type(&variable.ty, ns) { + ns.diagnostics.push(Diagnostic::error( + variable.loc, + format!( + "type '{unsupported_type}' is not supported as a Soroban public variable accessor return value" + ), + )); + } + } +} + +pub(super) fn validate_event_abi_types(contract_no: usize, ns: &mut Namespace) { + if ns.target != Target::Soroban { + return; + } + + for event_no in ns.contracts[contract_no].emits_events.clone() { + for field in &ns.events[event_no].fields { + if let Some(unsupported_type) = unsupported_event_type(&field.ty, ns) { + ns.diagnostics.push(Diagnostic::error( + field.ty_loc.unwrap_or(field.loc), + format!( + "type '{unsupported_type}' is not supported as a Soroban event parameter" + ), + )); + } + } + } +} + +pub(super) fn validate_abi_types(all_cfg: &[ControlFlowGraph], ns: &mut Namespace) { + if ns.target != Target::Soroban { + return; + } + + for cfg in all_cfg { + if !cfg.public { + continue; + } + + if is_public_accessor(cfg, ns) { + continue; + } + + if cfg.returns.len() > 1 { + let loc = cfg.returns[1].ty_loc.unwrap_or(cfg.returns[1].loc); + ns.diagnostics.push(Diagnostic::error( + loc, + "Soroban external functions can return at most one value".to_string(), + )); + continue; + } + + for param in cfg.params.as_ref() { + if let Some(unsupported_type) = unsupported_parameter_type(¶m.ty, ns) { + ns.diagnostics.push(Diagnostic::error( + param.ty_loc.unwrap_or(param.loc), + format!( + "type '{unsupported_type}' is not supported as a Soroban external function parameter" + ), + )); + } + } + + for ret in cfg.returns.as_ref() { + if let Some(unsupported_type) = unsupported_return_type(&ret.ty, ns) { + ns.diagnostics.push(Diagnostic::error( + ret.ty_loc.unwrap_or(ret.loc), + format!( + "type '{unsupported_type}' is not supported as a Soroban external function return value" + ), + )); + } + } + } + + validate_string_handle_code_paths(all_cfg, ns); +} + +fn unsupported_parameter_type(ty: &Type, ns: &Namespace) -> Option { + match ty { + Type::DynamicBytes => Some("bytes memory".to_string()), + Type::Bytes(n) => Some(format!("bytes{n}")), + Type::Struct(_) => Some(format!("{} memory", ty.to_string(ns))), + Type::Array(elem, _) if has_unsupported_soroban_array_element(elem.as_ref()) => { + Some(format!("{} memory", ty.to_string(ns))) + } + _ => None, + } +} + +fn is_public_accessor(cfg: &ControlFlowGraph, ns: &Namespace) -> bool { + match cfg.function_no { + ASTFunction::SolidityFunction(function_no) => ns.functions[function_no].is_accessor, + _ => false, + } +} + +fn unsupported_accessor_type(ty: &Type, ns: &Namespace) -> Option { + match ty { + Type::DynamicBytes => Some("bytes".to_string()), + Type::Bytes(n) => Some(format!("bytes{n}")), + Type::Mapping(mapping) => unsupported_accessor_type(mapping.value.as_ref(), ns), + Type::Array(elem, _) => unsupported_accessor_type(elem.as_ref(), ns), + Type::Struct(struct_ty) => { + let fields = &struct_ty.definition(ns).fields; + + if fields.len() > 1 { + Some(ty.to_string(ns)) + } else { + fields + .first() + .and_then(|field| unsupported_accessor_type(&field.ty, ns)) + } + } + _ => None, + } +} + +fn unsupported_event_type(ty: &Type, ns: &Namespace) -> Option { + match ty { + Type::DynamicBytes => Some("bytes".to_string()), + Type::Bytes(n) => Some(format!("bytes{n}")), + Type::Struct(_) => Some(ty.to_string(ns)), + _ => None, + } +} + +fn unsupported_return_type(ty: &Type, ns: &Namespace) -> Option { + match ty { + Type::String => Some("string memory".to_string()), + Type::DynamicBytes => Some("bytes memory".to_string()), + Type::Bytes(n) => Some(format!("bytes{n}")), + Type::Struct(_) => Some(format!("{} memory", ty.to_string(ns))), + Type::Array(_, _) => Some(format!("{} memory", ty.to_string(ns))), + _ => None, + } +} + +fn has_unsupported_soroban_array_element(ty: &Type) -> bool { + match ty { + Type::DynamicBytes | Type::Struct(_) => true, + Type::Array(elem, _) => has_unsupported_soroban_array_element(elem.as_ref()), + _ => false, + } +} + +fn validate_string_handle_code_paths(all_cfg: &[ControlFlowGraph], ns: &mut Namespace) { + for cfg in all_cfg { + let mut string_handle_vars = BTreeSet::new(); + + for block in &cfg.blocks { + for instr in &block.instr { + validate_string_handle_uses(instr, &string_handle_vars, ns); + + match instr { + Instr::Set { res, expr, .. } => { + update_string_handle_var(*res, expr, &mut string_handle_vars); + } + Instr::Call { + res, + return_tys, + call: InternalCallTy::Static { cfg_no }, + args, + } => { + validate_static_string_call_args( + &all_cfg[*cfg_no], + args, + &string_handle_vars, + ns, + ); + + for (res_no, ty) in res.iter().zip(return_tys.iter()) { + if matches!(ty, Type::String) { + string_handle_vars.insert(*res_no); + } else { + string_handle_vars.remove(res_no); + } + } + } + Instr::Call { + res, return_tys, .. + } => { + for (res_no, ty) in res.iter().zip(return_tys.iter()) { + if matches!(ty, Type::String) { + string_handle_vars.insert(*res_no); + } else { + string_handle_vars.remove(res_no); + } + } + } + _ => (), + } + } + } + } +} + +fn update_string_handle_var( + res: usize, + expr: &Expression, + string_handle_vars: &mut BTreeSet, +) { + if is_soroban_string_handle_expr(expr, string_handle_vars) { + string_handle_vars.insert(res); + } else { + string_handle_vars.remove(&res); + } +} + +fn validate_static_string_call_args( + callee: &ControlFlowGraph, + args: &[Expression], + string_handle_vars: &BTreeSet, + ns: &mut Namespace, +) { + for (arg, param) in args.iter().zip(callee.params.iter()) { + if matches!(param.ty, Type::String) + && !is_soroban_string_handle_expr(arg, string_handle_vars) + { + ns.diagnostics.push(Diagnostic::error( + arg.loc(), + "passing string memory values to internal functions is not supported for target soroban" + .to_string(), + )); + } + } +} + +fn validate_string_handle_uses( + instr: &Instr, + string_handle_vars: &BTreeSet, + ns: &mut Namespace, +) { + let mut cx = StringHandleUseContext { + string_handle_vars, + diagnostics: Vec::new(), + }; + + match instr { + Instr::Set { expr, .. } => { + expr.recurse(&mut cx, reject_string_handle_memory_use); + } + Instr::Call { args, .. } | Instr::Return { value: args } => { + for arg in args { + arg.recurse(&mut cx, reject_string_handle_memory_use); + } + } + Instr::BranchCond { cond, .. } | Instr::Print { expr: cond } => { + cond.recurse(&mut cx, reject_string_handle_memory_use); + } + Instr::Store { dest, data } => { + dest.recurse(&mut cx, reject_string_handle_memory_use); + data.recurse(&mut cx, reject_string_handle_memory_use); + } + Instr::AssertFailure { + encoded_args: Some(expr), + } => { + expr.recurse(&mut cx, reject_string_handle_memory_use); + } + Instr::LoadStorage { storage, .. } | Instr::ClearStorage { storage, .. } => { + storage.recurse(&mut cx, reject_string_handle_memory_use); + } + Instr::SetStorage { value, storage, .. } => { + value.recurse(&mut cx, reject_string_handle_memory_use); + storage.recurse(&mut cx, reject_string_handle_memory_use); + } + Instr::SetStorageBytes { + value, + storage, + offset, + } => { + value.recurse(&mut cx, reject_string_handle_memory_use); + storage.recurse(&mut cx, reject_string_handle_memory_use); + offset.recurse(&mut cx, reject_string_handle_memory_use); + } + Instr::PushStorage { value, storage, .. } => { + if let Some(value) = value { + value.recurse(&mut cx, reject_string_handle_memory_use); + } + + storage.recurse(&mut cx, reject_string_handle_memory_use); + } + Instr::PopStorage { storage, .. } => { + storage.recurse(&mut cx, reject_string_handle_memory_use); + } + Instr::PushMemory { array, value, .. } => { + Expression::Variable { + loc: pt::Loc::Codegen, + ty: Type::String, + var_no: *array, + } + .recurse(&mut cx, reject_string_handle_memory_use); + value.recurse(&mut cx, reject_string_handle_memory_use); + } + _ => (), + } + + for diagnostic in cx.diagnostics { + ns.diagnostics.push(diagnostic); + } +} + +struct StringHandleUseContext<'a> { + string_handle_vars: &'a BTreeSet, + diagnostics: Vec, +} + +fn reject_string_handle_memory_use(expr: &Expression, cx: &mut StringHandleUseContext<'_>) -> bool { + if let Expression::Builtin { + loc, + kind: Builtin::ArrayLength, + args, + .. + } = expr + { + if args.first().is_some_and(|arg| { + matches!(arg.ty(), Type::String) + && is_soroban_string_handle_expr(arg, cx.string_handle_vars) + }) { + cx.diagnostics.push(Diagnostic::error( + *loc, + "using string memory as bytes is not supported for target soroban".to_string(), + )); + } + } + + true +} + +fn is_soroban_string_handle_expr(expr: &Expression, string_handle_vars: &BTreeSet) -> bool { + match expr { + Expression::FunctionArg { + ty: Type::String, .. + } => true, + Expression::Variable { + ty: Type::String, + var_no, + .. + } => string_handle_vars.contains(var_no), + Expression::Cast { + ty: Type::String, + expr, + .. + } => is_soroban_string_handle_expr(expr, string_handle_vars), + _ => false, + } +} fn soroban_vec_handle_ty(vec_ty: &Type) -> Type { let inner_ty = if let Type::StorageRef(_, inner) = vec_ty { diff --git a/tests/soroban_testcases/mod.rs b/tests/soroban_testcases/mod.rs index 7b1ff2a4e..ba60ed21e 100644 --- a/tests/soroban_testcases/mod.rs +++ b/tests/soroban_testcases/mod.rs @@ -20,3 +20,4 @@ mod timelock; mod timestamp; mod token; mod ttl; +mod unsupported_parameters; diff --git a/tests/soroban_testcases/unsupported_parameters.rs b/tests/soroban_testcases/unsupported_parameters.rs new file mode 100644 index 000000000..05af8579b --- /dev/null +++ b/tests/soroban_testcases/unsupported_parameters.rs @@ -0,0 +1,418 @@ +// SPDX-License-Identifier: Apache-2.0 + +use solang::codegen::Options; +use solang::file_resolver::FileResolver; +use solang::sema::ast::Namespace; +use solang::sema::file::PathDisplay; +use solang::{compile, Target}; +use solang_parser::diagnostics::Level; +use std::ffi::OsStr; + +fn compile_target(src: &str, target: Target) -> Namespace { + let tmp_file = OsStr::new("test.sol"); + let mut cache = FileResolver::default(); + cache.set_file_contents(tmp_file.to_str().unwrap(), src.to_string()); + let opt = inkwell::OptimizationLevel::Default; + + let (_, ns) = compile( + tmp_file, + &mut cache, + target, + &Options { + opt_level: opt.into(), + log_runtime_errors: true, + log_prints: true, + #[cfg(feature = "wasm_opt")] + wasm_opt: Some(contract_build::OptimizationPasses::Z), + soroban_version: None, + ..Default::default() + }, + std::vec!["unknown".to_string()], + "0.0.1", + ); + + ns +} + +fn compile_soroban(src: &str) -> Namespace { + compile_target(src, Target::Soroban) +} + +#[test] +fn dynamic_bytes_external_parameters_are_rejected() { + let ns = compile_soroban( + r#"contract test { + function public_len(bytes memory data) public returns (uint64) { + return data.length; + } + + function external_len(bytes memory data) external returns (uint64) { + return data.length; + } +}"#, + ); + + let errors = ns + .diagnostics + .iter() + .filter(|diagnostic| diagnostic.level == Level::Error) + .collect::>(); + + assert_eq!(errors.len(), 2); + assert!(errors.iter().all(|diagnostic| diagnostic.message + == "type 'bytes memory' is not supported as a Soroban external function parameter")); + + let locations = errors + .iter() + .map(|diagnostic| ns.loc_to_string(PathDisplay::None, &diagnostic.loc)) + .collect::>(); + + assert!(locations.iter().any(|loc| loc.starts_with("2:"))); + assert!(locations.iter().any(|loc| loc.starts_with("6:"))); +} + +#[test] +fn static_bytes_external_parameters_are_rejected() { + let ns = compile_soroban( + r#"contract test { + function len(bytes32 data) public returns (uint64) { + return uint64(data.length); + } +}"#, + ); + + let errors = ns + .diagnostics + .iter() + .filter(|diagnostic| diagnostic.level == Level::Error) + .collect::>(); + + assert_eq!(errors.len(), 1); + assert_eq!( + errors[0].message, + "type 'bytes32' is not supported as a Soroban external function parameter" + ); + assert!(ns + .loc_to_string(PathDisplay::None, &errors[0].loc) + .starts_with("2:")); +} + +#[test] +fn string_external_returns_are_rejected() { + let ns = compile_soroban( + r#"contract test { + function public_make() public returns (string memory) { + return "hello"; + } + + function external_make() external returns (string memory) { + return "hello"; + } +}"#, + ); + + let errors = ns + .diagnostics + .iter() + .filter(|diagnostic| diagnostic.level == Level::Error) + .collect::>(); + + assert_eq!(errors.len(), 2); + assert!(errors.iter().all(|diagnostic| diagnostic.message + == "type 'string memory' is not supported as a Soroban external function return value")); + + let locations = errors + .iter() + .map(|diagnostic| ns.loc_to_string(PathDisplay::None, &diagnostic.loc)) + .collect::>(); + + assert!(locations.iter().any(|loc| loc.starts_with("2:"))); + assert!(locations.iter().any(|loc| loc.starts_with("6:"))); +} + +#[test] +fn bytes_external_returns_are_rejected() { + let ns = compile_soroban( + r#"contract test { + function public_make() public returns (bytes memory) { + return hex"01"; + } + + function external_make() external returns (bytes32) { + return bytes32(uint256(1)); + } +}"#, + ); + + let errors = ns + .diagnostics + .iter() + .filter(|diagnostic| diagnostic.level == Level::Error) + .collect::>(); + + assert_eq!(errors.len(), 2); + assert!(errors.iter().any(|diagnostic| diagnostic.message + == "type 'bytes memory' is not supported as a Soroban external function return value")); + assert!(errors.iter().any(|diagnostic| diagnostic.message + == "type 'bytes32' is not supported as a Soroban external function return value")); + + let locations = errors + .iter() + .map(|diagnostic| ns.loc_to_string(PathDisplay::None, &diagnostic.loc)) + .collect::>(); + + assert!(locations.iter().any(|loc| loc.starts_with("2:"))); + assert!(locations.iter().any(|loc| loc.starts_with("6:"))); +} + +#[test] +fn nested_and_struct_function_abi_types_are_rejected() { + let ns = compile_soroban( + r#"contract test { + struct Item { + uint64 value; + } + + function bytes_array(bytes[] memory data) public returns (uint64) { + return uint64(data.length); + } + + function struct_param(Item memory item) public returns (uint64) { + return item.value; + } + + function struct_return() public returns (Item memory) { + return Item({ value: 1 }); + } + + function array_return() public returns (uint64[] memory) { + uint64[] memory values = new uint64[](1); + values[0] = 1; + return values; + } + + function multiple_returns() public returns (uint64, uint64) { + return (1, 2); + } +}"#, + ); + + let errors = ns + .diagnostics + .iter() + .filter(|diagnostic| diagnostic.level == Level::Error) + .collect::>(); + + assert_eq!(errors.len(), 5); + assert!(errors.iter().any(|diagnostic| diagnostic.message + == "type 'bytes[] memory' is not supported as a Soroban external function parameter")); + assert!(errors.iter().any(|diagnostic| diagnostic.message + == "type 'struct test.Item memory' is not supported as a Soroban external function parameter")); + assert!(errors.iter().any(|diagnostic| diagnostic.message + == "type 'struct test.Item memory' is not supported as a Soroban external function return value")); + assert!(errors.iter().any(|diagnostic| diagnostic.message + == "type 'uint64[] memory' is not supported as a Soroban external function return value")); + assert!(errors.iter().any(|diagnostic| diagnostic.message + == "Soroban external functions can return at most one value")); +} + +#[test] +fn bytes_public_accessors_are_rejected() { + let ns = compile_soroban( + r#"contract test { + struct Pair { + uint64 first; + uint64 second; + } + + bytes public dynamic_data; + bytes32 public fixed_data; + bytes1 public tiny_data; + mapping(address => bytes) public keyed_data; + Pair public pair; +}"#, + ); + + let errors = ns + .diagnostics + .iter() + .filter(|diagnostic| diagnostic.level == Level::Error) + .collect::>(); + + assert_eq!(errors.len(), 5); + assert!(errors.iter().any(|diagnostic| diagnostic.message + == "type 'bytes' is not supported as a Soroban public variable accessor return value")); + assert!(errors.iter().any(|diagnostic| diagnostic.message + == "type 'bytes32' is not supported as a Soroban public variable accessor return value")); + assert!(errors.iter().any(|diagnostic| diagnostic.message + == "type 'bytes1' is not supported as a Soroban public variable accessor return value")); + assert!(errors.iter().any(|diagnostic| diagnostic.message + == "type 'struct test.Pair' is not supported as a Soroban public variable accessor return value")); + + let locations = errors + .iter() + .map(|diagnostic| ns.loc_to_string(PathDisplay::None, &diagnostic.loc)) + .collect::>(); + + assert!(locations.iter().any(|loc| loc.starts_with("7:"))); + assert!(locations.iter().any(|loc| loc.starts_with("8:"))); + assert!(locations.iter().any(|loc| loc.starts_with("9:"))); + assert!(locations.iter().any(|loc| loc.starts_with("10:"))); + assert!(locations.iter().any(|loc| loc.starts_with("11:"))); +} + +#[test] +fn unsupported_event_parameters_are_rejected() { + let ns = compile_soroban( + r#"contract test { + event Dynamic(bytes data); + event Fixed(bytes32 data); + event Struct(Item data); + + struct Item { + uint64 value; + } + + function emit_events() public { + bytes memory data = hex"01"; + emit Dynamic(data); + emit Fixed(bytes32(uint256(1))); + emit Struct(Item({ value: 1 })); + } +}"#, + ); + + let errors = ns + .diagnostics + .iter() + .filter(|diagnostic| diagnostic.level == Level::Error) + .collect::>(); + + assert_eq!(errors.len(), 3); + assert!(errors.iter().any(|diagnostic| diagnostic.message + == "type 'bytes' is not supported as a Soroban event parameter")); + assert!(errors.iter().any(|diagnostic| diagnostic.message + == "type 'bytes32' is not supported as a Soroban event parameter")); + assert!(errors.iter().any(|diagnostic| diagnostic.message + == "type 'struct test.Item' is not supported as a Soroban event parameter")); +} + +#[test] +fn internal_private_dynamic_bytes_parameters_are_allowed() { + let ns = compile_soroban( + r#"contract test { + function entry() public returns (uint64) { + bytes memory data = new bytes(2); + return internal_len(data) + private_len(data); + } + + function internal_len(bytes memory data) internal returns (uint64) { + return data.length; + } + + function private_len(bytes memory data) private returns (uint64) { + return data.length; + } +}"#, + ); + + assert!(!ns.diagnostics.any_errors()); +} + +#[test] +fn known_good_soroban_contract_still_compiles() { + let ns = compile_soroban( + r#"contract test { + function roundtrip(uint64 value) public returns (uint64) { + return value; + } +}"#, + ); + + assert!(!ns.diagnostics.any_errors()); +} + +#[test] +fn public_string_accessors_are_allowed() { + let ns = compile_soroban( + r#"contract test { + string public name; +}"#, + ); + + assert!(!ns.diagnostics.any_errors()); +} + +#[test] +fn unsafe_soroban_string_helper_paths_are_rejected() { + let ns = compile_soroban( + r#"contract test { + function helper(string memory data) internal returns (uint64) { + return uint64(bytes(data).length); + } + + function call_helper_with_literal() public returns (uint64) { + return helper("abc"); + } + + function make() internal returns (string memory) { + return "abc"; + } + + function returned_string_length() public returns (uint64) { + return uint64(bytes(make()).length); + } + + function local_string_length() public returns (uint64) { + string memory data = "abc"; + return uint64(bytes(data).length); + } +}"#, + ); + + let errors = ns + .diagnostics + .iter() + .filter(|diagnostic| diagnostic.level == Level::Error) + .collect::>(); + + assert_eq!(errors.len(), 3); + assert!(errors.iter().any(|diagnostic| { + diagnostic.message + == "passing string memory values to internal functions is not supported for target soroban" + })); + assert_eq!( + errors + .iter() + .filter(|diagnostic| diagnostic.message + == "using string memory as bytes is not supported for target soroban") + .count(), + 2 + ); + assert!(errors.iter().all(|diagnostic| ns + .loc_to_string(PathDisplay::None, &diagnostic.loc) + .split(':') + .next() + .is_some_and(|line| line.parse::().is_ok()))); +} + +#[test] +fn non_soroban_string_helpers_do_not_get_soroban_diagnostics() { + let ns = compile_target( + r#"contract test { + function helper(string memory data) internal returns (uint64) { + return uint64(bytes(data).length); + } + + function call_helper_with_literal() public returns (uint64) { + return helper("abc"); + } +}"#, + Target::default_polkadot(), + ); + + assert!(!ns + .diagnostics + .iter() + .any(|diagnostic| diagnostic.message.contains("target soroban"))); +}