Skip to content

Commit d663f38

Browse files
grandizzyDaniPopes
andauthored
feat(cheatcodes): additional cheatcodes to aid in symbolic testing (#8807)
* feat(cheatcodes): additional cheatcodes to aid in symbolic testing * Support copies from arbitrary storage, docs * Changes after review: - separate cheatcodes tests with specific seed - better way to match mocked function - arbitrary_storage_end instead multiple calls - generate arbitrary value only when needed * Update crates/cheatcodes/src/utils.rs Co-authored-by: DaniPopes <[email protected]> * Fix tests with isolate-by-default --------- Co-authored-by: DaniPopes <[email protected]>
1 parent 2c73013 commit d663f38

File tree

12 files changed

+667
-8
lines changed

12 files changed

+667
-8
lines changed

crates/cheatcodes/assets/cheatcodes.json

Lines changed: 60 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: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -473,6 +473,15 @@ interface Vm {
473473
function mockCallRevert(address callee, uint256 msgValue, bytes calldata data, bytes calldata revertData)
474474
external;
475475

476+
/// Whenever a call is made to `callee` with calldata `data`, this cheatcode instead calls
477+
/// `target` with the same calldata. This functionality is similar to a delegate call made to
478+
/// `target` contract from `callee`.
479+
/// Can be used to substitute a call to a function with another implementation that captures
480+
/// the primary logic of the original function but is easier to reason about.
481+
/// If calldata is not a strict match then partial match by selector is attempted.
482+
#[cheatcode(group = Evm, safety = Unsafe)]
483+
function mockFunction(address callee, address target, bytes calldata data) external;
484+
476485
// --- Impersonation (pranks) ---
477486

478487
/// Sets the *next* call's `msg.sender` to be the input address.
@@ -2303,6 +2312,14 @@ interface Vm {
23032312
/// Unpauses collection of call traces.
23042313
#[cheatcode(group = Utilities)]
23052314
function resumeTracing() external view;
2315+
2316+
/// Utility cheatcode to copy storage of `from` contract to another `to` contract.
2317+
#[cheatcode(group = Utilities)]
2318+
function copyStorage(address from, address to) external;
2319+
2320+
/// Utility cheatcode to set arbitrary storage for given target address.
2321+
#[cheatcode(group = Utilities)]
2322+
function setArbitraryStorage(address target) external;
23062323
}
23072324
}
23082325

crates/cheatcodes/src/evm.rs

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ use foundry_evm_core::{
1313
backend::{DatabaseExt, RevertSnapshotAction},
1414
constants::{CALLER, CHEATCODE_ADDRESS, HARDHAT_CONSOLE_ADDRESS, TEST_CONTRACT_ADDRESS},
1515
};
16+
use rand::Rng;
1617
use revm::{
1718
primitives::{Account, Bytecode, SpecId, KECCAK_EMPTY},
1819
InnerEvmContext,
@@ -89,7 +90,25 @@ impl Cheatcode for loadCall {
8990
let Self { target, slot } = *self;
9091
ensure_not_precompile!(&target, ccx);
9192
ccx.ecx.load_account(target)?;
92-
let val = ccx.ecx.sload(target, slot.into())?;
93+
let mut val = ccx.ecx.sload(target, slot.into())?;
94+
95+
if val.is_cold && val.data.is_zero() {
96+
if ccx.state.arbitrary_storage.is_arbitrary(&target) {
97+
// If storage slot is untouched and load from a target with arbitrary storage,
98+
// then set random value for current slot.
99+
let rand_value = ccx.state.rng().gen();
100+
ccx.state.arbitrary_storage.save(ccx.ecx, target, slot.into(), rand_value);
101+
val.data = rand_value;
102+
} else if ccx.state.arbitrary_storage.is_copy(&target) {
103+
// If storage slot is untouched and load from a target that copies storage from
104+
// a source address with arbitrary storage, then copy existing arbitrary value.
105+
// If no arbitrary value generated yet, then the random one is saved and set.
106+
let rand_value = ccx.state.rng().gen();
107+
val.data =
108+
ccx.state.arbitrary_storage.copy(ccx.ecx, target, slot.into(), rand_value);
109+
}
110+
}
111+
93112
Ok(val.abi_encode())
94113
}
95114
}

crates/cheatcodes/src/evm/mock.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,15 @@ impl Cheatcode for mockCallRevert_1Call {
8989
}
9090
}
9191

