Foundry Deep Dive

Foundry is a quite popular framework for Ethereum developers, enabling testing, fuzzing, and contract interaction. One of the key features of Foundry is its use of cheatcodes, which are custom commands that allow developers to alter EVM behavior and state for testing purposes.

If you are a curious developer like me, you’ve probably wondered how these cheatcodes work under the hood. Because there are multiple ways to achieve that.

I thought this could be a great showcase of how to dive into and analyze an unfamiliar and considerably big Rust project.

With a quick online search, I learned that Foundry is utilizing revm. However, I don’t extensively know how revm works. And that’s okay! I’m confident that we can easily uncover the mystery by diving into the source code.

First things first, we should think about potential ways to achieve cheatcodes functionality. If we have some ideas, diving into the source code will make much more sense. We won’t be wandering around blindly but will know what to search for.

The Potential Ways:

  • Forking revm and creating their own evm that has cheatcode capabilities within
    • Disgusting idea… They’d have to update their fork regularly and import new changes that may (and most probably will) conflict with their implementation. Please, god, let it not be a fork…
  • Middleware approach: for everything that will communicate with the EVM, it will first go through a proxy or middleware. This structure will also have access to cheatcodes, thus being able to modify/manipulate things by intercepting them before they are fed to the EVM as inputs.
    • This makes sense for manipulating inputs (like transactions).
    • No need to fork revm (or any other EVM).
    • Also good for separation of concerns (we’re not messing around with the internals of evm).
    • One bad thing about this is: the middleware approach falls short on things that manipulate the state of the EVM. For example, the deal cheatcode lets us change the balance of an account. Without changing the actual balance in the state of the EVM, things will get messy. The middleware might have to mock the state and rely on its own state instead of the actual state of the EVM. This started to sound ugly pretty quickly.
  • Manipulating/modifying the EVM through the revm API
    • I’ve heard that the revm API is pretty darn flexible, yet I haven’t used it myself. So, I don’t know whether the exposed API will be enough for all the cheatcodes that Foundry has. If it is enough, great! If not, we have to choose one of the above.

In the rest of this post, we’ll explore how Foundry achieves its magic. I’ll give the spoiler right away: Foundry uses the middleware/proxy approach for transactions and leverages revm’s API for state manipulation. So, we’ll dive into two critical aspects:

  1. Execution Logic (Middleware): How Foundry intercepts and modifies EVM execution.
  2. State Manipulation: How Foundry directly modifies the EVM’s state.

Initial Step: Exploring The Repository

I gave the spoiler already, but the exciting part is, how did I discover all that. Let’s start from the beginning.

This is the repo of Foundry: https://github.com/foundry-rs/foundry. At the time of this post, I’ve used the commit: f922a340dae8e347d573fc6a403694bcb7fea106.

This is how the repo looks:

At the root, we don’t see anything interesting. Let’s go into crates.

Wow, a folder named cheatcodes.

