diff --git a/prdoc/pr_9909.prdoc b/prdoc/pr_9909.prdoc new file mode 100644 index 0000000000000..8a6d6ae6b4b34 --- /dev/null +++ b/prdoc/pr_9909.prdoc @@ -0,0 +1,16 @@ +title: 'pallet-revive: add interface to implement mocks and pranks' +doc: +- audience: Node Dev + description: |- + Needed for: https://github.com/paritytech/foundry-polkadot/pull/334. + + In foundry-polkadot we need the ability to be able to manipulate the `msg.sender` and the `tx.origin` that a solidity contract sees cheatcode documentation, plus the ability to mock calls and functions. + + Currently all create/call methods use the `bare_instantiate`/`bare_call` to run things in pallet-revive, the caller then normally gets set automatically, based on what is the call stack, but for `forge test` we need to be able to manipulate, so that we can set it to custom values. + + Additionally, for delegate_call, bare_call is used, so there is no way to specify we are dealing with a delegate call, so the call is not working correcly. + + For both this paths, we need a way to inject this information into the execution environment, hence I added an optional hooks interface that we implement from foundry cheatcodes for prank and mock functionality. +crates: +- name: pallet-revive + bump: major diff --git a/substrate/frame/revive/src/call_builder.rs b/substrate/frame/revive/src/call_builder.rs index a4a781e52bbf4..c6b6886b4e900 100644 --- a/substrate/frame/revive/src/call_builder.rs +++ b/substrate/frame/revive/src/call_builder.rs @@ -56,7 +56,7 @@ pub struct CallSetup { value: BalanceOf, data: Vec, transient_storage_size: u32, - exec_config: ExecConfig, + exec_config: ExecConfig, } impl Default for CallSetup diff --git a/substrate/frame/revive/src/exec.rs b/substrate/frame/revive/src/exec.rs index ee9f6f8b14fc4..60901419c742d 100644 --- a/substrate/frame/revive/src/exec.rs +++ b/substrate/frame/revive/src/exec.rs @@ -562,7 +562,7 @@ pub struct Stack<'a, T: Config, E> { /// Transient storage used to store data, which is kept for the duration of a transaction. transient_storage: TransientStorage, /// Global behavior determined by the creater of this stack. - exec_config: &'a ExecConfig, + exec_config: &'a ExecConfig, /// The set of contracts that were created during this call stack. contracts_created: BTreeSet, /// The set of contracts that are registered for destruction at the end of this call stack. @@ -602,7 +602,8 @@ struct Frame { /// This structure is used to represent the arguments in a delegate call frame in order to /// distinguish who delegated the call and where it was delegated to. -struct DelegateInfo { +#[derive(Clone)] +pub struct DelegateInfo { /// The caller of the contract. pub caller: Origin, /// The address of the contract the call was delegated to. @@ -796,7 +797,7 @@ where storage_meter: &mut storage::meter::Meter, value: U256, input_data: Vec, - exec_config: &ExecConfig, + exec_config: &ExecConfig, ) -> ExecResult { let dest = T::AddressMapper::to_account_id(&dest); if let Some((mut stack, executable)) = Stack::<'_, T, E>::new( @@ -806,6 +807,7 @@ where storage_meter, value, exec_config, + &input_data, )? { stack.run(executable, input_data).map(|_| stack.first_frame.last_frame_output) } else { @@ -821,14 +823,21 @@ where ); }); - let result = Self::transfer_from_origin( - &origin, - &origin, - &dest, - value, - storage_meter, - exec_config, - ); + let result = if let Some(mock_answer) = + exec_config.mock_handler.as_ref().and_then(|handler| { + handler.mock_call(T::AddressMapper::to_address(&dest), &input_data, value) + }) { + Ok(mock_answer) + } else { + Self::transfer_from_origin( + &origin, + &origin, + &dest, + value, + storage_meter, + exec_config, + ) + }; if_tracing(|t| match result { Ok(ref output) => t.exit_child_span(&output, Weight::zero()), @@ -854,7 +863,7 @@ where value: U256, input_data: Vec, salt: Option<&[u8; 32]>, - exec_config: &ExecConfig, + exec_config: &ExecConfig, ) -> Result<(H160, ExecReturnValue), ExecError> { let deployer = T::AddressMapper::to_address(&origin); let (mut stack, executable) = Stack::<'_, T, E>::new( @@ -869,6 +878,7 @@ where storage_meter, value, exec_config, + &input_data, )? .expect(FRAME_ALWAYS_EXISTS_ON_INSTANTIATE); let address = T::AddressMapper::to_address(&stack.top_frame().account_id); @@ -891,7 +901,7 @@ where gas_meter: &'a mut GasMeter, storage_meter: &'a mut storage::meter::Meter, value: BalanceOf, - exec_config: &'a ExecConfig, + exec_config: &'a ExecConfig, ) -> (Self, E) { let call = Self::new( FrameArgs::Call { @@ -904,6 +914,7 @@ where storage_meter, value.into(), exec_config, + &Default::default(), ) .unwrap() .unwrap(); @@ -920,7 +931,8 @@ where gas_meter: &'a mut GasMeter, storage_meter: &'a mut storage::meter::Meter, value: U256, - exec_config: &'a ExecConfig, + exec_config: &'a ExecConfig, + input_data: &Vec, ) -> Result)>, ExecError> { origin.ensure_mapped()?; let Some((first_frame, executable)) = Self::new_frame( @@ -932,6 +944,8 @@ where BalanceOf::::max_value(), false, true, + input_data, + exec_config, )? else { return Ok(None); @@ -968,6 +982,8 @@ where deposit_limit: BalanceOf, read_only: bool, origin_is_caller: bool, + input_data: &[u8], + exec_config: &ExecConfig, ) -> Result, ExecutableOrPrecompile)>, ExecError> { let (account_id, contract_info, executable, delegate, entry_point) = match frame_args { FrameArgs::Call { dest, cached_info, delegated_call } => { @@ -996,6 +1012,11 @@ where (None, Some(_)) => CachedContract::None, }; + let delegated_call = delegated_call.or_else(|| { + exec_config.mock_handler.as_ref().and_then(|mock_handler| { + mock_handler.mock_delegated_caller(address, input_data) + }) + }); // in case of delegate the executable is not the one at `address` let executable = if let Some(delegated_call) = &delegated_call { if let Some(precompile) = @@ -1090,6 +1111,7 @@ where gas_limit: Weight, deposit_limit: BalanceOf, read_only: bool, + input_data: &[u8], ) -> Result>, ExecError> { if self.frames.len() as u32 == limits::CALL_STACK_DEPTH { return Err(Error::::MaxCallDepthReached.into()); @@ -1121,6 +1143,8 @@ where deposit_limit, read_only, false, + input_data, + self.exec_config, )? { self.frames.try_push(frame).map_err(|_| Error::::MaxCallDepthReached)?; Ok(Some(executable)) @@ -1152,7 +1176,17 @@ where frame.nested_gas.gas_left(), ); }); - + let mock_answer = self.exec_config.mock_handler.as_ref().and_then(|handler| { + handler.mock_call( + frame + .delegate + .as_ref() + .map(|delegate| delegate.callee) + .unwrap_or(T::AddressMapper::to_address(&frame.account_id)), + &input_data, + frame.value_transferred, + ) + }); // The output of the caller frame will be replaced by the output of this run. // It is also not accessible from nested frames. // Hence we drop it early to save the memory. @@ -1335,7 +1369,11 @@ where // transactional storage depth. let transaction_outcome = with_transaction(|| -> TransactionOutcome> { - let output = do_transaction(); + let output = if let Some(mock_answer) = mock_answer { + Ok(mock_answer) + } else { + do_transaction() + }; match &output { Ok(result) if !result.did_revert() => TransactionOutcome::Commit(Ok((true, output))), @@ -1481,7 +1519,7 @@ where to: &T::AccountId, value: U256, storage_meter: &mut storage::meter::GenericMeter, - exec_config: &ExecConfig, + exec_config: &ExecConfig, ) -> DispatchResult { fn transfer_with_dust( from: &AccountIdOf, @@ -1592,7 +1630,7 @@ where to: &T::AccountId, value: U256, storage_meter: &mut storage::meter::GenericMeter, - exec_config: &ExecConfig, + exec_config: &ExecConfig, ) -> ExecResult { // If the from address is root there is no account to transfer from, and therefore we can't // take any `value` other than 0. @@ -1742,6 +1780,7 @@ where gas_limit, deposit_limit.saturated_into::>(), self.is_read_only(), + &input_data, )? { self.run(executable, input_data) } else { @@ -1883,6 +1922,7 @@ where gas_limit, deposit_limit.saturated_into::>(), self.is_read_only(), + &input_data, )? }; let executable = executable.expect(FRAME_ALWAYS_EXISTS_ON_INSTANTIATE); @@ -1954,6 +1994,7 @@ where gas_limit, deposit_limit.saturated_into::>(), is_read_only, + &input_data, )? { self.run(executable, input_data) } else { @@ -1968,8 +2009,13 @@ where Weight::zero(), ); }); - - let result = if is_read_only && value.is_zero() { + let result = if let Some(mock_answer) = + self.exec_config.mock_handler.as_ref().and_then(|handler| { + handler.mock_call(T::AddressMapper::to_address(&dest), &input_data, value) + }) { + *self.last_frame_output_mut() = mock_answer.clone(); + Ok(mock_answer) + } else if is_read_only && value.is_zero() { Ok(Default::default()) } else if is_read_only { Err(Error::::StateChangeDenied.into()) @@ -2029,6 +2075,16 @@ where } fn caller(&self) -> Origin { + if let Some(Ok(mock_caller)) = self + .exec_config + .mock_handler + .as_ref() + .and_then(|mock_handler| mock_handler.mock_caller(self.frames.len())) + .map(|mock_caller| Origin::::from_runtime_origin(mock_caller)) + { + return mock_caller; + } + if let Some(DelegateInfo { caller, .. }) = &self.top_frame().delegate { caller.clone() } else { diff --git a/substrate/frame/revive/src/lib.rs b/substrate/frame/revive/src/lib.rs index fca8db4e3dbab..83ca888ad9c75 100644 --- a/substrate/frame/revive/src/lib.rs +++ b/substrate/frame/revive/src/lib.rs @@ -40,6 +40,7 @@ mod weightinfo_extension; pub mod evm; pub mod migrations; +pub mod mock; pub mod precompiles; pub mod test_utils; pub mod tracing; @@ -53,11 +54,11 @@ use crate::{ runtime::SetWeightLimit, CallTracer, GenericTransaction, PrestateTracer, Trace, Tracer, TracerType, TYPE_EIP1559, }, - exec::{AccountIdOf, ExecError, Executable, Stack as ExecStack}, + exec::{AccountIdOf, ExecError, Stack as ExecStack}, gas::GasMeter, storage::{meter::Meter as StorageMeter, AccountType, DeletionQueueManager}, tracing::if_tracing, - vm::{pvm::extract_code_and_data, CodeInfo, ContractBlob, RuntimeCosts}, + vm::{pvm::extract_code_and_data, CodeInfo, RuntimeCosts}, weightinfo_extension::OnFinalizeBlockParts, }; use alloc::{boxed::Box, format, vec}; @@ -95,9 +96,10 @@ pub use crate::{ }, debug::DebugSettings, evm::{block_hash::ReceiptGasInfo, Address as EthAddress, Block as EthBlock, ReceiptInfo}, - exec::{Key, MomentOf, Origin as ExecOrigin}, + exec::{DelegateInfo, Executable, Key, MomentOf, Origin as ExecOrigin}, pallet::{genesis, *}, storage::{AccountInfo, ContractInfo}, + vm::ContractBlob, }; pub use codec; pub use frame_support::{self, dispatch::DispatchInfo, weights::Weight}; @@ -1483,7 +1485,7 @@ impl Pallet { gas_limit: Weight, storage_deposit_limit: BalanceOf, data: Vec, - exec_config: ExecConfig, + exec_config: ExecConfig, ) -> ContractResult> { if let Err(contract_result) = Self::ensure_non_contract_if_signed(&origin) { return contract_result; @@ -1542,7 +1544,7 @@ impl Pallet { code: Code, data: Vec, salt: Option<[u8; 32]>, - exec_config: ExecConfig, + exec_config: ExecConfig, ) -> ContractResult> { // Enforce EIP-3607 for top-level signed origins: deny signed contract addresses. if let Err(contract_result) = Self::ensure_non_contract_if_signed(&origin) { @@ -2095,11 +2097,11 @@ impl Pallet { } /// Uploads new code and returns the Vm binary contract blob and deposit amount collected. - fn try_upload_pvm_code( + pub fn try_upload_pvm_code( origin: T::AccountId, code: Vec, storage_deposit_limit: BalanceOf, - exec_config: &ExecConfig, + exec_config: &ExecConfig, ) -> Result<(ContractBlob, BalanceOf), DispatchError> { let mut module = ContractBlob::from_pvm_code(code, origin)?; let deposit = module.store_code(exec_config, None)?; @@ -2127,7 +2129,7 @@ impl Pallet { } /// Convert a weight to a gas value. - fn evm_gas_from_weight(weight: Weight) -> U256 { + pub fn evm_gas_from_weight(weight: Weight) -> U256 { T::FeeInfo::weight_to_fee(&weight, Combinator::Max).into() } @@ -2214,7 +2216,7 @@ impl Pallet { from: &T::AccountId, to: &T::AccountId, amount: BalanceOf, - exec_config: &ExecConfig, + exec_config: &ExecConfig, ) -> DispatchResult { use frame_support::traits::tokens::{Fortitude, Precision, Preservation}; match (exec_config.collect_deposit_from_hold.is_some(), hold_reason) { @@ -2259,7 +2261,7 @@ impl Pallet { from: &T::AccountId, to: &T::AccountId, amount: BalanceOf, - exec_config: &ExecConfig, + exec_config: &ExecConfig, ) -> Result, DispatchError> { use frame_support::traits::{ tokens::{Fortitude, Precision, Preservation, Restriction}, diff --git a/substrate/frame/revive/src/mock.rs b/substrate/frame/revive/src/mock.rs new file mode 100644 index 0000000000000..c4d7a76018494 --- /dev/null +++ b/substrate/frame/revive/src/mock.rs @@ -0,0 +1,59 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Helper interfaces and functions, that help with controlling the execution of EVM contracts. +//! It is mostly used to help with the implementation of foundry cheatscodes for forge test +//! integration. + +use frame_system::pallet_prelude::OriginFor; +use sp_core::{H160, U256}; + +use crate::{pallet, DelegateInfo, ExecReturnValue}; + +/// A trait that provides hooks for mocking EVM contract calls and callers. +/// This is useful for testing and simulating contract interactions within foundry forge tests. +pub trait MockHandler { + /// Mock an EVM contract call. + /// + /// Returns `Some(ExecReturnValue)` if the call is mocked, otherwise `None`. + fn mock_call( + &self, + _callee: H160, + _call_data: &[u8], + _value_transferred: U256, + ) -> Option { + None + } + + /// Mock the caller of a contract. + /// + /// # Parameters + /// + /// * `_frames_len`: The current number of frames on the call stack. + /// + /// Returns `Some(OriginFor)` if the caller is mocked, otherwise `None`. + fn mock_caller(&self, _frames_len: usize) -> Option> { + None + } + + /// Mock a delegated caller for a contract call. + /// + /// Returns `Some(DelegateInfo)` if the delegated caller is mocked, otherwise `None`. + fn mock_delegated_caller(&self, _dest: H160, _input_data: &[u8]) -> Option> { + None + } +} diff --git a/substrate/frame/revive/src/primitives.rs b/substrate/frame/revive/src/primitives.rs index 6cb015b06e6c5..2180fd27c467c 100644 --- a/substrate/frame/revive/src/primitives.rs +++ b/substrate/frame/revive/src/primitives.rs @@ -17,8 +17,8 @@ //! A crate that hosts a common definitions that are relevant for the pallet-revive. -use crate::{storage::WriteOutcome, BalanceOf, Config, H160, U256}; -use alloc::{string::String, vec::Vec}; +use crate::{mock::MockHandler, storage::WriteOutcome, BalanceOf, Config, H160, U256}; +use alloc::{boxed::Box, fmt::Debug, string::String, vec::Vec}; use codec::{Decode, Encode, MaxEncodedLen}; use frame_support::weights::Weight; use pallet_revive_uapi::ReturnFlags; @@ -320,8 +320,7 @@ where } /// `Stack` wide configuration options. -#[derive(Debug, Clone)] -pub struct ExecConfig { +pub struct ExecConfig { /// Indicates whether the account nonce should be incremented after instantiating a new /// contract. /// @@ -351,9 +350,13 @@ pub struct ExecConfig { /// Whether this configuration was created for a dry-run execution. /// Use to enable logic that should only run in dry-run mode. pub is_dry_run: bool, + /// An optional mock handler that can be used to override certain behaviors. + /// This is primarily used for testing purposes and should be `None` in production + /// environments. + pub mock_handler: Option>>, } -impl ExecConfig { +impl ExecConfig { /// Create a default config appropriate when the call originated from a substrate tx. pub fn new_substrate_tx() -> Self { Self { @@ -361,6 +364,17 @@ impl ExecConfig { collect_deposit_from_hold: None, effective_gas_price: None, is_dry_run: false, + mock_handler: None, + } + } + + pub fn new_substrate_tx_without_bump() -> Self { + Self { + bump_nonce: false, + collect_deposit_from_hold: None, + effective_gas_price: None, + mock_handler: None, + is_dry_run: false, } } @@ -370,6 +384,7 @@ impl ExecConfig { bump_nonce: false, collect_deposit_from_hold: Some((encoded_len, base_weight)), effective_gas_price: Some(effective_gas_price), + mock_handler: None, is_dry_run: false, } } diff --git a/substrate/frame/revive/src/storage/meter.rs b/substrate/frame/revive/src/storage/meter.rs index c52cf589e4e5d..7c4b724fac460 100644 --- a/substrate/frame/revive/src/storage/meter.rs +++ b/substrate/frame/revive/src/storage/meter.rs @@ -67,7 +67,7 @@ pub trait Ext { contract: &T::AccountId, amount: &DepositOf, state: &ContractState, - exec_config: &ExecConfig, + exec_config: &ExecConfig, ) -> Result<(), DispatchError>; } @@ -396,7 +396,7 @@ where pub fn try_into_deposit( mut self, origin: &Origin, - exec_config: &ExecConfig, + exec_config: &ExecConfig, ) -> Result, DispatchError> { // Only refund or charge deposit if the origin is not root. let origin = match origin { @@ -507,7 +507,7 @@ impl Ext for ReservingExt { contract: &T::AccountId, amount: &DepositOf, state: &ContractState, - exec_config: &ExecConfig, + exec_config: &ExecConfig, ) -> Result<(), DispatchError> { match amount { Deposit::Charge(amount) | Deposit::Refund(amount) if amount.is_zero() => { @@ -649,7 +649,7 @@ mod tests { contract: &AccountIdOf, amount: &DepositOf, state: &ContractState, - _exec_config: &ExecConfig, + _exec_config: &ExecConfig, ) -> Result<(), DispatchError> { TestExtTestValue::mutate(|ext| { ext.charges.push(Charge { diff --git a/substrate/frame/revive/src/test_utils/builder.rs b/substrate/frame/revive/src/test_utils/builder.rs index 7ac7195f48736..3d52f0662b55b 100644 --- a/substrate/frame/revive/src/test_utils/builder.rs +++ b/substrate/frame/revive/src/test_utils/builder.rs @@ -134,7 +134,7 @@ builder!( code: Code, data: Vec, salt: Option<[u8; 32]>, - exec_config: ExecConfig, + exec_config: ExecConfig, ) -> ContractResult>; pub fn concat_evm_data(mut self, more_data: &[u8]) -> Self { @@ -212,7 +212,7 @@ builder!( gas_limit: Weight, storage_deposit_limit: BalanceOf, data: Vec, - exec_config: ExecConfig, + exec_config: ExecConfig, ) -> ContractResult>; /// Set the call's evm_value using a native_value amount. diff --git a/substrate/frame/revive/src/tests.rs b/substrate/frame/revive/src/tests.rs index 38fb4117ac122..f09afd070789a 100644 --- a/substrate/frame/revive/src/tests.rs +++ b/substrate/frame/revive/src/tests.rs @@ -21,6 +21,8 @@ mod precompiles; mod pvm; mod sol; +use std::collections::HashMap; + use crate::{ self as pallet_revive, evm::{ @@ -28,20 +30,22 @@ use crate::{ runtime::{EthExtra, SetWeightLimit}, }, genesis::{Account, ContractData}, + mock::MockHandler, test_utils::*, AccountId32Mapper, AddressMapper, BalanceOf, BalanceWithDust, Call, CodeInfoOf, Config, - ExecOrigin as Origin, GenesisConfig, OriginFor, Pallet, PristineCode, + DelegateInfo, ExecOrigin as Origin, ExecReturnValue, GenesisConfig, OriginFor, Pallet, + PristineCode, }; use frame_support::{ assert_ok, derive_impl, pallet_prelude::EnsureOrigin, parameter_types, - traits::{ConstU32, ConstU64, FindAuthor, StorageVersion}, + traits::{ConstU32, ConstU64, FindAuthor, OriginTrait, StorageVersion}, weights::{constants::WEIGHT_REF_TIME_PER_SECOND, FixedFee, Weight}, }; use pallet_revive_fixtures::compile_module; use pallet_transaction_payment::{ChargeTransactionPayment, ConstFeeMultiplier, Multiplier}; -use sp_core::U256; +use sp_core::{H160, U256}; use sp_keystore::{testing::MemoryKeystore, KeystoreExt}; use sp_runtime::{ generic::Header, @@ -515,6 +519,37 @@ impl Default for Origin { } } +/// A mock handler implementation for testing purposes. +pub struct MockHandlerImpl { + // Always return this caller if set. + mock_caller: Option, + // Map of callee address to mocked call return value. + mock_call: HashMap, + // Map of input data to mocked delegated caller info. + mock_delegate_caller: HashMap, DelegateInfo>, +} + +impl MockHandler for MockHandlerImpl { + fn mock_caller(&self, _frames_len: usize) -> Option> { + self.mock_caller.as_ref().map(|mock_caller| { + OriginFor::::signed(T::AddressMapper::to_fallback_account_id(mock_caller)) + }) + } + + fn mock_call( + &self, + _callee: H160, + _call_data: &[u8], + _value_transferred: U256, + ) -> Option { + self.mock_call.get(&_callee).cloned() + } + + fn mock_delegated_caller(&self, _dest: H160, input_data: &[u8]) -> Option> { + self.mock_delegate_caller.get(&input_data.to_vec()).cloned() + } +} + #[test] fn ext_builder_with_genesis_config_works() { let pvm_contract = Account { diff --git a/substrate/frame/revive/src/tests/pvm.rs b/substrate/frame/revive/src/tests/pvm.rs index 887063863fa81..297cc832922ff 100644 --- a/substrate/frame/revive/src/tests/pvm.rs +++ b/substrate/frame/revive/src/tests/pvm.rs @@ -4797,8 +4797,7 @@ fn bump_nonce_once_works() { let _ = ::Currency::set_balance(&ALICE, 1_000_000); frame_system::Account::::mutate(&ALICE, |account| account.nonce = 1); - let mut do_not_bump = ExecConfig::new_substrate_tx(); - do_not_bump.bump_nonce = false; + let do_not_bump = ExecConfig::new_substrate_tx_without_bump(); let _ = ::Currency::set_balance(&BOB, 1_000_000); frame_system::Account::::mutate(&BOB, |account| account.nonce = 1); @@ -4819,7 +4818,7 @@ fn bump_nonce_once_works() { builder::bare_instantiate(Code::Upload(code.clone())) .origin(RuntimeOrigin::signed(BOB)) - .exec_config(do_not_bump.clone()) + .exec_config(ExecConfig::new_substrate_tx_without_bump()) .salt(None) .build_and_unwrap_result(); assert_eq!(System::account_nonce(&BOB), 1); diff --git a/substrate/frame/revive/src/tests/sol/contract.rs b/substrate/frame/revive/src/tests/sol/contract.rs index 3eeef87675236..74144ac3db845 100644 --- a/substrate/frame/revive/src/tests/sol/contract.rs +++ b/substrate/frame/revive/src/tests/sol/contract.rs @@ -17,11 +17,14 @@ //! The pallet-revive shared VM integration test suite. +use core::iter; + use crate::{ + address::AddressMapper, evm::decode_revert_reason, - test_utils::{builder::Contract, ALICE, ALICE_ADDR}, - tests::{builder, ExtBuilder, Test}, - Code, Config, Error, + test_utils::{builder::Contract, ALICE, ALICE_ADDR, BOB_ADDR}, + tests::{builder, ExtBuilder, MockHandlerImpl, Test}, + Code, Config, DelegateInfo, Error, ExecConfig, ExecOrigin, ExecReturnValue, }; use alloy_core::{ primitives::{Bytes, FixedBytes}, @@ -29,6 +32,7 @@ use alloy_core::{ }; use frame_support::{assert_err, traits::fungible::Mutate}; use pallet_revive_fixtures::{compile_module_with_type, Callee, Caller, FixtureType}; +use pallet_revive_uapi::ReturnFlags; use pretty_assertions::assert_eq; use sp_core::H160; use test_case::test_case; @@ -362,6 +366,180 @@ fn delegatecall_works(caller_type: FixtureType, callee_type: FixtureType) { }); } +#[test_case(FixtureType::Solc, FixtureType::Solc; "solc->solc")] +#[test_case(FixtureType::Solc, FixtureType::Resolc; "solc->resolc")] +#[test_case(FixtureType::Resolc, FixtureType::Solc; "resolc->solc")] +#[test_case(FixtureType::Resolc, FixtureType::Resolc; "resolc->resolc")] +fn mock_caller_hook_works(caller_type: FixtureType, callee_type: FixtureType) { + let (caller_code, _) = compile_module_with_type("Caller", caller_type).unwrap(); + let (callee_code, _) = compile_module_with_type("Callee", callee_type).unwrap(); + + ExtBuilder::default().build().execute_with(|| { + let _ = ::Currency::set_balance(&ALICE, 100_000_000_000); + + // Instantiate the callee contract, which can echo a value. + let Contract { addr: callee_addr, .. } = + builder::bare_instantiate(Code::Upload(callee_code)).build_and_unwrap_contract(); + + // Instantiate the caller contract. + let Contract { addr: caller_addr, .. } = + builder::bare_instantiate(Code::Upload(caller_code)).build_and_unwrap_contract(); + + // Set BOB as the mock caller and check whoSender returns BOB's address. + let result = builder::bare_call(caller_addr) + .data( + Caller::normalCall { + _callee: callee_addr.0.into(), + _data: Callee::whoSenderCall {}.abi_encode().into(), + _gas: u64::MAX, + _value: 0, + } + .abi_encode(), + ) + .exec_config(ExecConfig { + bump_nonce: false, + collect_deposit_from_hold: None, + effective_gas_price: None, + is_dry_run: false, + mock_handler: Some(Box::new(MockHandlerImpl { + mock_caller: Some(BOB_ADDR), + mock_call: Default::default(), + mock_delegate_caller: Default::default(), + })), + }) + .build_and_unwrap_result(); + + let result = Caller::normalCall::abi_decode_returns(&result.data).unwrap(); + assert!(result.success, "the whoSender call must succeed"); + let decoded = Callee::whoSenderCall::abi_decode_returns(&result.output).unwrap(); + assert_eq!(BOB_ADDR, H160::from_slice(decoded.as_slice())); + }); +} + +#[test_case(FixtureType::Solc, FixtureType::Solc; "solc->solc")] +#[test_case(FixtureType::Solc, FixtureType::Resolc; "solc->resolc")] +#[test_case(FixtureType::Resolc, FixtureType::Solc; "resolc->solc")] +#[test_case(FixtureType::Resolc, FixtureType::Resolc; "resolc->resolc")] +fn mock_call_hook_works(caller_type: FixtureType, callee_type: FixtureType) { + let (caller_code, _) = compile_module_with_type("Caller", caller_type).unwrap(); + let (callee_code, _) = compile_module_with_type("Callee", callee_type).unwrap(); + + ExtBuilder::default().build().execute_with(|| { + let _ = ::Currency::set_balance(&ALICE, 100_000_000_000); + + // Instantiate the callee contract, which can echo a value. + let Contract { addr: callee_addr, .. } = + builder::bare_instantiate(Code::Upload(callee_code)).build_and_unwrap_contract(); + + // Instantiate the caller contract. + let Contract { addr: caller_addr, .. } = + builder::bare_instantiate(Code::Upload(caller_code)).build_and_unwrap_contract(); + + // Set up mocked_magic_number to be returned by the mock call handler and check that is + // returned instead of magic_number. + let magic_number = 42u64; + let mocked_magic_number = 99u64; + let result = builder::bare_call(caller_addr) + .data( + Caller::normalCall { + _callee: callee_addr.0.into(), + _value: 0, + _data: Callee::echoCall { _data: magic_number }.abi_encode().into(), + _gas: u64::MAX, + } + .abi_encode(), + ) + .exec_config(ExecConfig { + bump_nonce: false, + collect_deposit_from_hold: None, + effective_gas_price: None, + is_dry_run: false, + mock_handler: Some(Box::new(MockHandlerImpl { + mock_caller: None, + mock_call: iter::once(( + callee_addr, + ExecReturnValue { + flags: ReturnFlags::default(), + data: alloy_core::sol_types::SolValue::abi_encode(&mocked_magic_number) + .into(), + }, + )) + .collect(), + mock_delegate_caller: Default::default(), + })), + }) + .build_and_unwrap_result(); + + let result = Caller::normalCall::abi_decode_returns(&result.data).unwrap(); + assert!(result.success, "the call must succeed"); + let echo_output = Callee::echoCall::abi_decode_returns(&result.output).unwrap(); + assert_eq!(mocked_magic_number, echo_output, "the call must reproduce the magic number"); + }); +} + +#[test_case(FixtureType::Solc, FixtureType::Solc; "solc->solc")] +#[test_case(FixtureType::Solc, FixtureType::Resolc; "solc->resolc")] +#[test_case(FixtureType::Resolc, FixtureType::Solc; "resolc->solc")] +#[test_case(FixtureType::Resolc, FixtureType::Resolc; "resolc->resolc")] +fn mock_delegatecall_hook_works(caller_type: FixtureType, callee_type: FixtureType) { + let (caller_code, _) = compile_module_with_type("Caller", caller_type).unwrap(); + let (callee_code, _) = compile_module_with_type("Callee", callee_type).unwrap(); + + ExtBuilder::default().build().execute_with(|| { + let _ = ::Currency::set_balance(&ALICE, 100_000_000_000); + + // Instantiate the callee contract, which can echo a value. + let Contract { addr: callee_addr, .. } = + builder::bare_instantiate(Code::Upload(callee_code)).build_and_unwrap_contract(); + + // Instantiate the caller contract. + let Contract { addr: caller_addr, .. } = + builder::bare_instantiate(Code::Upload(caller_code)).build_and_unwrap_contract(); + + // Instantiate the call with an invalid callee and check that the delegatecall uses the + // mocked callee address. + let magic_number = 42u64; + let result = builder::bare_call(caller_addr) + .data( + Caller::normalCall { + _callee: caller_addr.0.into(), // Wrong callee, should be overridden by the mock hook. + _value: 0, + _data: Callee::echoCall { _data: magic_number }.abi_encode().into(), + _gas: u64::MAX, + } + .abi_encode(), + ) + .exec_config(ExecConfig { + bump_nonce: false, + collect_deposit_from_hold: None, + effective_gas_price: None, + is_dry_run: false, + mock_handler: Some(Box::new(MockHandlerImpl { + mock_caller: None, + mock_call: Default::default(), + mock_delegate_caller: iter::once(( + Callee::echoCall { _data: magic_number }.abi_encode().into(), + DelegateInfo { + callee: callee_addr, + caller: ExecOrigin::::from_runtime_origin(crate::OriginFor::::signed( + ::AddressMapper::to_fallback_account_id( + &caller_addr, + ), + )).expect("Conversion to ExecOrigin must work"), + }, + )) + .collect(), + })), + }) + .build_and_unwrap_result(); + + let result = Caller::normalCall::abi_decode_returns(&result.data).unwrap(); + assert!(result.success, "the call must succeed"); + let echo_output = Callee::echoCall::abi_decode_returns(&result.output).unwrap(); + assert_eq!(magic_number, echo_output, "the call must reproduce the magic number"); + }); +} + #[test] fn create_works() { let (caller_code, _) = compile_module_with_type("Caller", FixtureType::Solc).unwrap(); diff --git a/substrate/frame/revive/src/vm/mod.rs b/substrate/frame/revive/src/vm/mod.rs index 7b543669d6aea..4f80e3b13cd2a 100644 --- a/substrate/frame/revive/src/vm/mod.rs +++ b/substrate/frame/revive/src/vm/mod.rs @@ -193,7 +193,7 @@ impl ContractBlob { /// Puts the module blob into storage, and returns the deposit collected for the storage. pub fn store_code( &mut self, - exec_config: &ExecConfig, + exec_config: &ExecConfig, storage_meter: Option<&mut NestedMeter>, ) -> Result, DispatchError> { let code_hash = *self.code_hash();