Skip to content
← Mastering Foundry — Solidity testing discipline for engineers who already think in Rust
Orientation
Lesson 1 of 7·CONTENT15 min50 XP
Course
Mastering Foundry — Solidity testing discipline for engineers who already think in Rust
Lesson role
CONTENT
Sequence
1 / 7

Mastering Foundry — Solidity testing discipline for engineers who already think in Rust

Question

The Rust discipline you internalised in the openhl tracks — pure-compute primitives, state machines guarded by debug_assert! + saturating arithmetic, conservation laws checked by proptest!, byte-for-byte answer keys — needs to port to Solidity contracts. Foundry is the runtime that runs that discipline mechanically. Why has Foundry become the standard for Solidity testing, and what still sets it apart from Hardhat?

Principle (minimum model)

  • Three reasons Foundry became the standard. (1) Tests written in Solidity and run by one Rust binary directly on revm. (2) Fuzz and invariant testing built in (failing fuzz inputs are saved under cache/fuzz/ and rerun; failing invariant call sequences are shrunk). (3) In-EVM cheatcodes (vm.warp / vm.deal / vm.prank are calls to the cheatcode address 0x7109709ECfa91a80626fF3989D68f67F5b1DD12D, which Foundry's revm inspector intercepts).
  • Hardhat has moved toward the same model. Since Hardhat 2.22 (March 2024) its built-in network runs on EDR, a Rust EVM runtime, inside the test process (hardhat node is a separate process only when you start one), and Hardhat 3 (August 2025) runs Foundry-style Solidity tests with fuzzing and forge-std cheatcodes. Foundry's edge is that Solidity-first testing has been its default from the start, with cast and anvil in the same toolchain.
  • Discipline transfer table. cargo test ↔ forge test; proptest! (single-input) ↔ fuzz testing; proptest! (sequenced) ↔ invariant testing; debug_assert! ↔ require / vm.expectRevert; saturating_add ↔ an explicit clamp (Solidity 0.8 arithmetic reverts on overflow; unchecked wraps); conservation laws ↔ invariant assertions; byte-for-byte answer keys ↔ reference contract + forge corpus.
  • The Capstone (Lesson 6) is the proof. Port openhl-liquidation Stage 10b's InsuranceFund from Rust to Solidity, write four conservation-law invariants for it in Foundry, run 10 K iterations — the same conservation law checked in two languages.
  • 7-lesson, 4-module structure. Orientation (L0) → Test discipline (L1–L3) → CLI + state-aware testing (L4–L5) → Capstone (L6).
  • Prerequisites. (1) Basic Solidity syntax (read a function definition, distinguish mapping from struct). (2) cargo test in a Rust crate. (3) Read the openhl liquidation course at least through the lesson where the WithdrawOutcome proptest appears (the withdraw_shortfall lesson — the Capstone assumes you have internalised those semantics).
  • Out of scope. Gas-optimisation deep dives / Slither + Mythril + formal verification / frontend (ethers.js / viem) / full forge script deployment workflow.

Worked example + steps

Mastering Foundry — Solidity testing discipline for engineers who already think in Rust

What you'll build

If you've been through any of the openhl build courses (consensus, CLOB, funding, liquidation, ADL), you've learned a discipline: pure-compute primitives, state machines defended by debug_assert! + saturating_arithmetic, conservation laws proven by proptest!, byte-for-byte answer keys. That discipline transfers to Solidity contracts almost 1-to-1 — and Foundry is the tool that makes the transfer mechanical.

By the end of this course, you'll have:

  • A Solidity project initialized with forge init that builds, tests, and fuzzes locally with sub-second feedback loops.
  • First-hand experience with fuzz tests (testFuzz_*, run by forge test) — Solidity's closest proptest! equivalent: the same "assert over all valid inputs" workflow you learned in the liquidation course's withdraw_shortfall lesson. One difference to know up front: Foundry reports the failing fuzz input it found rather than shrinking it toward a minimum (it does shrink invariant call sequences).
  • Invariant testing (multi-call) — the closest Solidity primitive to per-scan conservation laws (the liquidation course's scanner-capstone proptests). Define a Handler, run thousands of random method-call sequences, assert a property holds at every step.
  • cast muscle memory — the chain CLI most production traders/engineers run dozens of times daily. Read storage slots, call view functions, decode ABIs.
  • anvil --fork-url + cheatcodes — local mainnet replication with vm.deal / vm.warp / vm.prank for state-aware testing. Cheatcodes are not chain precompiles, but they resemble one (see the openhl precompiles course): calls to a fixed address that the execution environment handles specially, here Foundry's test EVM rather than the chain.
  • A capstone: port openhl-liquidation Stage 10b's InsuranceFund from Rust to Solidity, write the conservation-law invariants from the liquidation course's withdraw_shortfall lesson in Foundry, and prove the same theorem mechanically in two languages.

You'll understand:

  • Why Foundry became the default for Solidity testing: a Rust-built single binary whose Solidity tests run directly on revm — the same revm you've been peering into across the openhl courses.
  • What happened to the alternatives: Truffle and Ganache were sunset by Consensys in 2023, Brownie is no longer actively maintained, and Hardhat moved onto a Rust runtime (EDR) and added Solidity tests — converging on Foundry's model.
  • What fuzz testing and invariant testing actually do under the hood — they're orchestrating REVM via the same patterns these courses teach in crates/evm of openhl, just exposed as Solidity-side tests.
  • How cheatcodes work — calls to one fixed address that Foundry's revm inspector intercepts in-process, and why that keeps state manipulation inside the same process as the test.

Why this course exists

Most Foundry tutorials answer "how do I use this tool?" This course answers a different question: "how do I take the rigorous-testing discipline I learned in Rust and apply it to Solidity contracts?"

The shape of that discipline, repeated across every openhl course:

   ┌──────────────────────────────────────────────────────────────┐
   │  openhl Rust discipline            ←→  Foundry Solidity      │
   │                                          equivalent           │
   ├──────────────────────────────────────────────────────────────┤
   │  cargo test                            forge test              │
   │  proptest! (single-input)              fuzz tests              │
   │  proptest! (sequenced ops)             invariant tests         │
   │  debug_assert!                         require / vm.expectRevert│
   │  saturating_add (consensus)            explicit clamp          │
   │  conservation laws                     invariant assertions    │
   │  byte-for-byte answer key             reference contract +    │
   │     vs openhl SHA                        forge test corpus     │
   └──────────────────────────────────────────────────────────────┘

Every row in the right column is what you'll be writing by Lesson 6. The capstone is the proof that the left column and the right column are saying the same thing.

Why Foundry, not Hardhat / Truffle / Brownie

A short history: Consensys sunset Truffle and Ganache in September 2023 (the Truffle repo is archived), Brownie's README says it is no longer actively maintained, and Foundry became the common choice for Solidity-first testing. Hardhat is still actively developed: version 2.22 (March 2024) moved its built-in network to EDR, a Rust EVM runtime, and Hardhat 3 (August 2025) runs Foundry-style Solidity tests. For the audience of this course (L1 / infra engineers), Foundry fluency is a commodity prerequisite — not a competitive advantage. This course teaches it so the discipline you already have transfers. Three things that made Foundry the default:

  1. Solidity tests, run directly on revm. forge test compiles your contracts and your tests, deploys each test contract into an in-memory revm, and calls its test* functions — one Rust process, with no JavaScript between the test and the EVM. The classic Hardhat setup is a mocha/TypeScript test that drives the EVM call by call through an ethers or viem provider. Its built-in network runs in the same process (on EDR, a Rust runtime, since Hardhat 2.22), so this is not an IPC round trip — but every call still crosses from JavaScript into the runtime, and the test is written in a different language from the contract:

    Hardhat (TypeScript tests)                  Foundry
    ┌──────────────┐  provider    ┌────────┐   ┌───────────────────────────────┐
    │ mocha / node │ ── calls ──► │  EDR   │   │ forge test (one Rust binary)  │
    │ test (TS/JS) │ ◄─────────── │ (Rust) │   │   Solidity test ──► revm      │
    └──────────────┘ same process └────────┘   └───────────────────────────────┘
    

    Hardhat 3 can also run Solidity tests, so today the difference is mostly default workflow and tooling rather than what is possible. No benchmark is quoted here; if speed matters, time your own suite.

  2. Fuzz and invariant testing built in. A test function with parameters is a fuzz test; invariant* functions are checked after random sequences of calls to target contracts. Failing fuzz inputs are saved under cache/fuzz/ and rerun on the next forge test; failing invariant sequences are shrunk to fewer calls. On the JavaScript side this used to mean wiring a library such as fast-check into mocha; Hardhat 3 now runs the same kind of Solidity fuzz and invariant tests.

  3. In-EVM cheatcodes. Hardhat's evm_snapshot / evm_increaseTime are JSON-RPC methods that ask a node to change its state. Foundry's vm.warp / vm.deal / vm.prank are Solidity calls to the cheatcode address 0x7109709ECfa91a80626fF3989D68f67F5b1DD12D (VM_ADDRESS in forge-std's Base.sol). Foundry's test EVM is built on upstream revm (from crates.io, not a fork); its cheatcode inspector (crates/cheatcodes/src/inspector.rs) checks whether a call targets CHEATCODE_ADDRESS and, if so, handles it itself and changes the EVM state from the inside — same process, no IPC. It is not a chain precompile, but for readers who came through the openhl precompiles course (Stage 9) the idea is close to the precompile-as-EVM-superpower pattern: an address whose calls the execution environment handles specially. Faster, more composable, and (importantly) testable inside the same Solidity file as the contracts they test. (Hardhat 3's Solidity tests accept the same forge-std cheatcodes.)

The strategic implication for an L1 engineer: if you write or read Reth/REVM/Alloy code (this curriculum's existing focus), Foundry is the same toolchain in a different language wrapper. Learning it is not switching ecosystems — it's adding a second language to the same execution engine.

The discipline transfer — three concrete invariants you'll port

A preview of what Lesson 2, Lesson 3, and Lesson 6 walk through:

From the liquidation course's withdraw_shortfall lesson (Rust proptest):

$$\text{amount} + \text{unfilled} = \text{shortfall}$$

withdraw_shortfall returns a WithdrawOutcome enum — Covered { amount }, PartiallyDrained { amount, unfilled } or Depleted { unfilled } — and in every case what was paid plus what is left unfilled equals the shortfall the caller passed in. The Rust proptest (openhl crates/liquidation/src/insurance.rs):

proptest! {
    #[test]
    fn withdraw_amount_plus_unfilled_equals_shortfall(
        initial in 0_i64..1_000_000,
        shortfall in 1_i64..1_000_000,
    ) {
        let mut f = InsuranceFund::new(initial);
        let out = f.withdraw_shortfall(shortfall);
        let total = match out {
            WithdrawOutcome::Covered { amount } => amount,
            WithdrawOutcome::PartiallyDrained { amount, unfilled } => amount + unfilled,
            WithdrawOutcome::Depleted { unfilled } => unfilled,
        };
        prop_assert_eq!(total, shortfall);
    }
}

To Foundry (a fuzz test) — what Lesson 2 teaches:

In the capstone's Solidity port, absorb(loss) plays the role of withdraw_shortfall and returns (absorbed, remaining):

function testFuzz_AbsorbedPlusRemainingEqualsLoss(uint256 initial, uint256 loss) public {
    initial = bound(initial, 1, 1_000_000); // deposit(0) reverts
    loss = bound(loss, 1, 1_000_000);       // absorb(0) reverts
    InsuranceFund f = new InsuranceFund(address(this));
    f.deposit(initial);
    (uint256 absorbed, uint256 remaining) = f.absorb(loss);
    assertEq(absorbed + remaining, loss);
}

Same theorem, different syntax. One difference: Rust's proptest shrinks a failing input toward a minimal one, while Foundry reports the fuzz input it found and shrinks only invariant call sequences (Lessons 2 and 3). By Lesson 6 you'll have ported InsuranceFund and written four invariants for it. Same theorem, two languages, both checked mechanically.

The 7 lessons

Module 0 — Orientation

  • Lesson 0 (this lesson) — Why Foundry, the discipline-transfer thesis, the 7-lesson roadmap.

Module 1 — Test discipline (Lesson 1–Lesson 3) — the core

  • Lesson 1 — forge test — first invariants, basic assertions, assertEq / vm.expectRevert, run with -vvv. The Solidity equivalent of cargo test.
  • Lesson 2 — Fuzz testing (testFuzz_*, run by forge test), Solidity's proptest!. Input filtering with vm.assume / bound, reproducing failures. Cross-references the liquidation course's withdraw_shortfall lesson.
  • Lesson 3 — Invariant testing — multi-call tests with Handler contracts and targetContract. Cross-references the liquidation course's scanner-capstone proptests (per-scan conservation laws).

Module 2 — CLI + state-aware testing (Lesson 4–Lesson 5)

  • Lesson 4 — cast — chain CLI deep dive. call / send / storage / abi-decode / 4byte. Mainnet examples via ethereum.reth.rs/rpc.
  • Lesson 5 — anvil --fork-url + cheatcodes — state-aware testing with vm.deal / vm.warp / vm.prank. How cheatcode calls are intercepted (compare with the openhl precompiles course).

Module 3 — Capstone (Lesson 6)

  • Lesson 6 — InsuranceFund.sol + invariant tests — port openhl-liquidation Stage 10b's InsuranceFund from Rust to Solidity, write the conservation-law invariants from the liquidation course's withdraw_shortfall lesson in Foundry, run 10K iterations, prove the same theorem mechanically in two languages. Answer-key contract + tests sit in-repo at examples/foundry-capstone/.

What's NOT in this course

  • Gas optimization deep dive — forge inspect for gas snapshots is a real topic, but it's optimization, not discipline. Out of scope. (Future course candidate: "Solidity for L1 engineers — gas, storage layouts, bytecode.")
  • Slither / Mythril / formal verification — Foundry-adjacent but a different tooling family. Not covered.
  • Frontend / ethers.js / viem — the JS side of the dApp stack. This curriculum's audience is L1 / contract / engine engineers; UI is its own concern.
  • Foundry script (forge script for deployments) — covered briefly in Lesson 4's cast send section. The deployment story is a separate skill from the testing discipline this course teaches.

License / asset discipline

This course's reference assets — the Lesson 6 InsuranceFund.sol capstone + the forge test corpus — live in the course's companion repository (psyto/rethlab, examples/foundry-capstone/). Pinned to whatever companion-repository git SHA the lesson ships at; the reader can git checkout <sha> to get a byte-for-byte working copy.

Foundry itself is updated frequently. The course pins to foundry-rs/foundry rev (as listed in foundryup defaults at the time the course ships). If a future Foundry version breaks any lesson, open an issue on the companion repository — the course is intended to track current stable Foundry.

Audience prerequisites

You should already be comfortable with:

  • Basic Solidity syntax (you can read a function definition and understand mapping vs struct).
  • Running cargo test against a Rust crate (the openhl build courses use this pattern throughout).
  • Reading the openhl liquidation course at least through the lesson where the first WithdrawOutcome proptest appears (withdraw_shortfall). Lesson 6's capstone assumes you've internalized the InsuranceFund semantics from that course.

If any of those feel shaky, no problem — the openhl liquidation course is the natural pre-req, and basic Solidity can be picked up from solidity-by-example.org in an afternoon.

Summary (3 lines)

  • Foundry = a Rust-native single binary whose Solidity tests run directly on revm. Fuzz and invariant testing are built in; cheatcodes are calls to a fixed address that Foundry intercepts (precompile-like, but not chain precompiles). Hardhat has since moved to a Rust runtime (EDR) and added Solidity tests. The proptest! / prop_assume! patterns port to Solidity almost 1:1.
  • This course is not "how to use Foundry". It is "how to transfer the Rust discipline from openhl into Solidity contracts". The discipline-transfer table is the spine; the Capstone is the proof.
  • 7 lessons / 4 modules. Prerequisite: the openhl liquidation course through the withdraw_shortfall lesson. Setup: foundryup installed before Lesson 1.