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