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 ownevm
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.
- I’ve heard that the
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:
- Execution Logic (Middleware): How Foundry intercepts and modifies EVM execution.
- 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 intosrc
folder - then went into
evm
folder, but found nothing significant -
went back to
src
folder, and readlib.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 incall_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 bycall
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:
- 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.
- The block starting with
- 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:
- 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.
- The
-
- 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.
- The
- Recording the Change:
- The
DealRecord
captures the modification, enabling tracing or debugging of changes made during the test.
- The
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.