I won’t be posting SS’s for each step I took, but here is a brief summary:

  • inside the cheatcodes folder, went into src folder
  • then went into evm folder, but found nothing significant
  • went back to src folder, and read lib.rs. Found this thing:

      /// The cheatcode context, used in `Cheatcode`.
      pub struct CheatsCtxt<'cheats, 'evm, 'db, 'db2> {
          /// The cheatcodes inspector state.
          pub(crate) state: &'cheats mut Cheatcodes,
    
    • interesting for sure. But not enough to draw conclusions.
  • decided to read inspector.rs
    • ctrl+f : cheatcode and skim through…
    • found apply_cheatcode function, let’s see where it is called. It is in call_with_executor.
    • call_with_exeuctor function is definitely interesting, due to below snippets:

        // Handle mocked calls
        if let Some(mocks) = self.mocked_calls.get_mut(&call.bytecode_address) {
        /* redacted */
      
      
        // Apply our prank
        if let Some(prank) = &self.prank {
        /* redacted */
      
        // Apply our broadcast
        if let Some(broadcast) = &self.broadcast {
        /* redacted */
      

      And call_with_executor is called by call function.

  • And this is how we got to the execution logic ladies and gentlemen! Behold, the next chapter starts!

1. Execution Logic (Middleware)

Foundry’s cheatcodes act as a middleware layer between the EVM and the normal execution flow. By intercepting execution calls, cheatcodes can modify behavior, mock functions, or even short-circuit execution to produce desired outcomes. Let’s look at the code for handling execution logic (specifically, call function):

fn call(
    &mut self,
    ecx: &mut EvmContext<&mut dyn DatabaseExt>,
    call: &mut CallInputs,
) -> Option<CallOutcome> {
    if self.in_inner_context && ecx.journaled_state.depth == 1 {
        self.adjust_evm_data_for_inner_context(ecx);
        return None;
    }

    if ecx.journaled_state.depth == 0 {
        self.top_level_frame_start(ecx);
    }

    call_inspectors!(
        #[ret]
        [&mut self.fuzzer, &mut self.tracer, &mut self.log_collector, &mut self.printer],
        |inspector| {
            let mut out = None;
            if let Some(output) = inspector.call(ecx, call) {
                if output.result.result != InstructionResult::Continue {
                    out = Some(Some(output));
                }
            }
            out
        },
    );

    if let Some(cheatcodes) = self.cheatcodes.as_deref_mut() {
        // Handle mocked functions, replace bytecode address with mock if matched.
        if let Some(mocks) = cheatcodes.mocked_functions.get(&call.target_address) {
            // Check if any mock function set for call data or if catch-all mock function set
            // for selector.
            if let Some(target) = mocks
                .get(&call.input)
                .or_else(|| call.input.get(..4).and_then(|selector| mocks.get(selector)))
            {
                call.bytecode_address = *target;
            }
        }

        if let Some(output) = cheatcodes.call_with_executor(ecx, call, self.inner) {
            if output.result.result != InstructionResult::Continue {
                return Some(output);
            }
        }
    }

    if self.enable_isolation &&
        call.scheme == CallScheme::Call &&
        !self.in_inner_context &&
        ecx.journaled_state.depth == 1
    {
        let (result, _) = self.transact_inner(
            ecx,
            TxKind::Call(call.target_address),
            call.caller,
            call.input.clone(),
            call.gas_limit,
            call.value.get(),
        );
        return Some(CallOutcome { result, memory_offset: call.return_memory_offset.clone() });
    }

    None
}

Explanation of The Code:

  1. Cheatcodes as Middleware:
    • The block starting with if let Some(cheatcodes) = self.cheatcodes.as_deref_mut() checks if any cheatcodes are active.
    • Cheatcodes can:
      • Mock function calls by replacing the bytecode address.
      • Short-circuit execution by returning results early.
    • This allows for flexible test scenarios where certain behaviors are simulated without actual on-chain execution.
  2. Normal EVM Execution:
    • If no cheatcodes modify the behavior, the execution defaults to the standard flow.
    • The block below simulates the transaction as it would normally occur in the EVM:

        let (result, _) = self.transact_inner(
            ecx,
            TxKind::Call(call.target_address),
            call.caller,
            call.input.clone(),
            call.gas_limit,
            call.value.get(),
        );
      
      

2. State Manipulation

We are still kind of clueless on how the state related cheatcodes are handled in the test environment. What I had in mind was to examine the deal cheatcode. So, after an extensive search for the keyword deal across the whole repository, I was able to find the below snippet from crates/cheatcodes/src/evm.rs file:

impl Cheatcode for dealCall {
    fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result {
        let Self { account: address, newBalance: new_balance } = *self;
        let account = journaled_account(ccx.ecx, address)?;
        let old_balance = std::mem::replace(&mut account.info.balance, new_balance);
        let record = DealRecord { address, old_balance, new_balance };
        ccx.state.eth_deals.push(record);
        Ok(Default::default())
    }
}

Key Points of The Code:

  1. Mutably Accessing the EVM State:
    • The journaled_account function retrieves the mutable state of the account:

        pub(super) fn journaled_account<'a>(
            ecx: InnerEcx<'a, '_, '_>,
            addr: Address,
        ) -> Result<&'a mut Account> {
            ecx.load_account(addr)?;
            ecx.journaled_state.touch(&addr);
            Ok(ecx.journaled_state.state.get_mut(&addr).expect("account is loaded"))
        }
      
      
      • The ecx.journaled_state.state.get_mut(&addr) provides direct mutable access to the account’s state.
  2. Modifying the State:
    • The std::mem::replace function is used to overwrite the account’s balance with the new value.
    • The old balance is stored for potential rollback or logging purposes.
  3. Recording the Change:
    • The DealRecord captures the modification, enabling tracing or debugging of changes made during the test.

EVM Context and State:

The ecx (execution context) plays a crucial role. The type of ecx is InnerEvmContext, which is defined as the following:

pub struct InnerEvmContext<DB: Database> {
    /// EVM Environment contains all the information about config, block and transaction that
    /// evm needs.
    pub env: Box<Env>,
    /// EVM State with journaling support.
    pub journaled_state: JournaledState,
    /// Database to load data from.
    pub db: DB,
    /// Error that happened during execution.
    pub error: Result<(), EVMError<DB::Error>>,
    /// Used as temporary value holder to store L1 block info.
    #[cfg(feature = "optimism")]
    pub l1_block_info: Option<crate::optimism::L1BlockInfo>,
}

ecx encapsulates the EVM environment, including the current state (journaled_state).

And JournaledState is defined as the following:

pub struct JournaledState {
    /// The current state.
    pub state: EvmState,
    /// Transient storage that is discarded after every transaction.
    ///
    /// See [EIP-1153](https://eips.ethereum.org/EIPS/eip-1153).
    pub transient_storage: TransientStorage,
    /// Emitted logs.
    pub logs: Vec<Log>,
    /// The current call stack depth.
    pub depth: usize,
    /// The journal of state changes, one for each call.
    pub journal: Vec<Vec<JournalEntry>>,
    /// The spec ID for the EVM.
    ///
    /// This spec is used for two things:
    ///
    /// - [EIP-161]: Prior to this EIP, Ethereum had separate definitions for empty and non-existing accounts.
    /// - [EIP-6780]: `SELFDESTRUCT` only in same transaction
    ///
    /// [EIP-161]: https://eips.ethereum.org/EIPS/eip-161
    /// [EIP-6780]: https://eips.ethereum.org/EIPS/eip-6780
    pub spec: SpecId,
    /// Warm loaded addresses are used to check if loaded address
    /// should be considered cold or warm loaded when the account
    /// is first accessed.
    ///
    /// Note that this not include newly loaded accounts, account and storage
    /// is considered warm if it is found in the `State`.
    pub warm_preloaded_addresses: HashSet<Address>,
}

We are interested especially in pub state: EvmState field. After seeing that EvmState is imported from revm, the mystery is fully resolved: Foundry is utilizing the API exposed by the revm to manipulate the state.


Conclusion

It turns out that, Foundry’s architecture for handling cheatcodes is a blend of middleware logic and direct state manipulation. The execution logic acts as a proxy, intercepting and potentially altering EVM behavior without disrupting the core logic. On the other hand, state manipulation directly mutates the EVM’s internal state.

I think it makes sense! Kudos to Foundry team!

Foundry authors when they hear about my opinion:

Thanks to Caner Çıdam for accompanying me in this journey! These stuff are much more fun when you have a friend to share them with.