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.prankare calls to the cheatcode address0x7109709ECfa91a80626fF3989D68f67F5b1DD12D, 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 nodeis 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, withcastandanvilin 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;uncheckedwraps); 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
InsuranceFundfrom 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
functiondefinition, distinguishmappingfromstruct). (2)cargo testin a Rust crate. (3) Read the openhl liquidation course at least through the lesson where theWithdrawOutcomeproptest appears (thewithdraw_shortfalllesson — 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 scriptdeployment 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 initthat builds, tests, and fuzzes locally with sub-second feedback loops. - First-hand experience with fuzz tests (
testFuzz_*, run byforge test) — Solidity's closestproptest!equivalent: the same "assert over all valid inputs" workflow you learned in the liquidation course'swithdraw_shortfalllesson. 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. castmuscle 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 withvm.deal/vm.warp/vm.prankfor 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
InsuranceFundfrom Rust to Solidity, write the conservation-law invariants from the liquidation course'swithdraw_shortfalllesson 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/evmof 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:
-
Solidity tests, run directly on revm.
forge testcompiles your contracts and your tests, deploys each test contract into an in-memory revm, and calls itstest*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.
-
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 undercache/fuzz/and rerun on the nextforge test; failing invariant sequences are shrunk to fewer calls. On the JavaScript side this used to mean wiring a library such asfast-checkinto mocha; Hardhat 3 now runs the same kind of Solidity fuzz and invariant tests. -
In-EVM cheatcodes. Hardhat's
evm_snapshot/evm_increaseTimeare JSON-RPC methods that ask a node to change its state. Foundry'svm.warp/vm.deal/vm.prankare Solidity calls to the cheatcode address0x7109709ECfa91a80626fF3989D68f67F5b1DD12D(VM_ADDRESSin forge-std'sBase.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 targetsCHEATCODE_ADDRESSand, 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 ofcargo test. - Lesson 2 — Fuzz testing (
testFuzz_*, run byforge test), Solidity'sproptest!. Input filtering withvm.assume/bound, reproducing failures. Cross-references the liquidation course'swithdraw_shortfalllesson. - Lesson 3 — Invariant testing — multi-call tests with
Handlercontracts andtargetContract. 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 viaethereum.reth.rs/rpc. - Lesson 5 —
anvil --fork-url+ cheatcodes — state-aware testing withvm.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
InsuranceFundfrom Rust to Solidity, write the conservation-law invariants from the liquidation course'swithdraw_shortfalllesson in Foundry, run 10K iterations, prove the same theorem mechanically in two languages. Answer-key contract + tests sit in-repo atexamples/foundry-capstone/.
What's NOT in this course
- Gas optimization deep dive —
forge inspectfor 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 scriptfor deployments) — covered briefly in Lesson 4'scast sendsection. 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
functiondefinition and understandmappingvsstruct). - Running
cargo testagainst a Rust crate (the openhl build courses use this pattern throughout). - Reading the openhl liquidation course at least through the lesson where the first
WithdrawOutcomeproptest appears (withdraw_shortfall). Lesson 6's capstone assumes you've internalized theInsuranceFundsemantics 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_shortfalllesson. Setup:foundryupinstalled before Lesson 1.