-
Notifications
You must be signed in to change notification settings - Fork 1.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(cheatcodes): additional cheatcodes to aid in symbolic testing #8807
Changes from 1 commit
10748dc
63de283
16f068f
f366d4d
7e69723
b4f36db
02086f9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -41,7 +41,7 @@ use revm::{ | |
EOFCreateInputs, EOFCreateKind, Gas, InstructionResult, Interpreter, InterpreterAction, | ||
InterpreterResult, | ||
}, | ||
primitives::{BlockEnv, CreateScheme, EVMError, SpecId, EOF_MAGIC_BYTES}, | ||
primitives::{BlockEnv, CreateScheme, EVMError, EvmStorageSlot, SpecId, EOF_MAGIC_BYTES}, | ||
EvmContext, InnerEvmContext, Inspector, | ||
}; | ||
use rustc_hash::FxHashMap; | ||
|
@@ -320,6 +320,9 @@ pub struct Cheatcodes { | |
// **Note**: inner must a BTreeMap because of special `Ord` impl for `MockCallDataContext` | ||
pub mocked_calls: HashMap<Address, BTreeMap<MockCallDataContext, MockCallReturnData>>, | ||
|
||
/// Mocked functions. Maps target address to be mocked to pair of (calldata, mock address). | ||
pub mocked_functions: HashMap<Address, HashMap<Bytes, Address>>, | ||
|
||
/// Expected calls | ||
pub expected_calls: ExpectedCallTracker, | ||
/// Expected emits | ||
|
@@ -368,6 +371,10 @@ pub struct Cheatcodes { | |
|
||
/// Ignored traces. | ||
pub ignored_traces: IgnoredTraces, | ||
|
||
/// Addresses that should have arbitrary storage generated (SLOADs return random value if | ||
/// storage slot wasn't accessed). | ||
pub arbitrary_storage: Vec<Address>, | ||
} | ||
|
||
// This is not derived because calling this in `fn new` with `..Default::default()` creates a second | ||
|
@@ -396,6 +403,7 @@ impl Cheatcodes { | |
recorded_account_diffs_stack: Default::default(), | ||
recorded_logs: Default::default(), | ||
mocked_calls: Default::default(), | ||
mocked_functions: Default::default(), | ||
expected_calls: Default::default(), | ||
expected_emits: Default::default(), | ||
allowed_mem_writes: Default::default(), | ||
|
@@ -410,6 +418,7 @@ impl Cheatcodes { | |
breakpoints: Default::default(), | ||
rng: Default::default(), | ||
ignored_traces: Default::default(), | ||
arbitrary_storage: Default::default(), | ||
} | ||
} | ||
|
||
|
@@ -1045,14 +1054,18 @@ impl<DB: DatabaseExt> Inspector<DB> for Cheatcodes { | |
} | ||
|
||
#[inline] | ||
fn step_end(&mut self, interpreter: &mut Interpreter, _ecx: &mut EvmContext<DB>) { | ||
fn step_end(&mut self, interpreter: &mut Interpreter, ecx: &mut EvmContext<DB>) { | ||
if self.gas_metering.paused { | ||
self.meter_gas_end(interpreter); | ||
} | ||
|
||
if self.gas_metering.touched { | ||
self.meter_gas_check(interpreter); | ||
} | ||
|
||
if self.arbitrary_storage.contains(&interpreter.contract().target_address) { | ||
self.ensure_arbitrary_storage(interpreter, ecx); | ||
} | ||
} | ||
|
||
fn log(&mut self, interpreter: &mut Interpreter, _ecx: &mut EvmContext<DB>, log: &Log) { | ||
|
@@ -1465,6 +1478,26 @@ impl Cheatcodes { | |
} | ||
} | ||
|
||
/// Generates arbitrary values for storage slots. | ||
#[cold] | ||
fn ensure_arbitrary_storage<DB: DatabaseExt>( | ||
&mut self, | ||
interpreter: &mut Interpreter, | ||
ecx: &mut EvmContext<DB>, | ||
) { | ||
if interpreter.current_opcode() == op::SLOAD { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. should move the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. changed in 7e69723 |
||
let key = try_or_return!(interpreter.stack().peek(0)); | ||
let target_address = interpreter.contract().target_address; | ||
if let Ok(value) = ecx.sload(target_address, key) { | ||
if value.is_cold && value.data.is_zero() { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. hmm, I think this would never be cold because revm marks slot as warm during execution of opcode? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. though it looks like tests are passing? would need to look closer There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it might be because we're reading the value instead of a key in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yes, that's my take too, the opcode is not yet executed / slot marked as warm at this point There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ah There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I was thinking that we're entering though can we document what's happening here anyway? especially that this would happen before sload even though we are in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. sure, will add comment to make this clear There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @klkvr please check 16f068f Had to accommodate copy from arbitrary storage scenario pointed here #8807 (comment) too, so a little bit bigger change, ptal. thanks! |
||
if let Ok(mut target_account) = ecx.load_account(target_address) { | ||
target_account.storage.insert(key, EvmStorageSlot::new(self.rng().gen())); | ||
} | ||
} | ||
} | ||
} | ||
} | ||
|
||
/// Records storage slots reads and writes. | ||
#[cold] | ||
fn record_accesses(&mut self, interpreter: &mut Interpreter) { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,6 @@ | ||
//! Implementations of [`Utilities`](spec::Group::Utilities) cheatcodes. | ||
|
||
use crate::{Cheatcode, Cheatcodes, Result, Vm::*}; | ||
use crate::{Cheatcode, Cheatcodes, CheatsCtxt, Result, Vm::*}; | ||
use alloy_primitives::{Address, U256}; | ||
use alloy_sol_types::SolValue; | ||
use foundry_common::ens::namehash; | ||
|
@@ -149,3 +149,25 @@ impl Cheatcode for resumeTracingCall { | |
Ok(Default::default()) | ||
} | ||
} | ||
impl Cheatcode for setArbitraryStorageCall { | ||
grandizzy marked this conversation as resolved.
Show resolved
Hide resolved
|
||
fn apply_stateful<DB: DatabaseExt>(&self, ccx: &mut CheatsCtxt<DB>) -> Result { | ||
let Self { target } = self; | ||
ccx.state.arbitrary_storage.push(*target); | ||
|
||
Ok(Default::default()) | ||
} | ||
} | ||
|
||
impl Cheatcode for copyStorageCall { | ||
fn apply_stateful<DB: DatabaseExt>(&self, ccx: &mut CheatsCtxt<DB>) -> Result { | ||
let Self { from, to } = self; | ||
if let Ok(from_account) = ccx.load_account(*from) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this won't work for forking but Ig it's out of scope? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Indeed, wasn't discussed to work for forking but could be added later if needed |
||
let from_storage = from_account.storage.clone(); | ||
if let Ok(mut to_account) = ccx.load_account(*to) { | ||
to_account.storage = from_storage; | ||
} | ||
} | ||
|
||
Ok(Default::default()) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -726,6 +726,18 @@ impl<'a, DB: DatabaseExt> Inspector<DB> for InspectorStackRefMut<'a> { | |
|
||
ecx.journaled_state.depth += self.in_inner_context as usize; | ||
if let Some(cheatcodes) = self.cheatcodes.as_deref_mut() { | ||
// Handle mocked functions, replace bytecode address with mock if matched. | ||
if let Some(mocks) = cheatcodes.mocked_functions.get(&call.target_address) { | ||
if let Some(target) = mocks.get(&call.input) { | ||
call.bytecode_address = *target; | ||
} else { | ||
// Check if we have a catch-all mock set for selector. | ||
if let Some(target) = mocks.get(&call.input.slice(..4)) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. can collapse into one There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. changed in 7e69723 |
||
call.bytecode_address = *target; | ||
} | ||
} | ||
} | ||
|
||
if let Some(output) = cheatcodes.call_with_executor(ecx, call, self.inner) { | ||
if output.result.result != InstructionResult::Continue { | ||
ecx.journaled_state.depth -= self.in_inner_context as usize; | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7,6 +7,7 @@ use crate::{ | |
TEST_DATA_MULTI_VERSION, | ||
}, | ||
}; | ||
use alloy_primitives::U256; | ||
use foundry_config::{fs_permissions::PathPermission, FsPermissions}; | ||
use foundry_test_utils::Filter; | ||
|
||
|
@@ -26,6 +27,7 @@ async fn test_cheats_local(test_data: &ForgeTestData) { | |
} | ||
|
||
let mut config = test_data.config.clone(); | ||
config.fuzz.seed = Some(U256::from(100)); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. should this be set in test_data? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. good point, not sure we should be adding a new |
||
config.fs_permissions = FsPermissions::new(vec![PathPermission::read_write("./")]); | ||
let runner = test_data.runner_with_config(config); | ||
|
||
|
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what's considered "untouched" here? eg in a single test calling the same contract two times from test scope would result in slot being cold on first call, and warm on second due to how our EVM works. is this expected?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yep, that's the expected behavior, subsequent loads of same slot should return same value
foundry/testdata/default/cheats/ArbitraryStorage.t.sol
Line 108 in 10748dc
unless explicitly rewritten
foundry/testdata/default/cheats/ArbitraryStorage.t.sol
Line 51 in 10748dc