Skip to content

Commit 8a710a0

Browse files
yash-atreyaYash AtreyazerosnacksmattsseDaniPopes
authored
feat(invariants): use SlotIdentifier for identifying complex types and sampling them (#11450)
* feat(`forge`): sample typed storage values * arc it * nit * clippy * nit * strip file prefixes * fmt * don't add adjacent values to sample * feat(cheatcodes): add contract identifier to AccountStateDiffs * forge fmt * doc nits * fix tests * feat(`cheatcodes`): include `SlotInfo` in SlotStateDiff * cleanup + identify slots of static arrays * nits * nit * nits * test + nits * docs * handle 2d arrays * use DynSolType * feat: decode storage values * doc nit * skip decoded serialization if none * nit * fmt * fix * fix * fix * feat(cheatcodes): decode structs in state diff output * fix * while decode * fix: show only decoded in plaintext / display output + test * feat: format slots to only significant bits in vm.getStateDiff output * encode_prefixed * nit * chore: add @onbjerg to `CODEOWNERS` (#11343) * add @onbjerg * add @0xrusowsky * resolve conflicts * fix: disable tx gas limit cap (#11347) * chore(deps): bump all dependencies (#11349) * chore: use get_or_calculate_hash better (#11350) * resolve more conflicts * fix(lint): 'unwrapped-modifier-logic' incorrectly marked with `Severity::Gas` (#11358) fix(lint): 'unwrapped-modifier-logic' incorrectly marked with Severity::Gas * feat: identify and decode nested structs * cleanup * decode structs and members recursively * cleanup * doc fix * feat(cheatcodes): decode mappings in state diffs (#11381) * feat(cheatcodes): decode mappings in state diffs * feat: decode nested mappings * assert vm.getStateDiff output * feat: add `keys` fields to `SlotInfo` in case of mappings * remove wrapper * refactor: moves state diff decoding to common (#11413) * refactor: storage decoder * cleanup * dedup MappingSlots by moving it to common * move decoding logic into SlotInfo * rename to SlotIndentifier * docs * fix: delegate identification according to encoding types * clippy + fmt * docs fix * fix * merge match arms * merge ifs * recurse handle_struct * dedup assertContains test util * fix * Update crates/common/src/slot_identifier.rs Co-authored-by: zerosnacks <[email protected]> * Review changes: simplify get or insert, use common fmt * alloy-dyn-abi.workspace * identify slot types using `SlotIdentifier` * clippy * feat(`invariants`): record mapping keys and slots to identify their types for sampling * fix * only insert if value decodes * tracing::info logs * remove logs * nit * rm --------- Co-authored-by: Yash Atreya <[email protected]> Co-authored-by: zerosnacks <[email protected]> Co-authored-by: Matthias Seitz <[email protected]> Co-authored-by: DaniPopes <[email protected]> Co-authored-by: srdtrk <[email protected]> Co-authored-by: 0xrusowsky <[email protected]> Co-authored-by: grandizzy <[email protected]> Co-authored-by: grandizzy <[email protected]>
1 parent c6df68f commit 8a710a0

File tree

9 files changed

+126
-159
lines changed

9 files changed

+126
-159
lines changed

Cargo.lock

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

crates/cheatcodes/src/evm/mapping.rs

Lines changed: 1 addition & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,7 @@
11
use crate::{Cheatcode, Cheatcodes, Result, Vm::*};
2-
use alloy_primitives::{Address, B256, U256, keccak256, map::AddressHashMap};
2+
use alloy_primitives::{Address, B256};
33
use alloy_sol_types::SolValue;
44
use foundry_common::mapping_slots::MappingSlots;
5-
use revm::{
6-
bytecode::opcode,
7-
interpreter::{Interpreter, interpreter_types::Jumps},
8-
};
95

106
impl Cheatcode for startMappingRecordingCall {
117
fn apply(&self, state: &mut Cheatcodes) -> Result {
@@ -74,29 +70,3 @@ fn slot_child<'a>(
7470
) -> Option<&'a Vec<B256>> {
7571
mapping_slot(state, target)?.children.get(slot)
7672
}
77-
78-
#[cold]
79-
pub(crate) fn step(mapping_slots: &mut AddressHashMap<MappingSlots>, interpreter: &Interpreter) {
80-
match interpreter.bytecode.opcode() {
81-
opcode::KECCAK256 => {
82-
if interpreter.stack.peek(1) == Ok(U256::from(0x40)) {
83-
let address = interpreter.input.target_address;
84-
let offset = interpreter.stack.peek(0).expect("stack size > 1").saturating_to();
85-
let data = interpreter.memory.slice_len(offset, 0x40);
86-
let low = B256::from_slice(&data[..0x20]);
87-
let high = B256::from_slice(&data[0x20..]);
88-
let result = keccak256(&*data);
89-
90-
mapping_slots.entry(address).or_default().seen_sha3.insert(result, (low, high));
91-
}
92-
}
93-
opcode::SSTORE => {
94-
if let Some(mapping_slots) = mapping_slots.get_mut(&interpreter.input.target_address)
95-
&& let Ok(slot) = interpreter.stack.peek(0)
96-
{
97-
mapping_slots.insert(slot.into());
98-
}
99-
}
100-
_ => {}
101-
}
102-
}

crates/cheatcodes/src/inspector.rs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use crate::{
44
CheatsConfig, CheatsCtxt, DynCheatcode, Error, Result,
55
Vm::{self, AccountAccess},
66
evm::{
7-
DealRecord, GasRecord, RecordAccess, mapping,
7+
DealRecord, GasRecord, RecordAccess,
88
mock::{MockCallDataContext, MockCallReturnData},
99
prank::Prank,
1010
},
@@ -33,7 +33,9 @@ use alloy_rpc_types::{
3333
};
3434
use alloy_sol_types::{SolCall, SolInterface, SolValue};
3535
use foundry_common::{
36-
SELECTOR_LEN, TransactionMaybeSigned, evm::Breakpoints, mapping_slots::MappingSlots,
36+
SELECTOR_LEN, TransactionMaybeSigned,
37+
evm::Breakpoints,
38+
mapping_slots::{MappingSlots, step as mapping_step},
3739
};
3840
use foundry_evm_core::{
3941
InspectorExt,
@@ -1103,7 +1105,7 @@ impl Inspector<EthEvmContext<&mut dyn DatabaseExt>> for Cheatcodes {
11031105

11041106
// `startMappingRecording`: record SSTORE and KECCAK256.
11051107
if let Some(mapping_slots) = &mut self.mapping_slots {
1106-
mapping::step(mapping_slots, interpreter);
1108+
mapping_step(mapping_slots, interpreter);
11071109
}
11081110

11091111
// `snapshotGas*`: take a snapshot of the current gas.

crates/common/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ alloy-transport.workspace = true
4444
alloy-consensus = { workspace = true, features = ["k256"] }
4545
alloy-network.workspace = true
4646

47+
revm.workspace = true
48+
4749
solar.workspace = true
4850

4951
tower.workspace = true

crates/common/src/mapping_slots.rs

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
1-
use alloy_primitives::{B256, map::B256HashMap};
1+
use alloy_primitives::{
2+
B256, U256, keccak256,
3+
map::{AddressHashMap, B256HashMap},
4+
};
5+
use revm::{
6+
bytecode::opcode,
7+
interpreter::{Interpreter, interpreter_types::Jumps},
8+
};
29

310
/// Recorded mapping slots.
411
#[derive(Clone, Debug, Default)]
@@ -36,3 +43,30 @@ impl MappingSlots {
3643
}
3744
}
3845
}
46+
47+
/// Function to be used in Inspector::step to record mapping slots and keys
48+
#[cold]
49+
pub fn step(mapping_slots: &mut AddressHashMap<MappingSlots>, interpreter: &Interpreter) {
50+
match interpreter.bytecode.opcode() {
51+
opcode::KECCAK256 => {
52+
if interpreter.stack.peek(1) == Ok(U256::from(0x40)) {
53+
let address = interpreter.input.target_address;
54+
let offset = interpreter.stack.peek(0).expect("stack size > 1").saturating_to();
55+
let data = interpreter.memory.slice_len(offset, 0x40);
56+
let low = B256::from_slice(&data[..0x20]);
57+
let high = B256::from_slice(&data[0x20..]);
58+
let result = keccak256(&*data);
59+
60+
mapping_slots.entry(address).or_default().seen_sha3.insert(result, (low, high));
61+
}
62+
}
63+
opcode::SSTORE => {
64+
if let Some(mapping_slots) = mapping_slots.get_mut(&interpreter.input.target_address)
65+
&& let Ok(slot) = interpreter.stack.peek(0)
66+
{
67+
mapping_slots.insert(slot.into());
68+
}
69+
}
70+
_ => {}
71+
}
72+
}

crates/common/src/slot_identifier.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,7 @@ impl SlotIdentifier {
197197
///
198198
/// It can also identify whether a slot belongs to a mapping if provided with [`MappingSlots`].
199199
pub fn identify(&self, slot: &B256, mapping_slots: Option<&MappingSlots>) -> Option<SlotInfo> {
200+
trace!(?slot, "identifying slot");
200201
let slot_u256 = U256::from_be_bytes(slot.0);
201202
let slot_str = slot_u256.to_string();
202203

@@ -629,7 +630,7 @@ impl SlotIdentifier {
629630

630631
// Check if the value is another mapping (nested case)
631632
if let Some(value_storage_type) = self.storage_layout.types.get(value_type_ref) {
632-
if value_storage_type.encoding == "mapping" {
633+
if value_storage_type.encoding == ENCODING_MAPPING {
633634
// Recursively resolve the nested mapping
634635
let (nested_keys, final_value, _) = self.resolve_mapping_type(value_type_ref)?;
635636
key_types.extend(nested_keys);

crates/evm/evm/src/executors/invariant/mod.rs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@ use crate::{
22
executors::{Executor, RawCallResult},
33
inspectors::Fuzzer,
44
};
5-
use alloy_primitives::{Address, Bytes, FixedBytes, Selector, U256, map::HashMap};
5+
use alloy_primitives::{
6+
Address, Bytes, FixedBytes, Selector, U256,
7+
map::{AddressMap, HashMap},
8+
};
69
use alloy_sol_types::{SolCall, sol};
710
use eyre::{ContextCompat, Result, eyre};
811
use foundry_common::contracts::{ContractsByAddress, ContractsByArtifact};
@@ -598,6 +601,15 @@ impl<'a> InvariantExecutor<'a> {
598601
));
599602
}
600603

604+
// If any of the targeted contracts have the storage layout enabled then we can sample
605+
// mapping values. To accomplish, we need to record the mapping storage slots and keys.
606+
let fuzz_state =
607+
if targeted_contracts.targets.lock().iter().any(|(_, t)| t.storage_layout.is_some()) {
608+
fuzz_state.with_mapping_slots(AddressMap::default())
609+
} else {
610+
fuzz_state
611+
};
612+
601613
self.executor.inspector_mut().set_fuzzer(Fuzzer {
602614
call_generator,
603615
fuzz_state: fuzz_state.clone(),

crates/evm/fuzz/src/inspector.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use crate::{invariant::RandomCallGenerator, strategies::EvmFuzzState};
2+
use foundry_common::mapping_slots::step as mapping_step;
23
use revm::{
34
Inspector,
45
context::{ContextTr, Transaction},
@@ -26,6 +27,9 @@ where
2627
// We only collect `stack` and `memory` data before and after calls.
2728
if self.collect {
2829
self.collect_data(interp);
30+
if let Some(mapping_slots) = &mut self.fuzz_state.mapping_slots {
31+
mapping_step(mapping_slots, interp);
32+
}
2933
}
3034
}
3135

0 commit comments

Comments
 (0)