Skip to content

Commit 6fb335d

Browse files
committed
fix(symbolic): replay setup arbitrary storage
Import setup-time vm.setArbitraryStorage targets into symbolic invariant execution and carry model values for concrete storage slots into replay. This lets invariant counterexamples that depend on symbolic storage behind deployed setup dependencies replay against Forge's concrete executor.
1 parent f838097 commit 6fb335d

8 files changed

Lines changed: 235 additions & 26 deletions

File tree

crates/cheatcodes/src/evm.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -288,7 +288,10 @@ impl Cheatcode for loadCall {
288288
if ccx.state.has_arbitrary_storage(&target) {
289289
// If storage slot is untouched and load from a target with arbitrary storage,
290290
// then set random value for current slot.
291-
let rand_value = ccx.state.rng().random();
291+
let rand_value = ccx
292+
.state
293+
.cached_arbitrary_storage_value(target, slot.into())
294+
.unwrap_or_else(|| ccx.state.rng().random());
292295
ccx.state.arbitrary_storage.as_mut().unwrap().save(
293296
ccx.ecx,
294297
target,

crates/cheatcodes/src/inspector.rs

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -403,11 +403,11 @@ pub struct ArbitraryStorage {
403403
/// Mapping of arbitrary storage addresses to generated values (slot, arbitrary value).
404404
/// (SLOADs return random value if storage slot wasn't accessed).
405405
/// Changed values are recorded and used to copy storage to different addresses.
406-
pub values: HashMap<Address, HashMap<U256, U256>>,
406+
values: HashMap<Address, HashMap<U256, U256>>,
407407
/// Mapping of address with storage copied to arbitrary storage address source.
408-
pub copies: HashMap<Address, Address>,
408+
copies: HashMap<Address, Address>,
409409
/// Address with storage slots that should be overwritten even if previously set.
410-
pub overwrites: HashSet<Address>,
410+
overwrites: HashSet<Address>,
411411
}
412412

413413
impl ArbitraryStorage {
@@ -428,6 +428,23 @@ impl ArbitraryStorage {
428428
}
429429
}
430430

431+
/// Returns addresses explicitly marked with arbitrary storage.
432+
fn targets(&self) -> impl Iterator<Item = Address> + '_ {
433+
self.values.keys().copied()
434+
}
435+
436+
/// Caches a concrete value for a slot on an arbitrary-storage address.
437+
fn cache_value(&mut self, address: Address, slot: U256, data: U256) {
438+
if let Some(values) = self.values.get_mut(&address) {
439+
values.insert(slot, data);
440+
}
441+
}
442+
443+
/// Returns a cached arbitrary value for a slot.
444+
fn cached_value(&self, address: Address, slot: U256) -> Option<U256> {
445+
self.values.get(&address).and_then(|values| values.get(&slot)).copied()
446+
}
447+
431448
/// Saves arbitrary storage value for a given address:
432449
/// - store value in changed values cache.
433450
/// - update account's storage with given value.
@@ -1298,6 +1315,23 @@ impl<FEN: FoundryEvmNetwork> Cheatcodes<FEN> {
12981315
self.arbitrary_storage.get_or_insert_with(ArbitraryStorage::default)
12991316
}
13001317

1318+
/// Returns addresses explicitly marked with arbitrary storage.
1319+
pub fn arbitrary_storage_targets(&self) -> impl Iterator<Item = Address> + '_ {
1320+
self.arbitrary_storage.as_ref().into_iter().flat_map(ArbitraryStorage::targets)
1321+
}
1322+
1323+
/// Caches a concrete replay value for a slot on an arbitrary-storage address.
1324+
pub fn cache_arbitrary_storage_value(&mut self, address: Address, slot: U256, value: U256) {
1325+
if let Some(storage) = &mut self.arbitrary_storage {
1326+
storage.cache_value(address, slot, value);
1327+
}
1328+
}
1329+
1330+
/// Returns a cached arbitrary-storage replay value for a slot.
1331+
pub fn cached_arbitrary_storage_value(&self, address: Address, slot: U256) -> Option<U256> {
1332+
self.arbitrary_storage.as_ref().and_then(|storage| storage.cached_value(address, slot))
1333+
}
1334+
13011335
/// Whether the given address has arbitrary storage.
13021336
pub fn has_arbitrary_storage(&self, address: &Address) -> bool {
13031337
match &self.arbitrary_storage {
@@ -2535,7 +2569,9 @@ impl<FEN: FoundryEvmNetwork> Cheatcodes<FEN> {
25352569
|| self.should_overwrite_arbitrary_storage(&target_address, key)
25362570
{
25372571
if self.has_arbitrary_storage(&target_address) {
2538-
let arbitrary_value = self.rng().random();
2572+
let arbitrary_value = self
2573+
.cached_arbitrary_storage_value(target_address, key)
2574+
.unwrap_or_else(|| self.rng().random());
25392575
self.arbitrary_storage.as_mut().unwrap().save(
25402576
ecx,
25412577
target_address,

crates/evm/symbolic/src/executor/invariant.rs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -224,9 +224,9 @@ impl SymbolicExecutor {
224224
&mut self,
225225
steps: &[SequenceStepTemplate],
226226
state: &PathState,
227-
) -> Result<Vec<SymbolicInvariantStep>, SymbolicError> {
227+
) -> Result<(Vec<SymbolicInvariantStep>, Vec<SymbolicStorageAssignment>), SymbolicError> {
228228
let model = self.solver.model(&mut self.cx, &state.constraints)?;
229-
steps
229+
let sequence = steps
230230
.iter()
231231
.map(|step| {
232232
let args = step.calldata.model_to_args(&mut self.cx, &model)?;
@@ -241,6 +241,8 @@ impl SymbolicExecutor {
241241
calldata,
242242
})
243243
})
244-
.collect()
244+
.collect::<Result<Vec<_>, SymbolicError>>()?;
245+
let storage = state.world.replay_storage_assignments(&model);
246+
Ok((sequence, storage))
245247
}
246248
}

crates/evm/symbolic/src/executor/run.rs

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -506,9 +506,11 @@ impl SymbolicExecutor {
506506
&mut completed_paths,
507507
)? {
508508
if outcome.failed {
509-
let sequence = self.materialize_sequence(&initial.steps, &outcome.state)?;
509+
let (sequence, storage) =
510+
self.materialize_sequence(&initial.steps, &outcome.state)?;
510511
return Ok(SymbolicInvariantRunResult::Counterexample {
511512
sequence,
513+
storage,
512514
stats: self.stats_with_paths(completed_paths),
513515
});
514516
}
@@ -558,20 +560,22 @@ impl SymbolicExecutor {
558560

559561
match outcome.status {
560562
TopLevelCallStatus::Failure => {
561-
let sequence =
563+
let (sequence, storage) =
562564
self.materialize_sequence(&steps, &outcome.state)?;
563565
return Ok(SymbolicInvariantRunResult::Counterexample {
564566
sequence,
567+
storage,
565568
stats: self.stats_with_paths(completed_paths),
566569
});
567570
}
568571
TopLevelCallStatus::Revert => {
569572
if input.fail_on_revert {
570-
let sequence =
573+
let (sequence, storage) =
571574
self.materialize_sequence(&steps, &outcome.state)?;
572575
return Ok(
573576
SymbolicInvariantRunResult::Counterexample {
574577
sequence,
578+
storage,
575579
stats: self.stats_with_paths(completed_paths),
576580
},
577581
);
@@ -592,13 +596,15 @@ impl SymbolicExecutor {
592596
&mut completed_paths,
593597
)? {
594598
if invariant_outcome.failed {
595-
let sequence = self.materialize_sequence(
596-
&steps,
597-
&invariant_outcome.state,
598-
)?;
599+
let (sequence, storage) = self
600+
.materialize_sequence(
601+
&steps,
602+
&invariant_outcome.state,
603+
)?;
599604
return Ok(
600605
SymbolicInvariantRunResult::Counterexample {
601606
sequence,
607+
storage,
602608
stats: self
603609
.stats_with_paths(completed_paths),
604610
},

crates/evm/symbolic/src/lib.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -340,6 +340,17 @@ pub struct SymbolicInvariantRunInput<'a, FEN: FoundryEvmNetwork> {
340340
pub ffi_enabled: bool,
341341
}
342342

343+
/// One concrete storage value required to replay a symbolic invariant candidate.
344+
#[derive(Clone, Debug, PartialEq, Eq)]
345+
pub struct SymbolicStorageAssignment {
346+
/// Account whose storage slot should be initialized.
347+
pub address: Address,
348+
/// Concrete storage slot.
349+
pub slot: U256,
350+
/// Concrete value extracted from the solver model.
351+
pub value: U256,
352+
}
353+
343354
/// Outcome of bounded symbolic invariant execution.
344355
#[derive(Clone, Debug)]
345356
pub enum SymbolicInvariantRunResult {
@@ -349,6 +360,8 @@ pub enum SymbolicInvariantRunResult {
349360
Counterexample {
350361
/// Concrete sequence extracted from the solver model.
351362
sequence: Vec<SymbolicInvariantStep>,
363+
/// Concrete setup-storage values needed for replay.
364+
storage: Vec<SymbolicStorageAssignment>,
352365
/// Execution counters collected before the counterexample was returned.
353366
stats: SymbolicStats,
354367
},

crates/evm/symbolic/src/runtime/state.rs

Lines changed: 46 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,11 @@ impl PathState {
143143
.and_then(|cheats| cheats.gas_price)
144144
.unwrap_or_else(|| executor.tx_env().gas_price());
145145
self.gas_price = SymExpr::constant(cx, U256::from(gas_price));
146+
if let Some(cheats) = executor.inspector().cheatcodes.as_ref() {
147+
for target in cheats.arbitrary_storage_targets() {
148+
self.world.enable_arbitrary_storage(target);
149+
}
150+
}
146151
}
147152

148153
pub(crate) fn child(&self, frame: CallFrame) -> Self {
@@ -1475,6 +1480,7 @@ struct SymbolicWorldSnapshot {
14751480
arbitrary_storage_all: bool,
14761481
zero_init_symbolic_storage: bool,
14771482
symbolic_address_aliases: HashMap<SymExpr, Address>,
1483+
replay_storage_slots: HashMap<Symbol, SymbolicReplayStorageSlot>,
14781484
}
14791485

14801486
impl From<&SymbolicWorld> for SymbolicWorldSnapshot {
@@ -1494,10 +1500,17 @@ impl From<&SymbolicWorld> for SymbolicWorldSnapshot {
14941500
arbitrary_storage_all: world.arbitrary_storage_all,
14951501
zero_init_symbolic_storage: world.zero_init_symbolic_storage,
14961502
symbolic_address_aliases: world.symbolic_address_aliases.clone(),
1503+
replay_storage_slots: world.replay_storage_slots.clone(),
14971504
}
14981505
}
14991506
}
15001507

1508+
#[derive(Clone, Debug)]
1509+
struct SymbolicReplayStorageSlot {
1510+
address: Address,
1511+
slot: U256,
1512+
}
1513+
15011514
#[derive(Clone, Debug, Default)]
15021515
pub(crate) struct SymbolicWorld {
15031516
storage: Vec<StorageWrite>,
@@ -1512,6 +1525,7 @@ pub(crate) struct SymbolicWorld {
15121525
arbitrary_storage_all: bool,
15131526
zero_init_symbolic_storage: bool,
15141527
symbolic_address_aliases: HashMap<SymExpr, Address>,
1528+
replay_storage_slots: HashMap<Symbol, SymbolicReplayStorageSlot>,
15151529
snapshots: HashMap<U256, SymbolicWorldSnapshot>,
15161530
next_snapshot_id: u64,
15171531
}
@@ -1547,7 +1561,7 @@ impl SymbolicWorld {
15471561
}
15481562

15491563
pub(crate) fn sload<FEN: FoundryEvmNetwork>(
1550-
&self,
1564+
&mut self,
15511565
cx: &mut SymCx,
15521566
executor: &Executor<FEN>,
15531567
address: Address,
@@ -1591,6 +1605,22 @@ impl SymbolicWorld {
15911605
self.arbitrary_storage_accounts.insert(address);
15921606
}
15931607

1608+
pub(crate) fn replay_storage_assignments(
1609+
&self,
1610+
model: &SymbolicModel,
1611+
) -> Vec<SymbolicStorageAssignment> {
1612+
self.replay_storage_slots
1613+
.iter()
1614+
.filter_map(|(symbol, slot)| {
1615+
model.get(symbol).copied().map(|value| SymbolicStorageAssignment {
1616+
address: slot.address,
1617+
slot: slot.slot,
1618+
value,
1619+
})
1620+
})
1621+
.collect()
1622+
}
1623+
15941624
pub(crate) fn resolve_address(&self, expr: &SymExpr) -> Option<Address> {
15951625
expr.as_const().map(word_to_address).or_else(|| {
15961626
self.symbolic_address_aliases.get(expr).copied().or_else(|| {
@@ -1639,6 +1669,7 @@ impl SymbolicWorld {
16391669
self.arbitrary_storage_all = snapshot.arbitrary_storage_all;
16401670
self.zero_init_symbolic_storage = snapshot.zero_init_symbolic_storage;
16411671
self.symbolic_address_aliases = snapshot.symbolic_address_aliases;
1672+
self.replay_storage_slots = snapshot.replay_storage_slots;
16421673
true
16431674
}
16441675

@@ -1651,15 +1682,21 @@ impl SymbolicWorld {
16511682
}
16521683

16531684
pub(crate) fn storage_base<FEN: FoundryEvmNetwork>(
1654-
&self,
1685+
&mut self,
16551686
cx: &mut SymCx,
16561687
executor: &Executor<FEN>,
16571688
address: Address,
16581689
key: &SymExpr,
16591690
concrete_key: Option<U256>,
16601691
) -> Result<SymExpr, SymbolicError> {
1661-
if self.arbitrary_storage_all || self.arbitrary_storage_accounts.contains(&address) {
1662-
let name = stable_symbol("storage", format!("{address:?}:{key:?}").as_bytes());
1692+
let has_arbitrary_storage = self.arbitrary_storage_accounts.contains(&address);
1693+
if self.arbitrary_storage_all || has_arbitrary_storage {
1694+
let name = symbolic_storage_symbol(address, key);
1695+
if has_arbitrary_storage && let Some(slot) = concrete_key.or_else(|| key.as_const()) {
1696+
self.replay_storage_slots
1697+
.entry(name.clone())
1698+
.or_insert(SymbolicReplayStorageSlot { address, slot });
1699+
}
16631700
return Ok(SymExpr::var_symbol(cx, name));
16641701
}
16651702
if let Some(key) = concrete_key {
@@ -1678,7 +1715,7 @@ impl SymbolicWorld {
16781715
} else if self.zero_init_symbolic_storage {
16791716
Ok(SymExpr::zero(cx))
16801717
} else {
1681-
let name = stable_symbol("storage", format!("{address:?}:{key:?}").as_bytes());
1718+
let name = symbolic_storage_symbol(address, key);
16821719
Ok(SymExpr::var_symbol(cx, name))
16831720
}
16841721
}
@@ -2092,6 +2129,10 @@ impl SymbolicWorld {
20922129
}
20932130
}
20942131

2132+
fn symbolic_storage_symbol(address: Address, key: &SymExpr) -> Symbol {
2133+
stable_symbol("storage", format!("{address:?}:{key:?}").as_bytes())
2134+
}
2135+
20952136
#[derive(Clone, Debug)]
20962137
pub(crate) struct SymbolicBlock {
20972138
pub(crate) chain_id: SymExpr,

0 commit comments

Comments
 (0)