92+
impl Cheatcode for mockFunctionCall {
93+
fn apply(&self, state: &mut Cheatcodes) -> Result {
94+
let Self { callee, target, data } = self;
95+
state.mocked_functions.entry(*callee).or_default().insert(data.clone(), *target);
96+
97+
Ok(Default::default())
98+
}
99+
}
100+
92101
#[allow(clippy::ptr_arg)] // Not public API, doesn't matter
93102
fn mock_call(
94103
state: &mut Cheatcodes,

crates/cheatcodes/src/inspector.rs

Lines changed: 138 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ use revm::{
4141
EOFCreateInputs, EOFCreateKind, Gas, InstructionResult, Interpreter, InterpreterAction,
4242
InterpreterResult,
4343
},
44-
primitives::{BlockEnv, CreateScheme, EVMError, SpecId, EOF_MAGIC_BYTES},
44+
primitives::{BlockEnv, CreateScheme, EVMError, EvmStorageSlot, SpecId, EOF_MAGIC_BYTES},
4545
EvmContext, InnerEvmContext, Inspector,
4646
};
4747
use rustc_hash::FxHashMap;
@@ -254,6 +254,89 @@ impl GasMetering {
254254
}
255255
}
256256

257+
/// Holds data about arbitrary storage.
258+
#[derive(Clone, Debug, Default)]
259+
pub struct ArbitraryStorage {
260+
/// Mapping of arbitrary storage addresses to generated values (slot, arbitrary value).
261+
/// (SLOADs return random value if storage slot wasn't accessed).
262+
/// Changed values are recorded and used to copy storage to different addresses.
263+
pub values: HashMap<Address, HashMap<U256, U256>>,
264+
/// Mapping of address with storage copied to arbitrary storage address source.
265+
pub copies: HashMap<Address, Address>,
266+
}
267+
268+
impl ArbitraryStorage {
269+
/// Whether the given address has arbitrary storage.
270+
pub fn is_arbitrary(&self, address: &Address) -> bool {
271+
self.values.contains_key(address)
272+
}
273+
274+
/// Whether the given address is a copy of an address with arbitrary storage.
275+
pub fn is_copy(&self, address: &Address) -> bool {
276+
self.copies.contains_key(address)
277+
}
278+
279+
/// Marks an address with arbitrary storage.
280+
pub fn mark_arbitrary(&mut self, address: &Address) {
281+
self.values.insert(*address, HashMap::default());
282+
}
283+
284+
/// Maps an address that copies storage with the arbitrary storage address.
285+
pub fn mark_copy(&mut self, from: &Address, to: &Address) {
286+
if self.is_arbitrary(from) {
287+
self.copies.insert(*to, *from);
288+
}
289+
}
290+
291+
/// Saves arbitrary storage value for a given address:
292+
/// - store value in changed values cache.
293+
/// - update account's storage with given value.
294+
pub fn save<DB: DatabaseExt>(
295+
&mut self,
296+
ecx: &mut InnerEvmContext<DB>,
297+
address: Address,
298+
slot: U256,
299+
data: U256,
300+
) {
301+
self.values.get_mut(&address).expect("missing arbitrary address entry").insert(slot, data);
302+
if let Ok(mut account) = ecx.load_account(address) {
303+
account.storage.insert(slot, EvmStorageSlot::new(data));
304+
}
305+
}
306+
307+
/// Copies arbitrary storage value from source address to the given target address:
308+
/// - if a value is present in arbitrary values cache, then update target storage and return
309+
/// existing value.
310+
/// - if no value was yet generated for given slot, then save new value in cache and update both
311+
/// source and target storages.
312+
pub fn copy<DB: DatabaseExt>(
313+
&mut self,
314+
ecx: &mut InnerEvmContext<DB>,
315+
target: Address,
316+
slot: U256,
317+
new_value: U256,
318+
) -> U256 {
319+
let source = self.copies.get(&target).expect("missing arbitrary copy target entry");
320+
let storage_cache = self.values.get_mut(source).expect("missing arbitrary source storage");
321+
let value = match storage_cache.get(&slot) {
322+
Some(value) => *value,
323+
None => {
324+
storage_cache.insert(slot, new_value);
325+
// Update source storage with new value.
326+
if let Ok(mut source_account) = ecx.load_account(*source) {
327+
source_account.storage.insert(slot, EvmStorageSlot::new(new_value));
328+
}
329+
new_value
330+
}
331+
};
332+
// Update target storage with new value.
333+
if let Ok(mut target_account) = ecx.load_account(target) {
334+
target_account.storage.insert(slot, EvmStorageSlot::new(value));
335+
}
336+
value
337+
}
338+
}
339+
257340
/// List of transactions that can be broadcasted.
258341
pub type BroadcastableTransactions = VecDeque<BroadcastableTransaction>;
259342

