Skip to content
← Building with the Stack — Real-World Rust EVM Apps
Application Patterns
Lesson 7 of 11·CONTENT45 min80 XP
Course
Building with the Stack — Real-World Rust EVM Apps
Lesson role
CONTENT
Sequence
7 / 11

Lab 6 — Build Your Own Foundry-Style Cheatcode in Rust

Question

Foundry cheatcodes are calls to the magic address 0x7109709E... that Foundry's Revm inspector intercepts and answers in Rust. Build a custom one. Here we get the same effect a simpler way: register a custom precompile at a magic address in a Revm test harness, so a Solidity call to that address runs your Rust function.

Principle (minimum model)

  • Precompile = a Rust function at a fixed address. When the EVM calls that address, Revm dispatches to your function instead of executing bytecode.
  • Precompile registration. In plain Revm 38, build the EVM with build_mainnet() and swap in your own PrecompileProvider with .with_precompiles(...) on the built Evm (Step 3 below). In a Reth node you do it inside a custom EvmFactory (see examples/custom-evm in the Reth repo).
  • Cheatcode contract. Your precompile receives calldata; decode it as (operation_id, ...args); execute the cheat (warp time / deal balance / etc); return data.
  • Calldata decoding via sol! + SolCall. measureGasCall::abi_decode_raw(calldata) — type-safe ABI decoding (alloy-sol-types 1.x dropped the old validate: bool argument).
  • State access needs more than a precompile function. A plain precompile function (fn(&[u8], u64) -> EthPrecompileResult) sees only its input bytes, not the state. A custom PrecompileProvider receives &mut CTX (journal included), which is how this lesson reads the target's code; Foundry reaches the journal from an inspector instead, which also lets cheats like vm.deal and vm.warp change what the test sees.
  • Test gate. Differential test: your Rust cheatcode and a Solidity-only gasleft() reference differ by the same constant CALL overhead on every input.
  • Use cases. Chain teams extend Foundry for their own features (Tempo maintains tempoxyz/tempo-foundry, a Foundry fork with Tempo support); the same trick works for any Revm-based simulator.

Worked example + steps

Build Your Own Foundry-Style Cheatcode in Rust

When you write vm.deal(alice, 100 ether) in a Foundry test, that's not an EVM opcode. It's a Rust function that runs when your test calls the magic address 0x7109709ECfa91a80626fF3989D68f67F5b1DD12D: Foundry's Revm inspector intercepts every call to that address and dispatches it to Rust (the interception is in Foundry's crates/cheatcodes/src/inspector.rs; the CHEATCODE_ADDRESS constant lives in crates/evm/core/src/constants.rs). Solidity sees it through the Vm.sol interface. It is not a precompile, but it behaves like one (a built-in contract whose code lives in the EVM engine, not on chain), which is why we can build our own with a precompile. Same for vm.warp(), vm.expectRevert(), the whole cheatcode surface. You can ship your own. This lesson builds cheats.measureGas(target, data) — a precompile that lets test authors measure sub-call gas without manual wrapping — as a precompile — a simpler mechanism than Foundry's inspector, with the same Solidity-facing shape.

📌 Scope honesty. We don't fork Foundry. We build the precompile + a minimal Revm-based test harness that loads it. The pattern (magic address + Solidity ABI surface + test runner that wires it in) matches how Foundry adds cheatcodes; the difference is that Foundry intercepts the call in an inspector, while we answer it with a precompile. You see every part instead of inheriting an opaque framework.

Acceptance criteria

