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