@@ -320,6 +403,9 @@ pub struct Cheatcodes {
320403
// **Note**: inner must a BTreeMap because of special `Ord` impl for `MockCallDataContext`
321404
pub mocked_calls: HashMap<Address, BTreeMap<MockCallDataContext, MockCallReturnData>>,
322405

406+
/// Mocked functions. Maps target address to be mocked to pair of (calldata, mock address).
407+
pub mocked_functions: HashMap<Address, HashMap<Bytes, Address>>,
408+
323409
/// Expected calls
324410
pub expected_calls: ExpectedCallTracker,
325411
/// Expected emits
@@ -368,6 +454,9 @@ pub struct Cheatcodes {
368454

369455
/// Ignored traces.
370456
pub ignored_traces: IgnoredTraces,
457+
458+
/// Addresses with arbitrary storage.
459+
pub arbitrary_storage: ArbitraryStorage,
371460
}
372461

373462
// This is not derived because calling this in `fn new` with `..Default::default()` creates a second
@@ -396,6 +485,7 @@ impl Cheatcodes {
396485
recorded_account_diffs_stack: Default::default(),
397486
recorded_logs: Default::default(),
398487
mocked_calls: Default::default(),
488+
mocked_functions: Default::default(),
399489
expected_calls: Default::default(),
400490
expected_emits: Default::default(),
401491
allowed_mem_writes: Default::default(),
@@ -410,6 +500,7 @@ impl Cheatcodes {
410500
breakpoints: Default::default(),
411501
rng: Default::default(),
412502
ignored_traces: Default::default(),
503+
arbitrary_storage: Default::default(),
413504
}
414505
}
415506

@@ -1045,14 +1136,22 @@ impl<DB: DatabaseExt> Inspector<DB> for Cheatcodes {
10451136
}
10461137

10471138
#[inline]
1048-
fn step_end(&mut self, interpreter: &mut Interpreter, _ecx: &mut EvmContext<DB>) {
1139+
fn step_end(&mut self, interpreter: &mut Interpreter, ecx: &mut EvmContext<DB>) {
10491140
if self.gas_metering.paused {
10501141
self.meter_gas_end(interpreter);
10511142
}
10521143

10531144
if self.gas_metering.touched {
10541145
self.meter_gas_check(interpreter);
10551146
}
1147+
1148+
// `setArbitraryStorage` and `copyStorage`: add arbitrary values to storage.
1149+
if (self.arbitrary_storage.is_arbitrary(&interpreter.contract().target_address) ||
1150+
self.arbitrary_storage.is_copy(&interpreter.contract().target_address)) &&
1151+
interpreter.current_opcode() == op::SLOAD
1152+
{
1153+
self.arbitrary_storage_end(interpreter, ecx);
1154+
}
10561155
}
10571156

10581157
fn log(&mut self, interpreter: &mut Interpreter, _ecx: &mut EvmContext<DB>, log: &Log) {
@@ -1465,6 +1564,43 @@ impl Cheatcodes {
14651564
}
14661565
}
14671566

1567+
/// Generates or copies arbitrary values for storage slots.
1568+
/// Invoked in inspector `step_end` (when the current opcode is not executed), if current opcode
1569+
/// to execute is `SLOAD` and storage slot is cold.
1570+
/// Ensures that in next step (when `SLOAD` opcode is executed) an arbitrary value is returned:
1571+
/// - copies the existing arbitrary storage value (or the new generated one if no value in
1572+
/// cache) from mapped source address to the target address.
1573+
/// - generates arbitrary value and saves it in target address storage.
1574+
#[cold]
1575+
fn arbitrary_storage_end<DB: DatabaseExt>(
1576+
&mut self,
1577+
interpreter: &mut Interpreter,
1578+
ecx: &mut EvmContext<DB>,
1579+
) {
1580+
let key = try_or_return!(interpreter.stack().peek(0));
1581+
let target_address = interpreter.contract().target_address;
1582+
if let Ok(value) = ecx.sload(target_address, key) {
1583+
if value.is_cold && value.data.is_zero() {
1584+
let arbitrary_value = self.rng().gen();
1585+
if self.arbitrary_storage.is_copy(&target_address) {
1586+
self.arbitrary_storage.copy(
1587+
&mut ecx.inner,
1588+
target_address,
1589+
key,
1590+
arbitrary_value,
1591+
);
1592+
} else {
1593+
self.arbitrary_storage.save(
1594+
&mut ecx.inner,
1595+
target_address,
1596+
key,
1597+
arbitrary_value,
1598+
);
1599+
}
1600+
}
1601+
}
1602+
}
1603+
14681604
/// Records storage slots reads and writes.
14691605
#[cold]
14701606
fn record_accesses(&mut self, interpreter: &mut Interpreter) {

0 commit comments

Comments
 (0)