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