From 66a95acd15bb21aaae58f68479720081c680bb1a Mon Sep 17 00:00:00 2001 From: MOZGIII Date: Fri, 2 Oct 2026 17:45:42 +0400 Subject: [PATCH 1/2] Enforce function modifiers in the stateful precompiles The EVM does not stop a precompile from writing Substrate storage or emitting logs inside a STATICCALL, so the state-changing functions of the native currency and swap precompiles must reject the static context themselves. Non-payable functions also now reject attached value, which previously stayed stuck at the precompile address. --- .../precompile-evm-to-native-swap/Cargo.toml | 1 + .../precompile-evm-to-native-swap/src/lib.rs | 5 +- .../src/tests.rs | 22 ++- crates/precompile-native-currency/src/lib.rs | 23 ++- .../precompile-native-currency/src/tests.rs | 145 ++++++++++++++++++ crates/precompile-utils/src/testing.rs | 5 + 6 files changed, 193 insertions(+), 8 deletions(-) diff --git a/crates/precompile-evm-to-native-swap/Cargo.toml b/crates/precompile-evm-to-native-swap/Cargo.toml index 8d3d75254..ae8a752d3 100644 --- a/crates/precompile-evm-to-native-swap/Cargo.toml +++ b/crates/precompile-evm-to-native-swap/Cargo.toml @@ -18,6 +18,7 @@ sp-core = { workspace = true } [dev-dependencies] pallet-evm-balances = { path = "../pallet-evm-balances", features = ["default"] } pallet-evm-system = { path = "../pallet-evm-system", features = ["default"] } +precompile-utils = { path = "../precompile-utils", features = ["testing"] } frame-system = { workspace = true } hex-literal = { workspace = true } diff --git a/crates/precompile-evm-to-native-swap/src/lib.rs b/crates/precompile-evm-to-native-swap/src/lib.rs index d9417ff39..48e510389 100644 --- a/crates/precompile-evm-to-native-swap/src/lib.rs +++ b/crates/precompile-evm-to-native-swap/src/lib.rs @@ -15,7 +15,8 @@ use pallet_evm::{ ExitError, Precompile, PrecompileFailure, PrecompileHandle, PrecompileOutput, PrecompileResult, }; use precompile_utils::{ - keccak256, succeed, EvmDataWriter, EvmResult, LogExt, LogsBuilder, PrecompileHandleExt, + keccak256, succeed, EvmDataWriter, EvmResult, FunctionModifier, LogExt, LogsBuilder, + PrecompileHandleExt, }; use sp_core::{Get, H160, H256, U256}; @@ -114,6 +115,8 @@ where { /// Swap EVM tokens to native chain tokens. fn swap(handle: &mut impl PrecompileHandle) -> EvmResult { + handle.check_function_modifier(FunctionModifier::Payable)?; + let mut input = handle.read_input()?; let fp_evm::Context { diff --git a/crates/precompile-evm-to-native-swap/src/tests.rs b/crates/precompile-evm-to-native-swap/src/tests.rs index a33ff1d76..24329e022 100644 --- a/crates/precompile-evm-to-native-swap/src/tests.rs +++ b/crates/precompile-evm-to-native-swap/src/tests.rs @@ -4,7 +4,7 @@ use fp_evm::{ExitError, ExitReason}; use frame_support::{assert_noop, traits::fungible::Unbalanced}; use pallet_evm::Runner; -use precompile_utils::{EvmDataWriter, LogsBuilder}; +use precompile_utils::{testing::*, EvmDataWriter, LogsBuilder}; use sp_core::H256; use crate::{mock::*, *}; @@ -460,3 +460,23 @@ fn runner_fail_value_overflow() { ); }); } + +/// This test verifies that the swap reverts in the static context (i.e. under `STATICCALL`), +/// as the EVM itself does not prevent the precompile from writing to the Substrate storage. +#[test] +fn swap_fail_static_context() { + new_test_ext().execute_with_ext(|_| { + let swap_action = EvmDataWriter::new_with_selector(Action::Swap) + .write(H256::from(target_swap_native_account().as_ref())) + .build(); + + PrecompilesValue::get() + .prepare_test(source_swap_evm_account(), *PRECOMPILE_ADDRESS, swap_action) + .with_static_call(true) + .expect_cost(200) + .expect_no_logs() + .execute_reverts(|output| { + output == b"can't call non-static function in static context" + }); + }); +} diff --git a/crates/precompile-native-currency/src/lib.rs b/crates/precompile-native-currency/src/lib.rs index ef998901c..bf75c9eb5 100644 --- a/crates/precompile-native-currency/src/lib.rs +++ b/crates/precompile-native-currency/src/lib.rs @@ -13,8 +13,8 @@ use pallet_evm::{ PrecompileResult, }; use precompile_utils::{ - keccak256, succeed, Address, Bytes, EvmData, EvmDataReader, EvmDataWriter, EvmResult, LogExt, - LogsBuilder, PrecompileHandleExt, + keccak256, succeed, Address, Bytes, EvmData, EvmDataReader, EvmDataWriter, EvmResult, + FunctionModifier, LogExt, LogsBuilder, PrecompileHandleExt, }; use sp_core::{Get, H160, U256}; @@ -123,28 +123,32 @@ where GasCost: Get, { /// Returns the name of the token. - fn name(_handle: &mut impl PrecompileHandle) -> EvmResult { + fn name(handle: &mut impl PrecompileHandle) -> EvmResult { + handle.check_function_modifier(FunctionModifier::View)?; let name: Bytes = Erc20SupportT::Metadata::name().into(); Ok(succeed(EvmDataWriter::new().write(name).build())) } /// Returns the symbol of the token. - fn symbol(_handle: &mut impl PrecompileHandle) -> EvmResult { + fn symbol(handle: &mut impl PrecompileHandle) -> EvmResult { + handle.check_function_modifier(FunctionModifier::View)?; let symbol: Bytes = Erc20SupportT::Metadata::symbol().into(); Ok(succeed(EvmDataWriter::new().write(symbol).build())) } /// Returns the decimals places of the token. - fn decimals(_handle: &mut impl PrecompileHandle) -> EvmResult { + fn decimals(handle: &mut impl PrecompileHandle) -> EvmResult { + handle.check_function_modifier(FunctionModifier::View)?; let decimals: u8 = Erc20SupportT::Metadata::decimals(); Ok(succeed(EvmDataWriter::new().write(decimals).build())) } /// Returns the amount of tokens in existence. - fn total_supply(_handle: &mut impl PrecompileHandle) -> EvmResult { + fn total_supply(handle: &mut impl PrecompileHandle) -> EvmResult { + handle.check_function_modifier(FunctionModifier::View)?; let total_supply: U256 = pallet_erc20_support::Pallet::::total_supply().into(); @@ -153,6 +157,7 @@ where /// Returns the amount of tokens owned by provided account. fn balance_of(handle: &mut impl PrecompileHandle) -> EvmResult { + handle.check_function_modifier(FunctionModifier::View)?; let mut input = handle.read_input()?; check_input(&mut input, 1)?; @@ -170,6 +175,7 @@ where /// Returns the remaining number of tokens that spender will be allowed to spend on behalf of /// owner through transferFrom. This is zero by default. fn allowance(handle: &mut impl PrecompileHandle) -> EvmResult { + handle.check_function_modifier(FunctionModifier::View)?; let mut input = handle.read_input()?; check_input(&mut input, 2)?; @@ -194,6 +200,7 @@ where /// Sets amount as the allowance of spender over the caller’s tokens. fn approve(handle: &mut impl PrecompileHandle) -> EvmResult { + handle.check_function_modifier(FunctionModifier::NonPayable)?; handle.record_cost(GasCost::get())?; let mut input = handle.read_input()?; @@ -233,6 +240,7 @@ where /// Moves amount tokens from the caller’s account to recipient. fn transfer(handle: &mut impl PrecompileHandle) -> EvmResult { + handle.check_function_modifier(FunctionModifier::NonPayable)?; handle.record_cost(GasCost::get())?; let mut input = handle.read_input()?; @@ -274,6 +282,7 @@ where /// Moves amount tokens from sender to recipient using the allowance mechanism, /// amount is then deducted from the caller’s allowance. fn transfer_from(handle: &mut impl PrecompileHandle) -> EvmResult { + handle.check_function_modifier(FunctionModifier::NonPayable)?; handle.record_cost(GasCost::get())?; let mut input = handle.read_input()?; @@ -319,6 +328,7 @@ where /// Simulate deposit logic as IWETH-like contract. /// Returns funds to sender as this precompile tokens and the native tokens are the same. fn deposit(handle: &mut impl PrecompileHandle) -> EvmResult { + handle.check_function_modifier(FunctionModifier::Payable)?; handle.record_cost(GasCost::get())?; let mut input = handle.read_input()?; @@ -365,6 +375,7 @@ where /// Simulate withdraw logic as IWETH-like contract. /// Do nothing. fn withdraw(handle: &mut impl PrecompileHandle) -> EvmResult { + handle.check_function_modifier(FunctionModifier::NonPayable)?; handle.record_cost(GasCost::get())?; let mut input = handle.read_input()?; diff --git a/crates/precompile-native-currency/src/tests.rs b/crates/precompile-native-currency/src/tests.rs index ea166a43a..9bbb6aba1 100644 --- a/crates/precompile-native-currency/src/tests.rs +++ b/crates/precompile-native-currency/src/tests.rs @@ -587,3 +587,148 @@ fn withdraw_fails_more_than_allowed() { ); }); } + +/// The static context must not block the view functions. +#[test] +fn view_functions_work_in_static_context() { + new_test_ext().execute_with_ext(|_| { + let alice_evm = H160::from(hex_literal::hex!( + "1000000000000000000000000000000000000001" + )); + let alice_evm_balance = 100 * 10u128.pow(18); + + // Prepare the test state. + EvmBalances::make_free_balance_be(&alice_evm, alice_evm_balance); + + let balance_of_action = EvmDataWriter::new_with_selector(Action::BalanceOf) + .write(Address::from(alice_evm)) + .build(); + + precompiles() + .prepare_test(alice_evm, *PRECOMPILE_ADDRESS, balance_of_action) + .with_static_call(true) + .expect_cost(0) + .expect_no_logs() + .execute_returns( + EvmDataWriter::new() + .write(U256::from(alice_evm_balance)) + .build(), + ); + }); +} + +/// The state changing functions must revert in the static context (i.e. under `STATICCALL`), +/// as the EVM itself does not prevent the precompile from writing to the Substrate storage. +#[test] +fn state_changing_functions_fail_in_static_context() { + new_test_ext().execute_with_ext(|_| { + let alice_evm = H160::from(hex_literal::hex!( + "1000000000000000000000000000000000000001" + )); + let alice_evm_balance = 100 * 10u128.pow(18); + let bob_evm = H160::from(hex_literal::hex!( + "7000000000000000000000000000000000000007" + )); + let amount = 10 * 10u128.pow(18); + + // Prepare the test state. + EvmBalances::make_free_balance_be(&alice_evm, alice_evm_balance); + + let actions = [ + EvmDataWriter::new_with_selector(Action::Approve) + .write(Address::from(bob_evm)) + .write(U256::from(amount)) + .build(), + EvmDataWriter::new_with_selector(Action::Transfer) + .write(Address::from(bob_evm)) + .write(U256::from(amount)) + .build(), + EvmDataWriter::new_with_selector(Action::TransferFrom) + .write(Address::from(alice_evm)) + .write(Address::from(bob_evm)) + .write(U256::from(amount)) + .build(), + EvmDataWriter::new_with_selector(Action::Deposit).build(), + EvmDataWriter::new_with_selector(Action::Withdraw) + .write(U256::from(amount)) + .build(), + ]; + + for action in actions { + precompiles() + .prepare_test(alice_evm, *PRECOMPILE_ADDRESS, action) + .with_static_call(true) + .expect_cost(0) + .expect_no_logs() + .execute_reverts(|output| { + output == b"can't call non-static function in static context" + }); + } + + // Assert state changes. + assert_eq!(EvmBalances::total_balance(&alice_evm), alice_evm_balance); + assert_eq!(EvmBalances::total_balance(&bob_evm), 0); + }); +} + +/// The non-payable functions must revert when value is attached, as otherwise the value would be +/// left stuck at the precompile address. +#[test] +fn non_payable_functions_fail_with_value() { + new_test_ext().execute_with_ext(|_| { + let alice_evm = H160::from(hex_literal::hex!( + "1000000000000000000000000000000000000001" + )); + let alice_evm_balance = 100 * 10u128.pow(18); + let bob_evm = H160::from(hex_literal::hex!( + "7000000000000000000000000000000000000007" + )); + let amount = 10 * 10u128.pow(18); + + // Prepare the test state. + EvmBalances::make_free_balance_be(&alice_evm, alice_evm_balance); + + let actions = [ + EvmDataWriter::new_with_selector(Action::Name).build(), + EvmDataWriter::new_with_selector(Action::Symbol).build(), + EvmDataWriter::new_with_selector(Action::Decimals).build(), + EvmDataWriter::new_with_selector(Action::TotalSupply).build(), + EvmDataWriter::new_with_selector(Action::BalanceOf) + .write(Address::from(alice_evm)) + .build(), + EvmDataWriter::new_with_selector(Action::Allowance) + .write(Address::from(alice_evm)) + .write(Address::from(bob_evm)) + .build(), + EvmDataWriter::new_with_selector(Action::Approve) + .write(Address::from(bob_evm)) + .write(U256::from(amount)) + .build(), + EvmDataWriter::new_with_selector(Action::Transfer) + .write(Address::from(bob_evm)) + .write(U256::from(amount)) + .build(), + EvmDataWriter::new_with_selector(Action::TransferFrom) + .write(Address::from(alice_evm)) + .write(Address::from(bob_evm)) + .write(U256::from(amount)) + .build(), + EvmDataWriter::new_with_selector(Action::Withdraw) + .write(U256::from(amount)) + .build(), + ]; + + for action in actions { + precompiles() + .prepare_test(alice_evm, *PRECOMPILE_ADDRESS, action) + .with_value(1) + .expect_cost(0) + .expect_no_logs() + .execute_reverts(|output| output == b"function is not payable"); + } + + // Assert state changes. + assert_eq!(EvmBalances::total_balance(&alice_evm), alice_evm_balance); + assert_eq!(EvmBalances::total_balance(&bob_evm), 0); + }); +} diff --git a/crates/precompile-utils/src/testing.rs b/crates/precompile-utils/src/testing.rs index 520985463..e160afafd 100644 --- a/crates/precompile-utils/src/testing.rs +++ b/crates/precompile-utils/src/testing.rs @@ -231,6 +231,11 @@ impl<'p, P: PrecompileSet> PrecompilesTester<'p, P> { self } + pub fn with_static_call(mut self, is_static: bool) -> Self { + self.handle.is_static = is_static; + self + } + pub fn with_subcall_handle(mut self, subcall_handle: impl SubcallTrait) -> Self { self.subcall_handle = Some(Box::new(subcall_handle)); self From 71709be7e4ea948f1fcf28d10329755ed1655889 Mon Sep 17 00:00:00 2001 From: MOZGIII Date: Fri, 2 Oct 2026 18:08:30 +0400 Subject: [PATCH 2/2] Cover CALLCODE and STATICCALL in the precompile runtime tests Generate the forwarder contract per call kind instead of hard-coding the DELEGATECALL variant, and assert that CALLCODE is rejected by the router guard while STATICCALL is rejected only for state-changing functions. --- .../src/tests/frontier_precompiles.rs | 258 +++++++++++++----- 1 file changed, 195 insertions(+), 63 deletions(-) diff --git a/crates/humanode-runtime/src/tests/frontier_precompiles.rs b/crates/humanode-runtime/src/tests/frontier_precompiles.rs index 0ed702e60..db3e1e796 100644 --- a/crates/humanode-runtime/src/tests/frontier_precompiles.rs +++ b/crates/humanode-runtime/src/tests/frontier_precompiles.rs @@ -17,49 +17,87 @@ static GAS_PRICE: Lazy = const INIT_BALANCE: Balance = 10u128.pow(18 + 6); -/// The address at which the forwarder contract is deployed at genesis. -static FORWARDER_ADDRESS: Lazy = Lazy::new(|| H160::from_low_u64_be(0xf0f0f0)); +/// The kind of call the forwarder contract issues to its target. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum ForwardKind { + DelegateCall, + CallCode, + StaticCall, +} + +/// All the forwarder kinds. +const FORWARD_KINDS: [ForwardKind; 3] = [ + ForwardKind::DelegateCall, + ForwardKind::CallCode, + ForwardKind::StaticCall, +]; + +/// The address at which the forwarder contract of the given kind is deployed at genesis. +fn forwarder_address(kind: ForwardKind) -> H160 { + H160::from_low_u64_be(0xf0f0f0 + kind as u64) +} -/// A minimal contract that `DELEGATECALL`s the target with the provided input and bubbles up -/// the result (return data on success, revert data on failure). +/// A minimal contract that forwards the provided input to the target via the given call kind +/// and bubbles up the result (return data on success, revert data on failure). /// /// Calldata layout: `abi.encode(target)` (32 bytes, address right-aligned) followed by the raw /// input to forward. -const DELEGATECALL_FORWARDER_CODE: &[u8] = &[ - // size = CALLDATASIZE - 32 - 0x60, 0x20, // PUSH1 0x20 - 0x36, // CALLDATASIZE - 0x03, // SUB - // CALLDATACOPY(dest = 0, offset = 32, size) - 0x80, // DUP1 - 0x60, 0x20, // PUSH1 0x20 - 0x60, 0x00, // PUSH1 0x00 - 0x37, // CALLDATACOPY - // DELEGATECALL(gas, target, args_offset = 0, args_size = size, ret_offset = 0, ret_size = 0) - 0x60, 0x00, // PUSH1 0x00 - 0x60, 0x00, // PUSH1 0x00 - 0x82, // DUP3 - 0x60, 0x00, // PUSH1 0x00 - 0x60, 0x00, // PUSH1 0x00 - 0x35, // CALLDATALOAD - 0x5a, // GAS - 0xf4, // DELEGATECALL - // RETURNDATACOPY(dest = 0, offset = 0, size = RETURNDATASIZE) - 0x3d, // RETURNDATASIZE - 0x60, 0x00, // PUSH1 0x00 - 0x60, 0x00, // PUSH1 0x00 - 0x3e, // RETURNDATACOPY +fn forwarder_code(kind: ForwardKind) -> Vec { + let mut code = vec![ + // size = CALLDATASIZE - 32 + 0x60, 0x20, // PUSH1 0x20 + 0x36, // CALLDATASIZE + 0x03, // SUB + // CALLDATACOPY(dest = 0, offset = 32, size) + 0x80, // DUP1 + 0x60, 0x20, // PUSH1 0x20 + 0x60, 0x00, // PUSH1 0x00 + 0x37, // CALLDATACOPY + // Call arguments, pushed in reverse: ret_size = 0, ret_offset = 0, args_size = size, + // args_offset = 0. + 0x60, 0x00, // PUSH1 0x00 + 0x60, 0x00, // PUSH1 0x00 + 0x82, // DUP3 + 0x60, 0x00, // PUSH1 0x00 + ]; + if kind == ForwardKind::CallCode { + // CALLCODE additionally takes a value, which we set to zero. + code.extend([0x60, 0x00]); // PUSH1 0x00 + } + code.extend([ + // target = CALLDATALOAD(0), gas = GAS + 0x60, + 0x00, // PUSH1 0x00 + 0x35, // CALLDATALOAD + 0x5a, // GAS + match kind { + ForwardKind::DelegateCall => 0xf4, // DELEGATECALL + ForwardKind::CallCode => 0xf2, // CALLCODE + ForwardKind::StaticCall => 0xfa, // STATICCALL + }, + // RETURNDATACOPY(dest = 0, offset = 0, size = RETURNDATASIZE) + 0x3d, // RETURNDATASIZE + 0x60, + 0x00, // PUSH1 0x00 + 0x60, + 0x00, // PUSH1 0x00 + 0x3e, // RETURNDATACOPY + ]); // if !success { REVERT(0, RETURNDATASIZE) } else { RETURN(0, RETURNDATASIZE) } - 0x3d, // RETURNDATASIZE - 0x60, 0x00, // PUSH1 0x00 - 0x82, // DUP3 - 0x15, // ISZERO - 0x60, 0x25, // PUSH1 0x25 - 0x57, // JUMPI - 0xf3, // RETURN - 0x5b, // JUMPDEST (0x25) - 0xfd, // REVERT -]; + let jumpdest = u8::try_from(code.len() + 9).unwrap(); + code.extend([ + 0x3d, // RETURNDATASIZE + 0x60, 0x00, // PUSH1 0x00 + 0x82, // DUP3 + 0x15, // ISZERO + 0x60, jumpdest, // PUSH1 jumpdest + 0x57, // JUMPI + 0xf3, // RETURN + 0x5b, // JUMPDEST + 0xfd, // REVERT + ]); + code +} /// The addresses of the custom Humanode precompiles. const HUMANODE_PRECOMPILES: [u64; 4] = [ @@ -137,27 +175,27 @@ fn new_test_ext_with() -> sp_io::TestExternalities { evm_endowed_accounts .into_iter() .map(|k| (k, init_genesis_account.clone())) - .chain([ - ( - EvmToNativeSwapBridgePot::account_id(), - fp_evm::GenesisAccount { - balance: >::minimum_balance() - .into(), - code: Default::default(), - nonce: Default::default(), - storage: Default::default(), - }, - ), + .chain([( + EvmToNativeSwapBridgePot::account_id(), + fp_evm::GenesisAccount { + balance: >::minimum_balance() + .into(), + code: Default::default(), + nonce: Default::default(), + storage: Default::default(), + }, + )]) + .chain(FORWARD_KINDS.into_iter().map(|kind| { ( - *FORWARDER_ADDRESS, + forwarder_address(kind), fp_evm::GenesisAccount { balance: INIT_BALANCE.into(), - code: DELEGATECALL_FORWARDER_CODE.to_vec(), + code: forwarder_code(kind), nonce: Default::default(), storage: Default::default(), }, - ), - ]) + ) + })) .collect() }, }, @@ -190,11 +228,31 @@ fn evm_call(to: H160, data: Vec, value: U256) -> fp_evm::CallInfo { .unwrap() } -/// Run `input` against `target` via a `DELEGATECALL` issued by the forwarder contract. -fn delegate_call(target: H160, input: &[u8]) -> fp_evm::CallInfo { +/// Run `input` against `target` via a call of the given kind issued by the forwarder contract. +fn forward_call(kind: ForwardKind, target: H160, input: &[u8]) -> fp_evm::CallInfo { let mut data = EvmDataWriter::new().write(Address(target)).build(); data.extend_from_slice(input); - evm_call(*FORWARDER_ADDRESS, data, U256::zero()) + evm_call(forwarder_address(kind), data, U256::zero()) +} + +/// Run `input` against `target` via a `DELEGATECALL` issued by the forwarder contract. +fn delegate_call(target: H160, input: &[u8]) -> fp_evm::CallInfo { + forward_call(ForwardKind::DelegateCall, target, input) +} + +/// A sample state-changing input for the custom Humanode precompiles that have one. +fn humanode_precompile_state_changing_input(precompile: u64) -> Option> { + match precompile { + NATIVE_CURRENCY => Some( + EvmDataWriter::new_with_selector(precompile_native_currency::Action::Transfer) + .write(Address(evm_account_id("EvmBob"))) + .write(U256::from(1)) + .build(), + ), + EVM_TO_NATIVE_SWAP => Some(humanode_precompile_input(precompile)), + BIOAUTH | EVM_ACCOUNTS_MAPPING => None, + _ => unreachable!("not a Humanode precompile: {precompile:#x}"), + } } /// A sample valid input for each of the custom Humanode precompiles. @@ -218,26 +276,100 @@ fn humanode_precompile_input(precompile: u64) -> Vec { } } -/// This test verifies that a `DELEGATECALL` to each custom Humanode precompile reverts. +/// This test verifies that a `DELEGATECALL` or a `CALLCODE` to each custom Humanode precompile +/// reverts. +#[test] +fn humanode_precompiles_reject_delegate_call_and_call_code() { + // Build the state from the config. + new_test_ext_with().execute_with(move || { + for kind in [ForwardKind::DelegateCall, ForwardKind::CallCode] { + for precompile in HUMANODE_PRECOMPILES { + let execinfo = forward_call( + kind, + hash(precompile), + &humanode_precompile_input(precompile), + ); + assert_eq!( + execinfo.exit_reason, + fp_evm::ExitReason::Revert(fp_evm::ExitRevert::Reverted), + "precompile {precompile:#x} did not revert on {kind:?}" + ); + assert_eq!( + execinfo.value, + b"cannot be called with DELEGATECALL or CALLCODE".to_vec(), + "precompile {precompile:#x} reverted with an unexpected message on {kind:?}" + ); + assert!( + execinfo.logs.is_empty(), + "precompile {precompile:#x} emitted logs on {kind:?}" + ); + } + } + }) +} + +/// This test verifies that a `STATICCALL` to a state-changing function of a custom Humanode +/// precompile reverts, and that no state changes take place. #[test] -fn humanode_precompiles_reject_delegate_call() { +fn humanode_precompiles_reject_static_call_for_state_changes() { // Build the state from the config. new_test_ext_with().execute_with(move || { + let forwarder = forwarder_address(ForwardKind::StaticCall); + let forwarder_balance_before = + >::total_balance(&forwarder); + let bob_balance_before = + >::total_balance(&evm_account_id("EvmBob")); + for precompile in HUMANODE_PRECOMPILES { - let execinfo = delegate_call(hash(precompile), &humanode_precompile_input(precompile)); + let Some(input) = humanode_precompile_state_changing_input(precompile) else { + continue; + }; + let execinfo = forward_call(ForwardKind::StaticCall, hash(precompile), &input); assert_eq!( execinfo.exit_reason, fp_evm::ExitReason::Revert(fp_evm::ExitRevert::Reverted), - "precompile {precompile:#x} did not revert on DELEGATECALL" + "precompile {precompile:#x} did not revert on STATICCALL" ); assert_eq!( execinfo.value, - b"cannot be called with DELEGATECALL or CALLCODE".to_vec(), - "precompile {precompile:#x} reverted with an unexpected message" + b"can't call non-static function in static context".to_vec(), + "precompile {precompile:#x} reverted with an unexpected message on STATICCALL" ); assert!( execinfo.logs.is_empty(), - "precompile {precompile:#x} emitted logs on DELEGATECALL" + "precompile {precompile:#x} emitted logs on STATICCALL" + ); + } + + // Assert state changes. + assert_eq!( + >::total_balance(&forwarder), + forwarder_balance_before + ); + assert_eq!( + >::total_balance(&evm_account_id("EvmBob")), + bob_balance_before + ); + }) +} + +/// This test verifies that a `STATICCALL` to a view function of a custom Humanode precompile +/// still works. +#[test] +fn humanode_precompiles_accept_static_call_for_views() { + // Build the state from the config. + new_test_ext_with().execute_with(move || { + for precompile in [BIOAUTH, EVM_ACCOUNTS_MAPPING, NATIVE_CURRENCY] { + let execinfo = forward_call( + ForwardKind::StaticCall, + hash(precompile), + &humanode_precompile_input(precompile), + ); + assert_eq!( + execinfo.exit_reason, + fp_evm::ExitReason::Succeed(fp_evm::ExitSucceed::Returned), + "precompile {precompile:#x} failed on STATICCALL: {:?}", + String::from_utf8_lossy(&execinfo.value) ); } })