Skip to content

Commit dd5c42a

Browse files
authored
Add polkadotskip cheatcode (#448)
1 parent 2b6ab4d commit dd5c42a

File tree

10 files changed

+185
-4
lines changed

10 files changed

+185
-4
lines changed

crates/cheatcodes/assets/cheatcodes.json

Lines changed: 20 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/cheatcodes/spec/src/vm.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2906,6 +2906,12 @@ interface Vm {
29062906
#[cheatcode(group = Utilities)]
29072907
function pvm(bool enabled) external;
29082908

2909+
/// When running in PVM context, skips the next CREATE or CALL, executing it on the EVM instead.
2910+
/// All `CREATE`s executed within this skip, will automatically have `CALL`s to their target addresses
2911+
/// executed in the EVM, and need not be marked with this cheatcode at every usage location.
2912+
#[cheatcode(group = Testing, safety = Safe)]
2913+
function polkadotSkip() external pure;
2914+
29092915
/// Generates the hash of the canonical EIP-712 type representation.
29102916
///
29112917
/// Supports 2 different inputs:

crates/cheatcodes/src/evm.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -950,6 +950,14 @@ impl Cheatcode for pvmCall {
950950
}
951951
}
952952

953+
impl Cheatcode for polkadotSkipCall {
954+
fn apply_stateful(&self, _ccx: &mut CheatsCtxt) -> Result {
955+
// Does nothing by default.
956+
// PVM-related logic is implemented in the corresponding strategy object.
957+
Ok(Default::default())
958+
}
959+
}
960+
953961
pub(super) fn get_nonce(ccx: &mut CheatsCtxt, address: &Address) -> Result {
954962
let account = ccx.ecx.journaled_state.load_account(*address)?;
955963
Ok(account.info.nonce.abi_encode())

crates/cheatcodes/src/inspector.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -937,6 +937,7 @@ impl Cheatcodes {
937937
}
938938

939939
self.strategy.runner.revive_remove_duplicate_account_access(self);
940+
self.strategy.runner.revive_record_create_address(self, outcome);
940941
}
941942

942943
// Tells whether PVM is enabled or not.

crates/cheatcodes/src/strategy.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,14 @@ pub trait CheatcodeInspectorStrategyExt {
267267

268268
// Remove duplicate accesses in storage_recorder
269269
fn revive_remove_duplicate_account_access(&self, _state: &mut crate::Cheatcodes) {}
270+
271+
// Record create address for skip_pvm_addresses tracking
272+
fn revive_record_create_address(
273+
&self,
274+
_state: &mut crate::Cheatcodes,
275+
_outcome: &revm::interpreter::CreateOutcome,
276+
) {
277+
}
270278
}
271279

272280
// Legacy type aliases for backward compatibility
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
use crate::{config::*, test_helpers::TEST_DATA_REVIVE};
2+
use foundry_test_utils::Filter;
3+
use revive_strategy::ReviveRuntimeMode;
4+
use revm::primitives::hardfork::SpecId;
5+
use rstest::rstest;
6+
7+
#[rstest]
8+
#[case::evm(ReviveRuntimeMode::Evm)]
9+
#[tokio::test(flavor = "multi_thread")]
10+
async fn test_polkadot_skip(#[case] runtime_mode: ReviveRuntimeMode) {
11+
let runner: forge::MultiContractRunner = TEST_DATA_REVIVE.runner_revive(runtime_mode);
12+
let filter = Filter::new(".*", "PolkadotSkipTest", ".*/revive/PolkadotSkip.t.sol");
13+
14+
TestConfig::with_filter(runner, filter).spec_id(SpecId::PRAGUE).run().await;
15+
}

crates/forge/tests/it/revive/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ pub mod cheat_gas_metering;
55
pub mod cheat_mock_call;
66
pub mod cheat_mock_calls;
77
pub mod cheat_mock_functions;
8+
pub mod cheat_polkadot_skip;
89
pub mod cheat_prank;
910
mod cheat_snapshot;
1011
pub mod cheat_store;

crates/revive-strategy/src/cheatcodes/mod.rs

Lines changed: 60 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@ use foundry_cheatcodes::{
88
CheatcodeInspectorStrategyContext, CheatcodeInspectorStrategyRunner, CheatsConfig, CheatsCtxt,
99
CommonCreateInput, Ecx, EvmCheatcodeInspectorStrategyRunner, Result,
1010
Vm::{
11-
chainIdCall, coinbaseCall, dealCall, etchCall, getNonce_0Call, loadCall, pvmCall,
12-
resetNonceCall, revertToStateAndDeleteCall, revertToStateCall, rollCall, setNonceCall,
13-
setNonceUnsafeCall, snapshotStateCall, storeCall, warpCall,
11+
chainIdCall, coinbaseCall, dealCall, etchCall, getNonce_0Call, loadCall, polkadotSkipCall,
12+
pvmCall, resetNonceCall, revertToStateAndDeleteCall, revertToStateCall, rollCall,
13+
setNonceCall, setNonceUnsafeCall, snapshotStateCall, storeCall, warpCall,
1414
},
1515
journaled_account, precompile_error,
1616
};
@@ -42,6 +42,7 @@ use crate::{
4242
tracing::{Tracer, storage_tracer::AccountAccess},
4343
};
4444
use foundry_cheatcodes::Vm::{AccountAccess as FAccountAccess, ChainInfo};
45+
use polkadot_sdk::pallet_revive::tracing::Tracing;
4546

4647
use revm::{
4748
bytecode::opcode as op,
@@ -113,6 +114,14 @@ impl PvmStartupMigration {
113114
pub struct PvmCheatcodeInspectorStrategyContext {
114115
/// Whether we're currently using pallet-revive (migrated from REVM)
115116
pub using_pvm: bool,
117+
/// When in PVM context, execute the next CALL or CREATE in the EVM instead.
118+
pub skip_pvm: bool,
119+
/// Any contracts that were deployed in `skip_pvm` step.
120+
/// This makes it easier to dispatch calls to any of these addresses in PVM context, directly
121+
/// to EVM. Alternatively, we'd need to add `vm.polkadotSkip()` to these calls manually.
122+
pub skip_pvm_addresses: std::collections::HashSet<Address>,
123+
/// Records the next create address for `skip_pvm_addresses`.
124+
pub record_next_create_address: bool,
116125
/// Controls automatic migration to pallet-revive
117126
pub pvm_startup_migration: PvmStartupMigration,
118127
pub dual_compiled_contracts: DualCompiledContracts,
@@ -131,6 +140,9 @@ impl PvmCheatcodeInspectorStrategyContext {
131140
Self {
132141
// Start in REVM mode by default
133142
using_pvm: false,
143+
skip_pvm: false,
144+
skip_pvm_addresses: Default::default(),
145+
record_next_create_address: Default::default(),
134146
// Will be set to Allow when test contract deploys
135147
pvm_startup_migration: PvmStartupMigration::Defer,
136148
dual_compiled_contracts,
@@ -275,6 +287,12 @@ impl CheatcodeInspectorStrategyRunner for PvmCheatcodeInspectorStrategyRunner {
275287
}
276288
Ok(Default::default())
277289
}
290+
t if is::<polkadotSkipCall>(t) => {
291+
let polkadotSkipCall { .. } = cheatcode.as_any().downcast_ref().unwrap();
292+
let ctx = get_context_ref_mut(ccx.state.strategy.context.as_mut());
293+
ctx.skip_pvm = true;
294+
Ok(Default::default())
295+
}
278296
t if using_pvm && is::<dealCall>(t) => {
279297
tracing::info!(cheatcode = ?cheatcode.as_debug() , using_pvm = ?using_pvm);
280298
let dealCall { account, newBalance } = cheatcode.as_any().downcast_ref().unwrap();
@@ -783,6 +801,13 @@ impl foundry_cheatcodes::CheatcodeInspectorStrategyExt for PvmCheatcodeInspector
783801
return None;
784802
}
785803

804+
if ctx.skip_pvm {
805+
ctx.skip_pvm = false; // handled the skip, reset flag
806+
ctx.record_next_create_address = true;
807+
tracing::info!("running create in EVM, instead of pallet-revive (skipped)");
808+
return None;
809+
}
810+
786811
if let Some(CreateScheme::Create) = input.scheme() {
787812
let caller = input.caller();
788813
let nonce = ecx
@@ -832,9 +857,12 @@ impl foundry_cheatcodes::CheatcodeInspectorStrategyExt for PvmCheatcodeInspector
832857
let gas_price_pvm =
833858
sp_core::U256::from_little_endian(&U256::from(ecx.tx.gas_price).as_le_bytes());
834859
let mut tracer = Tracer::new(true);
860+
let caller_h160 = H160::from_slice(input.caller().as_slice());
861+
835862
let res = ctx.externalities.execute_with(|| {
863+
tracer.watch_address(&caller_h160);
864+
836865
tracer.trace(|| {
837-
let caller_h160 = H160::from_slice(input.caller().as_slice());
838866
let origin_account_id = AccountId::to_fallback_account_id(&caller_h160);
839867
let origin = OriginFor::<Runtime>::signed(origin_account_id.clone());
840868
let evm_value = sp_core::U256::from_little_endian(&input.value().as_le_bytes());
@@ -956,6 +984,12 @@ impl foundry_cheatcodes::CheatcodeInspectorStrategyExt for PvmCheatcodeInspector
956984
return None;
957985
}
958986

987+
if ctx.skip_pvm || ctx.skip_pvm_addresses.contains(&call.target_address) {
988+
ctx.skip_pvm = false; // handled the skip, reset flag
989+
tracing::info!("running call in EVM, instead of pallet-revive (skipped)");
990+
return None;
991+
}
992+
959993
if ecx
960994
.journaled_state
961995
.database
@@ -991,6 +1025,9 @@ impl foundry_cheatcodes::CheatcodeInspectorStrategyExt for PvmCheatcodeInspector
9911025

9921026
let mut tracer = Tracer::new(true);
9931027
let res = ctx.externalities.execute_with(|| {
1028+
// Watch the caller's address so its nonce changes get tracked in prestate trace
1029+
tracer.watch_address(&caller_h160);
1030+
9941031
tracer.trace(|| {
9951032
let origin =
9961033
OriginFor::<Runtime>::signed(AccountId::to_fallback_account_id(&caller_h160));
@@ -1121,6 +1158,25 @@ impl foundry_cheatcodes::CheatcodeInspectorStrategyExt for PvmCheatcodeInspector
11211158

11221159
apply_revm_storage_diff(ctx, ecx, call.target_address);
11231160
}
1161+
1162+
fn revive_record_create_address(
1163+
&self,
1164+
state: &mut foundry_cheatcodes::Cheatcodes,
1165+
outcome: &CreateOutcome,
1166+
) {
1167+
let ctx = get_context_ref_mut(state.strategy.context.as_mut());
1168+
1169+
if ctx.record_next_create_address {
1170+
ctx.record_next_create_address = false;
1171+
if let Some(address) = outcome.address {
1172+
ctx.skip_pvm_addresses.insert(address);
1173+
tracing::info!(
1174+
"recorded address {:?} for skip execution in the pallet-revive",
1175+
address
1176+
);
1177+
}
1178+
}
1179+
}
11241180
}
11251181

11261182
fn post_exec(

testdata/cheats/Vm.sol

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
// SPDX-License-Identifier: MIT OR Apache-2.0
2+
pragma solidity ^0.8.18;
3+
4+
import "ds-test/test.sol";
5+
import "cheats/Vm.sol";
6+
7+
contract Calculator {
8+
event Added(uint8 indexed sum);
9+
10+
function add(uint8 a, uint8 b) public returns (uint8) {
11+
uint8 sum = a + b;
12+
emit Added(sum);
13+
return sum;
14+
}
15+
}
16+
17+
contract EvmTargetContract is DSTest {
18+
Vm constant vm = Vm(HEVM_ADDRESS);
19+
20+
event Added(uint8 indexed sum);
21+
22+
function exec() public {
23+
emit Added(3);
24+
25+
Calculator calc = new Calculator();
26+
uint8 sum = calc.add(1, 2);
27+
assertEq(3, sum);
28+
vm.setNonce(address(this), 10);
29+
}
30+
}
31+
32+
contract PolkadotSkipTest is DSTest {
33+
Vm constant vm = Vm(HEVM_ADDRESS);
34+
EvmTargetContract helper;
35+
36+
function setUp() external {
37+
assertEq(vm.getNonce(address(this)), 1);
38+
helper = new EvmTargetContract();
39+
assertEq(vm.getNonce(address(this)), 2);
40+
41+
// ensure we can call cheatcodes from the helper
42+
vm.allowCheatcodes(address(helper));
43+
}
44+
45+
function testUseCheatcodesInEvmWithSkip() external {
46+
vm.polkadotSkip();
47+
helper.exec();
48+
assertEq(vm.getNonce(address(helper)), 10);
49+
}
50+
51+
function testAutoSkipAfterDeployInEvmWithSkip() external {
52+
assertEq(vm.getNonce(address(this)), 2);
53+
vm.polkadotSkip();
54+
EvmTargetContract helper2 = new EvmTargetContract();
55+
// this should auto execute in EVM
56+
helper2.exec();
57+
assertEq(vm.getNonce(address(helper2)), 10);
58+
}
59+
60+
function testreviveWhenUseCheatcodeWithoutSkip() external {
61+
uint256 nonceBefore = vm.getNonce(address(helper));
62+
helper.exec();
63+
assertEq(vm.getNonce(address(helper)), nonceBefore + 1);
64+
}
65+
}

0 commit comments

Comments
 (0)