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