evm_fork_cache/cache/overlay.rs
1//! Per-simulation state overlays layered over a snapshot or the live cache.
2//!
3//! An [`EvmOverlay`] wraps a read-only base (an
4//! [`EvmSnapshot`] or the cache itself) with
5//! a scratch write layer, so a simulation can mutate balances, storage, and code
6//! and run calls without disturbing the base or other overlays. Overlays are the
7//! `Send` unit of parallel fan-out: snapshot once, clone cheaply, overlay per
8//! candidate. Reads fall through the write layer to the base; writes and reverts
9//! stay local to the overlay.
10
11use std::cell::RefCell;
12use std::collections::HashMap;
13use std::rc::Rc;
14use std::sync::Arc;
15
16use alloy_eips::eip2930::{AccessList, AccessListItem};
17use alloy_primitives::{Address, B256, Bytes, TxKind, U256};
18use foundry_fork_db::{DatabaseError, SharedBackend};
19use revm::{
20 Context, ExecuteCommitEvm, ExecuteEvm, InspectEvm, MainBuilder, MainContext,
21 context::{BlockEnv, CfgEnv, Journal, LocalContext, TxEnv, result::ExecutionResult},
22 database_interface::{Database, DatabaseRef},
23 state::{AccountInfo, Bytecode},
24};
25
26use super::snapshot::EvmSnapshot;
27use super::{CallSimulationResult, IERC20, SimStatus, TxConfig, unix_timestamp_secs_saturating};
28use crate::access_set::StorageAccessList;
29use crate::bundle::{BundleOptions, BundleResult, BundleTx, RevertPolicy, TxOutcome};
30use crate::errors::{
31 OverlayError, OverlayResult as Result, SimError, SimHostError, SimulationError,
32 SimulationResult,
33};
34use crate::inspector::TransferInspector;
35use crate::mapping_probe::HashStorageProbe;
36use alloy_sol_types::SolCall;
37
38type OverlayEvm<'a> = revm::MainnetEvm<
39 Context<BlockEnv, TxEnv, CfgEnv, &'a mut EvmOverlay, Journal<&'a mut EvmOverlay>, ()>,
40>;
41
42type InspectorOverlayEvm<'a, INSP> = revm::MainnetEvm<
43 Context<BlockEnv, TxEnv, CfgEnv, &'a mut EvmOverlay, Journal<&'a mut EvmOverlay>, ()>,
44 INSP,
45>;
46
47/// Per-simulation mutable overlay on an immutable snapshot.
48///
49/// Lookup order: dirty layer → snapshot → ext_db (optional RPC fallback).
50///
51/// This type is `Send` (unlike `EvmCache`) because it uses no `Rc`/`RefCell`.
52/// Each simulation task gets its own `EvmOverlay` with a cheap `Arc::clone`
53/// of the shared `EvmSnapshot`.
54///
55/// # Reuse across simulations (Pillar A.2)
56///
57/// A worker doing many sims against the same snapshot can call [`Self::new`]
58/// once and [`Self::reset`] between sims instead of allocating a fresh overlay
59/// each time. The reusable shared-memory buffer is also recycled across calls —
60/// see [`Self::call_raw`] — without making the overlay `!Send`.
61pub struct EvmOverlay {
62 snapshot: Arc<EvmSnapshot>,
63 /// Per-simulation mutations (accounts fetched from ext_db, committed changes).
64 dirty_accounts: HashMap<Address, AccountInfo>,
65 /// Per-simulation storage mutations.
66 dirty_storage: HashMap<Address, HashMap<U256, U256>>,
67 /// Optional RPC fallback for data not in snapshot.
68 ext_db: Option<SharedBackend>,
69 /// Reusable shared-memory buffer, recycled across the build→transact→revert
70 /// call methods to avoid reallocating a 64 KB `Vec` per call.
71 ///
72 /// Stored as a plain `Vec<u8>` (not an `Rc`) so the overlay stays `Send`. A
73 /// call method `mem::take`s it, wraps it in a method-local `Rc<RefCell<_>>`
74 /// for revm's [`LocalContext`], runs, then reclaims and clears it after the
75 /// EVM is dropped (see [`Self::build_evm_with_local`]).
76 reusable_buffer: Vec<u8>,
77 /// Target pre-allocation (bytes) for [`Self::reusable_buffer`] and each
78 /// per-call buffer, taken from the snapshot's configured
79 /// [`SharedMemoryCapacity`](super::SharedMemoryCapacity) so overlays honor the
80 /// capacity set on the originating [`EvmCache`].
81 buffer_capacity: usize,
82 /// Set when a `BLOCKHASH` read fell through to the ZERO fallback (no
83 /// snapshot-provided hash and no `ext_db`). The freshness validator reads
84 /// this via [`Self::blockhash_zero_fallback`] to fail closed instead of
85 /// confirming a sim whose control flow may rest on a hash its overlays
86 /// cannot resolve. Cleared by [`Self::reset`].
87 blockhash_zero_fallback: bool,
88}
89
90impl EvmOverlay {
91 /// Create a new overlay on the given snapshot.
92 ///
93 /// The reusable shared-memory buffer is pre-allocated to the snapshot's
94 /// configured shared-memory capacity (see
95 /// [`SharedMemoryCapacity`](super::SharedMemoryCapacity)).
96 pub fn new(snapshot: Arc<EvmSnapshot>, ext_db: Option<SharedBackend>) -> Self {
97 let buffer_capacity = snapshot.shared_memory_capacity;
98 Self {
99 snapshot,
100 dirty_accounts: HashMap::new(),
101 dirty_storage: HashMap::new(),
102 ext_db,
103 reusable_buffer: Vec::with_capacity(buffer_capacity),
104 buffer_capacity,
105 blockhash_zero_fallback: false,
106 }
107 }
108
109 /// Clear the per-simulation dirty layer so this overlay can be reused for the
110 /// next simulation against the same snapshot, without reallocating (Pillar
111 /// A.2).
112 ///
113 /// A worker doing K sims calls [`Self::new`] once and `reset()` between sims
114 /// instead of allocating a fresh overlay (plus dirty maps plus an `Arc`
115 /// clone) each time. After `reset()` the overlay reads the pristine snapshot
116 /// again — it is exactly equivalent to a freshly-built overlay on the same
117 /// snapshot. The snapshot `Arc`, the optional `ext_db`, and the reusable
118 /// shared-memory buffer (kept at capacity) are retained.
119 pub fn reset(&mut self) {
120 self.dirty_accounts.clear();
121 self.dirty_storage.clear();
122 self.blockhash_zero_fallback = false;
123 // Keep: snapshot Arc, ext_db, and the reusable buffer. The buffer is
124 // already cleared after each call, so nothing to do for it here.
125 }
126
127 /// `true` if any `BLOCKHASH` read on this overlay fell through to the ZERO
128 /// fallback (no snapshot-provided hash for that number and no `ext_db`)
129 /// since construction or the last [`reset`](Self::reset).
130 ///
131 /// The freshness validator uses this to **fail closed**: a sim that read a
132 /// hash its ext-db-less overlays cannot resolve is reported
133 /// [`Unverified`](crate::freshness::Validation::Unverified) rather than
134 /// silently confirmed against a ZERO stand-in.
135 ///
136 /// Only reads revm actually routes to the database can set this: requests
137 /// outside the EVM's valid lookback window (`[current − 256, current)`)
138 /// return spec-mandated ZERO without a database call — that value is
139 /// correct on-chain too, so such reads are deliberately not flagged.
140 pub fn blockhash_zero_fallback(&self) -> bool {
141 self.blockhash_zero_fallback
142 }
143
144 /// Chain ID of the block context captured by the underlying snapshot.
145 ///
146 /// This is the value installed into `cfg.chain_id` by [`Self::build_evm`].
147 pub fn chain_id(&self) -> u64 {
148 self.snapshot.chain_id
149 }
150
151 /// Block number of the snapshot's block context, or `None` if it was not
152 /// captured.
153 ///
154 /// When present this is the `block.number` simulations run against; when
155 /// `None`, [`Self::build_evm`] leaves revm's default block number in place.
156 pub fn block_number(&self) -> Option<u64> {
157 self.snapshot.block_number
158 }
159
160 /// Base fee of the snapshot's block context, or `None` if it was not
161 /// captured.
162 ///
163 /// Note that base-fee checks are disabled in the simulation EVM, so this is
164 /// informational rather than enforced against the transaction.
165 pub fn basefee(&self) -> Option<u64> {
166 self.snapshot.basefee
167 }
168
169 /// Timestamp of the snapshot's block context, or `None` if it was not
170 /// captured.
171 ///
172 /// When `None`, [`Self::build_evm`] substitutes the current wall-clock time
173 /// for `block.timestamp`.
174 pub fn timestamp(&self) -> Option<u64> {
175 self.snapshot.timestamp
176 }
177
178 /// A fresh [`LocalContext`] with a newly-allocated 64 KB shared-memory buffer.
179 ///
180 /// Used by the public [`Self::build_evm`], which hands out the EVM and cannot
181 /// reclaim its buffer afterwards. The internal call methods instead recycle
182 /// [`Self::reusable_buffer`] via [`Self::build_evm_with_local`].
183 fn fresh_local(&self) -> LocalContext {
184 LocalContext {
185 shared_memory_buffer: Rc::new(RefCell::new(Vec::with_capacity(self.buffer_capacity))),
186 precompile_error_message: None,
187 }
188 }
189
190 /// Build a revm EVM instance backed by this overlay, using a caller-supplied
191 /// [`LocalContext`].
192 ///
193 /// This is the shared body behind [`Self::build_evm`] and the internal call
194 /// methods. The call methods pass a `local` wrapping the recycled
195 /// [`Self::reusable_buffer`] (Pillar A.2) and reclaim it after the EVM is
196 /// dropped; [`Self::build_evm`] passes a fresh one.
197 ///
198 /// Note: the returned EVM is `!Send` (due to `LocalContext`'s `Rc<RefCell>`),
199 /// but this is fine because it's created and used within a single task.
200 fn build_evm_with_local(&mut self, local: LocalContext) -> OverlayEvm<'_> {
201 // Read snapshot values before the mutable borrow of self
202 let chain_id = self.snapshot.chain_id;
203 let spec_id = self.snapshot.spec_id;
204 let timestamp = self
205 .snapshot
206 .timestamp
207 .unwrap_or_else(|| unix_timestamp_secs_saturating(std::time::SystemTime::now()));
208 let block_number = self.snapshot.block_number;
209 let basefee = self.snapshot.basefee;
210 let coinbase = self.snapshot.coinbase;
211 let prevrandao = self.snapshot.prevrandao;
212 let gas_limit = self.snapshot.gas_limit;
213
214 let mut evm = Context::mainnet()
215 .with_db(&mut *self)
216 .with_local(local)
217 .modify_cfg_chained(|cfg| {
218 cfg.disable_nonce_check = true;
219 cfg.disable_eip3607 = true;
220 cfg.disable_base_fee = true;
221 cfg.disable_balance_check = true;
222 cfg.chain_id = chain_id;
223 cfg.limit_contract_code_size = None;
224 cfg.tx_chain_id_check = false;
225 cfg.spec = spec_id;
226 })
227 .build_mainnet();
228
229 evm.block.timestamp = U256::from(timestamp);
230 if let Some(number) = block_number {
231 evm.block.number = U256::from(number);
232 }
233 if let Some(basefee) = basefee {
234 evm.block.basefee = basefee;
235 }
236 if let Some(coinbase) = coinbase {
237 evm.block.beneficiary = coinbase;
238 }
239 if let Some(prevrandao) = prevrandao {
240 evm.block.prevrandao = Some(prevrandao);
241 }
242 if let Some(gas_limit) = gas_limit {
243 evm.block.gas_limit = gas_limit;
244 }
245 evm
246 }
247
248 /// Build a revm EVM instance backed by this overlay.
249 ///
250 /// This allocates a fresh 64 KB shared-memory buffer each call: it hands the
251 /// EVM out to the caller and cannot reclaim the buffer afterwards, so it
252 /// cannot recycle the overlay's reusable buffer. The internal call methods
253 /// ([`Self::call_raw`], etc.) recycle the buffer instead (Pillar A.2).
254 ///
255 /// Note: The returned EVM is `!Send` (due to `LocalContext`'s `Rc<RefCell>`),
256 /// but this is fine because it's created and used within a single task.
257 pub fn build_evm(&mut self) -> OverlayEvm<'_> {
258 let local = self.fresh_local();
259 self.build_evm_with_local(local)
260 }
261
262 /// Execute a non-committing call and return the raw [`ExecutionResult`].
263 ///
264 /// The EVM state is reverted to a checkpoint after execution on *both*
265 /// success and failure, so the call never mutates this overlay's dirty
266 /// layer. Each overlay simulation is therefore isolated: repeated calls all
267 /// observe the same base state.
268 ///
269 /// A revert or halt is *not* an error here — it is reported through the
270 /// returned [`ExecutionResult`] variant. Only failure to build or transact
271 /// the call yields `Err`.
272 ///
273 /// # Errors
274 ///
275 /// Returns an error if the [`TxEnv`] cannot be built from the given inputs,
276 /// or if revm fails to transact the call (for example a database error
277 /// while loading state from the RPC fallback).
278 ///
279 /// # Examples
280 ///
281 /// ```no_run
282 /// # use std::sync::Arc;
283 /// # use alloy_primitives::{Address, Bytes};
284 /// # use evm_fork_cache::cache::{EvmOverlay, EvmSnapshot};
285 /// # fn run(snapshot: Arc<EvmSnapshot>) -> Result<(), Box<dyn std::error::Error>> {
286 /// let mut overlay = EvmOverlay::new(snapshot, None);
287 /// let result = overlay.call_raw(Address::ZERO, Address::ZERO, Bytes::new())?;
288 /// // State is reverted; a second call sees the same base state.
289 /// let _again = overlay.call_raw(Address::ZERO, Address::ZERO, Bytes::new())?;
290 /// # let _ = result;
291 /// # Ok(())
292 /// # }
293 /// ```
294 pub fn call_raw(
295 &mut self,
296 from: Address,
297 to: Address,
298 calldata: Bytes,
299 ) -> Result<ExecutionResult> {
300 let tx = TxEnv::builder()
301 .caller(from)
302 .kind(TxKind::Call(to))
303 .data(calldata)
304 .value(U256::ZERO)
305 .build()
306 .map_err(OverlayError::tx_env)?;
307
308 // Recycle the reusable buffer (Pillar A.2): take it out as a plain Vec
309 // (keeping the overlay Send), lend it to a method-local Rc<RefCell> for
310 // revm's LocalContext, then reclaim and clear it after the EVM is dropped.
311 let buffer = Rc::new(RefCell::new(std::mem::take(&mut self.reusable_buffer)));
312 let local = LocalContext {
313 shared_memory_buffer: Rc::clone(&buffer),
314 precompile_error_message: None,
315 };
316
317 let result = {
318 let mut evm = self.build_evm_with_local(local);
319 use revm::context_interface::JournalTr;
320 let checkpoint = evm.journaled_state.checkpoint();
321 let result = evm.transact_one(tx).map_err(OverlayError::transact);
322 evm.journaled_state.checkpoint_revert(checkpoint);
323 result
324 };
325
326 self.reclaim_buffer(buffer);
327 result
328 }
329
330 /// Execute one non-committing call with temporary account-code overrides.
331 ///
332 /// Overrides live only for this call: the previous dirty-layer account (or
333 /// its absence) is restored before this method returns, including when the
334 /// EVM returns an execution error. The shared snapshot is never mutated.
335 /// This is useful for execution helpers whose control flow depends on an
336 /// external call but whose result does not depend on that callee's state;
337 /// for example, a revert-based V3 quoter transfers the output token before
338 /// deliberately reverting with the quote payload.
339 ///
340 /// Each supplied runtime bytecode replaces only the account's code and code
341 /// hash. Its balance, nonce, and account id continue to come from the
342 /// overlay/snapshot account when present.
343 ///
344 /// # Errors
345 ///
346 /// Returns an error if an overridden account cannot be loaded, the
347 /// transaction environment cannot be built, or revm cannot transact the
348 /// call. Empty bytecode is accepted and behaves like an EOA.
349 pub fn call_raw_with_code_overrides(
350 &mut self,
351 from: Address,
352 to: Address,
353 calldata: Bytes,
354 overrides: &[(Address, Bytes)],
355 ) -> Result<ExecutionResult> {
356 let mut prior = Vec::with_capacity(overrides.len());
357 for (address, runtime_bytecode) in overrides {
358 let old_dirty = self.dirty_accounts.get(address).cloned();
359 let mut info = match self.basic(*address) {
360 Ok(info) => info.unwrap_or_default(),
361 Err(error) => {
362 for (prior_address, prior_info) in prior.into_iter().rev() {
363 if let Some(info) = prior_info {
364 self.dirty_accounts.insert(prior_address, info);
365 } else {
366 self.dirty_accounts.remove(&prior_address);
367 }
368 }
369 return Err(OverlayError::transact(error));
370 }
371 };
372 let bytecode = Bytecode::new_raw(runtime_bytecode.clone());
373 info.code_hash = bytecode.hash_slow();
374 info.code = Some(bytecode);
375 self.dirty_accounts.insert(*address, info);
376 prior.push((*address, old_dirty));
377 }
378
379 let result = self.call_raw(from, to, calldata);
380
381 for (address, old_dirty) in prior.into_iter().rev() {
382 if let Some(info) = old_dirty {
383 self.dirty_accounts.insert(address, info);
384 } else {
385 self.dirty_accounts.remove(&address);
386 }
387 }
388 result
389 }
390
391 /// Reclaim the recycled shared-memory buffer after the EVM (and its
392 /// `LocalContext` clone of the `Rc`) has been dropped, clearing it for the
393 /// next call.
394 ///
395 /// The `Rc` was only ever held by the dropped EVM and this method's local, so
396 /// `try_unwrap` succeeds in the normal path. If a panic somewhere left an
397 /// extra strong reference the buffer is simply re-allocated next call — no
398 /// correctness impact.
399 fn reclaim_buffer(&mut self, buffer: Rc<RefCell<Vec<u8>>>) {
400 if let Ok(cell) = Rc::try_unwrap(buffer) {
401 let mut buf = cell.into_inner();
402 buf.clear();
403 self.reusable_buffer = buf;
404 } else {
405 self.reusable_buffer = Vec::with_capacity(self.buffer_capacity);
406 }
407 }
408
409 /// Build a revm EVM instance with an inspector, backed by this overlay, using
410 /// a caller-supplied [`LocalContext`].
411 ///
412 /// Like [`Self::build_evm_with_local`] but attaches `inspector`. The call
413 /// methods pass a `local` wrapping the recycled [`Self::reusable_buffer`]
414 /// (Pillar A.2) and reclaim it after the EVM is dropped.
415 fn build_evm_with_inspector_local<INSP>(
416 &mut self,
417 inspector: INSP,
418 local: LocalContext,
419 ) -> InspectorOverlayEvm<'_, INSP> {
420 let chain_id = self.snapshot.chain_id;
421 let spec_id = self.snapshot.spec_id;
422 let timestamp = self
423 .snapshot
424 .timestamp
425 .unwrap_or_else(|| unix_timestamp_secs_saturating(std::time::SystemTime::now()));
426 let block_number = self.snapshot.block_number;
427 let basefee = self.snapshot.basefee;
428 let coinbase = self.snapshot.coinbase;
429 let prevrandao = self.snapshot.prevrandao;
430 let gas_limit = self.snapshot.gas_limit;
431
432 let mut evm = Context::mainnet()
433 .with_db(&mut *self)
434 .with_local(local)
435 .modify_cfg_chained(|cfg| {
436 cfg.disable_nonce_check = true;
437 cfg.disable_eip3607 = true;
438 cfg.disable_base_fee = true;
439 cfg.disable_balance_check = true;
440 cfg.chain_id = chain_id;
441 cfg.limit_contract_code_size = None;
442 cfg.tx_chain_id_check = false;
443 cfg.spec = spec_id;
444 })
445 .build_mainnet_with_inspector(inspector);
446
447 evm.block.timestamp = U256::from(timestamp);
448 if let Some(number) = block_number {
449 evm.block.number = U256::from(number);
450 }
451 if let Some(basefee) = basefee {
452 evm.block.basefee = basefee;
453 }
454 if let Some(coinbase) = coinbase {
455 evm.block.beneficiary = coinbase;
456 }
457 if let Some(prevrandao) = prevrandao {
458 evm.block.prevrandao = Some(prevrandao);
459 }
460 if let Some(gas_limit) = gas_limit {
461 evm.block.gas_limit = gas_limit;
462 }
463 evm
464 }
465
466 /// Simulate a call with transfer tracking via the `TransferInspector`.
467 ///
468 /// This is the overlay-compatible equivalent of
469 /// [`super::EvmCache::simulate_with_transfer_tracking`]. It captures ERC20
470 /// Transfer events during execution to compute balance deltas for `owner`
471 /// (restricted to `tokens` when provided) without relying on pre/post
472 /// balance queries.
473 ///
474 /// On a reverting or halting call the EVM state is reverted to a checkpoint
475 /// before returning, so a failed simulation never mutates this overlay. On
476 /// success the call either commits the journaled changes into the overlay's
477 /// dirty layer (`commit == true`) or reverts them (`commit == false`); a
478 /// non-committing run leaves each overlay simulation isolated from the next.
479 ///
480 /// # Errors
481 ///
482 /// Returns an error if the [`TxEnv`] cannot be built, if revm fails to
483 /// transact the call, if the call reverts (mapped from the revert payload),
484 /// or if the call halts. In every error case the EVM state is reverted
485 /// first, regardless of `commit`.
486 ///
487 /// # Examples
488 ///
489 /// ```no_run
490 /// # use std::sync::Arc;
491 /// # use alloy_primitives::{Address, Bytes};
492 /// # use evm_fork_cache::cache::{EvmOverlay, EvmSnapshot};
493 /// # fn run(snapshot: Arc<EvmSnapshot>, token: Address, owner: Address) -> Result<(), Box<dyn std::error::Error>> {
494 /// let mut overlay = EvmOverlay::new(snapshot, None);
495 /// let sim = overlay.simulate_with_transfer_tracking(
496 /// owner,
497 /// token,
498 /// Bytes::new(),
499 /// owner,
500 /// Some([token]),
501 /// false, // non-committing: state is reverted afterwards
502 /// )?;
503 /// let _delta = sim.token_deltas.get(&token);
504 /// # Ok(())
505 /// # }
506 /// ```
507 pub fn simulate_with_transfer_tracking(
508 &mut self,
509 from: Address,
510 to: Address,
511 calldata: Bytes,
512 owner: Address,
513 tokens: Option<impl IntoIterator<Item = Address>>,
514 commit: bool,
515 ) -> SimulationResult<CallSimulationResult> {
516 let tx = TxEnv::builder()
517 .caller(from)
518 .kind(TxKind::Call(to))
519 .data(calldata)
520 .value(U256::ZERO)
521 .build()
522 .map_err(|e| SimError::Other(SimHostError::tx_env(e)))?;
523
524 let inspector = TransferInspector::new();
525
526 // Recycle the reusable buffer (Pillar A.2); reclaimed after the EVM drops.
527 let buffer = Rc::new(RefCell::new(std::mem::take(&mut self.reusable_buffer)));
528 let local = LocalContext {
529 shared_memory_buffer: Rc::clone(&buffer),
530 precompile_error_message: None,
531 };
532
533 let outcome = {
534 let mut evm = self.build_evm_with_inspector_local(inspector, local);
535
536 use revm::context_interface::JournalTr;
537 let checkpoint = evm.journaled_state.checkpoint();
538
539 let result = evm
540 .inspect_one_tx(tx)
541 .map_err(|e| SimError::Other(SimHostError::transact(e)));
542
543 match result {
544 Ok(ExecutionResult::Success {
545 logs,
546 gas_used,
547 output,
548 ..
549 }) => {
550 let token_deltas = if let Some(token_list) = tokens {
551 evm.inspector.balance_deltas_for_tokens(owner, token_list)
552 } else {
553 evm.inspector.balance_deltas(owner)
554 };
555
556 // Extract EIP-2930 access list from journaled state
557 let access_list = extract_access_list(&evm.journaled_state.state);
558
559 if commit {
560 evm.commit_inner();
561 } else {
562 evm.journaled_state.checkpoint_revert(checkpoint);
563 }
564
565 Ok(CallSimulationResult {
566 status: SimStatus::Success,
567 gas_used,
568 token_deltas,
569 logs,
570 access_list,
571 output: output.into_data(),
572 })
573 }
574 Ok(ExecutionResult::Revert { gas_used, output }) => {
575 evm.journaled_state.checkpoint_revert(checkpoint);
576 Err(SimulationError::from_revert(gas_used, output).into())
577 }
578 Ok(ExecutionResult::Halt { reason, gas_used }) => {
579 evm.journaled_state.checkpoint_revert(checkpoint);
580 Err(SimError::Halt {
581 reason: format!("{reason:?}"),
582 gas_used,
583 })
584 }
585 Err(err) => {
586 evm.journaled_state.checkpoint_revert(checkpoint);
587 Err(err)
588 }
589 }
590 };
591
592 self.reclaim_buffer(buffer);
593 outcome
594 }
595
596 /// Run a single call with a caller-supplied [`Inspector`](revm::Inspector),
597 /// returning the raw [`ExecutionResult`] and handing the inspector back for the
598 /// caller to read.
599 ///
600 /// This is the inspector-generic public seam: where
601 /// [`Self::simulate_with_transfer_tracking`] hard-wires the
602 /// [`TransferInspector`], this accepts any
603 /// [`revm::Inspector`] — a [`CallTracer`](crate::tracing::CallTracer), an
604 /// [`InspectorStack`](crate::tracing::InspectorStack) composing several, or a
605 /// caller-defined one. It honors a full [`TxConfig`] (value/gas/nonce/access
606 /// list) exactly like [`Self::call_raw_with_access_list_with`] and recycles the
607 /// reusable shared-memory buffer like the other call methods.
608 ///
609 /// Unlike `simulate_with_transfer_tracking`, a revert or halt is **not** an
610 /// error: the raw [`ExecutionResult`] variant
611 /// ([`Success`](ExecutionResult::Success) /
612 /// [`Revert`](ExecutionResult::Revert) / [`Halt`](ExecutionResult::Halt)) is
613 /// returned as `Ok` so the inspector's captured frames (e.g. a reverted call
614 /// tree) remain observable. Only a tx-env build failure or a transact/database
615 /// error yields `Err`.
616 ///
617 /// On a successful transact the journaled changes are either committed into the
618 /// overlay's dirty layer (`commit == true`) or reverted (`commit == false`),
619 /// matching [`Self::simulate_with_transfer_tracking`]. On a revert/halt the
620 /// checkpoint is always reverted regardless of `commit`, so a failed call never
621 /// mutates this overlay. On a transact error the checkpoint is reverted too.
622 ///
623 /// # Errors
624 ///
625 /// Returns an error if the [`TxEnv`] cannot be built from `from`/`to`/`tx`, or
626 /// if revm fails to transact the call (e.g. a database error while loading
627 /// state).
628 ///
629 /// # Examples
630 ///
631 /// ```no_run
632 /// # use std::sync::Arc;
633 /// # use alloy_primitives::{Address, Bytes};
634 /// # use evm_fork_cache::cache::{EvmOverlay, EvmSnapshot, TxConfig};
635 /// # use evm_fork_cache::CallTracer;
636 /// # fn run(snapshot: Arc<EvmSnapshot>, to: Address) -> Result<(), Box<dyn std::error::Error>> {
637 /// let mut overlay = EvmOverlay::new(snapshot, None);
638 /// let (result, tracer) = overlay.call_raw_with_inspector(
639 /// Address::ZERO,
640 /// to,
641 /// Bytes::new(),
642 /// &TxConfig::default(),
643 /// CallTracer::new(),
644 /// false,
645 /// )?;
646 /// let _ = result;
647 /// let _trace = tracer.into_trace();
648 /// # Ok(())
649 /// # }
650 /// ```
651 pub fn call_raw_with_inspector<I>(
652 &mut self,
653 from: Address,
654 to: Address,
655 calldata: Bytes,
656 tx: &TxConfig,
657 inspector: I,
658 commit: bool,
659 ) -> SimulationResult<(ExecutionResult, I)>
660 where
661 I: for<'a> revm::Inspector<
662 Context<
663 BlockEnv,
664 TxEnv,
665 CfgEnv,
666 &'a mut EvmOverlay,
667 Journal<&'a mut EvmOverlay>,
668 (),
669 >,
670 >,
671 {
672 let mut builder = TxEnv::builder()
673 .caller(from)
674 .kind(TxKind::Call(to))
675 .data(calldata)
676 .value(tx.value);
677 if let Some(gas_limit) = tx.gas_limit {
678 builder = builder.gas_limit(gas_limit);
679 }
680 if let Some(gas_price) = tx.gas_price {
681 builder = builder.gas_price(gas_price);
682 }
683 if let Some(nonce) = tx.nonce {
684 builder = builder.nonce(nonce);
685 }
686 if let Some(access_list) = &tx.access_list {
687 builder = builder.access_list(access_list.clone());
688 }
689 let tx_env = builder
690 .build()
691 .map_err(|e| SimError::Other(SimHostError::tx_env(e)))?;
692
693 // Recycle the reusable buffer (Pillar A.2); reclaimed after the EVM drops.
694 let buffer = Rc::new(RefCell::new(std::mem::take(&mut self.reusable_buffer)));
695 let local = LocalContext {
696 shared_memory_buffer: Rc::clone(&buffer),
697 precompile_error_message: None,
698 };
699
700 let outcome = {
701 let mut evm = self.build_evm_with_inspector_local(inspector, local);
702
703 use revm::context_interface::JournalTr;
704 let checkpoint = evm.journaled_state.checkpoint();
705
706 match evm.inspect_one_tx(tx_env) {
707 Ok(result) => {
708 if commit && matches!(result, ExecutionResult::Success { .. }) {
709 evm.commit_inner();
710 } else {
711 evm.journaled_state.checkpoint_revert(checkpoint);
712 }
713 // Hand the inspector back to the caller.
714 Ok((result, evm.inspector))
715 }
716 Err(e) => {
717 evm.journaled_state.checkpoint_revert(checkpoint);
718 Err(SimError::Other(SimHostError::transact(e)))
719 }
720 }
721 };
722
723 self.reclaim_buffer(buffer);
724 outcome
725 }
726
727 /// Apply `txs` in order against this overlay over **cumulative** block state,
728 /// with a revert policy and coinbase/miner-payment accounting (Phase 6
729 /// Track A+B).
730 ///
731 /// Each transaction observes the committed writes of the ones before it:
732 /// the bundle runs on a single overlay/EVM with one outer checkpoint plus a
733 /// per-transaction inner checkpoint, so it does **not** rebuild a fresh
734 /// overlay per transaction. See the [`bundle`](crate::bundle) module for the
735 /// public vocabulary ([`BundleTx`], [`BundleOptions`], [`RevertPolicy`],
736 /// [`TxOutcome`], [`BundleResult`]).
737 ///
738 /// # Revert policy
739 ///
740 /// - [`RevertPolicy::Atomic`]: the first transaction that reverts/halts
741 /// rolls the whole bundle back to the outer checkpoint, sets
742 /// `succeeded = false`, and stops (`per_tx` ends at the failing
743 /// transaction). `coinbase_payment` is `0` and the overlay is unchanged.
744 /// - [`RevertPolicy::AllowReverts`]: a revert at a whitelisted index rolls
745 /// back only that transaction (inner checkpoint) and execution continues;
746 /// a revert at a non-whitelisted index behaves like `Atomic`.
747 ///
748 /// # Coinbase accounting
749 ///
750 /// `coinbase_payment` is the block beneficiary's balance delta across the kept
751 /// transactions. Under EIP-1559 revm credits the beneficiary only the priority
752 /// fee (`(effective_gas_price − basefee) × gas_used`) and burns the base fee
753 /// in-EVM, so the delta is the honest miner payment (plus any direct coinbase
754 /// tips). Saturating.
755 ///
756 /// # Commit semantics
757 ///
758 /// `opts.commit == true` folds the bundle's cumulative state into this
759 /// overlay's dirty layer (observable by subsequent overlay calls);
760 /// `false` reverts the outer checkpoint so the overlay is unchanged. A
761 /// failed atomic bundle never leaves partial state regardless of `commit`.
762 ///
763 /// # Errors
764 ///
765 /// Returns [`SimError`] if a transaction environment cannot be built or revm
766 /// fails to transact (e.g. a database error). A transaction *reverting* is
767 /// not an error — it is reported through the per-transaction
768 /// [`TxOutcome`] and the revert policy.
769 pub fn simulate_bundle(
770 &mut self,
771 txs: &[BundleTx],
772 opts: &BundleOptions,
773 ) -> SimulationResult<BundleResult> {
774 // Build every TxEnv up front so a build failure surfaces as an error
775 // before we touch the EVM/journal (and the borrow of `self` is clean).
776 let tx_envs: Vec<TxEnv> = txs
777 .iter()
778 .map(|bt| {
779 let mut builder = TxEnv::builder()
780 .caller(bt.from)
781 .kind(TxKind::Call(bt.to))
782 .data(bt.calldata.clone())
783 .value(bt.tx.value);
784 if let Some(gas_limit) = bt.tx.gas_limit {
785 builder = builder.gas_limit(gas_limit);
786 }
787 if let Some(gas_price) = bt.tx.gas_price {
788 builder = builder.gas_price(gas_price);
789 }
790 if let Some(nonce) = bt.tx.nonce {
791 builder = builder.nonce(nonce);
792 }
793 if let Some(access_list) = &bt.tx.access_list {
794 builder = builder.access_list(access_list.clone());
795 }
796 builder
797 .build()
798 .map_err(|e| SimError::Other(SimHostError::tx_env(e)))
799 })
800 .collect::<std::result::Result<_, _>>()?;
801
802 // Resolve the beneficiary and read its pre-bundle balance before the
803 // mutable borrow of `self` by the EVM (the post-bundle delta is the miner
804 // payment; revm already burns the base fee per EIP-1559).
805 let beneficiary = self
806 .snapshot
807 .coinbase
808 .unwrap_or_else(|| revm::context::BlockEnv::default().beneficiary);
809 let pre_beneficiary_balance = self
810 .basic(beneficiary)
811 .map_err(|e| SimError::Other(SimHostError::database(e)))?
812 .map(|info| info.balance)
813 .unwrap_or(U256::ZERO);
814
815 // Recycle the reusable buffer (Pillar A.2); reclaimed after the EVM drops.
816 let buffer = Rc::new(RefCell::new(std::mem::take(&mut self.reusable_buffer)));
817 let local = LocalContext {
818 shared_memory_buffer: Rc::clone(&buffer),
819 precompile_error_message: None,
820 };
821
822 let outcome = {
823 use revm::context_interface::JournalTr;
824 let mut evm = self.build_evm_with_local(local);
825
826 // Outer checkpoint: the whole-bundle savepoint.
827 let outer = evm.journaled_state.checkpoint();
828
829 let mut per_tx: Vec<TxOutcome> = Vec::with_capacity(tx_envs.len());
830 let mut total_gas: u64 = 0;
831 let mut aborted = false;
832
833 'bundle: for (idx, tx_env) in tx_envs.into_iter().enumerate() {
834 // Inner checkpoint: this transaction's savepoint.
835 let inner = evm.journaled_state.checkpoint();
836 let result = match evm.transact_one(tx_env) {
837 Ok(result) => result,
838 Err(e) => {
839 // Host/transact error: undo this tx and the whole bundle,
840 // reclaim the buffer, and surface as SimError.
841 evm.journaled_state.checkpoint_revert(inner);
842 evm.journaled_state.checkpoint_revert(outer);
843 drop(evm);
844 self.reclaim_buffer(buffer);
845 return Err(SimError::Other(SimHostError::transact(e)));
846 }
847 };
848
849 let gas_used = result.gas_used();
850 let reverted = !result.is_success();
851 let logs = result.logs().to_vec();
852 total_gas = total_gas.saturating_add(gas_used);
853
854 per_tx.push(TxOutcome {
855 result,
856 gas_used,
857 reverted,
858 logs,
859 });
860
861 if reverted {
862 let allowed = match &opts.revert_policy {
863 RevertPolicy::Atomic => false,
864 RevertPolicy::AllowReverts(idxs) => idxs.contains(&idx),
865 };
866 if allowed {
867 // Roll back only this transaction; later txs still run.
868 evm.journaled_state.checkpoint_revert(inner);
869 continue 'bundle;
870 } else {
871 // Atomic abort: roll the whole bundle back and stop.
872 evm.journaled_state.checkpoint_revert(outer);
873 aborted = true;
874 break 'bundle;
875 }
876 }
877 // Successful tx: its effects stay journaled for the next tx.
878 }
879
880 // Partition total gas into successful/reverted buckets in a single
881 // pass. Saturating (consistent with `total_gas`); the invariant
882 // `successful_tx_gas + reverted_tx_gas == total_gas` holds by
883 // construction since every executed tx lands in exactly one bucket.
884 let (successful_tx_gas, reverted_tx_gas) =
885 per_tx.iter().fold((0u64, 0u64), |(succ, rev), tx| {
886 if tx.reverted {
887 (succ, rev.saturating_add(tx.gas_used))
888 } else {
889 (succ.saturating_add(tx.gas_used), rev)
890 }
891 });
892
893 if aborted {
894 // State is reverted to the pre-bundle outer checkpoint regardless
895 // of `commit`; no payment.
896 BundleResult {
897 per_tx,
898 coinbase_payment: U256::ZERO,
899 gas_used: total_gas,
900 successful_tx_gas,
901 reverted_tx_gas,
902 succeeded: false,
903 }
904 } else {
905 // Read the beneficiary's post-bundle balance from the journaled
906 // state (present iff it was touched) BEFORE commit/revert, since
907 // `commit_inner` finalizes (drains) the journal and an outer
908 // revert would undo the credit.
909 let post_beneficiary_balance = evm
910 .journaled_state
911 .state
912 .get(&beneficiary)
913 .map(|acct| acct.info.balance)
914 .unwrap_or(pre_beneficiary_balance);
915 // revm already excludes the base fee from the beneficiary credit
916 // (EIP-1559), so the delta is the honest miner payment.
917 let coinbase_payment =
918 post_beneficiary_balance.saturating_sub(pre_beneficiary_balance);
919
920 if opts.commit {
921 evm.commit_inner();
922 } else {
923 evm.journaled_state.checkpoint_revert(outer);
924 }
925
926 BundleResult {
927 per_tx,
928 coinbase_payment,
929 gas_used: total_gas,
930 successful_tx_gas,
931 reverted_tx_gas,
932 succeeded: true,
933 }
934 }
935 };
936
937 self.reclaim_buffer(buffer);
938 Ok(outcome)
939 }
940
941 /// Execute a non-committing call and return the result plus the touched
942 /// [`StorageAccessList`].
943 ///
944 /// The access list is collected from every account marked touched in the
945 /// journaled state after execution, recording both the touched accounts and
946 /// the storage slots accessed under each.
947 ///
948 /// The EVM state is reverted to a checkpoint after a successful transact on
949 /// both success and revert/halt outcomes, so the call never mutates this
950 /// overlay's dirty layer and each overlay simulation stays isolated. As with
951 /// [`Self::call_raw`], a revert or halt is reported through the returned
952 /// [`ExecutionResult`] rather than as an error.
953 ///
954 /// # Errors
955 ///
956 /// Returns an error if the [`TxEnv`] cannot be built, or if revm fails to
957 /// transact the call (for example a database error while loading state).
958 ///
959 /// # Examples
960 ///
961 /// ```no_run
962 /// # use std::sync::Arc;
963 /// # use alloy_primitives::{Address, Bytes};
964 /// # use evm_fork_cache::cache::{EvmOverlay, EvmSnapshot};
965 /// # fn run(snapshot: Arc<EvmSnapshot>) -> Result<(), Box<dyn std::error::Error>> {
966 /// let mut overlay = EvmOverlay::new(snapshot, None);
967 /// let (result, access_list) =
968 /// overlay.call_raw_with_access_list(Address::ZERO, Address::ZERO, Bytes::new())?;
969 /// # let _ = (result, access_list);
970 /// # Ok(())
971 /// # }
972 /// ```
973 pub fn call_raw_with_access_list(
974 &mut self,
975 from: Address,
976 to: Address,
977 calldata: Bytes,
978 ) -> Result<(ExecutionResult, StorageAccessList)> {
979 self.call_raw_with_access_list_with(from, to, calldata, &TxConfig::default())
980 }
981
982 /// Like [`call_raw_with_access_list`](Self::call_raw_with_access_list) but
983 /// honors a full [`TxConfig`]: native `value`, `gas_limit`, `gas_price`,
984 /// `nonce`, and a pre-warming EIP-2930 `access_list`.
985 ///
986 /// This is what the freshness optimistic loop uses so a [`SimRequest`]'s tx
987 /// environment — e.g. a payable call carrying `value`, or a gas-bounded call
988 /// — is reproduced faithfully instead of silently running as a zero-value,
989 /// default-gas call. Like the shorthand it is non-committing (the checkpoint
990 /// is reverted) and returns the captured storage access list.
991 ///
992 /// [`SimRequest`]: crate::freshness::SimRequest
993 pub fn call_raw_with_access_list_with(
994 &mut self,
995 from: Address,
996 to: Address,
997 calldata: Bytes,
998 tx: &TxConfig,
999 ) -> Result<(ExecutionResult, StorageAccessList)> {
1000 let mut builder = TxEnv::builder()
1001 .caller(from)
1002 .kind(TxKind::Call(to))
1003 .data(calldata)
1004 .value(tx.value);
1005 if let Some(gas_limit) = tx.gas_limit {
1006 builder = builder.gas_limit(gas_limit);
1007 }
1008 if let Some(gas_price) = tx.gas_price {
1009 builder = builder.gas_price(gas_price);
1010 }
1011 if let Some(nonce) = tx.nonce {
1012 builder = builder.nonce(nonce);
1013 }
1014 if let Some(access_list) = &tx.access_list {
1015 builder = builder.access_list(access_list.clone());
1016 }
1017 let tx_env = builder.build().map_err(OverlayError::tx_env)?;
1018
1019 // Recycle the reusable buffer (Pillar A.2); reclaimed after the EVM drops.
1020 let buffer = Rc::new(RefCell::new(std::mem::take(&mut self.reusable_buffer)));
1021 let local = LocalContext {
1022 shared_memory_buffer: Rc::clone(&buffer),
1023 precompile_error_message: None,
1024 };
1025
1026 let outcome = {
1027 let mut evm = self.build_evm_with_local(local);
1028 use revm::context_interface::JournalTr;
1029 let checkpoint = evm.journaled_state.checkpoint();
1030 match evm.transact_one(tx_env) {
1031 Ok(result) => {
1032 let mut access_list = StorageAccessList::default();
1033 for (address, account) in evm.journaled_state.state.iter() {
1034 if account.is_touched() {
1035 access_list.accounts.insert(*address);
1036 for slot_key in account.storage.keys() {
1037 access_list.slots.insert((*address, *slot_key));
1038 }
1039 }
1040 }
1041 evm.journaled_state.checkpoint_revert(checkpoint);
1042 Ok((result, access_list))
1043 }
1044 Err(e) => {
1045 // Revert the checkpoint even on a host/transact error so the EVM
1046 // journal is not left dirty (mirrors `call_raw`).
1047 evm.journaled_state.checkpoint_revert(checkpoint);
1048 Err(OverlayError::transact(e))
1049 }
1050 }
1051 };
1052
1053 self.reclaim_buffer(buffer);
1054 outcome
1055 }
1056
1057 /// Write a storage value into this overlay's dirty layer.
1058 ///
1059 /// The dirty layer takes precedence over the snapshot on subsequent reads
1060 /// (see the lookup order on [`EvmOverlay`]), so this injects a value into a
1061 /// snapshot-backed overlay without mutating the shared snapshot.
1062 ///
1063 /// # Freshness validation
1064 ///
1065 /// This is the freshness validator's correction step. When a slot the
1066 /// snapshot captured is found to be stale, the validator writes the
1067 /// freshly-fetched value here and then re-runs the simulation (e.g. via
1068 /// [`Self::call_raw`]): the re-run reads the corrected slot out of the dirty
1069 /// layer instead of the stale snapshot value, so the corrected result
1070 /// becomes observable. Because the override lives only in this overlay,
1071 /// other overlays sharing the same `Arc<EvmSnapshot>` are unaffected.
1072 ///
1073 /// # Examples
1074 ///
1075 /// ```no_run
1076 /// # use std::sync::Arc;
1077 /// # use alloy_primitives::{Address, Bytes, U256};
1078 /// # use evm_fork_cache::cache::{EvmOverlay, EvmSnapshot};
1079 /// # fn run(snapshot: Arc<EvmSnapshot>, token: Address, slot: U256) -> Result<(), Box<dyn std::error::Error>> {
1080 /// let mut overlay = EvmOverlay::new(snapshot, None);
1081 /// // Inject the fresh value, then re-run to observe the corrected result.
1082 /// overlay.override_slot(token, slot, U256::from(42u64));
1083 /// let corrected = overlay.call_raw(Address::ZERO, token, Bytes::new())?;
1084 /// # let _ = corrected;
1085 /// # Ok(())
1086 /// # }
1087 /// ```
1088 pub fn override_slot(&mut self, address: Address, slot: U256, value: U256) {
1089 self.dirty_storage
1090 .entry(address)
1091 .or_default()
1092 .insert(slot, value);
1093 }
1094
1095 /// Execute a non-committing typed Solidity call from [`Address::ZERO`],
1096 /// decoding the return — the overlay counterpart to
1097 /// [`EvmCache::call_sol`](super::EvmCache::call_sol).
1098 ///
1099 /// ```no_run
1100 /// # use std::sync::Arc;
1101 /// # use alloy_primitives::Address;
1102 /// # use alloy_sol_types::sol;
1103 /// # use evm_fork_cache::cache::{EvmOverlay, EvmSnapshot};
1104 /// # sol! { interface IErc20 { function balanceOf(address account) returns (uint256); } }
1105 /// # fn run(mut overlay: EvmOverlay, token: Address, alice: Address) -> Result<(), Box<dyn std::error::Error>> {
1106 /// let bal = overlay.call_sol(token, IErc20::balanceOfCall { account: alice })?;
1107 /// # let _ = bal; Ok(()) }
1108 /// ```
1109 pub fn call_sol<C: SolCall>(&mut self, to: Address, call: C) -> Result<C::Return> {
1110 self.call_sol_from(Address::ZERO, to, call)
1111 }
1112
1113 /// Execute a non-committing typed Solidity call from an explicit sender,
1114 /// decoding the return.
1115 pub fn call_sol_from<C: SolCall>(
1116 &mut self,
1117 from: Address,
1118 to: Address,
1119 call: C,
1120 ) -> Result<C::Return> {
1121 let result = self.call_raw(from, to, Bytes::from(call.abi_encode()))?;
1122 match result {
1123 ExecutionResult::Success { output, .. } => {
1124 let output = output.into_data();
1125 C::abi_decode_returns(&output).map_err(|error| OverlayError::SolCallDecode {
1126 signature: C::SIGNATURE,
1127 from,
1128 to,
1129 output_len: output.len(),
1130 details: format!("{error:?}"),
1131 })
1132 }
1133 other => Err(OverlayError::SolCallFailed {
1134 signature: C::SIGNATURE,
1135 from,
1136 to,
1137 result: format!("{other:?}"),
1138 }),
1139 }
1140 }
1141
1142 /// Mock `holder`'s ERC-20 balance of `token` to `amount` — **overlay-local**.
1143 ///
1144 /// Discovers the balance mapping slot and layout (Solidity / Vyper / Solady)
1145 /// from a single `balanceOf(holder)` simulation, writes `amount` to that slot
1146 /// in this overlay's dirty layer via [`override_slot`](Self::override_slot),
1147 /// and verifies. The cache and snapshot are never mutated; the mock is
1148 /// dropped with the overlay.
1149 ///
1150 /// Returns `Ok(true)` if set and verified, `Ok(false)` if no balance slot was
1151 /// discoverable or the write did not drive the return (e.g. a rebasing token,
1152 /// or `holder == Address::ZERO`, which is refused). A failed attempt leaves no
1153 /// stray write.
1154 pub fn mock_balance(
1155 &mut self,
1156 token: Address,
1157 holder: Address,
1158 amount: U256,
1159 ) -> SimulationResult<bool> {
1160 if holder == Address::ZERO {
1161 return Ok(false);
1162 }
1163 let calldata = Bytes::from(IERC20::balanceOfCall { target: holder }.abi_encode());
1164 let holder_word = holder.into_word();
1165 self.mock_slot_driving(token, calldata, amount, move |probe, ret| {
1166 probe
1167 .accesses(&[holder_word])
1168 .into_iter()
1169 .filter(|a| a.keyed_by(holder_word))
1170 .max_by_key(|a| (a.value == ret, a.confidence))
1171 .map(|a| (a.slot, a.value))
1172 })
1173 }
1174
1175 /// Mock `owner`'s ERC-20 allowance to `spender` on `token` — overlay-local.
1176 ///
1177 /// Discovers the (nested) `allowance` mapping entry keyed by both addresses,
1178 /// writes `amount` (pass `U256::MAX` for "unlimited"), and verifies. Refuses
1179 /// `owner == Address::ZERO`. Same isolation and failure semantics as
1180 /// [`mock_balance`](Self::mock_balance).
1181 pub fn mock_allowance(
1182 &mut self,
1183 token: Address,
1184 owner: Address,
1185 spender: Address,
1186 amount: U256,
1187 ) -> SimulationResult<bool> {
1188 if owner == Address::ZERO {
1189 return Ok(false);
1190 }
1191 let calldata = Bytes::from(IERC20::allowanceCall { owner, spender }.abi_encode());
1192 let (owner_word, spender_word) = (owner.into_word(), spender.into_word());
1193 self.mock_slot_driving(token, calldata, amount, move |probe, _ret| {
1194 probe
1195 .accesses(&[owner_word, spender_word])
1196 .into_iter()
1197 .filter(|a| a.keyed_by(owner_word) && a.keyed_by(spender_word))
1198 .max_by_key(|a| (a.depth, a.confidence))
1199 .map(|a| (a.slot, a.value))
1200 })
1201 }
1202
1203 /// Mock the return value of a single-word view call by finding the storage
1204 /// slot that drives it and overriding that slot — overlay-local.
1205 ///
1206 /// Runs `to.calldata`, identifies the `SLOAD` whose loaded value equals the
1207 /// call's returned word (see
1208 /// [`HashStorageProbe::slots_returning`](crate::mapping_probe::HashStorageProbe::slots_returning)),
1209 /// writes `desired` there, and verifies the call now returns `desired`. Works
1210 /// for balances, allowances, `totalSupply`, and any getter that returns a
1211 /// single stored word. Returns `Ok(false)` (leaving no stray write) when the
1212 /// return is computed from more than one slot, so it can't be set by a single
1213 /// override.
1214 pub fn mock_view(
1215 &mut self,
1216 to: Address,
1217 calldata: Bytes,
1218 desired: U256,
1219 ) -> SimulationResult<bool> {
1220 self.mock_slot_driving(to, calldata, desired, |probe, ret| {
1221 probe
1222 .slots_returning(ret)
1223 .into_iter()
1224 .next()
1225 .map(|slot| (slot, ret))
1226 })
1227 }
1228
1229 /// Typed [`mock_view`](Self::mock_view): mock the `desired` return of a
1230 /// [`SolCall`] getter that returns a single word.
1231 ///
1232 /// ```no_run
1233 /// # use std::sync::Arc;
1234 /// # use alloy_primitives::{Address, U256};
1235 /// # use alloy_sol_types::sol;
1236 /// # use evm_fork_cache::cache::{EvmOverlay, EvmSnapshot};
1237 /// # sol! { interface IErc20 { function totalSupply() returns (uint256); } }
1238 /// # fn run(mut overlay: EvmOverlay, token: Address) -> Result<(), Box<dyn std::error::Error>> {
1239 /// overlay.mock_call(token, IErc20::totalSupplyCall {}, U256::from(1_000u64))?;
1240 /// # Ok(()) }
1241 /// ```
1242 pub fn mock_call<C: SolCall>(
1243 &mut self,
1244 to: Address,
1245 call: C,
1246 desired: U256,
1247 ) -> SimulationResult<bool> {
1248 self.mock_view(to, Bytes::from(call.abi_encode()), desired)
1249 }
1250
1251 /// Extract the leading 32-byte word of a successful call's return data.
1252 fn success_word(result: &ExecutionResult) -> Option<U256> {
1253 match result {
1254 ExecutionResult::Success { output, .. } => {
1255 let data = output.data();
1256 (data.len() >= 32).then(|| U256::from_be_slice(&data[..32]))
1257 }
1258 _ => None,
1259 }
1260 }
1261
1262 /// Shared core for the `mock_*` methods: discover the slot driving
1263 /// `to.calldata`'s return via `choose`, override it to `desired`, verify, and
1264 /// restore the slot on a failed verify so a mis-pick leaves no stray write.
1265 fn mock_slot_driving<F>(
1266 &mut self,
1267 to: Address,
1268 calldata: Bytes,
1269 desired: U256,
1270 choose: F,
1271 ) -> SimulationResult<bool>
1272 where
1273 F: FnOnce(&HashStorageProbe, U256) -> Option<(B256, U256)>,
1274 {
1275 let (result, probe) = self.call_raw_with_inspector(
1276 Address::ZERO,
1277 to,
1278 calldata.clone(),
1279 &TxConfig::default(),
1280 HashStorageProbe::new(),
1281 false,
1282 )?;
1283 let Some(ret) = Self::success_word(&result) else {
1284 return Ok(false);
1285 };
1286 let Some((slot, prev)) = choose(&probe, ret) else {
1287 return Ok(false);
1288 };
1289 let slot_u = U256::from_be_bytes(slot.0);
1290 self.override_slot(to, slot_u, desired);
1291
1292 let (verify, _) = self.call_raw_with_inspector(
1293 Address::ZERO,
1294 to,
1295 calldata,
1296 &TxConfig::default(),
1297 HashStorageProbe::new(),
1298 false,
1299 )?;
1300 if Self::success_word(&verify) == Some(desired) {
1301 Ok(true)
1302 } else {
1303 self.override_slot(to, slot_u, prev); // undo the mis-pick
1304 Ok(false)
1305 }
1306 }
1307}
1308
1309impl revm::database_interface::DatabaseCommit for EvmOverlay {
1310 fn commit(&mut self, changes: alloy_primitives::map::HashMap<Address, revm::state::Account>) {
1311 for (address, account) in changes {
1312 self.dirty_accounts.insert(address, account.info);
1313 let storage = self.dirty_storage.entry(address).or_default();
1314 for (slot, value) in account.storage {
1315 storage.insert(slot, value.present_value);
1316 }
1317 }
1318 }
1319}
1320
1321impl Database for EvmOverlay {
1322 type Error = DatabaseError;
1323
1324 fn basic(&mut self, address: Address) -> Result<Option<AccountInfo>, Self::Error> {
1325 // 1. Check dirty layer
1326 if let Some(info) = self.dirty_accounts.get(&address) {
1327 return Ok(Some(info.clone()));
1328 }
1329 // 2. Check snapshot (O(1) HashMap lookup, no locks). `account_info` folds
1330 // the two snapshot tiers (overlay ▸ base) and already short-circuits a
1331 // NotExisting account to None — it must NOT fall through to the ext_db,
1332 // mirroring revm `DbAccount::info()` and the live `EvmCache` read.
1333 if self.snapshot.accounts_not_existing.contains(&address) {
1334 return Ok(None);
1335 }
1336 if let Some(info) = self.snapshot.account_info(address) {
1337 return Ok(Some(info.clone()));
1338 }
1339 // 3. RPC fallback
1340 if let Some(ref ext_db) = self.ext_db {
1341 let info = ext_db.basic_ref(address)?;
1342 if let Some(ref info) = info {
1343 self.dirty_accounts.insert(address, info.clone());
1344 }
1345 return Ok(info);
1346 }
1347 Ok(None)
1348 }
1349
1350 fn code_by_hash(&mut self, code_hash: B256) -> Result<Bytecode, Self::Error> {
1351 // Check dirty accounts first
1352 for info in self.dirty_accounts.values() {
1353 if info.code_hash == code_hash
1354 && let Some(code) = &info.code
1355 {
1356 return Ok(code.clone());
1357 }
1358 }
1359 // Check the snapshot's code index (overlay ▸ base).
1360 if let Some(code) = self.snapshot.code(code_hash) {
1361 return Ok(code.clone());
1362 }
1363 // RPC fallback
1364 if let Some(ref ext_db) = self.ext_db {
1365 return ext_db.code_by_hash_ref(code_hash);
1366 }
1367 Ok(Bytecode::default())
1368 }
1369
1370 fn storage(&mut self, address: Address, index: U256) -> Result<U256, Self::Error> {
1371 // 1. Check dirty layer
1372 if let Some(account_storage) = self.dirty_storage.get(&address)
1373 && let Some(value) = account_storage.get(&index)
1374 {
1375 return Ok(*value);
1376 }
1377 // 2. Check snapshot (O(1)). `storage_value` folds the two tiers (overlay ▸
1378 // cleared-as-ZERO ▸ base); a cleared account's absent slot reads ZERO
1379 // and must NOT fall through to the ext_db, mirroring the live EVM SLOAD
1380 // for a StorageCleared/NotExisting account.
1381 if let Some(value) = self.snapshot.storage_value(address, index) {
1382 return Ok(value);
1383 }
1384 // 3. RPC fallback
1385 if let Some(ref ext_db) = self.ext_db {
1386 let value = ext_db.storage_ref(address, index)?;
1387 self.dirty_storage
1388 .entry(address)
1389 .or_default()
1390 .insert(index, value);
1391 return Ok(value);
1392 }
1393 Ok(U256::ZERO)
1394 }
1395
1396 fn block_hash(&mut self, number: u64) -> Result<B256, Self::Error> {
1397 if let Some(hash) = self.snapshot.block_hashes.get(&number) {
1398 return Ok(*hash);
1399 }
1400 if let Some(ref ext_db) = self.ext_db {
1401 return ext_db.block_hash_ref(number);
1402 }
1403 // Snapshots never populate `block_hashes` (the live cache does not track
1404 // block hashes), so without an `ext_db` the `BLOCKHASH` opcode resolves to
1405 // ZERO. Overlays built internally (e.g. the freshness validator) pass
1406 // `ext_db = None`; the fallback is recorded so the validator can fail
1407 // closed (`Unverified`) instead of confirming a sim whose control flow
1408 // may depend on the real hash. See `blockhash_zero_fallback()`.
1409 self.blockhash_zero_fallback = true;
1410 Ok(B256::ZERO)
1411 }
1412}
1413
1414fn extract_access_list(state: &revm::state::EvmState) -> AccessList {
1415 let items: Vec<AccessListItem> = state
1416 .iter()
1417 .filter(|(_, account)| account.is_touched())
1418 .map(|(address, account)| AccessListItem {
1419 address: *address,
1420 storage_keys: account
1421 .storage
1422 .keys()
1423 .map(|slot| B256::from(*slot))
1424 .collect(),
1425 })
1426 .collect();
1427 AccessList(items)
1428}
1429
1430#[cfg(test)]
1431mod tests {
1432 use super::*;
1433 use crate::cache::snapshot::BaseState;
1434 use revm::primitives::hardfork::SpecId;
1435 use std::collections::HashSet;
1436
1437 /// Build a two-tier `EvmSnapshot` whose cold base holds the given accounts,
1438 /// storage, and code, with an empty hot overlay — the shape
1439 /// `snapshot_deep_clone` produces. The `Arc`-per-account storage of the
1440 /// base is built from the plain per-account maps.
1441 fn snap(
1442 accounts: HashMap<Address, AccountInfo>,
1443 storage: HashMap<Address, HashMap<U256, U256>>,
1444 code_by_hash: HashMap<B256, Bytecode>,
1445 block_hashes: HashMap<u64, B256>,
1446 ) -> Arc<EvmSnapshot> {
1447 let base = BaseState {
1448 accounts,
1449 storage: storage
1450 .into_iter()
1451 .map(|(addr, slots)| (addr, Arc::new(slots)))
1452 .collect(),
1453 code_by_hash,
1454 };
1455 Arc::new(EvmSnapshot {
1456 base: Arc::new(base),
1457 overlay_accounts: HashMap::new(),
1458 overlay_storage: HashMap::new(),
1459 overlay_code_by_hash: HashMap::new(),
1460 storage_cleared: HashSet::new(),
1461 accounts_not_existing: HashSet::new(),
1462 block_hashes,
1463 block_number: None,
1464 basefee: None,
1465 coinbase: None,
1466 prevrandao: None,
1467 gas_limit: None,
1468 chain_id: 42161,
1469 timestamp: None,
1470 spec_id: SpecId::CANCUN,
1471 shared_memory_capacity: 64_000,
1472 })
1473 }
1474
1475 #[test]
1476 fn test_overlay_is_send() {
1477 fn assert_send<T: Send>() {}
1478 assert_send::<EvmOverlay>();
1479 }
1480
1481 #[test]
1482 fn blockhash_zero_fallback_flags_only_unresolved_reads() {
1483 let known = B256::repeat_byte(0xAB);
1484 let snapshot = snap(
1485 HashMap::new(),
1486 HashMap::new(),
1487 HashMap::new(),
1488 HashMap::from([(5u64, known)]),
1489 );
1490 let mut overlay = EvmOverlay::new(snapshot, None);
1491
1492 // A snapshot-provided hash resolves for real: no flag.
1493 assert_eq!(overlay.block_hash(5).unwrap(), known);
1494 assert!(!overlay.blockhash_zero_fallback());
1495
1496 // An untracked number falls back to ZERO and is flagged so the
1497 // freshness validator can fail closed.
1498 assert_eq!(overlay.block_hash(6).unwrap(), B256::ZERO);
1499 assert!(overlay.blockhash_zero_fallback());
1500
1501 // The flag is per-simulation state: reset clears it.
1502 overlay.reset();
1503 assert!(!overlay.blockhash_zero_fallback());
1504 }
1505
1506 #[test]
1507 fn test_overlay_basic_from_snapshot() {
1508 let mut accounts = HashMap::new();
1509 let info = AccountInfo {
1510 balance: U256::from(1000),
1511 nonce: 1,
1512 code_hash: B256::ZERO,
1513 code: None,
1514 account_id: None,
1515 };
1516 let addr = Address::repeat_byte(0x01);
1517 accounts.insert(addr, info);
1518
1519 let snapshot = snap(accounts, HashMap::new(), HashMap::new(), HashMap::new());
1520
1521 let mut overlay = EvmOverlay::new(snapshot, None);
1522 let result = overlay.basic(addr).unwrap();
1523 assert!(result.is_some());
1524 assert_eq!(result.unwrap().balance, U256::from(1000));
1525 }
1526
1527 #[test]
1528 fn test_overlay_storage_from_snapshot() {
1529 let addr = Address::repeat_byte(0x01);
1530 let slot = U256::from(42);
1531 let value = U256::from(999);
1532
1533 let mut storage = HashMap::new();
1534 let mut account_storage = HashMap::new();
1535 account_storage.insert(slot, value);
1536 storage.insert(addr, account_storage);
1537
1538 let snapshot = snap(HashMap::new(), storage, HashMap::new(), HashMap::new());
1539
1540 let mut overlay = EvmOverlay::new(snapshot, None);
1541 let result = overlay.storage(addr, slot).unwrap();
1542 assert_eq!(result, value);
1543 }
1544
1545 #[test]
1546 fn test_overlay_dirty_overrides_snapshot() {
1547 let addr = Address::repeat_byte(0x01);
1548 let slot = U256::from(42);
1549
1550 let mut storage = HashMap::new();
1551 let mut account_storage = HashMap::new();
1552 account_storage.insert(slot, U256::from(100));
1553 storage.insert(addr, account_storage);
1554
1555 let snapshot = snap(HashMap::new(), storage, HashMap::new(), HashMap::new());
1556
1557 let mut overlay = EvmOverlay::new(snapshot, None);
1558
1559 // Write to dirty layer
1560 overlay
1561 .dirty_storage
1562 .entry(addr)
1563 .or_default()
1564 .insert(slot, U256::from(200));
1565
1566 // Should read dirty value, not snapshot
1567 let result = overlay.storage(addr, slot).unwrap();
1568 assert_eq!(result, U256::from(200));
1569 }
1570
1571 #[test]
1572 fn test_overlay_missing_returns_zero() {
1573 let snapshot = snap(
1574 HashMap::new(),
1575 HashMap::new(),
1576 HashMap::new(),
1577 HashMap::new(),
1578 );
1579
1580 let mut overlay = EvmOverlay::new(snapshot, None);
1581 let addr = Address::repeat_byte(0x99);
1582 let result = overlay.storage(addr, U256::from(1)).unwrap();
1583 assert_eq!(result, U256::ZERO);
1584
1585 let account = overlay.basic(addr).unwrap();
1586 assert!(account.is_none());
1587 }
1588
1589 #[test]
1590 fn test_overlay_code_by_hash_from_snapshot() {
1591 let code = Bytecode::new_raw(Bytes::from(vec![0x60, 0x00, 0x60, 0x00]));
1592 let hash = code.hash_slow();
1593
1594 let mut code_by_hash = HashMap::new();
1595 code_by_hash.insert(hash, code.clone());
1596
1597 let snapshot = snap(HashMap::new(), HashMap::new(), code_by_hash, HashMap::new());
1598
1599 let mut overlay = EvmOverlay::new(snapshot, None);
1600 let result = overlay.code_by_hash(hash).unwrap();
1601 assert_eq!(result.len(), 4);
1602 }
1603
1604 #[test]
1605 fn test_overlay_block_hash() {
1606 let mut block_hashes = HashMap::new();
1607 let hash = B256::repeat_byte(0xAB);
1608 block_hashes.insert(42u64, hash);
1609
1610 let snapshot = snap(HashMap::new(), HashMap::new(), HashMap::new(), block_hashes);
1611
1612 let mut overlay = EvmOverlay::new(snapshot, None);
1613 assert_eq!(overlay.block_hash(42).unwrap(), hash);
1614 assert_eq!(overlay.block_hash(99).unwrap(), B256::ZERO);
1615 }
1616}