Skip to main content

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}