From a57b7550b7e06843fc969358956cc6626f43ffe1 Mon Sep 17 00:00:00 2001 From: Alexandru Gheorghe Date: Tue, 7 Oct 2025 15:57:50 +0300 Subject: [PATCH 01/14] add mock handlers Signed-off-by: Alexandru Gheorghe --- substrate/frame/revive/src/call_builder.rs | 2 +- substrate/frame/revive/src/exec.rs | 88 +++++++++++++++---- substrate/frame/revive/src/lib.rs | 44 +++++----- substrate/frame/revive/src/mock.rs | 54 ++++++++++++ substrate/frame/revive/src/primitives.rs | 27 ++++-- substrate/frame/revive/src/storage/meter.rs | 8 +- .../frame/revive/src/test_utils/builder.rs | 4 +- substrate/frame/revive/src/tests/pvm.rs | 5 +- substrate/frame/revive/src/vm/mod.rs | 2 +- 9 files changed, 178 insertions(+), 56 deletions(-) create mode 100644 substrate/frame/revive/src/mock.rs diff --git a/substrate/frame/revive/src/call_builder.rs b/substrate/frame/revive/src/call_builder.rs index e176c62e73811..3a60024f7ea1a 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 b62faaba7ca33..58f49bf9d3441 100644 --- a/substrate/frame/revive/src/exec.rs +++ b/substrate/frame/revive/src/exec.rs @@ -544,7 +544,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, /// No executable is held by the struct but influences its behaviour. _phantom: PhantomData, } @@ -579,7 +579,7 @@ 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 { +pub struct DelegateInfo { /// The caller of the contract. pub caller: Origin, /// The address of the contract the call was delegated to. @@ -794,7 +794,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( @@ -804,6 +804,7 @@ where storage_meter, value, exec_config, + &input_data, )? { stack.run(executable, input_data).map(|_| stack.first_frame.last_frame_output) } else { @@ -819,14 +820,25 @@ 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.clone(), + 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()), @@ -852,7 +864,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( @@ -867,6 +879,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); @@ -889,7 +902,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 { @@ -902,6 +915,7 @@ where storage_meter, value.into(), exec_config, + &Default::default(), ) .unwrap() .unwrap(); @@ -918,7 +932,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( @@ -930,6 +945,8 @@ where BalanceOf::::max_value(), false, true, + input_data, + exec_config, )? else { return Ok(None); @@ -964,6 +981,8 @@ where deposit_limit: BalanceOf, read_only: bool, origin_is_caller: bool, + input_data: &Vec, + 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 } => { @@ -991,6 +1010,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.clone()) + }) + }); // 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) = @@ -1085,6 +1109,7 @@ where gas_limit: Weight, deposit_limit: BalanceOf, read_only: bool, + input_data: &Vec, ) -> Result>, ExecError> { if self.frames.len() as u32 == limits::CALL_STACK_DEPTH { return Err(Error::::MaxCallDepthReached.into()); @@ -1116,6 +1141,8 @@ where deposit_limit, read_only, false, + input_data, + self.exec_config, )? { self.frames.try_push(frame).map_err(|_| Error::::MaxCallDepthReached)?; Ok(Some(executable)) @@ -1251,11 +1278,23 @@ where .map(|exec| exec.code_info().deposit()) .unwrap_or_default(); + 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.clone(), + frame.value_transferred, + ) + }); let mut output = match executable { - ExecutableOrPrecompile::Executable(executable) => + ExecutableOrPrecompile::Executable(executable) if mock_answer.is_none() => executable.execute(self, entry_point, input_data), - ExecutableOrPrecompile::Precompile { instance, .. } => + ExecutableOrPrecompile::Precompile { instance, .. } if mock_answer.is_none() => instance.call(input_data, self), + _ => Ok(mock_answer.expect("Checked above; qed")), } .and_then(|output| { if u32::try_from(output.data.len()) @@ -1467,7 +1506,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, @@ -1579,7 +1618,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. @@ -1692,6 +1731,7 @@ where gas_limit, deposit_limit.saturated_into::>(), self.is_read_only(), + &input_data, )? { self.run(executable, input_data) } else { @@ -1859,6 +1899,7 @@ where gas_limit, deposit_limit.saturated_into::>(), self.is_read_only(), + &input_data, )? }; let executable = executable.expect(FRAME_ALWAYS_EXISTS_ON_INSTANTIATE); @@ -1926,6 +1967,7 @@ where gas_limit, deposit_limit.saturated_into::>(), is_read_only, + &input_data, )? { self.run(executable, input_data) } else { @@ -2001,6 +2043,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 a4ff10ce52847..07a50c390d96f 100644 --- a/substrate/frame/revive/src/lib.rs +++ b/substrate/frame/revive/src/lib.rs @@ -38,11 +38,20 @@ mod vm; pub mod evm; pub mod migrations; +pub mod mock; pub mod precompiles; pub mod test_utils; pub mod tracing; pub mod weights; +pub use crate::{ + address::{ + create1, create2, is_eth_derived, AccountId32Mapper, AddressMapper, TestAccountMapper, + }, + exec::{DelegateInfo, Key, MomentOf, Origin as ExecOrigin}, + pallet::{genesis, *}, + storage::{AccountInfo, ContractInfo}, +}; use crate::{ evm::{ create_call, fees::InfoT as FeeInfo, runtime::SetWeightLimit, CallTracer, @@ -54,9 +63,12 @@ use crate::{ tracing::if_tracing, vm::{pvm::extract_code_and_data, CodeInfo, ContractBlob, RuntimeCosts}, }; +pub use alloc::collections::{BTreeMap, VecDeque}; use alloc::{boxed::Box, format, vec}; +pub use codec; use codec::{Codec, Decode, Encode}; use environmental::*; +pub use frame_support::{self, dispatch::DispatchInfo, weights::Weight}; use frame_support::{ dispatch::{ DispatchErrorWithPostInfo, DispatchResult, DispatchResultWithPostInfo, GetDispatchInfo, @@ -72,31 +84,20 @@ use frame_support::{ weights::WeightMeter, BoundedVec, RuntimeDebugNoBound, }; +pub use frame_system::{self, limits::BlockWeights}; use frame_system::{ ensure_signed, pallet_prelude::{BlockNumberFor, OriginFor}, Pallet as System, }; +pub use primitives::*; use scale_info::TypeInfo; +pub use sp_core::{H160, H256, U256}; +pub use sp_runtime; use sp_runtime::{ traits::{BadOrigin, Bounded, Convert, Dispatchable, Saturating, UniqueSaturatedInto, Zero}, AccountId32, DispatchError, FixedPointNumber, FixedU128, }; - -pub use crate::{ - address::{ - create1, create2, is_eth_derived, AccountId32Mapper, AddressMapper, TestAccountMapper, - }, - exec::{Key, MomentOf, Origin as ExecOrigin}, - pallet::{genesis, *}, - storage::{AccountInfo, ContractInfo}, -}; -pub use codec; -pub use frame_support::{self, dispatch::DispatchInfo, weights::Weight}; -pub use frame_system::{self, limits::BlockWeights}; -pub use primitives::*; -pub use sp_core::{H160, H256, U256}; -pub use sp_runtime; pub use weights::WeightInfo; #[cfg(doc)] @@ -1275,7 +1276,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; @@ -1334,12 +1335,13 @@ 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) { return contract_result; } + let mut gas_meter = GasMeter::new(gas_limit); let mut storage_deposit = Default::default(); let try_instantiate = || { @@ -1853,7 +1855,7 @@ impl Pallet { 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)?; @@ -1881,7 +1883,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).into() } @@ -1967,7 +1969,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, hold_reason) { @@ -2012,7 +2014,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..6887d57f70879 --- /dev/null +++ b/substrate/frame/revive/src/mock.rs @@ -0,0 +1,54 @@ +// 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 function, that help with controlling the execution of EVM contracts. +//! It is mostly used to help with the foundry forget 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: Vec, + _value_transferred: U256, + ) -> Option { + None + } + + /// Mock the caller of a contract. + /// + /// 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: Vec) -> Option> { + None + } +} diff --git a/substrate/frame/revive/src/primitives.rs b/substrate/frame/revive/src/primitives.rs index d3a60478c2b2a..9df9edae4e480 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::{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. /// @@ -346,12 +345,27 @@ pub struct ExecConfig { /// /// It is determined when transforming `eth_transact` into a proper extrinsic. pub effective_gas_price: Option, + pub mock_handler: Option>>, } -impl ExecConfig { +impl ExecConfig { /// Create a default config appropriate when the call originated from a subtrate tx. pub fn new_substrate_tx() -> Self { - Self { bump_nonce: true, collect_deposit_from_hold: false, effective_gas_price: None } + Self { + bump_nonce: true, + collect_deposit_from_hold: false, + effective_gas_price: None, + mock_handler: None, + } + } + + pub fn new_substrate_tx_without_bump() -> Self { + Self { + bump_nonce: false, + collect_deposit_from_hold: false, + effective_gas_price: None, + mock_handler: None, + } } /// Create a default config appropriate when the call originated from a ethereum tx. @@ -360,6 +374,7 @@ impl ExecConfig { bump_nonce: false, collect_deposit_from_hold: true, effective_gas_price: Some(effective_gas_price), + mock_handler: None, } } } diff --git a/substrate/frame/revive/src/storage/meter.rs b/substrate/frame/revive/src/storage/meter.rs index 3072d69c0b659..7fd6117a4e913 100644 --- a/substrate/frame/revive/src/storage/meter.rs +++ b/substrate/frame/revive/src/storage/meter.rs @@ -65,7 +65,7 @@ pub trait Ext { contract: &T::AccountId, amount: &DepositOf, state: &ContractState, - exec_config: &ExecConfig, + exec_config: &ExecConfig, ) -> Result<(), DispatchError>; } @@ -365,7 +365,7 @@ where pub fn try_into_deposit( 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 { @@ -453,7 +453,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() => return Ok(()), @@ -545,7 +545,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 dd8fd0b6e0de6..d005f7d6715de 100644 --- a/substrate/frame/revive/src/test_utils/builder.rs +++ b/substrate/frame/revive/src/test_utils/builder.rs @@ -133,7 +133,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 { @@ -211,7 +211,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/pvm.rs b/substrate/frame/revive/src/tests/pvm.rs index eb92d7a734391..abd5e50a658e1 100644 --- a/substrate/frame/revive/src/tests/pvm.rs +++ b/substrate/frame/revive/src/tests/pvm.rs @@ -4672,8 +4672,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); @@ -4694,7 +4693,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/vm/mod.rs b/substrate/frame/revive/src/vm/mod.rs index 6e1fc9853f590..bb85d4e52724a 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(); From 9d13964cf3f5e10a5c91a25275966868f39e24b3 Mon Sep 17 00:00:00 2001 From: Alexandru Gheorghe Date: Wed, 8 Oct 2025 14:31:45 +0300 Subject: [PATCH 02/14] minor fixes Signed-off-by: Alexandru Gheorghe --- substrate/frame/revive/src/mock.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/substrate/frame/revive/src/mock.rs b/substrate/frame/revive/src/mock.rs index 6887d57f70879..5afadfd24c94b 100644 --- a/substrate/frame/revive/src/mock.rs +++ b/substrate/frame/revive/src/mock.rs @@ -15,8 +15,9 @@ // See the License for the specific language governing permissions and // limitations under the License. -//! Helper Interfaces and function, that help with controlling the execution of EVM contracts. -//! It is mostly used to help with the foundry forget test integration. +//! 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}; From 2c25da994ebdfe628acffca543635ff58266a7f8 Mon Sep 17 00:00:00 2001 From: Alexandru Gheorghe Date: Wed, 8 Oct 2025 17:57:00 +0300 Subject: [PATCH 03/14] make no_std build work Signed-off-by: Alexandru Gheorghe --- substrate/frame/revive/src/mock.rs | 2 ++ substrate/frame/revive/src/primitives.rs | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/substrate/frame/revive/src/mock.rs b/substrate/frame/revive/src/mock.rs index 5afadfd24c94b..ad262a644732c 100644 --- a/substrate/frame/revive/src/mock.rs +++ b/substrate/frame/revive/src/mock.rs @@ -22,6 +22,8 @@ use frame_system::pallet_prelude::OriginFor; use sp_core::{H160, U256}; +use alloc::vec::Vec; + use crate::{pallet, DelegateInfo, ExecReturnValue}; /// A trait that provides hooks for mocking EVM contract calls and callers. diff --git a/substrate/frame/revive/src/primitives.rs b/substrate/frame/revive/src/primitives.rs index 9df9edae4e480..68c8698f134fc 100644 --- a/substrate/frame/revive/src/primitives.rs +++ b/substrate/frame/revive/src/primitives.rs @@ -18,7 +18,7 @@ //! A crate that hosts a common definitions that are relevant for the pallet-revive. use crate::{mock::MockHandler, storage::WriteOutcome, BalanceOf, Config, H160, U256}; -use alloc::{fmt::Debug, string::String, vec::Vec}; +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; From ec1c7c21ab6e3d8335e73c88d71299d238df546c Mon Sep 17 00:00:00 2001 From: Alexandru Gheorghe Date: Wed, 8 Oct 2025 17:57:00 +0300 Subject: [PATCH 04/14] review feedback Signed-off-by: Alexandru Gheorghe --- substrate/frame/revive/src/lib.rs | 32 +++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/substrate/frame/revive/src/lib.rs b/substrate/frame/revive/src/lib.rs index 07a50c390d96f..8de56deb25b02 100644 --- a/substrate/frame/revive/src/lib.rs +++ b/substrate/frame/revive/src/lib.rs @@ -44,31 +44,20 @@ pub mod test_utils; pub mod tracing; pub mod weights; -pub use crate::{ - address::{ - create1, create2, is_eth_derived, AccountId32Mapper, AddressMapper, TestAccountMapper, - }, - exec::{DelegateInfo, Key, MomentOf, Origin as ExecOrigin}, - pallet::{genesis, *}, - storage::{AccountInfo, ContractInfo}, -}; use crate::{ evm::{ create_call, fees::InfoT as FeeInfo, 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}, }; -pub use alloc::collections::{BTreeMap, VecDeque}; use alloc::{boxed::Box, format, vec}; -pub use codec; use codec::{Codec, Decode, Encode}; use environmental::*; -pub use frame_support::{self, dispatch::DispatchInfo, weights::Weight}; use frame_support::{ dispatch::{ DispatchErrorWithPostInfo, DispatchResult, DispatchResultWithPostInfo, GetDispatchInfo, @@ -84,20 +73,31 @@ use frame_support::{ weights::WeightMeter, BoundedVec, RuntimeDebugNoBound, }; -pub use frame_system::{self, limits::BlockWeights}; use frame_system::{ ensure_signed, pallet_prelude::{BlockNumberFor, OriginFor}, Pallet as System, }; -pub use primitives::*; use scale_info::TypeInfo; -pub use sp_core::{H160, H256, U256}; -pub use sp_runtime; use sp_runtime::{ traits::{BadOrigin, Bounded, Convert, Dispatchable, Saturating, UniqueSaturatedInto, Zero}, AccountId32, DispatchError, FixedPointNumber, FixedU128, }; + +pub use crate::{ + address::{ + create1, create2, is_eth_derived, AccountId32Mapper, AddressMapper, TestAccountMapper, + }, + exec::{DelegateInfo, Executable, Key, MomentOf, Origin as ExecOrigin}, + pallet::{genesis, *}, + storage::{AccountInfo, ContractInfo}, +}; +pub use codec; +pub use frame_support::{self, dispatch::DispatchInfo, weights::Weight}; +pub use frame_system::{self, limits::BlockWeights}; +pub use primitives::*; +pub use sp_core::{H160, H256, U256}; +pub use sp_runtime; pub use weights::WeightInfo; #[cfg(doc)] From 1c246198de235c95ccbf73e09e12735494fc0829 Mon Sep 17 00:00:00 2001 From: Alexandru Gheorghe Date: Thu, 9 Oct 2025 17:37:47 +0300 Subject: [PATCH 05/14] pallet-revive: make a try_upload_pvm_code/code_hash public ... to help with foundry-polkadot implementation for etch cheatcode Signed-off-by: Alexandru Gheorghe --- substrate/frame/revive/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/substrate/frame/revive/src/lib.rs b/substrate/frame/revive/src/lib.rs index 8de56deb25b02..05b8925be95c7 100644 --- a/substrate/frame/revive/src/lib.rs +++ b/substrate/frame/revive/src/lib.rs @@ -1851,7 +1851,7 @@ 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, From 5ab45ad06917ee8a06ea616b0c7c9387c98cc011 Mon Sep 17 00:00:00 2001 From: Alexandru Gheorghe Date: Fri, 10 Oct 2025 12:33:27 +0300 Subject: [PATCH 06/14] use references Signed-off-by: Alexandru Gheorghe --- substrate/frame/revive/src/exec.rs | 20 ++++++++------------ substrate/frame/revive/src/lib.rs | 1 - substrate/frame/revive/src/mock.rs | 8 +++----- substrate/frame/revive/src/primitives.rs | 7 +++++-- 4 files changed, 16 insertions(+), 20 deletions(-) diff --git a/substrate/frame/revive/src/exec.rs b/substrate/frame/revive/src/exec.rs index 58f49bf9d3441..680a46ee9a379 100644 --- a/substrate/frame/revive/src/exec.rs +++ b/substrate/frame/revive/src/exec.rs @@ -822,11 +822,7 @@ where 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.clone(), - value, - ) + handler.mock_call(T::AddressMapper::to_address(&dest), &input_data, value) }) { Ok(mock_answer) } else { @@ -981,7 +977,7 @@ where deposit_limit: BalanceOf, read_only: bool, origin_is_caller: bool, - input_data: &Vec, + input_data: &[u8], exec_config: &ExecConfig, ) -> Result, ExecutableOrPrecompile)>, ExecError> { let (account_id, contract_info, executable, delegate, entry_point) = match frame_args { @@ -1012,7 +1008,7 @@ where 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.clone()) + mock_handler.mock_delegated_caller(address, input_data) }) }); // in case of delegate the executable is not the one at `address` @@ -1285,16 +1281,16 @@ where .as_ref() .map(|delegate| delegate.callee) .unwrap_or(T::AddressMapper::to_address(&frame.account_id)), - input_data.clone(), + &input_data, frame.value_transferred, ) }); - let mut output = match executable { - ExecutableOrPrecompile::Executable(executable) if mock_answer.is_none() => + let mut output = match (executable, mock_answer) { + (ExecutableOrPrecompile::Executable(executable), None) => executable.execute(self, entry_point, input_data), - ExecutableOrPrecompile::Precompile { instance, .. } if mock_answer.is_none() => + (ExecutableOrPrecompile::Precompile { instance, .. }, None) => instance.call(input_data, self), - _ => Ok(mock_answer.expect("Checked above; qed")), + (_, Some(mock_answer)) => Ok(mock_answer), } .and_then(|output| { if u32::try_from(output.data.len()) diff --git a/substrate/frame/revive/src/lib.rs b/substrate/frame/revive/src/lib.rs index 05b8925be95c7..886d49a955146 100644 --- a/substrate/frame/revive/src/lib.rs +++ b/substrate/frame/revive/src/lib.rs @@ -1341,7 +1341,6 @@ impl Pallet { if let Err(contract_result) = Self::ensure_non_contract_if_signed(&origin) { return contract_result; } - let mut gas_meter = GasMeter::new(gas_limit); let mut storage_deposit = Default::default(); let try_instantiate = || { diff --git a/substrate/frame/revive/src/mock.rs b/substrate/frame/revive/src/mock.rs index ad262a644732c..e6af81b333874 100644 --- a/substrate/frame/revive/src/mock.rs +++ b/substrate/frame/revive/src/mock.rs @@ -22,20 +22,18 @@ use frame_system::pallet_prelude::OriginFor; use sp_core::{H160, U256}; -use alloc::vec::Vec; - 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 { +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: Vec, + _call_data: &[u8], _value_transferred: U256, ) -> Option { None @@ -51,7 +49,7 @@ pub trait MockHandler { /// 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: Vec) -> Option> { + 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 68c8698f134fc..ebcf70fd3754b 100644 --- a/substrate/frame/revive/src/primitives.rs +++ b/substrate/frame/revive/src/primitives.rs @@ -320,7 +320,7 @@ where } /// `Stack` wide configuration options. -pub struct ExecConfig { +pub struct ExecConfig { /// Indicates whether the account nonce should be incremented after instantiating a new /// contract. /// @@ -345,10 +345,13 @@ pub struct ExecConfig { /// /// It is determined when transforming `eth_transact` into a proper extrinsic. pub effective_gas_price: Option, + /// 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 subtrate tx. pub fn new_substrate_tx() -> Self { Self { From 42e82ddcc829455a8d5110cb1659e47364983b1b Mon Sep 17 00:00:00 2001 From: Alexandru Gheorghe Date: Fri, 10 Oct 2025 13:03:01 +0300 Subject: [PATCH 07/14] address revivew feedback Signed-off-by: Alexandru Gheorghe --- substrate/frame/revive/src/exec.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/substrate/frame/revive/src/exec.rs b/substrate/frame/revive/src/exec.rs index 680a46ee9a379..9cf6c3910bc2b 100644 --- a/substrate/frame/revive/src/exec.rs +++ b/substrate/frame/revive/src/exec.rs @@ -1105,7 +1105,7 @@ where gas_limit: Weight, deposit_limit: BalanceOf, read_only: bool, - input_data: &Vec, + input_data: &[u8], ) -> Result>, ExecError> { if self.frames.len() as u32 == limits::CALL_STACK_DEPTH { return Err(Error::::MaxCallDepthReached.into()); From 5f4e784c6b29e53f0108d0dc6198e340741044a4 Mon Sep 17 00:00:00 2001 From: Alexandru Gheorghe Date: Mon, 20 Oct 2025 14:25:57 +0300 Subject: [PATCH 08/14] add mock hooks tests Signed-off-by: Alexandru Gheorghe --- substrate/frame/revive/src/exec.rs | 1 + substrate/frame/revive/src/tests.rs | 41 +++- .../frame/revive/src/tests/sol/contract.rs | 181 +++++++++++++++++- 3 files changed, 217 insertions(+), 6 deletions(-) diff --git a/substrate/frame/revive/src/exec.rs b/substrate/frame/revive/src/exec.rs index a2b8c2ea2cedf..79c70b0ed202f 100644 --- a/substrate/frame/revive/src/exec.rs +++ b/substrate/frame/revive/src/exec.rs @@ -588,6 +588,7 @@ 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. +#[derive(Clone)] pub struct DelegateInfo { /// The caller of the contract. pub caller: Origin, 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/sol/contract.rs b/substrate/frame/revive/src/tests/sol/contract.rs index 3eeef87675236..9195b48047fff 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,177 @@ 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, + 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, + 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, + 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(); From 47418cafee8f70ba9c23187030b455795a58b7fb Mon Sep 17 00:00:00 2001 From: Alexandru Gheorghe Date: Thu, 23 Oct 2025 17:03:43 +0300 Subject: [PATCH 09/14] make sure mock calls don't consume gas, storage, transfer value Signed-off-by: Alexandru Gheorghe --- substrate/frame/revive/src/exec.rs | 37 ++++++++++++++++-------------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/substrate/frame/revive/src/exec.rs b/substrate/frame/revive/src/exec.rs index 0a57e3b120429..d6bd27297be2a 100644 --- a/substrate/frame/revive/src/exec.rs +++ b/substrate/frame/revive/src/exec.rs @@ -1178,7 +1178,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. @@ -1282,23 +1292,11 @@ where .map(|exec| exec.code_info().deposit()) .unwrap_or_default(); - 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, - ) - }); - let mut output = match (executable, mock_answer) { - (ExecutableOrPrecompile::Executable(executable), None) => + let mut output = match executable { + ExecutableOrPrecompile::Executable(executable) => executable.execute(self, entry_point, input_data), - (ExecutableOrPrecompile::Precompile { instance, .. }, None) => + ExecutableOrPrecompile::Precompile { instance, .. } => instance.call(input_data, self), - (_, Some(mock_answer)) => Ok(mock_answer), } .and_then(|output| { if u32::try_from(output.data.len()) @@ -1371,9 +1369,14 @@ where // // `with_transactional` may return an error caused by a limit in the // 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))), From 47f5cf8f61ba32aaae595626f2129a8f5ca151fc Mon Sep 17 00:00:00 2001 From: Alexandru Gheorghe Date: Thu, 23 Oct 2025 17:07:24 +0300 Subject: [PATCH 10/14] remove unneeded empty Signed-off-by: Alexandru Gheorghe --- substrate/frame/revive/src/exec.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/substrate/frame/revive/src/exec.rs b/substrate/frame/revive/src/exec.rs index d6bd27297be2a..186c2607f252c 100644 --- a/substrate/frame/revive/src/exec.rs +++ b/substrate/frame/revive/src/exec.rs @@ -1369,7 +1369,6 @@ where // // `with_transactional` may return an error caused by a limit in the // transactional storage depth. - let transaction_outcome = with_transaction(|| -> TransactionOutcome> { let output = if let Some(mock_answer) = mock_answer { From ebff833151f01dfc13df93993f6a875b457cbf43 Mon Sep 17 00:00:00 2001 From: "cmd[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 27 Oct 2025 09:07:20 +0000 Subject: [PATCH 11/14] Update from github-actions[bot] running command 'prdoc --audience node_dev --bump minor' --- prdoc/pr_9909.prdoc | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 prdoc/pr_9909.prdoc diff --git a/prdoc/pr_9909.prdoc b/prdoc/pr_9909.prdoc new file mode 100644 index 0000000000000..161898cc019a0 --- /dev/null +++ b/prdoc/pr_9909.prdoc @@ -0,0 +1,19 @@ +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. + + ## TODO + - [x] Add tests to make sure the hooks functionality does not regress. +crates: +- name: pallet-revive + bump: minor From 077ef11fb1ac4177647fd1e70308a529fe0b110b Mon Sep 17 00:00:00 2001 From: Alexandru Gheorghe <49718502+alexggh@users.noreply.github.com> Date: Mon, 27 Oct 2025 11:08:56 +0200 Subject: [PATCH 12/14] Update pr_9909.prdoc --- prdoc/pr_9909.prdoc | 3 --- 1 file changed, 3 deletions(-) diff --git a/prdoc/pr_9909.prdoc b/prdoc/pr_9909.prdoc index 161898cc019a0..dec199bc3e8f6 100644 --- a/prdoc/pr_9909.prdoc +++ b/prdoc/pr_9909.prdoc @@ -11,9 +11,6 @@ doc: 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. - - ## TODO - - [x] Add tests to make sure the hooks functionality does not regress. crates: - name: pallet-revive bump: minor From c63805e3098fdb9744f266e4f4197bf3d16ce379 Mon Sep 17 00:00:00 2001 From: Alexandru Gheorghe Date: Tue, 28 Oct 2025 13:52:39 +0200 Subject: [PATCH 13/14] address review feedback Signed-off-by: Alexandru Gheorghe --- substrate/frame/revive/src/exec.rs | 9 +++++++-- substrate/frame/revive/src/mock.rs | 4 ++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/substrate/frame/revive/src/exec.rs b/substrate/frame/revive/src/exec.rs index 9abe9b44dff23..60901419c742d 100644 --- a/substrate/frame/revive/src/exec.rs +++ b/substrate/frame/revive/src/exec.rs @@ -2009,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()) diff --git a/substrate/frame/revive/src/mock.rs b/substrate/frame/revive/src/mock.rs index e6af81b333874..c4d7a76018494 100644 --- a/substrate/frame/revive/src/mock.rs +++ b/substrate/frame/revive/src/mock.rs @@ -41,6 +41,10 @@ pub trait MockHandler { /// 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 From fc05c4614a612eb36974cfc808bda71d465f9e68 Mon Sep 17 00:00:00 2001 From: Alexandru Gheorghe Date: Tue, 28 Oct 2025 13:58:16 +0200 Subject: [PATCH 14/14] make semver happy Signed-off-by: Alexandru Gheorghe --- prdoc/pr_9909.prdoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/prdoc/pr_9909.prdoc b/prdoc/pr_9909.prdoc index dec199bc3e8f6..8a6d6ae6b4b34 100644 --- a/prdoc/pr_9909.prdoc +++ b/prdoc/pr_9909.prdoc @@ -13,4 +13,4 @@ doc: 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: minor + bump: major