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