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