Skip to content

Commit 04c2270

Browse files
pallet-revive: add interface to implement mocks and pranks (#9909)
Needed for: paritytech/foundry-polkadot#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. --------- Signed-off-by: Alexandru Gheorghe <[email protected]> Co-authored-by: cmd[bot] <41898282+github-actions[bot]@users.noreply.github.com>
1 parent c483327 commit 04c2270

File tree

12 files changed

+412
-52
lines changed

12 files changed

+412
-52
lines changed

prdoc/pr_9909.prdoc

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
title: 'pallet-revive: add interface to implement mocks and pranks'
2+
doc:
3+
- audience: Node Dev
4+
description: |-
5+
Needed for: https://github.com/paritytech/foundry-polkadot/pull/334.
6+
7+
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.
8+
9+
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.
10+
11+
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.
12+
13+
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.
14+
crates:
15+
- name: pallet-revive
16+
bump: major

substrate/frame/revive/src/call_builder.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ pub struct CallSetup<T: Config> {
5656
value: BalanceOf<T>,
5757
data: Vec<u8>,
5858
transient_storage_size: u32,
59-
exec_config: ExecConfig,
59+
exec_config: ExecConfig<T>,
6060
}
6161

6262
impl<T> Default for CallSetup<T>

substrate/frame/revive/src/exec.rs

Lines changed: 76 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -562,7 +562,7 @@ pub struct Stack<'a, T: Config, E> {
562562
/// Transient storage used to store data, which is kept for the duration of a transaction.
563563
transient_storage: TransientStorage<T>,
564564
/// Global behavior determined by the creater of this stack.
565-
exec_config: &'a ExecConfig,
565+
exec_config: &'a ExecConfig<T>,
566566
/// The set of contracts that were created during this call stack.
567567
contracts_created: BTreeSet<T::AccountId>,
568568
/// The set of contracts that are registered for destruction at the end of this call stack.
@@ -602,7 +602,8 @@ struct Frame<T: Config> {
602602

603603
/// This structure is used to represent the arguments in a delegate call frame in order to
604604
/// distinguish who delegated the call and where it was delegated to.
605-
struct DelegateInfo<T: Config> {
605+
#[derive(Clone)]
606+
pub struct DelegateInfo<T: Config> {
606607
/// The caller of the contract.
607608
pub caller: Origin<T>,
608609
/// The address of the contract the call was delegated to.
@@ -796,7 +797,7 @@ where
796797
storage_meter: &mut storage::meter::Meter<T>,
797798
value: U256,
798799
input_data: Vec<u8>,
799-
exec_config: &ExecConfig,
800+
exec_config: &ExecConfig<T>,
800801
) -> ExecResult {
801802
let dest = T::AddressMapper::to_account_id(&dest);
802803
if let Some((mut stack, executable)) = Stack::<'_, T, E>::new(
@@ -806,6 +807,7 @@ where
806807
storage_meter,
807808
value,
808809
exec_config,
810+
&input_data,
809811
)? {
810812
stack.run(executable, input_data).map(|_| stack.first_frame.last_frame_output)
811813
} else {
@@ -821,14 +823,21 @@ where
821823
);
822824
});
823825

824-
let result = Self::transfer_from_origin(
825-
&origin,
826-
&origin,
827-
&dest,
828-
value,
829-
storage_meter,
830-
exec_config,
831-
);
826+
let result = if let Some(mock_answer) =
827+
exec_config.mock_handler.as_ref().and_then(|handler| {
828+
handler.mock_call(T::AddressMapper::to_address(&dest), &input_data, value)
829+
}) {
830+
Ok(mock_answer)
831+
} else {
832+
Self::transfer_from_origin(
833+
&origin,
834+
&origin,
835+
&dest,
836+
value,
837+
storage_meter,
838+
exec_config,
839+
)
840+
};
832841

833842
if_tracing(|t| match result {
834843
Ok(ref output) => t.exit_child_span(&output, Weight::zero()),
@@ -854,7 +863,7 @@ where
854863
value: U256,
855864
input_data: Vec<u8>,
856865
salt: Option<&[u8; 32]>,
857-
exec_config: &ExecConfig,
866+
exec_config: &ExecConfig<T>,
858867
) -> Result<(H160, ExecReturnValue), ExecError> {
859868
let deployer = T::AddressMapper::to_address(&origin);
860869
let (mut stack, executable) = Stack::<'_, T, E>::new(
@@ -869,6 +878,7 @@ where
869878
storage_meter,
870879
value,
871880
exec_config,
881+
&input_data,
872882
)?
873883
.expect(FRAME_ALWAYS_EXISTS_ON_INSTANTIATE);
874884
let address = T::AddressMapper::to_address(&stack.top_frame().account_id);
@@ -891,7 +901,7 @@ where
891901
gas_meter: &'a mut GasMeter<T>,
892902
storage_meter: &'a mut storage::meter::Meter<T>,
893903
value: BalanceOf<T>,
894-
exec_config: &'a ExecConfig,
904+
exec_config: &'a ExecConfig<T>,
895905
) -> (Self, E) {
896906
let call = Self::new(
897907
FrameArgs::Call {
@@ -904,6 +914,7 @@ where
904914
storage_meter,
905915
value.into(),
906916
exec_config,
917+
&Default::default(),
907918
)
908919
.unwrap()
909920
.unwrap();
@@ -920,7 +931,8 @@ where
920931
gas_meter: &'a mut GasMeter<T>,
921932
storage_meter: &'a mut storage::meter::Meter<T>,
922933
value: U256,
923-
exec_config: &'a ExecConfig,
934+
exec_config: &'a ExecConfig<T>,
935+
input_data: &Vec<u8>,
924936
) -> Result<Option<(Self, ExecutableOrPrecompile<T, E, Self>)>, ExecError> {
925937
origin.ensure_mapped()?;
926938
let Some((first_frame, executable)) = Self::new_frame(
@@ -932,6 +944,8 @@ where
932944
BalanceOf::<T>::max_value(),
933945
false,
934946
true,
947+
input_data,
948+
exec_config,
935949
)?
936950
else {
937951
return Ok(None);
@@ -968,6 +982,8 @@ where
968982
deposit_limit: BalanceOf<T>,
969983
read_only: bool,
970984
origin_is_caller: bool,
985+
input_data: &[u8],
986+
exec_config: &ExecConfig<T>,
971987
) -> Result<Option<(Frame<T>, ExecutableOrPrecompile<T, E, Self>)>, ExecError> {
972988
let (account_id, contract_info, executable, delegate, entry_point) = match frame_args {
973989
FrameArgs::Call { dest, cached_info, delegated_call } => {
@@ -996,6 +1012,11 @@ where
9961012
(None, Some(_)) => CachedContract::None,
9971013
};
9981014

1015+
let delegated_call = delegated_call.or_else(|| {
1016+
exec_config.mock_handler.as_ref().and_then(|mock_handler| {
1017+
mock_handler.mock_delegated_caller(address, input_data)
1018+
})
1019+
});
9991020
// in case of delegate the executable is not the one at `address`
10001021
let executable = if let Some(delegated_call) = &delegated_call {
10011022
if let Some(precompile) =
@@ -1090,6 +1111,7 @@ where
10901111
gas_limit: Weight,
10911112
deposit_limit: BalanceOf<T>,
10921113
read_only: bool,
1114+
input_data: &[u8],
10931115
) -> Result<Option<ExecutableOrPrecompile<T, E, Self>>, ExecError> {
10941116
if self.frames.len() as u32 == limits::CALL_STACK_DEPTH {
10951117
return Err(Error::<T>::MaxCallDepthReached.into());
@@ -1121,6 +1143,8 @@ where
11211143
deposit_limit,
11221144
read_only,
11231145
false,
1146+
input_data,
1147+
self.exec_config,
11241148
)? {
11251149
self.frames.try_push(frame).map_err(|_| Error::<T>::MaxCallDepthReached)?;
11261150
Ok(Some(executable))
@@ -1152,7 +1176,17 @@ where
11521176
frame.nested_gas.gas_left(),
11531177
);
11541178
});
1155-
1179+
let mock_answer = self.exec_config.mock_handler.as_ref().and_then(|handler| {
1180+
handler.mock_call(
1181+
frame
1182+
.delegate
1183+
.as_ref()
1184+
.map(|delegate| delegate.callee)
1185+
.unwrap_or(T::AddressMapper::to_address(&frame.account_id)),
1186+
&input_data,
1187+
frame.value_transferred,
1188+
)
1189+
});
11561190
// The output of the caller frame will be replaced by the output of this run.
11571191
// It is also not accessible from nested frames.
11581192
// Hence we drop it early to save the memory.
@@ -1335,7 +1369,11 @@ where
13351369
// transactional storage depth.
13361370
let transaction_outcome =
13371371
with_transaction(|| -> TransactionOutcome<Result<_, DispatchError>> {
1338-
let output = do_transaction();
1372+
let output = if let Some(mock_answer) = mock_answer {
1373+
Ok(mock_answer)
1374+
} else {
1375+
do_transaction()
1376+
};
13391377
match &output {
13401378
Ok(result) if !result.did_revert() =>
13411379
TransactionOutcome::Commit(Ok((true, output))),
@@ -1481,7 +1519,7 @@ where
14811519
to: &T::AccountId,
14821520
value: U256,
14831521
storage_meter: &mut storage::meter::GenericMeter<T, S>,
1484-
exec_config: &ExecConfig,
1522+
exec_config: &ExecConfig<T>,
14851523
) -> DispatchResult {
14861524
fn transfer_with_dust<T: Config>(
14871525
from: &AccountIdOf<T>,
@@ -1592,7 +1630,7 @@ where
15921630
to: &T::AccountId,
15931631
value: U256,
15941632
storage_meter: &mut storage::meter::GenericMeter<T, S>,
1595-
exec_config: &ExecConfig,
1633+
exec_config: &ExecConfig<T>,
15961634
) -> ExecResult {
15971635
// If the from address is root there is no account to transfer from, and therefore we can't
15981636
// take any `value` other than 0.
@@ -1742,6 +1780,7 @@ where
17421780
gas_limit,
17431781
deposit_limit.saturated_into::<BalanceOf<T>>(),
17441782
self.is_read_only(),
1783+
&input_data,
17451784
)? {
17461785
self.run(executable, input_data)
17471786
} else {
@@ -1883,6 +1922,7 @@ where
18831922
gas_limit,
18841923
deposit_limit.saturated_into::<BalanceOf<T>>(),
18851924
self.is_read_only(),
1925+
&input_data,
18861926
)?
18871927
};
18881928
let executable = executable.expect(FRAME_ALWAYS_EXISTS_ON_INSTANTIATE);
@@ -1954,6 +1994,7 @@ where
19541994
gas_limit,
19551995
deposit_limit.saturated_into::<BalanceOf<T>>(),
19561996
is_read_only,
1997+
&input_data,
19571998
)? {
19581999
self.run(executable, input_data)
19592000
} else {
@@ -1968,8 +2009,13 @@ where
19682009
Weight::zero(),
19692010
);
19702011
});
1971-
1972-
let result = if is_read_only && value.is_zero() {
2012+
let result = if let Some(mock_answer) =
2013+
self.exec_config.mock_handler.as_ref().and_then(|handler| {
2014+
handler.mock_call(T::AddressMapper::to_address(&dest), &input_data, value)
2015+
}) {
2016+
*self.last_frame_output_mut() = mock_answer.clone();
2017+
Ok(mock_answer)
2018+
} else if is_read_only && value.is_zero() {
19732019
Ok(Default::default())
19742020
} else if is_read_only {
19752021
Err(Error::<T>::StateChangeDenied.into())
@@ -2029,6 +2075,16 @@ where
20292075
}
20302076

20312077
fn caller(&self) -> Origin<T> {
2078+
if let Some(Ok(mock_caller)) = self
2079+
.exec_config
2080+
.mock_handler
2081+
.as_ref()
2082+
.and_then(|mock_handler| mock_handler.mock_caller(self.frames.len()))
2083+
.map(|mock_caller| Origin::<T>::from_runtime_origin(mock_caller))
2084+
{
2085+
return mock_caller;
2086+
}
2087+
20322088
if let Some(DelegateInfo { caller, .. }) = &self.top_frame().delegate {
20332089
caller.clone()
20342090
} else {

substrate/frame/revive/src/lib.rs

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ mod weightinfo_extension;
4040

4141
pub mod evm;
4242
pub mod migrations;
43+
pub mod mock;
4344
pub mod precompiles;
4445
pub mod test_utils;
4546
pub mod tracing;
@@ -53,11 +54,11 @@ use crate::{
5354
runtime::SetWeightLimit,
5455
CallTracer, GenericTransaction, PrestateTracer, Trace, Tracer, TracerType, TYPE_EIP1559,
5556
},
56-
exec::{AccountIdOf, ExecError, Executable, Stack as ExecStack},
57+
exec::{AccountIdOf, ExecError, Stack as ExecStack},
5758
gas::GasMeter,
5859
storage::{meter::Meter as StorageMeter, AccountType, DeletionQueueManager},
5960
tracing::if_tracing,
60-
vm::{pvm::extract_code_and_data, CodeInfo, ContractBlob, RuntimeCosts},
61+
vm::{pvm::extract_code_and_data, CodeInfo, RuntimeCosts},
6162
weightinfo_extension::OnFinalizeBlockParts,
6263
};
6364
use alloc::{boxed::Box, format, vec};
@@ -95,9 +96,10 @@ pub use crate::{
9596
},
9697
debug::DebugSettings,
9798
evm::{block_hash::ReceiptGasInfo, Address as EthAddress, Block as EthBlock, ReceiptInfo},
98-
exec::{Key, MomentOf, Origin as ExecOrigin},
99+
exec::{DelegateInfo, Executable, Key, MomentOf, Origin as ExecOrigin},
99100
pallet::{genesis, *},
100101
storage::{AccountInfo, ContractInfo},
102+
vm::ContractBlob,
101103
};
102104
pub use codec;
103105
pub use frame_support::{self, dispatch::DispatchInfo, weights::Weight};
@@ -1491,7 +1493,7 @@ impl<T: Config> Pallet<T> {
14911493
gas_limit: Weight,
14921494
storage_deposit_limit: BalanceOf<T>,
14931495
data: Vec<u8>,
1494-
exec_config: ExecConfig,
1496+
exec_config: ExecConfig<T>,
14951497
) -> ContractResult<ExecReturnValue, BalanceOf<T>> {
14961498
let mut gas_meter = GasMeter::new(gas_limit);
14971499
let mut storage_deposit = Default::default();
@@ -1547,7 +1549,7 @@ impl<T: Config> Pallet<T> {
15471549
code: Code,
15481550
data: Vec<u8>,
15491551
salt: Option<[u8; 32]>,
1550-
exec_config: ExecConfig,
1552+
exec_config: ExecConfig<T>,
15511553
) -> ContractResult<InstantiateReturnValue, BalanceOf<T>> {
15521554
let mut gas_meter = GasMeter::new(gas_limit);
15531555
let mut storage_deposit = Default::default();
@@ -2129,11 +2131,11 @@ impl<T: Config> Pallet<T> {
21292131
}
21302132

21312133
/// Uploads new code and returns the Vm binary contract blob and deposit amount collected.
2132-
fn try_upload_pvm_code(
2134+
pub fn try_upload_pvm_code(
21332135
origin: T::AccountId,
21342136
code: Vec<u8>,
21352137
storage_deposit_limit: BalanceOf<T>,
2136-
exec_config: &ExecConfig,
2138+
exec_config: &ExecConfig<T>,
21372139
) -> Result<(ContractBlob<T>, BalanceOf<T>), DispatchError> {
21382140
let mut module = ContractBlob::from_pvm_code(code, origin)?;
21392141
let deposit = module.store_code(exec_config, None)?;
@@ -2161,7 +2163,7 @@ impl<T: Config> Pallet<T> {
21612163
}
21622164

21632165
/// Convert a weight to a gas value.
2164-
fn evm_gas_from_weight(weight: Weight) -> U256 {
2166+
pub fn evm_gas_from_weight(weight: Weight) -> U256 {
21652167
T::FeeInfo::weight_to_fee(&weight, Combinator::Max).into()
21662168
}
21672169

@@ -2174,7 +2176,7 @@ impl<T: Config> Pallet<T> {
21742176
from: &T::AccountId,
21752177
to: &T::AccountId,
21762178
amount: BalanceOf<T>,
2177-
exec_config: &ExecConfig,
2179+
exec_config: &ExecConfig<T>,
21782180
) -> DispatchResult {
21792181
use frame_support::traits::tokens::{Fortitude, Precision, Preservation};
21802182
match (exec_config.collect_deposit_from_hold.is_some(), hold_reason) {
@@ -2219,7 +2221,7 @@ impl<T: Config> Pallet<T> {
22192221
from: &T::AccountId,
22202222
to: &T::AccountId,
22212223
amount: BalanceOf<T>,
2222-
exec_config: &ExecConfig,
2224+
exec_config: &ExecConfig<T>,
22232225
) -> Result<BalanceOf<T>, DispatchError> {
22242226
use frame_support::traits::{
22252227
tokens::{Fortitude, Precision, Preservation, Restriction},

0 commit comments

Comments
 (0)