The lesson is complete when these tests pass (full code at the end in §Test gate):

  1. matches_reference_for_known_input — for one fixed input, your Rust cheatcode returns less than the Solidity-only gasleft() reference, and the gap (the reference's own CALL overhead) is under 1,000 gas.
  2. always_agrees_with_reference — over 256 inputs, that gap is identical for every input (499 gas in our run), so for this workload — a freshly deployed target with empty storage that calls no other contract — the cheatcode tracks the target's execution cost exactly. It says nothing about targets with existing storage or callees, which the nested DB doesn't have (see the simplification below).

Test-first reading. The walkthrough below shows how the precompile is registered and how its output is computed — the parts both tests measure against the Solidity reference.

What you'll build

A new Solidity-callable cheatcode:

interface Cheats {
    function measureGas(address target, bytes calldata data) external returns (uint256 gasUsed);
}

contract MyTest {
    Cheats constant cheats = Cheats(0x7110000000000000000000000000000000000000);

    function test_swap_gas() public {
        uint256 gas = cheats.measureGas(
            address(uniswapRouter),
            abi.encodeWithSignature("swapExactTokensForTokens(...)", ...)
        );
        assertLt(gas, 200_000, "swap exceeded gas budget");
    }
}
flowchart TB
    Test["Solidity test"] -->|call| Cheats["0x7110... precompile"]
    Cheats -->|nested EVM call| Inner["Revm sub-EVM<br/>executes target.data"]
    Inner -->|gas_used| Cheats
    Cheats -->|abi-encoded uint256| Test

Why precompile (not contract, not opcode)

ApproachCan call into Revm internals?Consensus impactEffort
Regular Solidity contractNo — only EVM opsNonetrivial
New EVM opcodeYes — full controlForks consensus immediately (Intermediate lesson)massive
Precompile (this lesson)Yes — full Rust accessOnly present in your Revm build, not mainnet~100 lines
Inspector that intercepts calls to a magic address (Foundry's choice)Yes — plus journal/state accessOnly present in your test runner, not mainnetlarger

A precompile sits in the executor, not the protocol. Mainnet Revm doesn't have your precompile; your test runner Revm does. No consensus break, full Rust power. Foundry gets the same property by intercepting calls in its test-runner inspector, not by adding opcodes.

Cargo.toml

[package]
name = "rust-cheatcode"
version = "0.1.0"
edition = "2021"

[dependencies]
revm                = { version = "38" }
revm-precompile     = { version = "34" }
alloy-primitives    = "1.5"
alloy-sol-types     = "1.5"
eyre                = "0.6"

[dev-dependencies]
serde_json          = "1"   # reads forge's build artifacts in the Test gate

The three Rust blocks below go into src/lib.rs in order; together they compile as-is against these versions.

Step 1: The precompile provider and selector dispatch

Revm 38 has two levels of "precompile". A plain precompile function (fn(&[u8], u64) -> EthPrecompileResult, wrapped into a Precompile with eth_precompile_fn!) sees only its input bytes and gas limit. One level up, the EVM asks a PrecompileProvider whether an address is a precompile and, if so, calls its run(&mut self, context: &mut CTX, inputs: &CallInputs). The provider gets the whole context, including the parent EVM's journal. measureGas needs that (it has to read the target's code), so we write a provider that answers CHEATS_ADDRESS itself and forwards every other address to the standard EthPrecompiles:

use alloy_primitives::{address, Address, Bytes, U256};
use alloy_sol_types::{sol, SolCall, SolValue};
use revm::{
    context::{Cfg, ContextTr, JournalTr},
    handler::{precompile_output_to_interpreter_result, EthPrecompiles, PrecompileProvider},
    interpreter::{CallInputs, InterpreterResult},
    primitives::hardfork::SpecId,
};
use revm_precompile::{PrecompileHalt, PrecompileOutput};

pub const CHEATS_ADDRESS: Address = address!("7110000000000000000000000000000000000000");

sol! {
    function measureGas(address target, bytes calldata data) external returns (uint256 gasUsed);
}

/// Flat gas charged to the caller for the cheatcode call itself.
const CHEATCODE_BASE_COST: u64 = 100;

/// Mainnet precompiles plus our cheatcode at `CHEATS_ADDRESS`.
#[derive(Debug, Clone)]
pub struct CheatsPrecompiles {
    inner: EthPrecompiles,
}

impl CheatsPrecompiles {
    pub fn new(spec: SpecId) -> Self {
        Self { inner: EthPrecompiles::new(spec) }
    }
}

impl<CTX: ContextTr> PrecompileProvider<CTX> for CheatsPrecompiles {
    type Output = InterpreterResult;

    fn set_spec(&mut self, spec: <CTX::Cfg as Cfg>::Spec) -> bool {
        <EthPrecompiles as PrecompileProvider<CTX>>::set_spec(&mut self.inner, spec)
    }

    fn run(
        &mut self,
        context: &mut CTX,
        inputs: &CallInputs,
    ) -> Result<Option<InterpreterResult>, String> {
        if inputs.bytecode_address != CHEATS_ADDRESS {
            // ECRECOVER, SHA256, ..., KZG: unchanged mainnet behaviour.
            return self.inner.run(context, inputs);
        }
        let input = inputs.input.as_bytes(context).to_vec();
        let output = match cheats_run(context, &input, inputs.gas_limit) {
            Ok((gas_used, bytes)) => PrecompileOutput::new(gas_used, bytes, inputs.reservoir),
            Err(halt) => PrecompileOutput::halt(halt, inputs.reservoir),
        };
        Ok(Some(precompile_output_to_interpreter_result(output, inputs.gas_limit)))
    }

    fn warm_addresses(&self) -> Box<impl Iterator<Item = Address>> {
        Box::new(self.inner.warm_addresses().chain(std::iter::once(CHEATS_ADDRESS)))
    }

    fn contains(&self, address: &Address) -> bool {
        *address == CHEATS_ADDRESS || self.inner.contains(address)
    }
}

/// Selector dispatch: same 4-byte ABI model as a Solidity contract.
fn cheats_run<CTX: ContextTr>(
    context: &mut CTX,
    input: &[u8],
    gas_limit: u64,
) -> Result<(u64, Bytes), PrecompileHalt> {
    if gas_limit < CHEATCODE_BASE_COST {
        return Err(PrecompileHalt::OutOfGas);
    }
    if input.len() < 4 {
        return Err(PrecompileHalt::other_static("cheats: missing selector"));
    }
    let (selector, calldata) = input.split_at(4);

    if selector == measureGasCall::SELECTOR {
        let call = measureGasCall::abi_decode_raw(calldata)
            .map_err(|_| PrecompileHalt::other_static("cheats: bad measureGas args"))?;

        // Read the target account (balance, nonce, code) from the *parent* EVM's journal,
        // so contracts deployed earlier in the same transaction are visible.
        let target_info = context
            .journal_mut()
            .load_account_with_code(call.target)
            .map_err(|_| PrecompileHalt::other_static("cheats: database error"))?
            .data
            .info
            .clone();

        let gas_used = run_measure_gas(call.target, target_info, call.data, gas_limit)?;
        return Ok((CHEATCODE_BASE_COST, U256::from(gas_used).abi_encode().into()));
    }

    Err(PrecompileHalt::other_static("cheats: unknown selector"))
}

Walk:

  • CHEATS_ADDRESS — 0x7110... deliberately just above Foundry's 0x7109 so we don't collide. Cheatcode addresses are convention; pick anything that doesn't conflict with mainnet precompiles or other dev tools.
  • CheatsPrecompiles wraps EthPrecompiles. run returns Ok(None) from the inner set for addresses that aren't precompiles (normal bytecode then executes), and contains / warm_addresses add our address, so calls to 0x7110... are routed to run and the address is warm like the other precompiles. ECRECOVER, SHA256, modexp, BN254, KZG, BLS and so on keep working.
  • Selector dispatch — same 4-byte ABI selector machinery as a Solidity contract. The sol! macro generates measureGasCall; the SolCall trait (hence the import) provides measureGasCall::SELECTOR (a [u8; 4]) and measureGasCall::abi_decode_raw(calldata) (alloy-sol-types 1.x: no validate flag). Type-safe end to end — no manual byte slicing.
  • Two outcomes — PrecompileOutput::new(gas, bytes, reservoir) returns the abi-encoded result and charges the caller a flat CHEATCODE_BASE_COST; PrecompileOutput::halt(PrecompileHalt::*, reservoir) halts the calling frame. precompile_output_to_interpreter_result (the same helper EthPrecompiles uses) turns either into the InterpreterResult the interpreter expects.

Step 2: The cheatcode logic

The interesting part: measureGas runs a nested EVM execution of target with data, measures gas, and returns the number:

use revm::{
    context::TxEnv,
    context_interface::{cfg::gas::calculate_initial_tx_gas, result::ExecutionResult},
    database::{CacheDB, EmptyDB},
    primitives::TxKind,
    state::AccountInfo,
    Context, ExecuteEvm, MainBuilder, MainContext,
};

fn run_measure_gas(
    target: Address,
    target_info: AccountInfo,
    data: Bytes,
    gas_limit: u64,
) -> Result<u64, PrecompileHalt> {
    // Simplification: the nested EVM gets the target's code and balance, but
    // empty storage and no other accounts. Sharing the full parent state is Drill 3.
    let mut db = CacheDB::new(EmptyDB::default());
    db.insert_account_info(target, target_info);

    let mut evm = Context::mainnet().with_db(&mut db).build_mainnet();

    // Intrinsic cost of the nested tx (21,000 + calldata), subtracted below so we
    // report only the gas spent executing `target`.
    let intrinsic = calculate_initial_tx_gas(SpecId::default(), &data, false, 0, 0, 0)
        .initial_total_gas;

    let tx = TxEnv::builder()
        .caller(Address::ZERO)
        .kind(TxKind::Call(target))
        .data(data)
        .gas_limit(gas_limit)
        .build()
        .map_err(|_| PrecompileHalt::other_static("cheats: bad nested tx"))?;

    let result = evm
        .transact_one(tx)
        .map_err(|_| PrecompileHalt::other_static("cheats: nested tx invalid"))?;

    // Success, Revert and Halt all carry a `gas: ResultGas`. Even reverted txs consumed gas.
    let spent = match &result {
        ExecutionResult::Success { gas, .. }
        | ExecutionResult::Revert { gas, .. }
        | ExecutionResult::Halt { gas, .. } => gas.total_gas_spent(),
    };
    Ok(spent.saturating_sub(intrinsic))
}

Walk:

  • Context::mainnet().with_db(&mut db).build_mainnet() — same builder you used in the MEV searcher lab. The cheatcode is a tiny EVM-on-EVM. Once you've run one Revm, you've run them all.
  • Result variants. In Revm 38, transact_one returns the ExecutionResult directly (no .result wrapper), and Success, Revert and Halt each carry gas: ResultGas, not a gas_used field. Even reverted txs consumed gas, so we report all three; the test author decides what counts.
  • Execution gas only. total_gas_spent() is gas spent before refunds and includes the nested transaction's intrinsic cost (21,000 + calldata), which the parent never pays for this call. Subtracting calculate_initial_tx_gas(...).initial_total_gas leaves the gas spent executing target. For Counter.increment() on a fresh counter that is 22,396 gas (cold SLOAD + zero-to-nonzero SSTORE dominate).
  • The nested DB is a simplification. It holds the target's account (code + balance, copied from the parent journal in Step 1) and nothing else: storage starts empty, and other contracts the target calls don't exist. Real Foundry cheatcodes act on the parent test EVM's state directly (because vm.deal() needs to mutate balances the parent test will see). Drill 3 explores that.

Step 3: Wire into a Revm test harness

Now we need a test runner that installs our provider and executes Solidity test contracts against it. The minimum viable harness:

use revm::ExecuteCommitEvm;

pub fn run_test_contract(
    test_contract_bytecode: Bytes,
    test_calldata: Bytes,
) -> eyre::Result<ExecutionResult> {
    let mut db = CacheDB::new(EmptyDB::default());

    // Standard mainnet EVM, then swap in our precompile provider.
    let mut evm = Context::mainnet()
        .with_db(&mut db)
        .build_mainnet()
        .with_precompiles(CheatsPrecompiles::new(SpecId::default()));

    let deployer = Address::repeat_byte(0xAB);

    // 1. Deploy the test contract (committed, so the next tx sees it).
    let deploy_tx = TxEnv::builder()
        .caller(deployer)
        .kind(TxKind::Create)
        .data(test_contract_bytecode)
        .gas_limit(10_000_000)
        .nonce(0)
        .build()?;
    let test_addr = match evm.transact_commit(deploy_tx)? {
        ExecutionResult::Success { output, .. } => output
            .address()
            .copied()
            .ok_or_else(|| eyre::eyre!("no created address"))?,
        other => eyre::bail!("test contract deploy failed: {other:?}"),
    };

    // 2. Call the test method.
    let test_tx = TxEnv::builder()
        .caller(deployer)
        .kind(TxKind::Call(test_addr))
        .data(test_calldata)
        .gas_limit(10_000_000)
        .nonce(1)
        .build()?;
    Ok(evm.transact_commit(test_tx)?)
}

Walk:

  • .build_mainnet().with_precompiles(CheatsPrecompiles::new(...)) — build_mainnet() returns an Evm with the standard EthPrecompiles; Evm::with_precompiles swaps in any other PrecompileProvider. There is no with_precompiles on Context itself, and EthPrecompiles holds a &'static Precompiles, which is why we wrap it instead of extending its set. Reth's examples/custom-evm does the equivalent inside a custom EvmFactory (with PrecompilesMap).
  • transact_commit, not transact_one. The deploy has to be committed to the DB so the second transaction can call the contract. The caller's nonce goes 0 → 1, so the second tx sets .nonce(1).
  • The harness is ~40 lines. Foundry adds: a Solidity compiler integration (forge does this via solc), test discovery (find functions starting with test), structured failure reporting, parallel execution. The kernel is what you wrote.

🔍 Find in repo. Open forge-std/src/Vm.sol and skim the cheatcode interface. Every function in there is a Solidity ABI surface for a Rust handler in Foundry's cheatcode crate. Scroll until you can name 3 cheatcodes you didn't realize were Rust.

Step 4: Write a test

Now from the Solidity side, calling our cheatcode is identical to calling vm.deal or any other:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

interface Cheats {
    function measureGas(address target, bytes calldata data) external returns (uint256);
}

contract Counter {
    uint256 public count;
    function increment() public { count++; }
}

contract CounterTest {
    Cheats constant cheats = Cheats(0x7110000000000000000000000000000000000000);

    function test_increment_gas_under_25k() public returns (uint256 gas) {
        Counter c = new Counter();
        bytes memory data = abi.encodeWithSignature("increment()");
        gas = cheats.measureGas(address(c), data);
        require(gas < 25_000, "increment too expensive");
    }
}

Save it as sol/src/CounterTest.sol, run forge build in sol/ (forge picks a solc matching the pragma; our run used 0.8.35), and pass the bytecode from sol/out/CounterTest.sol/CounterTest.json plus the test_increment_gas_under_25k() calldata to run_test_contract. The call succeeds and returns 22,396: a Solidity test called your Rust cheatcode end to end. (The test returns the measured gas only so the harness can print it.)

What's missing for production-grade test framework

GapWhat Foundry does
Solidity compilationforge shells out to solc, caches artifacts, handles imports. Reproduce only if you really need it; otherwise let users pre-compile.
Shared parent statevm.deal() mutates balances the test sees. Our nested EVM only gets a copy of the target account; writing back means mutating the parent journal (Foundry does it from its inspector) — a non-trivial extension.
ParallelismFoundry runs tests across threads with isolated DBs per test. Trivial to add (one tokio task per test contract).
Better failure reportingStack traces, decoded revert reasons, fuzz shrink. All polish on top of the kernel above.
Cheatcode persistence between callsE.g., vm.expectRevert sets state for the next call only. Foundry keeps it in inspector state; a provider like ours could hold fields in &mut self, but it only sees calls to its own address, not the next call.
Permissionless cheatcode discoveryA real plugin system would let cheatcodes be loaded as dynamic libraries. Foundry doesn't do this — they're compiled in. We won't either.

The architecture you wrote — magic address + selector dispatch + ABI-decoded args + nested EVM execution + harness that registers it — mirrors how Foundry's cheatcode system works. Foundry intercepts the call in an inspector instead of a precompile provider (so it sees every call, not just its own address, and can keep state across them) and adds Solidity-level ergonomics.

Drill

  1. Add balanceOf(address). A second selector that returns the balance of any address from the parent journal (context.journal_mut().load_account(addr), then .data.info.balance). (15 min)
  2. Make the call payable. Add a value argument to measureGas and pass it through to the inner tx. What changes about the Solidity side when the cheatcode becomes payable? (30 min)
  3. Shared state cheatcode. Implement cheats.deal(address, uint256) that mutates the parent test's state. Hint: write through the parent journal (JournalTr, e.g. load_account_mut / balance_incr) rather than an isolated nested EVM, or move the cheat into a Revm Inspector as Foundry does. (3 hours)
  4. Solidity test discovery. Build a minimal test runner that takes a directory, compiles all .sol files via solc, finds every function starting with test_, runs each, and prints pass/fail. (4 hours)
  5. Performance comparison. Run the same test (Counter increment × 1000) under (a) your custom harness, (b) forge test. What's the latency gap, and where does it come from? (1 hour profiling)

Finish drill 4 and you have, structurally, a fork of Foundry. Add fuzz testing + invariant testing on top and you're at parity with what's in the wild.

Test gate

Per Test gate — every app in this tier ships with passing tests, this lesson's minimum gate is a differential test against a reference implementation of the same primitive.

Custom cheatcodes are dual-use code: a Rust precompile for speed, plus a Solidity-only reference for trust. If your cheats.measureGas(target, data) doesn't track the manual gasleft() before/after pattern in plain Solidity, all tests using your cheatcode are silently lying. The differential test is the only way to know.

forge test can't run this: forge's EVM doesn't have your provider, so 0x7110... is an empty account there. The test contract runs through your harness instead. Save it as sol/src/Differential.sol and run forge build in sol/:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

interface Cheats {
    function measureGas(address target, bytes calldata data) external returns (uint256);
}

contract Target {
    uint256 public acc;
    function work(uint256 n) external {
        for (uint256 i; i < n; i++) acc += i;
    }
}

contract MeasureGasDifferential {
    Cheats constant cheats = Cheats(0x7110000000000000000000000000000000000000);

    function check(uint256 input) external returns (uint256 viaCheat, uint256 referenceGas) {
        Target target = new Target();
        bytes memory data = abi.encodeCall(Target.work, (input));

        // (a) Rust cheatcode
        viaCheat = cheats.measureGas(address(target), data);

        // (b) Solidity-only reference
        uint256 before = gasleft();
        (bool ok,) = address(target).call(data);
        referenceGas = before - gasleft();
        require(ok);
    }
}

Each check deploys a fresh Target, so the nested EVM's empty storage matches the parent's. The Rust side (tests/cheats.rs) drives both Solidity contracts through run_test_contract:

// tests/cheats.rs — run `forge build` in sol/ first
use alloy_primitives::{Bytes, U256};
use alloy_sol_types::{sol, SolCall};
use revm::context_interface::result::ExecutionResult;
use rust_cheatcode::run_test_contract;

sol! {
    function test_increment_gas_under_25k() returns (uint256 gas);
    function check(uint256 input) returns (uint256 viaCheat, uint256 referenceGas);
}

fn bytecode(artifact: &str) -> Bytes {
    let json: serde_json::Value =
        serde_json::from_str(&std::fs::read_to_string(artifact).unwrap()).unwrap();
    json["bytecode"]["object"].as_str().unwrap().parse().unwrap()
}

fn call<C: SolCall>(artifact: &str, call: C) -> C::Return {
    match run_test_contract(bytecode(artifact), call.abi_encode().into()).unwrap() {
        ExecutionResult::Success { output, .. } => C::abi_decode_returns(output.data()).unwrap(),
        other => panic!("test call failed: {other:?}"),
    }
}

const COUNTER: &str = "sol/out/CounterTest.sol/CounterTest.json";
const DIFF: &str = "sol/out/Differential.sol/MeasureGasDifferential.json";

/// (c, r) = (cheatcode result, Solidity reference)
fn check(input: u64) -> (u64, u64) {
    let r = call(DIFF, checkCall { input: U256::from(input) });
    (r.viaCheat.to::<u64>(), r.referenceGas.to::<u64>())
}

#[test]
fn counter_increment_under_budget() {
    let gas = call(COUNTER, test_increment_gas_under_25kCall {});
    println!("measureGas(increment) = {gas}");
}

#[test]
fn matches_reference_for_known_input() {
    let (c, r) = check(42);
    println!("input=42 cheat={c} reference={r} gap={}", r - c);
    assert!(c > 0 && r > c && r - c < 1_000, "cheat={c} reference={r}");
}

#[test]
fn always_agrees_with_reference() {
    let (c0, r0) = check(0);
    let gap = r0 - c0; // the reference's own CALL overhead
    for i in 0..256u64 {
        let input = (i * 7_919) % 1_001; // 256 deterministic inputs in 0..=1000
        let (c, r) = check(input);
        assert_eq!(r - c, gap, "input={input} cheat={c} reference={r}");
    }
    println!("256 inputs, constant gap = {gap} gas");
}

The two numbers can't be equal: the reference also pays for issuing its own CALL (warm account access, copying calldata to memory, the GAS reads around it). That overhead doesn't depend on the input, so the gate is: the gap is identical for every input. Output of cargo test --test cheats -- --nocapture (revm 38.0.0, solc 0.8.35):

running 3 tests
measureGas(increment) = 22396
test counter_increment_under_budget ... ok
input=42 cheat=41882 reference=42381 gap=499
test matches_reference_for_known_input ... ok
256 inputs, constant gap = 499 gas
test always_agrees_with_reference ... ok

test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 2.53s

The lesson is not complete until all 256 inputs show the same gap. If one input diverges, your cheatcode would silently corrupt anyone's gas accounting on that exact input. (The exact gap depends on the solc version and settings; the constancy is what matters.) The gate only covers what the nested DB can reproduce: each check deploys a fresh Target, so its empty storage matches. A target with existing storage, or one that calls other contracts, would be measured against the wrong state; Drill 3 is where that gets fixed.

📺 Further watching

sJpL21yJpgs | Horsefacts — Invariant Testing WETH with Foundry (the cheatcode patterns this lesson reverse-engineers)

🧭 Where you are now in the stack: you've shipped a VM-layer extension — a custom precompile registered at a high address that dispatches into a Rust function, with a differential test against a Solidity reference locking in correctness. Same pattern as JNI and V8 native bindings, applied to Revm. Next lesson moves to the database layer's consistent-snapshot read: a swap aggregator over forked DEX state.

Summary (3 lines)

  • Custom cheatcode here = a Rust precompile at a chosen address, served by a custom PrecompileProvider installed with .build_mainnet().with_precompiles(...) in the harness (Foundry itself intercepts calls to 0x7109709E... in an inspector).
  • Calldata decode via sol! + SolCall::abi_decode_raw. Parent state is read and written through the journal (context.journal_mut(): load_account_with_code, sstore, balance_incr, ...).
  • Used by chain teams extending Foundry (e.g. tempoxyz/tempo-foundry) + by MEV searchers for simulation. Next: swap aggregator.