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