FABRKNT
Building with the Stack — Real-World Rust EVM Apps
Application Patterns
Lesson 7 of 11·CONTENT45 min80 XP

Treat this page as a workbench, not a blog post. The goal is to extract a reusable mental model from the source and carry it into the rest of the Fabrknt stack.

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 precompiles at 0x71097.... Build a custom one. Reth SDK lets you register a precompile that intercepts a magic address — same pattern as Foundry's vm.warp / vm.deal.

Principle (minimum model)

  • Precompile = a Rust function at a fixed address. When the EVM calls that address, Reth dispatches to your function instead of executing bytecode.
  • Reth SDK precompile registration. EvmConfig::with_precompiles(vec![(address, precompile_fn)]) adds your precompile to the node's EVM config.
  • 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 alloy_sol_types::SolValue. <(uint256, address)>::abi_decode(&calldata, true)? — type-safe ABI decoding.
  • State mutation in a precompile. Get the journal database; call db.touch(addr)? / db.set_balance(addr, value)? / db.set_storage(addr, slot, value)?.
  • Test gate. Spin up a Reth node with your precompile registered; call from a Solidity test; assert the state changed.
  • Use cases. Production sequencers ship custom cheatcodes for testing (Tempo / Hyperliquid / OP-stack); MEV searchers ship them for simulation.

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 — a precompile (a built-in contract whose code lives in the EVM engine, not on chain) — that Foundry installs at the magic address 0x7109709E... and exposes to Solidity via the Vm.sol interface. 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 — using the exact pattern Foundry uses internally.

