Skip to main content

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::{
31        hardfork::SpecId, Address, AddressMap, AddressSet, HashSet, Log, StorageKey, StorageValue,
32        B256, U256,
33    },
34    state::{Account, Bytecode, EvmState},
35    Context, Database, DatabaseCommit, InspectEvm, Inspector, Journal, JournalEntry,
36};
37use std::{convert::Infallible, fmt::Debug};
38
39/// Backend for cheatcodes.
40/// The problematic cheatcodes are only supported in fork mode, so we'll omit the non-fork behavior of the Foundry
41/// `Backend`.
42#[derive(Clone, Debug)]
43struct Backend {
44    /// In fork mode, Foundry stores (`JournaledState`, `Database`) pairs for each fork.
45    journaled_state: Journal<InMemoryDB>,
46    /// Counters to be able to assert that we mutated the object that we expected to mutate.
47    method_with_inspector_counter: usize,
48    method_without_inspector_counter: usize,
49}
50
51impl Backend {
52    fn new(spec: SpecId, db: InMemoryDB) -> Self {
53        let mut journaled_state = Journal::new(db);
54        journaled_state.set_spec_id(spec);
55        Self {
56            journaled_state,
57            method_with_inspector_counter: 0,
58            method_without_inspector_counter: 0,
59        }
60    }
61}
62
63impl JournalTr for Backend {
64    type Database = InMemoryDB;
65    type State = EvmState;
66    type JournaledAccount<'a> = JournaledAccount<'a, InMemoryDB, JournalEntry>;
67
68    fn new(database: InMemoryDB) -> Self {
69        Self::new(SpecId::default(), database)
70    }
71
72    fn db(&self) -> &Self::Database {
73        self.journaled_state.db()
74    }
75
76    fn db_mut(&mut self) -> &mut Self::Database {
77        self.journaled_state.db_mut()
78    }
79
80    fn sload(
81        &mut self,
82        address: Address,
83        key: StorageKey,
84    ) -> Result<StateLoad<StorageValue>, <Self::Database as Database>::Error> {
85        self.journaled_state.sload(address, key)
86    }
87
88    fn sstore(
89        &mut self,
90        address: Address,
91        key: StorageKey,
92        value: StorageValue,
93    ) -> Result<StateLoad<SStoreResult>, <Self::Database as Database>::Error> {
94        self.journaled_state.sstore(address, key, value)
95    }
96
97    fn tload(&mut self, address: Address, key: StorageKey) -> StorageValue {
98        self.journaled_state.tload(address, key)
99    }
100
101    fn tstore(&mut self, address: Address, key: StorageKey, value: StorageValue) {
102        self.journaled_state.tstore(address, key, value)
103    }
104
105    fn log(&mut self, log: Log) {
106        self.journaled_state.log(log)
107    }
108
109    fn logs(&self) -> &[Log] {
110        self.journaled_state.logs()
111    }
112
113    fn selfdestruct(
114        &mut self,
115        address: Address,
116        target: Address,
117        skip_cold_load: bool,
118    ) -> Result<StateLoad<SelfDestructResult>, JournalLoadError<Infallible>> {
119        self.journaled_state
120            .selfdestruct(address, target, skip_cold_load)
121    }
122
123    fn warm_access_list(&mut self, access_list: AddressMap<HashSet<StorageKey>>) {
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: AddressSet) {
132        self.journaled_state.warm_precompiles(addresses)
133    }
134
135    fn precompile_addresses(&self) -> &AddressSet {
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    #[allow(deprecated)]
239    fn caller_accounting_journal_entry(
240        &mut self,
241        address: Address,
242        old_balance: U256,
243        bump_nonce: bool,
244    ) {
245        #[allow(deprecated)]
246        self.journaled_state
247            .caller_accounting_journal_entry(address, old_balance, bump_nonce)
248    }
249
250    fn balance_incr(
251        &mut self,
252        address: Address,
253        balance: U256,
254    ) -> Result<(), <Self::Database as Database>::Error> {
255        self.journaled_state.balance_incr(address, balance)
256    }
257
258    #[allow(deprecated)]
259    fn nonce_bump_journal_entry(&mut self, address: Address) {
260        #[allow(deprecated)]
261        self.journaled_state.nonce_bump_journal_entry(address)
262    }
263
264    fn take_logs(&mut self) -> Vec<Log> {
265        self.journaled_state.take_logs()
266    }
267
268    fn commit_tx(&mut self) {
269        self.journaled_state.commit_tx()
270    }
271
272    fn discard_tx(&mut self) {
273        self.journaled_state.discard_tx()
274    }
275
276    fn sload_skip_cold_load(
277        &mut self,
278        address: Address,
279        key: StorageKey,
280        skip_cold_load: bool,
281    ) -> Result<StateLoad<StorageValue>, JournalLoadError<<Self::Database as Database>::Error>>
282    {
283        self.journaled_state
284            .sload_skip_cold_load(address, key, skip_cold_load)
285    }
286
287    fn sstore_skip_cold_load(
288        &mut self,
289        address: Address,
290        key: StorageKey,
291        value: StorageValue,
292        skip_cold_load: bool,
293    ) -> Result<StateLoad<SStoreResult>, JournalLoadError<<Self::Database as Database>::Error>>
294    {
295        self.journaled_state
296            .sstore_skip_cold_load(address, key, value, skip_cold_load)
297    }
298
299    fn load_account_mut_skip_cold_load(
300        &mut self,
301        address: Address,
302        skip_cold_load: bool,
303    ) -> Result<StateLoad<Self::JournaledAccount<'_>>, Infallible> {
304        self.journaled_state
305            .load_account_mut_skip_cold_load(address, skip_cold_load)
306    }
307
308    fn load_account_info_skip_cold_load(
309        &mut self,
310        address: Address,
311        load_code: bool,
312        skip_cold_load: bool,
313    ) -> Result<AccountInfoLoad<'_>, JournalLoadError<Infallible>> {
314        self.journaled_state
315            .load_account_info_skip_cold_load(address, load_code, skip_cold_load)
316    }
317
318    fn load_account_mut_optional_code(
319        &mut self,
320        address: Address,
321        load_code: bool,
322    ) -> Result<StateLoad<Self::JournaledAccount<'_>>, Infallible> {
323        self.journaled_state
324            .load_account_mut_optional_code(address, load_code)
325    }
326}
327
328impl JournalExt for Backend {
329    fn journal(&self) -> &[JournalEntry] {
330        self.journaled_state.journal()
331    }
332
333    fn evm_state(&self) -> &EvmState {
334        self.journaled_state.evm_state()
335    }
336
337    fn evm_state_mut(&mut self) -> &mut EvmState {
338        self.journaled_state.evm_state_mut()
339    }
340}
341
342/// Used in Foundry to provide extended functionality to cheatcodes.
343/// The methods are called from the `Cheatcodes` inspector.
344trait DatabaseExt: JournalTr {
345    /// Mimics `DatabaseExt::transact`
346    /// See `commit_transaction` for the generics
347    fn method_that_takes_inspector_as_argument<
348        InspectorT,
349        BlockT,
350        TxT,
351        CfgT,
352        InstructionProviderT,
353        PrecompileT,
354    >(
355        &mut self,
356        env: Env<BlockT, TxT, CfgT>,
357        inspector: InspectorT,
358    ) -> anyhow::Result<()>
359    where
360        InspectorT: Inspector<Context<BlockT, TxT, CfgT, InMemoryDB, Backend>, EthInterpreter>,
361        BlockT: Block,
362        TxT: Transaction + Clone,
363        CfgT: Cfg,
364        InstructionProviderT: InstructionProvider<
365                Context = Context<BlockT, TxT, CfgT, InMemoryDB, Backend>,
366                InterpreterTypes = EthInterpreter,
367            > + Default,
368        PrecompileT: PrecompileProvider<
369                Context<BlockT, TxT, CfgT, InMemoryDB, Backend>,
370                Output = InterpreterResult,
371            > + Default;
372
373    /// Mimics `DatabaseExt::roll_fork_to_transaction`
374    fn method_that_constructs_inspector<BlockT, TxT, CfgT, InstructionProviderT, PrecompileT>(
375        &mut self,
376        env: Env<BlockT, TxT, CfgT>,
377    ) -> anyhow::Result<()>
378    where
379        BlockT: Block,
380        TxT: Transaction + Clone,
381        CfgT: Cfg,
382        InstructionProviderT: InstructionProvider<
383                Context = Context<BlockT, TxT, CfgT, InMemoryDB, Backend>,
384                InterpreterTypes = EthInterpreter,
385            > + Default,
386        PrecompileT: PrecompileProvider<
387                Context<BlockT, TxT, CfgT, InMemoryDB, Backend>,
388                Output = InterpreterResult,
389            > + Default;
390}
391
392impl DatabaseExt for Backend {
393    fn method_that_takes_inspector_as_argument<
394        InspectorT,
395        BlockT,
396        TxT,
397        CfgT,
398        InstructionProviderT,
399        PrecompileT,
400    >(
401        &mut self,
402        env: Env<BlockT, TxT, CfgT>,
403        inspector: InspectorT,
404    ) -> anyhow::Result<()>
405    where
406        InspectorT: Inspector<Context<BlockT, TxT, CfgT, InMemoryDB, Backend>, EthInterpreter>,
407        BlockT: Block,
408        TxT: Transaction + Clone,
409        CfgT: Cfg,
410        InstructionProviderT: InstructionProvider<
411                Context = Context<BlockT, TxT, CfgT, InMemoryDB, Backend>,
412                InterpreterTypes = EthInterpreter,
413            > + Default,
414        PrecompileT: PrecompileProvider<
415                Context<BlockT, TxT, CfgT, InMemoryDB, Backend>,
416                Output = InterpreterResult,
417            > + Default,
418    {
419        commit_transaction::<InspectorT, BlockT, TxT, CfgT, InstructionProviderT, PrecompileT>(
420            self, env, inspector,
421        )?;
422        self.method_with_inspector_counter += 1;
423        Ok(())
424    }
425
426    fn method_that_constructs_inspector<BlockT, TxT, CfgT, InstructionProviderT, PrecompileT>(
427        &mut self,
428        env: Env<BlockT, TxT, CfgT>,
429    ) -> anyhow::Result<()>
430    where
431        BlockT: Block,
432        TxT: Transaction + Clone,
433        CfgT: Cfg,
434        InstructionProviderT: InstructionProvider<
435                Context = Context<BlockT, TxT, CfgT, InMemoryDB, Backend>,
436                InterpreterTypes = EthInterpreter,
437            > + Default,
438        PrecompileT: PrecompileProvider<
439                Context<BlockT, TxT, CfgT, InMemoryDB, Backend>,
440                Output = InterpreterResult,
441            > + Default,
442    {
443        let inspector = TracerEip3155::new(Box::new(std::io::sink()));
444        commit_transaction::<
445            // Generic interpreter types are not supported yet in the `Evm` implementation
446            TracerEip3155,
447            BlockT,
448            TxT,
449            CfgT,
450            InstructionProviderT,
451            PrecompileT,
452        >(self, env, inspector)?;
453
454        self.method_without_inspector_counter += 1;
455        Ok(())
456    }
457}
458
459/// An REVM inspector that intercepts calls to the cheatcode address and executes them with the help of the
460/// `DatabaseExt` trait.
461#[derive(Clone, Default)]
462struct Cheatcodes<BlockT, TxT, CfgT, InstructionProviderT, PrecompileT> {
463    call_count: usize,
464    phantom: core::marker::PhantomData<(BlockT, TxT, CfgT, InstructionProviderT, PrecompileT)>,
465}
466
467impl<BlockT, TxT, CfgT, InstructionProviderT, PrecompileT>
468    Cheatcodes<BlockT, TxT, CfgT, InstructionProviderT, PrecompileT>
469where
470    BlockT: Block + Clone,
471    TxT: Transaction + Clone,
472    CfgT: Cfg + Clone,
473    InstructionProviderT: InstructionProvider<
474            Context = Context<BlockT, TxT, CfgT, InMemoryDB, Backend>,
475            InterpreterTypes = EthInterpreter,
476        > + Default,
477    PrecompileT: PrecompileProvider<
478            Context<BlockT, TxT, CfgT, InMemoryDB, Backend>,
479            Output = InterpreterResult,
480        > + Default,
481{
482    fn apply_cheatcode(
483        &mut self,
484        context: &mut Context<BlockT, TxT, CfgT, InMemoryDB, Backend>,
485    ) -> anyhow::Result<()> {
486        // We cannot avoid cloning here, because we need to mutably borrow the context to get the journal.
487        let block = context.block.clone();
488        let tx = context.tx.clone();
489        let cfg = context.cfg.clone();
490
491        // `transact` cheatcode would do this
492        context
493            .journal_mut()
494            .method_that_takes_inspector_as_argument::<_, _, _, _, InstructionProviderT, PrecompileT>(
495                Env {
496                    block: block.clone(),
497                    tx: tx.clone(),
498                    cfg: cfg.clone(),
499                },
500                self,
501            )?;
502
503        // `rollFork(bytes32 transaction)` cheatcode would do this
504        context
505            .journal_mut()
506            .method_that_constructs_inspector::<_, _, _, InstructionProviderT, PrecompileT>(
507                Env { block, tx, cfg },
508            )?;
509        Ok(())
510    }
511}
512
513impl<BlockT, TxT, CfgT, InstructionProviderT, PrecompileT>
514    Inspector<Context<BlockT, TxT, CfgT, InMemoryDB, Backend>>
515    for Cheatcodes<BlockT, TxT, CfgT, InstructionProviderT, PrecompileT>
516where
517    BlockT: Block + Clone,
518    TxT: Transaction + Clone,
519    CfgT: Cfg + Clone,
520    InstructionProviderT: InstructionProvider<
521            Context = Context<BlockT, TxT, CfgT, InMemoryDB, Backend>,
522            InterpreterTypes = EthInterpreter,
523        > + Default,
524    PrecompileT: PrecompileProvider<
525            Context<BlockT, TxT, CfgT, InMemoryDB, Backend>,
526            Output = InterpreterResult,
527        > + Default,
528{
529    /// Note that precompiles are no longer accessible via `EvmContext::precompiles`.
530    fn call(
531        &mut self,
532        context: &mut Context<BlockT, TxT, CfgT, InMemoryDB, Backend>,
533        _inputs: &mut CallInputs,
534    ) -> Option<CallOutcome> {
535        self.call_count += 1;
536        // Don't apply cheatcodes recursively.
537        if self.call_count == 1 {
538            // Instead of calling unwrap here, we would want to return an appropriate call outcome based on the result
539            // in a real project.
540            self.apply_cheatcode(context).unwrap();
541        }
542        None
543    }
544}
545
546/// EVM environment
547#[derive(Clone, Debug)]
548struct Env<BlockT, TxT, CfgT> {
549    block: BlockT,
550    tx: TxT,
551    cfg: CfgT,
552}
553
554impl Env<BlockEnv, TxEnv, CfgEnv> {
555    fn mainnet() -> Self {
556        // `CfgEnv` is non-exhaustive, so we need to set the field after construction.
557        let mut cfg = CfgEnv::default();
558        cfg.disable_nonce_check = true;
559
560        Self {
561            block: BlockEnv::default(),
562            tx: TxEnv::default(),
563            cfg,
564        }
565    }
566}
567
568/// Executes a transaction and runs the inspector using the `Backend` as the state.
569/// Mimics `commit_transaction` <https://github.com/foundry-rs/foundry/blob/25cc1ac68b5f6977f23d713c01ec455ad7f03d21/crates/evm/core/src/backend/mod.rs#L1931>
570fn commit_transaction<InspectorT, BlockT, TxT, CfgT, InstructionProviderT, PrecompileT>(
571    backend: &mut Backend,
572    env: Env<BlockT, TxT, CfgT>,
573    inspector: InspectorT,
574) -> Result<(), EVMError<Infallible, InvalidTransaction>>
575where
576    InspectorT: Inspector<Context<BlockT, TxT, CfgT, InMemoryDB, Backend>, EthInterpreter>,
577    BlockT: Block,
578    TxT: Transaction + Clone,
579    CfgT: Cfg,
580    InstructionProviderT: InstructionProvider<
581            Context = Context<BlockT, TxT, CfgT, InMemoryDB, Backend>,
582            InterpreterTypes = EthInterpreter,
583        > + Default,
584    PrecompileT: PrecompileProvider<
585            Context<BlockT, TxT, CfgT, InMemoryDB, Backend>,
586            Output = InterpreterResult,
587        > + Default,
588{
589    // Create new journaled state and backend with the same DB and journaled state as the original for the transaction.
590    // This new backend and state will be discarded after the transaction is done and the changes are applied to the
591    // original backend.
592    // Mimics https://github.com/foundry-rs/foundry/blob/25cc1ac68b5f6977f23d713c01ec455ad7f03d21/crates/evm/core/src/backend/mod.rs#L1950-L1953
593    let new_backend = backend.clone();
594    let tx = env.tx.clone();
595
596    let context = Context {
597        tx: env.tx,
598        block: env.block,
599        cfg: env.cfg,
600        journaled_state: new_backend,
601        chain: (),
602        local: LocalContext::default(),
603        error: Ok(()),
604    };
605
606    let mut evm = Evm::new_with_inspector(
607        context,
608        inspector,
609        InstructionProviderT::default(),
610        PrecompileT::default(),
611    );
612
613    let state = evm.inspect_tx(tx)?.state;
614
615    // Persist the changes to the original backend.
616    backend.journaled_state.database.commit(state);
617    update_state(
618        &mut backend.journaled_state.inner.state,
619        &mut backend.journaled_state.database,
620    )?;
621
622    Ok(())
623}
624
625/// Mimics <https://github.com/foundry-rs/foundry/blob/25cc1ac68b5f6977f23d713c01ec455ad7f03d21/crates/evm/core/src/backend/mod.rs#L1968>
626/// Omits persistent accounts (accounts that should be kept persistent when switching forks) for simplicity.
627fn update_state<DB: Database>(state: &mut EvmState, db: &mut DB) -> Result<(), DB::Error> {
628    for (addr, acc) in state.iter_mut() {
629        acc.info = db.basic(*addr)?.unwrap_or_default();
630        for (key, val) in acc.storage.iter_mut() {
631            val.present_value = db.storage(*addr, *key)?;
632        }
633    }
634
635    Ok(())
636}
637
638fn main() -> anyhow::Result<()> {
639    let backend = Backend::new(SpecId::default(), InMemoryDB::default());
640    let mut inspector = Cheatcodes::<
641        BlockEnv,
642        TxEnv,
643        CfgEnv,
644        EthInstructions<EthInterpreter, Context<BlockEnv, TxEnv, CfgEnv, InMemoryDB, Backend>>,
645        EthPrecompiles,
646    >::default();
647    let env = Env::mainnet();
648    let tx = env.tx.clone();
649
650    let context = Context {
651        tx: env.tx,
652        block: env.block,
653        cfg: env.cfg,
654        journaled_state: backend,
655        chain: (),
656        local: LocalContext::default(),
657        error: Ok(()),
658    };
659
660    let mut evm = Evm::new_with_inspector(
661        context,
662        &mut inspector,
663        EthInstructions::default(),
664        EthPrecompiles::default(),
665    );
666    evm.inspect_tx(tx)?;
667
668    // Sanity check
669    assert_eq!(evm.inspector.call_count, 2);
670    assert_eq!(evm.journaled_state.method_with_inspector_counter, 1);
671    assert_eq!(evm.journaled_state.method_without_inspector_counter, 1);
672
673    Ok(())
674}