Skip to main content

example_cheatcode_inspector/
main.rs

1//! An example that shows how to implement a Foundry-style Solidity test cheatcode inspector.
2//!
3//! The code below mimics relevant parts of the implementation of the [`transact`](https://book.getfoundry.sh/cheatcodes/transact)
4//! and [`rollFork(uint256 forkId, bytes32 transaction)`](https://book.getfoundry.sh/cheatcodes/roll-fork#rollfork) cheatcodes.
5//! Both of these cheatcodes initiate transactions from a call step in the cheatcode inspector which is the most
6//! advanced cheatcode use-case.
7#![cfg_attr(not(test), warn(unused_crate_dependencies))]
8
9use revm::{
10    context::{
11        journaled_state::{account::JournaledAccount, AccountInfoLoad, JournalLoadError},
12        result::InvalidTransaction,
13        BlockEnv, Cfg, CfgEnv, ContextTr, Evm, LocalContext, TxEnv,
14    },
15    context_interface::{
16        journaled_state::{AccountLoad, JournalCheckpoint, TransferError},
17        result::EVMError,
18        Block, JournalTr, Transaction,
19    },
20    database::InMemoryDB,
21    handler::{instructions::EthInstructions, EthPrecompiles},
22    inspector::{inspectors::TracerEip3155, JournalExt},
23    interpreter::{
24        interpreter::EthInterpreter, CallInputs, CallOutcome, SStoreResult, SelfDestructResult,
25        StateLoad,
26    },
27    primitives::{
28        hardfork::SpecId, Address, AddressMap, AddressSet, HashSet, Log, StorageKey, StorageValue,
29        B256, U256,
30    },
31    state::{Account, Bytecode, EvmState},
32    Context, Database, DatabaseCommit, InspectEvm, Inspector, Journal, JournalEntry,
33};
34use std::{convert::Infallible, fmt::Debug};
35
36/// Backend for cheatcodes.
37/// The problematic cheatcodes are only supported in fork mode, so we'll omit the non-fork behavior of the Foundry
38/// `Backend`.
39#[derive(Clone, Debug)]
40struct Backend {
41    /// In fork mode, Foundry stores (`JournaledState`, `Database`) pairs for each fork.
42    journaled_state: Journal<InMemoryDB>,
43    /// Counters to be able to assert that we mutated the object that we expected to mutate.
44    method_with_inspector_counter: usize,
45    method_without_inspector_counter: usize,
46}
47
48impl Backend {
49    fn new(spec: SpecId, db: InMemoryDB) -> Self {
50        let mut journaled_state = Journal::new(db);
51        journaled_state.set_spec_id(spec);
52        Self {
53            journaled_state,
54            method_with_inspector_counter: 0,
55            method_without_inspector_counter: 0,
56        }
57    }
58}
59
60impl JournalTr for Backend {
61    type Database = InMemoryDB;
62    type State = EvmState;
63    type JournaledAccount<'a> = JournaledAccount<'a, InMemoryDB, JournalEntry>;
64
65    fn new(database: InMemoryDB) -> Self {
66        Self::new(SpecId::default(), database)
67    }
68
69    fn db_and_state(&self) -> (&Self::Database, &Self::State) {
70        self.journaled_state.db_and_state()
71    }
72
73    fn db_and_state_mut(&mut self) -> (&mut Self::Database, &mut Self::State) {
74        self.journaled_state.db_and_state_mut()
75    }
76
77    fn sload(
78        &mut self,
79        address: Address,
80        key: StorageKey,
81    ) -> Result<StateLoad<StorageValue>, <Self::Database as Database>::Error> {
82        self.journaled_state.sload(address, key)
83    }
84
85    fn sstore(
86        &mut self,
87        address: Address,
88        key: StorageKey,
89        value: StorageValue,
90    ) -> Result<StateLoad<SStoreResult>, <Self::Database as Database>::Error> {
91        self.journaled_state.sstore(address, key, value)
92    }
93
94    fn tload(&mut self, address: Address, key: StorageKey) -> StorageValue {
95        self.journaled_state.tload(address, key)
96    }
97
98    fn tstore(&mut self, address: Address, key: StorageKey, value: StorageValue) {
99        self.journaled_state.tstore(address, key, value)
100    }
101
102    fn log(&mut self, log: Log) {
103        self.journaled_state.log(log)
104    }
105
106    fn logs(&self) -> &[Log] {
107        self.journaled_state.logs()
108    }
109
110    fn selfdestruct(
111        &mut self,
112        address: Address,
113        target: Address,
114        skip_cold_load: bool,
115    ) -> Result<StateLoad<SelfDestructResult>, JournalLoadError<Infallible>> {
116        self.journaled_state
117            .selfdestruct(address, target, skip_cold_load)
118    }
119
120    fn warm_access_list(&mut self, access_list: AddressMap<HashSet<StorageKey>>) {
121        self.journaled_state.warm_access_list(access_list);
122    }
123
124    fn warm_coinbase_account(&mut self, address: Address) {
125        self.journaled_state.warm_coinbase_account(address)
126    }
127
128    fn warm_precompiles(&mut self, addresses: &AddressSet) {
129        self.journaled_state.warm_precompiles(addresses)
130    }
131
132    fn precompile_addresses(&self) -> &AddressSet {
133        self.journaled_state.precompile_addresses()
134    }
135
136    fn set_spec_id(&mut self, spec_id: SpecId) {
137        self.journaled_state.set_spec_id(spec_id);
138    }
139
140    fn set_eip7708_config(&mut self, disabled: bool, delayed_burn_disabled: bool) {
141        self.journaled_state
142            .set_eip7708_config(disabled, delayed_burn_disabled);
143    }
144
145    fn touch_account(&mut self, address: Address) {
146        self.journaled_state.touch_account(address);
147    }
148
149    fn transfer(
150        &mut self,
151        from: Address,
152        to: Address,
153        balance: U256,
154    ) -> Result<Option<TransferError>, Infallible> {
155        self.journaled_state.transfer(from, to, balance)
156    }
157
158    fn transfer_loaded(
159        &mut self,
160        from: Address,
161        to: Address,
162        balance: U256,
163    ) -> Option<TransferError> {
164        self.journaled_state.transfer_loaded(from, to, balance)
165    }
166
167    fn load_account(&mut self, address: Address) -> Result<StateLoad<&Account>, Infallible> {
168        self.journaled_state.load_account(address)
169    }
170
171    fn load_account_with_code(
172        &mut self,
173        address: Address,
174    ) -> Result<StateLoad<&Account>, Infallible> {
175        self.journaled_state.load_account_with_code(address)
176    }
177
178    fn load_account_delegated(
179        &mut self,
180        address: Address,
181    ) -> Result<StateLoad<AccountLoad>, Infallible> {
182        self.journaled_state.load_account_delegated(address)
183    }
184
185    fn set_code_with_hash(&mut self, address: Address, code: Bytecode, hash: B256) {
186        self.journaled_state.set_code_with_hash(address, code, hash);
187    }
188
189    fn code(
190        &mut self,
191        address: Address,
192    ) -> Result<StateLoad<revm::primitives::Bytes>, <Self::Database as Database>::Error> {
193        self.journaled_state.code(address)
194    }
195
196    fn code_hash(
197        &mut self,
198        address: Address,
199    ) -> Result<StateLoad<B256>, <Self::Database as Database>::Error> {
200        self.journaled_state.code_hash(address)
201    }
202
203    fn clear(&mut self) {
204        self.journaled_state.clear();
205    }
206
207    fn checkpoint(&mut self) -> JournalCheckpoint {
208        self.journaled_state.checkpoint()
209    }
210
211    fn checkpoint_commit(&mut self) {
212        self.journaled_state.checkpoint_commit()
213    }
214
215    fn checkpoint_revert(&mut self, checkpoint: JournalCheckpoint) {
216        self.journaled_state.checkpoint_revert(checkpoint)
217    }
218
219    fn create_account_checkpoint(
220        &mut self,
221        caller: Address,
222        address: Address,
223        balance: U256,
224        spec_id: SpecId,
225    ) -> Result<JournalCheckpoint, TransferError> {
226        self.journaled_state
227            .create_account_checkpoint(caller, address, balance, spec_id)
228    }
229
230    /// Returns call depth.
231    #[inline]
232    fn depth(&self) -> usize {
233        self.journaled_state.depth()
234    }
235
236    fn finalize(&mut self) -> Self::State {
237        self.journaled_state.finalize()
238    }
239
240    fn caller_accounting_journal_entry(
241        &mut self,
242        address: Address,
243        old_balance: U256,
244        bump_nonce: bool,
245    ) {
246        #[expect(deprecated)]
247        self.journaled_state
248            .caller_accounting_journal_entry(address, old_balance, bump_nonce)
249    }
250
251    fn balance_incr(
252        &mut self,
253        address: Address,
254        balance: U256,
255    ) -> Result<(), <Self::Database as Database>::Error> {
256        self.journaled_state.balance_incr(address, balance)
257    }
258
259    fn nonce_bump_journal_entry(&mut self, address: Address) {
260        #[expect(deprecated)]
261        self.journaled_state.nonce_bump_journal_entry(address)
262    }
263
264    fn take_logs(&mut self) -> Vec<Log> {
265        self.journaled_state.take_logs()
266    }
267
268    fn commit_tx(&mut self) {
269        self.journaled_state.commit_tx()
270    }
271
272    fn discard_tx(&mut self) {
273        self.journaled_state.discard_tx()
274    }
275
276    fn sload_skip_cold_load(
277        &mut self,
278        address: Address,
279        key: StorageKey,
280        skip_cold_load: bool,
281    ) -> Result<StateLoad<StorageValue>, JournalLoadError<<Self::Database as Database>::Error>>
282    {
283        self.journaled_state
284            .sload_skip_cold_load(address, key, skip_cold_load)
285    }
286
287    fn sstore_skip_cold_load(
288        &mut self,
289        address: Address,
290        key: StorageKey,
291        value: StorageValue,
292        skip_cold_load: bool,
293    ) -> Result<StateLoad<SStoreResult>, JournalLoadError<<Self::Database as Database>::Error>>
294    {
295        self.journaled_state
296            .sstore_skip_cold_load(address, key, value, skip_cold_load)
297    }
298
299    fn load_account_mut_skip_cold_load(
300        &mut self,
301        address: Address,
302        skip_cold_load: bool,
303    ) -> Result<StateLoad<Self::JournaledAccount<'_>>, JournalLoadError<Infallible>> {
304        self.journaled_state
305            .load_account_mut_skip_cold_load(address, skip_cold_load)
306    }
307
308    fn load_account_info_skip_cold_load(
309        &mut self,
310        address: Address,
311        load_code: bool,
312        skip_cold_load: bool,
313    ) -> Result<AccountInfoLoad<'_>, JournalLoadError<Infallible>> {
314        self.journaled_state
315            .load_account_info_skip_cold_load(address, load_code, skip_cold_load)
316    }
317
318    fn load_account_mut_optional_code(
319        &mut self,
320        address: Address,
321        load_code: bool,
322    ) -> Result<StateLoad<Self::JournaledAccount<'_>>, Infallible> {
323        self.journaled_state
324            .load_account_mut_optional_code(address, load_code)
325    }
326}
327
328impl JournalExt for Backend {
329    fn journal(&self) -> &[JournalEntry] {
330        self.journaled_state.journal()
331    }
332}
333
334/// Used in Foundry to provide extended functionality to cheatcodes.
335/// The methods are called from the `Cheatcodes` inspector.
336trait DatabaseExt: JournalTr {
337    /// Mimics `DatabaseExt::transact`
338    /// See `commit_transaction` for the generics
339    fn method_that_takes_inspector_as_argument<InspectorT, BlockT, TxT, CfgT>(
340        &mut self,
341        env: Env<BlockT, TxT, CfgT>,
342        inspector: InspectorT,
343    ) -> anyhow::Result<()>
344    where
345        InspectorT: Inspector<Context<BlockT, TxT, CfgT, InMemoryDB, Backend>, EthInterpreter>,
346        BlockT: Block,
347        TxT: Transaction + Clone,
348        CfgT: Cfg;
349
350    /// Mimics `DatabaseExt::roll_fork_to_transaction`
351    fn method_that_constructs_inspector<BlockT, TxT, CfgT>(
352        &mut self,
353        env: Env<BlockT, TxT, CfgT>,
354    ) -> anyhow::Result<()>
355    where
356        BlockT: Block,
357        TxT: Transaction + Clone,
358        CfgT: Cfg;
359}
360
361impl DatabaseExt for Backend {
362    fn method_that_takes_inspector_as_argument<InspectorT, BlockT, TxT, CfgT>(
363        &mut self,
364        env: Env<BlockT, TxT, CfgT>,
365        inspector: InspectorT,
366    ) -> anyhow::Result<()>
367    where
368        InspectorT: Inspector<Context<BlockT, TxT, CfgT, InMemoryDB, Backend>, EthInterpreter>,
369        BlockT: Block,
370        TxT: Transaction + Clone,
371        CfgT: Cfg,
372    {
373        commit_transaction(self, env, inspector)?;
374        self.method_with_inspector_counter += 1;
375        Ok(())
376    }
377
378    fn method_that_constructs_inspector<BlockT, TxT, CfgT>(
379        &mut self,
380        env: Env<BlockT, TxT, CfgT>,
381    ) -> anyhow::Result<()>
382    where
383        BlockT: Block,
384        TxT: Transaction + Clone,
385        CfgT: Cfg,
386    {
387        let inspector = TracerEip3155::new(Box::new(std::io::sink()));
388        commit_transaction(self, env, inspector)?;
389
390        self.method_without_inspector_counter += 1;
391        Ok(())
392    }
393}
394
395/// An REVM inspector that intercepts calls to the cheatcode address and executes them with the help of the
396/// `DatabaseExt` trait.
397#[derive(Clone, Default)]
398struct Cheatcodes<BlockT, TxT, CfgT> {
399    call_count: usize,
400    phantom: core::marker::PhantomData<(BlockT, TxT, CfgT)>,
401}
402
403impl<BlockT, TxT, CfgT> Cheatcodes<BlockT, TxT, CfgT>
404where
405    BlockT: Block + Clone,
406    TxT: Transaction + Clone,
407    CfgT: Cfg + Clone,
408{
409    fn apply_cheatcode(
410        &mut self,
411        context: &mut Context<BlockT, TxT, CfgT, InMemoryDB, Backend>,
412    ) -> anyhow::Result<()> {
413        // We cannot avoid cloning here, because we need to mutably borrow the context to get the journal.
414        let block = context.block.clone();
415        let tx = context.tx.clone();
416        let cfg = context.cfg.clone();
417
418        // `transact` cheatcode would do this
419        context
420            .journal_mut()
421            .method_that_takes_inspector_as_argument(
422                Env {
423                    block: block.clone(),
424                    tx: tx.clone(),
425                    cfg: cfg.clone(),
426                },
427                self,
428            )?;
429
430        // `rollFork(bytes32 transaction)` cheatcode would do this
431        context
432            .journal_mut()
433            .method_that_constructs_inspector(Env { block, tx, cfg })?;
434        Ok(())
435    }
436}
437
438impl<BlockT, TxT, CfgT> Inspector<Context<BlockT, TxT, CfgT, InMemoryDB, Backend>>
439    for Cheatcodes<BlockT, TxT, CfgT>
440where
441    BlockT: Block + Clone,
442    TxT: Transaction + Clone,
443    CfgT: Cfg + Clone,
444{
445    /// Note that precompiles are no longer accessible via `EvmContext::precompiles`.
446    fn call(
447        &mut self,
448        context: &mut Context<BlockT, TxT, CfgT, InMemoryDB, Backend>,
449        _inputs: &mut CallInputs,
450    ) -> Option<CallOutcome> {
451        self.call_count += 1;
452        // Don't apply cheatcodes recursively.
453        if self.call_count == 1 {
454            // Instead of calling unwrap here, we would want to return an appropriate call outcome based on the result
455            // in a real project.
456            self.apply_cheatcode(context).unwrap();
457        }
458        None
459    }
460}
461
462/// EVM environment
463#[derive(Clone, Debug)]
464struct Env<BlockT, TxT, CfgT> {
465    block: BlockT,
466    tx: TxT,
467    cfg: CfgT,
468}
469
470impl Env<BlockEnv, TxEnv, CfgEnv> {
471    fn mainnet() -> Self {
472        // `CfgEnv` is non-exhaustive, so we need to set the field after construction.
473        let mut cfg = CfgEnv::default();
474        cfg.disable_nonce_check = true;
475
476        Self {
477            block: BlockEnv::default(),
478            tx: TxEnv::default(),
479            cfg,
480        }
481    }
482}
483
484/// Executes a transaction and runs the inspector using the `Backend` as the state.
485/// Mimics `commit_transaction` <https://github.com/foundry-rs/foundry/blob/25cc1ac68b5f6977f23d713c01ec455ad7f03d21/crates/evm/core/src/backend/mod.rs#L1931>
486fn commit_transaction<InspectorT, BlockT, TxT, CfgT>(
487    backend: &mut Backend,
488    env: Env<BlockT, TxT, CfgT>,
489    inspector: InspectorT,
490) -> Result<(), EVMError<Infallible, InvalidTransaction>>
491where
492    InspectorT: Inspector<Context<BlockT, TxT, CfgT, InMemoryDB, Backend>, EthInterpreter>,
493    BlockT: Block,
494    TxT: Transaction + Clone,
495    CfgT: Cfg,
496{
497    // Create new journaled state and backend with the same DB and journaled state as the original for the transaction.
498    // This new backend and state will be discarded after the transaction is done and the changes are applied to the
499    // original backend.
500    // Mimics https://github.com/foundry-rs/foundry/blob/25cc1ac68b5f6977f23d713c01ec455ad7f03d21/crates/evm/core/src/backend/mod.rs#L1950-L1953
501    let new_backend = backend.clone();
502    let tx = env.tx.clone();
503
504    let context = Context {
505        tx: env.tx,
506        block: env.block,
507        cfg: env.cfg,
508        journaled_state: new_backend,
509        chain: (),
510        local: LocalContext::default(),
511        error: Ok(()),
512    };
513
514    let mut evm = Evm::new_with_inspector(
515        context,
516        inspector,
517        EthInstructions::new_mainnet_with_spec(SpecId::default()),
518        EthPrecompiles::new(SpecId::default()),
519    );
520
521    let state = evm.inspect_tx(tx)?.state;
522
523    // Persist the changes to the original backend.
524    backend.journaled_state.database.commit(state);
525    update_state(
526        &mut backend.journaled_state.inner.state,
527        &mut backend.journaled_state.database,
528    )?;
529
530    Ok(())
531}
532
533/// Mimics <https://github.com/foundry-rs/foundry/blob/25cc1ac68b5f6977f23d713c01ec455ad7f03d21/crates/evm/core/src/backend/mod.rs#L1968>
534/// Omits persistent accounts (accounts that should be kept persistent when switching forks) for simplicity.
535fn update_state<DB: Database>(state: &mut EvmState, db: &mut DB) -> Result<(), DB::Error> {
536    for (addr, acc) in state.iter_mut() {
537        acc.info = db.basic(*addr)?.unwrap_or_default();
538        for (key, val) in acc.storage.iter_mut() {
539            val.present_value = db.storage(*addr, *key)?;
540        }
541    }
542
543    Ok(())
544}
545
546fn main() -> anyhow::Result<()> {
547    let backend = Backend::new(SpecId::default(), InMemoryDB::default());
548    let mut inspector = Cheatcodes::<BlockEnv, TxEnv, CfgEnv>::default();
549    let env = Env::mainnet();
550    let tx = env.tx.clone();
551
552    let context = Context {
553        tx: env.tx,
554        block: env.block,
555        cfg: env.cfg,
556        journaled_state: backend,
557        chain: (),
558        local: LocalContext::default(),
559        error: Ok(()),
560    };
561
562    let mut evm = Evm::new_with_inspector(
563        context,
564        &mut inspector,
565        EthInstructions::new_mainnet_with_spec(SpecId::default()),
566        EthPrecompiles::new(SpecId::default()),
567    );
568    evm.inspect_tx(tx)?;
569
570    // Sanity check
571    assert_eq!(evm.inspector.call_count, 2);
572    assert_eq!(evm.journaled_state.method_with_inspector_counter, 1);
573    assert_eq!(evm.journaled_state.method_without_inspector_counter, 1);
574
575    Ok(())
576}