📌 Scope honesty. We don't fork Foundry. We build the precompile + a minimal Revm-based test harness that loads it. The pattern (high-address precompile + Solidity ABI surface + test runner that wires it in) is identical to how Foundry adds cheatcodes — you just see all of it 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. testMatches_referenceForKnownInput — for one fixed input, your Rust precompile and the Solidity-only gasleft() reference agree within ε (a few gas).
  2. testFuzz_alwaysAgreesWithReference — over Foundry's default 256 fuzz iterations, precompile and reference agree on every input.

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 (Foundry's choice)Yes — full Rust accessOnly present in your Revm build, not mainnet~50 lines

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. That's why Foundry's cheatcodes are precompiles, not 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"

Step 1: The precompile function

A Revm precompile is a Rust function with the signature fn(input: &[u8], gas_limit: u64) -> PrecompileResult. We layer the cheatcode dispatch inside:

use alloy_primitives::{Address, U256};
use alloy_sol_types::{sol, SolValue};
use revm::{
    context::TxEnv,
    context_interface::result::ExecutionResult,
    primitives::TxKind,
    Context, ExecuteEvm, MainBuilder, MainContext,
};
use revm_precompile::{
    EthPrecompileOutput, EthPrecompileResult, Precompile, PrecompileHalt, PrecompileId,
};

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

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

/// The precompile entry point.
pub fn cheats_run(input: &[u8], gas_limit: u64) -> EthPrecompileResult {
    // The first 4 bytes are the function selector — same dispatch model as a Solidity contract.
    if input.len() < 4 {
        return Err(PrecompileHalt::OutOfGas); // really "bad input"; map to a real error in prod
    }

    let selector = &input[..4];
    let calldata = &input[4..];

    if selector == measureGasCall::SELECTOR {
        let decoded = measureGasCall::abi_decode_raw(calldata, true)
            .map_err(|_| PrecompileHalt::OutOfGas)?;

        let gas_used = run_measure_gas(decoded.target, decoded.data, gas_limit)?;
        return Ok(EthPrecompileOutput::new(
            21_000, // flat cost for the cheatcode call itself
            U256::from(gas_used).abi_encode().into(),
        ));
    }

    Err(PrecompileHalt::OutOfGas)
}

Walk:

  • CHEATS_ADDRESS0x7110... 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.
  • Selector dispatch — same 4-byte ABI selector machinery as a Solidity contract. The sol! macro generates measureGasCall::SELECTOR (a constant [u8; 4]) and abi_decode_raw. Type-safe end to end — no manual byte slicing.
  • Two return pathsOk(EthPrecompileOutput) carries gas-used + abi-encoded result bytes; Err(PrecompileHalt::*) halts the calling frame. Production cheatcodes use specific halt variants (Foundry has its own); we keep it simple.

Step 2: The cheatcode logic

The interesting part: measureGas runs a nested EVM execution against the same world state, measures gas, and returns the number. The key API: spin up a fresh Context over the existing journal (so state is shared) and run a one-off transaction:

fn run_measure_gas(target: Address, data: Vec<u8>, gas_limit: u64) -> Result<u64, PrecompileHalt> {
    // In a real cheatcode, we'd be handed access to the parent EVM's state via
    // a custom Inspector. For lesson clarity, we spin up an isolated EVM
    // against an empty in-memory DB — enough to demonstrate the gas math.
    let mut db = revm::database::CacheDB::new(revm::database::EmptyDB::default());

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

    let tx = TxEnv::builder()
        .caller(Address::ZERO)
        .kind(TxKind::Call(target))
        .data(data.into())
        .gas_limit(gas_limit)
        .build()
        .map_err(|_| PrecompileHalt::OutOfGas)?;

    let result = evm.transact_one(tx).map_err(|_| PrecompileHalt::OutOfGas)?;

    match result.result {
        ExecutionResult::Success { gas_used, .. } => Ok(gas_used),
        ExecutionResult::Revert { gas_used, .. } => Ok(gas_used),
        ExecutionResult::Halt { gas_used, .. } => Ok(gas_used),
    }
}

Walk:

  • Context::mainnet().with_db(&mut db).build_mainnet() — same builder you used in Lesson 1 (MEV searcher). The cheatcode is a tiny EVM-on-EVM. Once you've run one Revm, you've run them all.
  • All three result variants return gas_used — Success, Revert, Halt. Even reverted txs consumed gas. We return the real number; the test author can decide what counts.
  • db = EmptyDB in this lesson is a simplification. Real Foundry cheatcodes share state with the parent test EVM via a custom Inspector hook (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 registers our precompile + executes Solidity test contracts against it. The minimum viable harness:

use revm::Context;
use revm_precompile::{Precompiles, PrecompileSpecId};

// (from the standard precompile interface)
revm_precompile::eth_precompile_fn!(cheats_precompile_fn, cheats_run);

const CHEATS_PRECOMPILE: Precompile = Precompile::new(
    PrecompileId::Custom(std::borrow::Cow::Borrowed("cheats")),
    CHEATS_ADDRESS,
    cheats_precompile_fn,
);

fn build_test_evm_context<'db, DB>(db: &'db mut DB) -> impl ExecuteEvm + 'db
where
    DB: revm::Database<Error: std::fmt::Debug>,
{
    // Start from mainnet precompiles, extend with ours
    let mut precompiles = Precompiles::new(PrecompileSpecId::OSAKA).clone();
    precompiles.extend([CHEATS_PRECOMPILE]);

    Context::mainnet()
        .with_db(db)
        .with_precompiles(precompiles)
        .build_mainnet()
}

fn run_test_contract<DB>(db: &mut DB, test_contract_bytecode: Vec<u8>, test_calldata: Vec<u8>)
    -> eyre::Result<bool>
where
    DB: revm::Database<Error: std::fmt::Debug>,
{
    // 1. Deploy the test contract
    let mut evm = build_test_evm_context(db);
    let deploy_tx = TxEnv::builder()
        .caller(Address::from([0xAB; 20]))
        .kind(TxKind::Create)
        .data(test_contract_bytecode.into())
        .gas_limit(10_000_000)
        .build()?;
    let deploy_result = evm.transact_one(deploy_tx)?;

    let test_addr = match deploy_result.result {
        ExecutionResult::Success { output: revm::context_interface::result::Output::Create(_, Some(a)), .. } => a,
        _ => eyre::bail!("test contract deploy failed"),
    };

    // 2. Call the test method
    let test_tx = TxEnv::builder()
        .caller(Address::from([0xAB; 20]))
        .kind(TxKind::Call(test_addr))
        .data(test_calldata.into())
        .gas_limit(10_000_000)
        .build()?;
    let test_result = evm.transact_one(test_tx)?;

    Ok(matches!(test_result.result, ExecutionResult::Success { .. }))
}

Walk:

  • Precompiles::new(PrecompileSpecId::OSAKA).clone() — start from the standard mainnet set (ECRECOVER, SHA256, RIPEMD160, IDENTITY, modexp, BN254, KZG, BLS) and extend with ours. The standard precompiles are still available; your new one is additive.
  • with_precompiles(...) — Revm 38's API for installing a custom precompile registry. Same line wires in any number of cheatcodes.
  • The harness is ~30 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 precompile 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 {
        Counter c = new Counter();
        bytes memory data = abi.encodeWithSignature("increment()");
        uint256 gas = cheats.measureGas(address(c), data);
        require(gas < 25_000, "increment too expensive");
    }
}

