From 07bc473b4eb8862d965ea68cff9324e4855c588b Mon Sep 17 00:00:00 2001 From: Sean Young Date: Wed, 13 Dec 2023 21:43:53 +0000 Subject: [PATCH] Various fallback improvements Allow the following: contract A { // Take raw calldata as argument and return return data fallback(bytes calldata input) external payable (bytes memory) {} } Allow a function to be called fallback: contract A { // Will give a warning that's not a fallback function, // just a regular functon function fallback() public {} } Signed-off-by: Sean Young --- .../function_fallback_and_receive.sol | 3 +- docs/language/functions.rst | 9 +- solang-parser/src/solidity.lalrpop | 2 + solang-parser/src/tests.rs | 12 +-- src/codegen/cfg.rs | 14 ++++ src/codegen/constant_folding.rs | 9 ++ src/codegen/dispatch/polkadot.rs | 80 ++++++++++++++---- src/codegen/dispatch/solana.rs | 75 ++++++++++++----- src/codegen/mod.rs | 8 ++ src/emit/expression.rs | 20 +++++ src/emit/solana/target.rs | 12 ++- src/sema/functions.rs | 83 +++++++++++++------ tests/contract_testcases/evm/fallback.sol | 28 +++++++ .../polkadot/builtins/abi_decode_01.sol | 3 +- .../polkadot/calls/payable_functions_02.sol | 1 - .../polkadot/functions/payable_03.sol | 3 +- .../solana/annotations/at_whitespace.sol | 2 +- tests/evm.rs | 2 +- tests/polkadot_tests/functions.rs | 33 +++++++- tests/solana.rs | 1 - tests/solana_tests/balance.rs | 35 ++++++++ 21 files changed, 354 insertions(+), 81 deletions(-) create mode 100644 tests/contract_testcases/evm/fallback.sol diff --git a/docs/examples/polkadot/function_fallback_and_receive.sol b/docs/examples/polkadot/function_fallback_and_receive.sol index 6eae5e298..137961240 100644 --- a/docs/examples/polkadot/function_fallback_and_receive.sol +++ b/docs/examples/polkadot/function_fallback_and_receive.sol @@ -5,8 +5,9 @@ contract test { bar = x; } - fallback() external { + fallback(bytes calldata input) external returns (bytes memory ret) { // execute if function selector does not match "foo(uint32)" and no value sent + ret = "testdata"; } receive() external payable { diff --git a/docs/language/functions.rst b/docs/language/functions.rst index 73cb395da..8987c5dde 100644 --- a/docs/language/functions.rst +++ b/docs/language/functions.rst @@ -347,11 +347,11 @@ The difference to a regular ``call`` is that ``delegatecall`` executes the call * ``value`` can't be specified for ``delegatecall``; instead it will always stay the same in the callee. * ``msg.sender`` does not change; it stays the same as in the callee. -Refer to the `contracts pallet `_ +Refer to the `contracts pallet `_ and `Ethereum Solidity `_ documentations for more information. -``delegatecall`` is commonly used to implement re-usable libraries and +``delegatecall`` is commonly used to implement re-usable libraries and `upgradeable contracts `_. .. code-block:: solidity @@ -384,6 +384,11 @@ is executed. This made clear in the declarations; ``receive()`` must be declared with value and no ``receive()`` function is defined, then the call reverts, likewise if call is made without value and no ``fallback()`` is defined, then the call also reverts. +The fallback function can defined in two ways. First, it can have no parameters or return +values. Alternatively, it must have a ``bytes`` parameter and ``bytes`` return value. In this +case, the parameter contains the undecoded input (also known as calldata or instruction data), +and the return value is the raw return data for the contact. + Both functions must be declared ``external``. .. include:: ../examples/polkadot/function_fallback_and_receive.sol diff --git a/solang-parser/src/solidity.lalrpop b/solang-parser/src/solidity.lalrpop index 1ec180e4b..65d5362f1 100644 --- a/solang-parser/src/solidity.lalrpop +++ b/solang-parser/src/solidity.lalrpop @@ -194,6 +194,8 @@ SolIdentifier: Identifier = { "case" => Identifier{loc: Loc::File(file_no, l, r), name: "case".to_string()}, "default" => Identifier{loc: Loc::File(file_no, l, r), name: "default".to_string()}, "revert" => Identifier{loc: Loc::File(file_no, l, r), name: "revert".to_string()}, + "fallback" => Identifier{loc: Loc::File(file_no, l, r), name: "fallback".to_string()}, + "receive" => Identifier{loc: Loc::File(file_no, l, r), name: "receive".to_string()}, } SolAnnotation: Identifier = { diff --git a/solang-parser/src/tests.rs b/solang-parser/src/tests.rs index 91d0b96e5..139ddcffd 100644 --- a/solang-parser/src/tests.rs +++ b/solang-parser/src/tests.rs @@ -46,13 +46,13 @@ contract 9c { Diagnostic { loc: File(0, 17, 21), level: Error, ty: ParserError, message: "'frum' found where 'from' expected".to_string(), notes: vec![]}, Diagnostic { loc: File(0, 48, 49), level: Error, ty: ParserError, message: "unrecognised token ';', expected \"*\", \"<\", \"<=\", \"=\", \">\", \">=\", \"^\", \"~\", identifier, number, string".to_string(), notes: vec![] }, Diagnostic { loc: File(0, 62, 65), level: Error, ty: ParserError, message: r#"unrecognised token 'for', expected "(", ";", "=""#.to_string(), notes: vec![] }, - Diagnostic { loc: File(0, 78, 79), level: Error, ty: ParserError, message: r#"unrecognised token '9', expected "case", "default", "leave", "revert", "switch", identifier"#.to_string(), notes: vec![] }, - Diagnostic { loc: File(0, 95, 96), level: Error, ty: ParserError, message: "unrecognised token '0', expected \"(\", \"++\", \"--\", \".\", \"[\", \"case\", \"constant\", \"default\", \"external\", \"immutable\", \"internal\", \"leave\", \"override\", \"private\", \"public\", \"revert\", \"switch\", \"{\", identifier".to_string(), notes: vec![] }, + Diagnostic { loc: File(0, 78, 79), level: Error, ty: ParserError, message: r#"unrecognised token '9', expected "case", "default", "fallback", "leave", "receive", "revert", "switch", identifier"#.to_string(), notes: vec![] }, + Diagnostic { loc: File(0, 95, 96), level: Error, ty: ParserError, message: "unrecognised token '0', expected \"(\", \"++\", \"--\", \".\", \"[\", \"case\", \"constant\", \"default\", \"external\", \"fallback\", \"immutable\", \"internal\", \"leave\", \"override\", \"private\", \"public\", \"receive\", \"revert\", \"switch\", \"{\", identifier".to_string(), notes: vec![] }, Diagnostic { loc: File(0, 116, 123), level: Error, ty: ParserError, message: "unrecognised token 'uint256', expected \"++\", \"--\", \".\", \"[\", \"case\", \"default\", \"leave\", \"switch\", identifier".to_string(), notes: vec![] }, - Diagnostic { loc: File(0, 403, 404), level: Error, ty: ParserError, message: "unrecognised token '3', expected \"(\", \"++\", \"--\", \".\", \"[\", \"case\", \"constant\", \"default\", \"external\", \"immutable\", \"internal\", \"leave\", \"override\", \"private\", \"public\", \"revert\", \"switch\", \"{\", identifier".to_string(), notes: vec![] }, - Diagnostic { loc: File(0, 441, 442), level: Error, ty: ParserError, message: r#"unrecognised token '4', expected "(", "case", "default", "leave", "revert", "switch", identifier"#.to_string(), notes: vec![] }, - Diagnostic { loc: File(0, 460, 461), level: Error, ty: ParserError, message: "unrecognised token '!', expected \";\", \"case\", \"constant\", \"default\", \"external\", \"immutable\", \"internal\", \"leave\", \"override\", \"payable\", \"private\", \"public\", \"pure\", \"return\", \"returns\", \"revert\", \"switch\", \"view\", \"virtual\", \"{\", identifier".to_string(), notes: vec![] }, - Diagnostic { loc: File(0, 482, 483), level: Error, ty: ParserError, message: "unrecognised token '3', expected \"!=\", \"%\", \"%=\", \"&\", \"&&\", \"&=\", \"(\", \"*\", \"**\", \"*=\", \"+\", \"++\", \"+=\", \"-\", \"--\", \"-=\", \".\", \"/\", \"/=\", \";\", \"<\", \"<<\", \"<<=\", \"<=\", \"=\", \"==\", \">\", \">=\", \">>\", \">>=\", \"?\", \"[\", \"^\", \"^=\", \"calldata\", \"case\", \"default\", \"leave\", \"memory\", \"revert\", \"storage\", \"switch\", \"{\", \"|\", \"|=\", \"||\", identifier".to_string(), notes: vec![] }, + Diagnostic { loc: File(0, 403, 404), level: Error, ty: ParserError, message: "unrecognised token '3', expected \"(\", \"++\", \"--\", \".\", \"[\", \"case\", \"constant\", \"default\", \"external\", \"fallback\", \"immutable\", \"internal\", \"leave\", \"override\", \"private\", \"public\", \"receive\", \"revert\", \"switch\", \"{\", identifier".to_string(), notes: vec![] }, + Diagnostic { loc: File(0, 441, 442), level: Error, ty: ParserError, message: r#"unrecognised token '4', expected "(", "case", "default", "fallback", "leave", "receive", "revert", "switch", identifier"#.to_string(), notes: vec![] }, + Diagnostic { loc: File(0, 460, 461), level: Error, ty: ParserError, message: "unrecognised token '!', expected \";\", \"case\", \"constant\", \"default\", \"external\", \"fallback\", \"immutable\", \"internal\", \"leave\", \"override\", \"payable\", \"private\", \"public\", \"pure\", \"receive\", \"return\", \"returns\", \"revert\", \"switch\", \"view\", \"virtual\", \"{\", identifier".to_string(), notes: vec![] }, + Diagnostic { loc: File(0, 482, 483), level: Error, ty: ParserError, message: "unrecognised token '3', expected \"!=\", \"%\", \"%=\", \"&\", \"&&\", \"&=\", \"(\", \"*\", \"**\", \"*=\", \"+\", \"++\", \"+=\", \"-\", \"--\", \"-=\", \".\", \"/\", \"/=\", \";\", \"<\", \"<<\", \"<<=\", \"<=\", \"=\", \"==\", \">\", \">=\", \">>\", \">>=\", \"?\", \"[\", \"^\", \"^=\", \"calldata\", \"case\", \"default\", \"fallback\", \"leave\", \"memory\", \"receive\", \"revert\", \"storage\", \"switch\", \"{\", \"|\", \"|=\", \"||\", identifier".to_string(), notes: vec![] }, Diagnostic { loc: File(0, 518, 522), level: Error, ty: ParserError, message: "unrecognised token 'uint256', expected \"!=\", \"%\", \"%=\", \"&\", \"&&\", \"&=\", \"*\", \"**\", \"*=\", \"+\", \"++\", \"+=\", \"-\", \"--\", \"-=\", \".\", \"/\", \"/=\", \";\", \"<\", \"<<\", \"<<=\", \"<=\", \"=\", \"==\", \">\", \">=\", \">>\", \">>=\", \"?\", \"[\", \"^\", \"^=\", \"case\", \"default\", \"leave\", \"switch\", \"|\", \"|=\", \"||\", identifier".to_string(), notes: vec![] }, Diagnostic { loc: File(0, 555, 556), level: Error, ty: ParserError, message: "unrecognised token '}', expected \"!\", \"(\", \"+\", \"++\", \"-\", \"--\", \"[\", \"address\", \"assembly\", \"bool\", \"break\", \"byte\", \"bytes\", \"case\", \"continue\", \"default\", \"delete\", \"do\", \"emit\", \"false\", \"for\", \"function\", \"if\", \"leave\", \"mapping\", \"new\", \"payable\", \"return\", \"revert\", \"string\", \"switch\", \"true\", \"try\", \"type\", \"unchecked\", \"while\", \"{\", \"~\", Bytes, Int, Uint, address, hexnumber, hexstring, identifier, number, rational, string".to_string(), notes: vec![] }, Diagnostic { loc: File(0, 557, 558), level: Error, ty: ParserError, message: "unrecognised token '}', expected \"(\", \";\", \"[\", \"abstract\", \"address\", \"bool\", \"byte\", \"bytes\", \"case\", \"contract\", \"default\", \"enum\", \"event\", \"false\", \"function\", \"import\", \"interface\", \"leave\", \"library\", \"mapping\", \"payable\", \"pragma\", \"string\", \"struct\", \"switch\", \"true\", \"type\", \"using\", Bytes, Int, Uint, address, annotation, hexnumber, hexstring, identifier, number, rational, string".to_string(), notes: vec![] } diff --git a/src/codegen/cfg.rs b/src/codegen/cfg.rs index 23dd23cb6..df4f032d5 100644 --- a/src/codegen/cfg.rs +++ b/src/codegen/cfg.rs @@ -920,6 +920,20 @@ impl ControlFlowGraph { } ) } + Expression::FromBufferPointer { ty, ptr, size, .. } => { + let ty = if let Type::Slice(ty) = ty { + format!("slice {}", ty.to_string(ns)) + } else { + ty.to_string(ns) + }; + + format!( + "(alloc {} ptr {} size {})", + ty, + self.expr_to_string(contract, ns, ptr), + self.expr_to_string(contract, ns, size) + ) + } Expression::StringCompare { left, right, .. } => format!( "(strcmp ({}) ({}))", self.location_to_string(contract, ns, left), diff --git a/src/codegen/constant_folding.rs b/src/codegen/constant_folding.rs index 800362b79..5c3f218f9 100644 --- a/src/codegen/constant_folding.rs +++ b/src/codegen/constant_folding.rs @@ -642,6 +642,15 @@ fn expression( }, false, ), + Expression::FromBufferPointer { loc, ty, ptr, size } => ( + Expression::FromBufferPointer { + loc: *loc, + ty: ty.clone(), + ptr: Box::new(expression(ptr, vars, cfg, ns).0), + size: Box::new(expression(size, vars, cfg, ns).0), + }, + false, + ), Expression::NumberLiteral { .. } | Expression::RationalNumberLiteral { .. } diff --git a/src/codegen/dispatch/polkadot.rs b/src/codegen/dispatch/polkadot.rs index f8c21c077..9c25edaf9 100644 --- a/src/codegen/dispatch/polkadot.rs +++ b/src/codegen/dispatch/polkadot.rs @@ -465,24 +465,68 @@ impl<'a> Dispatch<'a> { self.cfg.set_basic_block(fallback_block); if let Some(cfg_no) = fallback_cfg { - self.add(Instr::Call { - res: vec![], - return_tys: vec![], - call: InternalCallTy::Static { cfg_no }, - args: vec![], - }); - let data_len = Expression::NumberLiteral { - loc: Codegen, - ty: Uint(32), - value: 0.into(), - }; - let data = Expression::AllocDynamicBytes { - loc: Codegen, - ty: Type::DynamicBytes, - size: data_len.clone().into(), - initializer: None, - }; - self.add(Instr::ReturnData { data, data_len }) + if self.all_cfg[cfg_no].params.is_empty() { + self.add(Instr::Call { + res: vec![], + return_tys: vec![], + call: InternalCallTy::Static { cfg_no }, + args: vec![], + }); + let data_len = Expression::NumberLiteral { + loc: Codegen, + ty: Uint(32), + value: 0.into(), + }; + let data = Expression::AllocDynamicBytes { + loc: Codegen, + ty: Type::DynamicBytes, + size: data_len.clone().into(), + initializer: None, + }; + self.add(Instr::ReturnData { data, data_len }) + } else { + let arg = Expression::FromBufferPointer { + loc: Codegen, + ty: Type::DynamicBytes, + ptr: Expression::FunctionArg { + loc: Codegen, + ty: Type::BufferPointer, + arg_no: 0, + } + .into(), + size: Expression::Variable { + loc: Codegen, + ty: Type::Uint(32), + var_no: self.input_len, + } + .into(), + }; + + let var_no = self + .vartab + .temp_name("fallback_return_data", &Type::DynamicBytes); + + self.add(Instr::Call { + res: vec![var_no], + return_tys: vec![Type::DynamicBytes], + call: InternalCallTy::Static { cfg_no }, + args: vec![arg], + }); + + let data = Expression::Variable { + loc: Codegen, + ty: Type::DynamicBytes, + var_no, + }; + let data_len = Expression::Builtin { + loc: Codegen, + tys: vec![Type::Uint(32)], + kind: Builtin::ArrayLength, + args: vec![data.clone()], + }; + + self.add(Instr::ReturnData { data, data_len }); + } } else { self.selector_invalid(); } diff --git a/src/codegen/dispatch/solana.rs b/src/codegen/dispatch/solana.rs index 5439488c0..4efd6b7bb 100644 --- a/src/codegen/dispatch/solana.rs +++ b/src/codegen/dispatch/solana.rs @@ -126,8 +126,8 @@ pub(crate) fn function_dispatch( ], }; - let argsdata = Expression::AdvancePointer { - pointer: Box::new(argsdata), + let argsdata_after = Expression::AdvancePointer { + pointer: argsdata.clone().into(), bytes_offset: Box::new(Expression::NumberLiteral { loc: Loc::Codegen, ty: Type::Uint(32), @@ -157,7 +157,7 @@ pub(crate) fn function_dispatch( add_function_dispatch_case( cfg_no, func_cfg, - &argsdata, + &argsdata_after, argslen.clone(), contract_no, ns, @@ -168,7 +168,7 @@ pub(crate) fn function_dispatch( add_constructor_dispatch_case( contract_no, cfg_no, - &argsdata, + &argsdata_after, argslen.clone(), func_cfg, ns, @@ -235,22 +235,57 @@ pub(crate) fn function_dispatch( check_magic(ns.contracts[contract_no].selector(), &mut cfg, &mut vartab); } - cfg.add( - &mut vartab, - Instr::Call { - res: vec![], - return_tys: vec![], - args: vec![], - call: InternalCallTy::Static { cfg_no }, - }, - ); - - cfg.add( - &mut vartab, - Instr::ReturnCode { - code: ReturnCode::Success, - }, - ); + if all_cfg[cfg_no].params.len() == 1 { + let arg = Expression::FromBufferPointer { + loc: Loc::Codegen, + ty: Type::DynamicBytes, + ptr: argsdata.into(), + size: argslen.into(), + }; + + let var_no = vartab.temp_name("fallback_return_data", &Type::DynamicBytes); + + cfg.add( + &mut vartab, + Instr::Call { + res: vec![var_no], + return_tys: vec![Type::DynamicBytes], + call: InternalCallTy::Static { cfg_no }, + args: vec![arg], + }, + ); + + let data = Expression::Variable { + loc: Loc::Codegen, + ty: Type::DynamicBytes, + var_no, + }; + let data_len = Expression::Builtin { + loc: Loc::Codegen, + tys: vec![Type::Uint(32)], + kind: Builtin::ArrayLength, + args: vec![data.clone()], + }; + + cfg.add(&mut vartab, Instr::ReturnData { data, data_len }); + } else { + cfg.add( + &mut vartab, + Instr::Call { + res: vec![], + return_tys: vec![], + args: vec![], + call: InternalCallTy::Static { cfg_no }, + }, + ); + + cfg.add( + &mut vartab, + Instr::ReturnCode { + code: ReturnCode::Success, + }, + ); + } } None => { cfg.add( diff --git a/src/codegen/mod.rs b/src/codegen/mod.rs index 7c91e2a73..90eb04f6b 100644 --- a/src/codegen/mod.rs +++ b/src/codegen/mod.rs @@ -398,6 +398,12 @@ pub enum Expression { size: Box, initializer: Option>, }, + FromBufferPointer { + loc: pt::Loc, + ty: Type, + ptr: Box, + size: Box, + }, ArrayLiteral { loc: pt::Loc, ty: Type, @@ -708,6 +714,7 @@ impl CodeLocation for Expression { | Expression::ShiftLeft { loc, .. } | Expression::RationalNumberLiteral { loc, .. } | Expression::AllocDynamicBytes { loc, .. } + | Expression::FromBufferPointer { loc, .. } | Expression::BytesCast { loc, .. } | Expression::More { loc, .. } | Expression::ZeroExt { loc, .. } => *loc, @@ -850,6 +857,7 @@ impl RetrieveType for Expression { | Expression::StructMember { ty, .. } | Expression::FunctionArg { ty, .. } | Expression::AllocDynamicBytes { ty, .. } + | Expression::FromBufferPointer { ty, .. } | Expression::BytesCast { ty, .. } | Expression::RationalNumberLiteral { ty, .. } | Expression::Subscript { ty, .. } diff --git a/src/emit/expression.rs b/src/emit/expression.rs index 67e173151..8d27dc6b0 100644 --- a/src/emit/expression.rs +++ b/src/emit/expression.rs @@ -1428,6 +1428,26 @@ pub(super) fn expression<'a, T: TargetRuntime<'a> + ?Sized>( bin.vector_new(size, elem_size, initializer.as_ref()).into() } } + Expression::FromBufferPointer { ptr, size, .. } => { + // TODO: convert to slice if possible (requires analysis to see if this is possible) + let ptr = expression(target, bin, ptr, vartab, function, ns).into_pointer_value(); + let elem_size = i32_const!(1); + let size = bin.builder.build_int_truncate( + expression(target, bin, size, vartab, function, ns).into_int_value(), + bin.context.i32_type(), + "", + ); + + bin.builder + .build_call( + bin.module.get_function("vector_new").unwrap(), + &[size.into(), elem_size.into(), ptr.into()], + "", + ) + .try_as_basic_value() + .left() + .unwrap() + } Expression::Builtin { kind: Builtin::ArrayLength, args, diff --git a/src/emit/solana/target.rs b/src/emit/solana/target.rs index 5ce153a7b..5de3e2764 100644 --- a/src/emit/solana/target.rs +++ b/src/emit/solana/target.rs @@ -2003,7 +2003,17 @@ impl<'a> TargetRuntime<'a> for SolanaTarget { ) { binary.builder.build_call( binary.module.get_function("sol_set_return_data").unwrap(), - &[data.into(), data_len.into()], + &[ + data.into(), + binary + .builder + .build_int_z_extend( + data_len.into_int_value(), + binary.context.i64_type(), + "length", + ) + .into(), + ], "", ); diff --git a/src/sema/functions.rs b/src/sema/functions.rs index 791bd76cf..bf422763f 100644 --- a/src/sema/functions.rs +++ b/src/sema/functions.rs @@ -36,13 +36,18 @@ pub fn contract_function( match func.ty { pt::FunctionTy::Function => { // Function name cannot be the same as the contract name - if let Some(n) = &func.name { - if n.name == ns.contracts[contract_no].id.name { + if let Some(id) = &func.name { + if id.name == ns.contracts[contract_no].id.name { ns.diagnostics.push(Diagnostic::error( func.loc_prototype, "function cannot have same name as the contract".to_string(), )); return None; + } else if id.name == "fallback" || id.name == "receive" { + ns.diagnostics.push(Diagnostic::warning( + func.loc_prototype, + format!("function named {} is not the {} function of the contract. Remove the function keyword to define the {} function", id.name, id.name, id.name), + )); } } else { ns.diagnostics.push(Diagnostic::error( @@ -70,20 +75,6 @@ pub fn contract_function( } } pt::FunctionTy::Fallback | pt::FunctionTy::Receive => { - if !func.returns.is_empty() { - ns.diagnostics.push(Diagnostic::error( - func.loc_prototype, - format!("{} function cannot have return values", func.ty), - )); - success = false; - } - if !func.params.is_empty() { - ns.diagnostics.push(Diagnostic::error( - func.loc_prototype, - format!("{} function cannot have parameters", func.ty), - )); - success = false; - } if func.name.is_some() { ns.diagnostics.push(Diagnostic::error( func.loc_prototype, @@ -326,6 +317,45 @@ pub fn contract_function( &mut diagnostics, ); + if params_success && returns_success { + match func.ty { + pt::FunctionTy::Receive => { + if !returns.is_empty() { + diagnostics.push(Diagnostic::error( + func.loc_prototype, + format!("{} function cannot have return values", func.ty), + )); + success = false; + } + if !params.is_empty() { + diagnostics.push(Diagnostic::error( + func.loc_prototype, + format!("{} function cannot have parameters", func.ty), + )); + success = false; + } + } + pt::FunctionTy::Fallback => { + if returns.is_empty() && params.is_empty() { + // fallback is allowed to have no parameters or returns + } else if returns.len() != 1 + || params.len() != 1 + || returns[0].ty != Type::DynamicBytes + || params[0].ty != Type::DynamicBytes + { + diagnostics.push(Diagnostic::error( + func.loc_prototype, + format!( + "{} can be fallback(bytes calldata) returns (bytes memory) or fallback()", + func.ty + ), + )); + } + } + _ => (), + } + } + ns.diagnostics.extend(diagnostics); if ns.contracts[contract_no].is_interface() { @@ -592,15 +622,7 @@ pub fn contract_function( return None; } - if fdecl.is_payable() { - if func.ty == pt::FunctionTy::Fallback { - ns.diagnostics.push(Diagnostic::error( - func.loc_prototype, - format!("{} function must not be declare payable, use 'receive() external payable' instead", func.ty), - )); - return None; - } - } else if func.ty == pt::FunctionTy::Receive { + if !fdecl.is_payable() && func.ty == pt::FunctionTy::Receive { ns.diagnostics.push(Diagnostic::error( func.loc_prototype, format!("{} function must be declared payable", func.ty), @@ -773,7 +795,16 @@ pub fn function( } let name = match &func.name { - Some(s) => s.to_owned(), + Some(id) => { + if id.name == "fallback" || id.name == "receive" { + ns.diagnostics.push(Diagnostic::warning( + func.loc_prototype, + format!("function named {} is not the {} function of the contract. Remove the function keyword to define the {} function", id.name, id.name, id.name), + )); + } + + id.to_owned() + } None => { ns.diagnostics.push(Diagnostic::error( func.loc_prototype, diff --git a/tests/contract_testcases/evm/fallback.sol b/tests/contract_testcases/evm/fallback.sol new file mode 100644 index 000000000..b34a3c152 --- /dev/null +++ b/tests/contract_testcases/evm/fallback.sol @@ -0,0 +1,28 @@ + +function fallback() pure returns (int) { + return 1; +} + +function receive(int a) pure returns (int) { + return 2; +} + +contract C { + function fallback() pure public {} + function receive() pure public {} + + fallback() external returns (int) {} + receive(int) external {} +} + +// ---- Expect: diagnostics ---- +// warning: 2:1-39: function named fallback is not the fallback function of the contract. Remove the function keyword to define the fallback function +// warning: 6:1-43: function named receive is not the receive function of the contract. Remove the function keyword to define the receive function +// warning: 11:2-33: function named fallback is not the fallback function of the contract. Remove the function keyword to define the fallback function +// warning: 11:11-19: fallback is already defined as a function +// note 2:10-18: location of previous definition +// warning: 12:2-32: function named receive is not the receive function of the contract. Remove the function keyword to define the receive function +// warning: 12:11-18: receive is already defined as a function +// note 6:10-17: location of previous definition +// error: 14:2-35: fallback can be fallback(bytes calldata) returns (bytes memory) or fallback() +// error: 15:2-23: receive function cannot have parameters diff --git a/tests/contract_testcases/polkadot/builtins/abi_decode_01.sol b/tests/contract_testcases/polkadot/builtins/abi_decode_01.sol index 358e6c77c..6021c8913 100644 --- a/tests/contract_testcases/polkadot/builtins/abi_decode_01.sol +++ b/tests/contract_testcases/polkadot/builtins/abi_decode_01.sol @@ -4,5 +4,6 @@ (int a) = abi.decode(hex"00", (int storage)); } } + // ---- Expect: diagnostics ---- -// error: 4:59-60: unrecognised token ')', expected "case", "default", "leave", "revert", "switch", identifier +// error: 4:59-60: unrecognised token ')', expected "case", "default", "fallback", "leave", "receive", "revert", "switch", identifier diff --git a/tests/contract_testcases/polkadot/calls/payable_functions_02.sol b/tests/contract_testcases/polkadot/calls/payable_functions_02.sol index 5b7ca4a8a..1424ddf80 100644 --- a/tests/contract_testcases/polkadot/calls/payable_functions_02.sol +++ b/tests/contract_testcases/polkadot/calls/payable_functions_02.sol @@ -6,4 +6,3 @@ } // ---- Expect: diagnostics ---- -// error: 3:13-40: fallback function must not be declare payable, use 'receive() external payable' instead diff --git a/tests/contract_testcases/polkadot/functions/payable_03.sol b/tests/contract_testcases/polkadot/functions/payable_03.sol index 5f3c08cc8..5513edcd1 100644 --- a/tests/contract_testcases/polkadot/functions/payable_03.sol +++ b/tests/contract_testcases/polkadot/functions/payable_03.sol @@ -8,5 +8,6 @@ i = 2; } } + // ---- Expect: diagnostics ---- -// error: 7:13-40: fallback function must not be declare payable, use 'receive() external payable' instead +// warning: 5:13-24: storage variable 'i' has been assigned, but never read diff --git a/tests/contract_testcases/solana/annotations/at_whitespace.sol b/tests/contract_testcases/solana/annotations/at_whitespace.sol index d363ac8bf..eb0dc4af6 100644 --- a/tests/contract_testcases/solana/annotations/at_whitespace.sol +++ b/tests/contract_testcases/solana/annotations/at_whitespace.sol @@ -13,4 +13,4 @@ contract Seed1 { // ---- Expect: diagnostics ---- // error: 5:17-18: unrecognised token '@' -// error: 5:24-29: unrecognised token 'bytes', expected "(", ")", "++", ",", "--", ".", "[", "calldata", "case", "default", "leave", "memory", "revert", "storage", "switch", "{", identifier +// error: 5:24-29: unrecognised token 'bytes', expected "(", ")", "++", ",", "--", ".", "[", "calldata", "case", "default", "fallback", "leave", "memory", "receive", "revert", "storage", "switch", "{", identifier diff --git a/tests/evm.rs b/tests/evm.rs index ae50eb3d4..5562f7932 100644 --- a/tests/evm.rs +++ b/tests/evm.rs @@ -255,7 +255,7 @@ fn ethereum_solidity_tests() { }) .sum(); - assert_eq!(errors, 909); + assert_eq!(errors, 888); } fn set_file_contents(source: &str, path: &Path) -> (FileResolver, Vec) { diff --git a/tests/polkadot_tests/functions.rs b/tests/polkadot_tests/functions.rs index e46ffbc07..b8dfa6007 100644 --- a/tests/polkadot_tests/functions.rs +++ b/tests/polkadot_tests/functions.rs @@ -158,6 +158,37 @@ fn fallback() { assert_eq!(runtime.output(), Val(356).encode()); } +#[test] +fn fallback_with_args() { + #[derive(Debug, PartialEq, Eq, Encode, Decode)] + struct Val(u64); + + // parse + let mut runtime = build_solidity( + " + contract test { + int64 result = 102; + + function get() public returns (int64) { + return result; + } + + fallback(bytes foo) external returns (bytes) { + print(string(foo)); + result = 356; + return bytes.concat(foo, ' to me'); + } + }", + ); + + runtime.raw_function(b"to you".to_vec()); + println!("go: {}", String::from_utf8_lossy(&runtime.output())); + assert_eq!(runtime.output(), b"to you to me"); + runtime.function("get", Vec::new()); + + assert_eq!(runtime.output(), Val(356).encode()); +} + #[test] fn function_wrong_selector() { let mut runtime = build_solidity( @@ -607,7 +638,7 @@ fn virtual_function_member_access() { @selector([1, 2, 3, 4]) function onERC1155Received() external returns (bytes4); } - + abstract contract ERC1155 { function _doSafeTransferAcceptanceCheck() internal pure returns (bytes4) { return IERC1155Receiver.onERC1155Received.selector; diff --git a/tests/solana.rs b/tests/solana.rs index d89c20bbc..0c6da8449 100644 --- a/tests/solana.rs +++ b/tests/solana.rs @@ -1764,7 +1764,6 @@ impl<'a, 'b> VmFunction<'a, 'b> { assert_eq!(offset, return_data.len()); Ok(Some(decoded)) } else { - assert_eq!(return_data.len(), 0); Ok(None) } } diff --git a/tests/solana_tests/balance.rs b/tests/solana_tests/balance.rs index 65508c867..fa93c3ffa 100644 --- a/tests/solana_tests/balance.rs +++ b/tests/solana_tests/balance.rs @@ -37,3 +37,38 @@ fn fallback() { assert_eq!(vm.logs, "fallback"); } + +#[test] +fn fallback_with_args() { + let mut vm = build_solidity( + r#" + contract c { + fallback(bytes input) external returns (bytes) { + return "secret"; + } + }"#, + ); + + let data_account = vm.initialize_data_account(); + vm.function("new") + .accounts(vec![("dataAccount", data_account)]) + .call(); + + if let Some(idl) = &vm.stack[0].idl { + let mut idl = idl.clone(); + + idl.instructions.push(IdlInstruction { + name: "extinct".to_string(), + docs: None, + accounts: vec![], + args: vec![], + returns: None, + }); + + vm.stack[0].idl = Some(idl); + } + + vm.function("extinct").call(); + + assert_eq!(vm.return_data.unwrap().1, b"secret"); +}