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