Compile with solc, hand the bytecode + the test_increment_gas_under_25k() selector to run_test_contract, and you've executed a Solidity test that called your custom Rust cheatcode end to end.

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. That requires a custom Inspector hook into the parent EVM — a non-trivial extension to our isolated harness.
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. Stored in inspector state, not in the precompile itself.
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 — high-address precompile + selector dispatch + ABI-decoded args + nested EVM execution + harness that registers it — is the kernel of how Foundry's cheatcode system works. Foundry adds Rust-level glue and Solidity-level ergonomics; the foundation is identical.

Drill

  1. Add balanceOf(address). A second selector that returns the balance of any address using evm.db.basic(addr).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: you'll need a custom Revm Inspector rather than an isolated nested EVM. (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) returns a different number than the manual gasleft() - gasleft() pattern in plain Solidity, all tests using your cheatcode are silently lying. The differential test is the only way to know.

// test/MeasureGasDifferential.t.sol
import "forge-std/Test.sol";
import {Cheats} from "src/Cheats.sol";

contract MeasureGasDifferential is Test {
    Cheats cheats = Cheats(0x7109709ECfa91a80626fF3989D68f67F5b1DD12E);  // your address
    Target target = new Target();

    function testMatches_referenceForKnownInput() public {
        bytes memory data = abi.encodeCall(target.work, (42));

        // (a) Your Rust precompile
        uint256 viaPrecompile = cheats.measureGas(address(target), data);

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

        assertApproxEqAbs(viaPrecompile, referenceGas, 5);  // tiny ε for measurement overhead
    }

    function testFuzz_alwaysAgreesWithReference(uint256 input) public {
        input = bound(input, 0, 10_000);
        bytes memory data = abi.encodeCall(target.work, (input));

        uint256 viaPrecompile = cheats.measureGas(address(target), data);
        uint256 before = gasleft();
        (bool ok,) = address(target).call(data);
        uint256 referenceGas = before - gasleft();
        require(ok);

        assertApproxEqAbs(viaPrecompile, referenceGas, 5);
    }
}

Run forge test --match-test testFuzz_ -vvv with the default 256 fuzz iterations. The lesson is not complete until the precompile and the reference agree across all 256 inputs (give or take a few-gas measurement window). If they diverge on one input, your cheatcode would silently corrupt anyone's gas accounting on that exact input.

📺 Further watching

sJpLesson 21yJpgs | 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 differential fuzz 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 = a Rust precompile at a chosen address. EvmConfig::with_precompiles registers; Reth dispatches.
  • Calldata decode via alloy_sol_types::SolValue. State mutation via the journal database (set_balance / set_storage etc).
  • Used by production sequencers (Tempo / Hyperliquid / OP-stack) for testing + by MEV searchers for simulation. Next: swap aggregator.