Foundry for beginners - compiling, tests, fuzzing and a local network
A practical introduction to Foundry: installation, project structure, unit tests, vm cheatcodes, fuzzing and invariants, forge, cast and anvil, with an example.
In this article
Foundry is a toolkit for developing smart contracts in which tests are written in Solidity itself. It is fast and convenient, so it has become the standard in many teams. This article is a practical introduction: from installation to a fuzz test. The theory of testing is in the article testing smart contracts, the language in the Solidity tour.
What Foundry includes
| Tool | Purpose |
|---|---|
forge | build, tests, deployment, gas reports |
cast | commands for talking to a network from the terminal |
anvil | a local blockchain for development |
chisel | an interactive Solidity console |
Installation and a project
curl -L https://foundry.paradigm.xyz | bash
foundryup
forge init escrow-demo
cd escrow-demoThe project structure:
src/ contract source code
test/ tests (files *.t.sol)
script/ deployment scripts
lib/ dependencies (forge-std and others)
foundry.toml settings: compiler version, optimizerAn example of settings close to what our contract uses:
[profile.default]
solc = "0.8.24"
evm_version = "paris"
optimizer = true
optimizer_runs = 200
bytecode_hash = "none"These parameters make the build reproducible - important for verifying a contract.
A contract for the example
Let us take a simplified version of the escrow's share calculation (the math explained):
// src/Splitter.sol
library Splitter {
uint256 internal constant BPS = 10_000;
uint256 internal constant PLATFORM_FEE_BPS = 100; // 1%
function split(uint256 amount, uint256 arbiterFeeBps, uint256 buyerShareBps)
internal
pure
returns (uint256 buyer, uint256 seller, uint256 arbiter, uint256 platform)
{
platform = (amount * PLATFORM_FEE_BPS) / BPS;
uint256 net = amount - platform;
arbiter = (net * arbiterFeeBps) / BPS;
uint256 pot = net - arbiter;
buyer = (pot * buyerShareBps) / BPS;
seller = pot - buyer;
}
}The first test
// test/Splitter.t.sol
import {Test} from "forge-std/Test.sol";
import {Splitter} from "../src/Splitter.sol";
contract SplitterTest is Test {
function test_completedDeal() public {
// 1000 USDC (6 decimals), guarantor 2%, everything to the seller
(uint256 b, uint256 s, uint256 a, uint256 p) = Splitter.split(1_000_000_000, 200, 0);
assertEq(p, 10_000_000); // 10 USDC to the platform
assertEq(a, 19_800_000); // 19.80 to the guarantor
assertEq(b, 0);
assertEq(s, 970_200_000); // 970.20 to the seller
}
}Running:
forge test -vvvThe -v flags show details: the more v, the more detail (logs, traces).
Fuzzing: thousands of random inputs
Instead of checking a few numbers, let us state a property: the sum of the parts always equals the whole.
function testFuzz_partsEqualWhole(uint96 amount, uint16 arbBps, uint16 buyerBps) public {
arbBps = uint16(bound(arbBps, 0, 3000)); // limit to sensible values
buyerBps = uint16(bound(buyerBps, 0, 10_000));
(uint256 b, uint256 s, uint256 a, uint256 p) = Splitter.split(amount, arbBps, buyerBps);
assertEq(b + s + a + p, amount);
}Foundry will substitute hundreds of random values (256 by default) and on failure show a minimal counterexample. bound neatly brings a value into the needed range, and vm.assume(cond) discards unsuitable ones.
Such tests catch what you did not think of: an amount of 0, the uint96 maximum, boundary shares.
The vm cheatcodes
A test contract inherits Test and so gets a vm object with "cheats" that control the environment.
| Cheat | What it does |
|---|---|
vm.prank(addr) | the next call runs as addr |
vm.startPrank(addr) / vm.stopPrank() | the same for a series of calls |
vm.expectRevert(selector) | the next call must revert with this error |
vm.warp(time) | sets block.timestamp |
vm.roll(block) | sets the block number |
deal(addr, amount) | gives ETH to an address (a token variant exists) |
vm.expectEmit(...) | checks an event |
makeAddr("alice") | creates a test address |
Example checks for the escrow
function test_strangerCannotConfirm() public {
uint256 id = _createActiveDeal();
vm.prank(makeAddr("stranger"));
vm.expectRevert(Escrow.NotAuthorized.selector);
escrow.confirmDelivery(id);
}
function test_arbiterTimeoutSplitsEvenly() public {
uint256 id = _createDisputedDeal();
vm.warp(block.timestamp + 30 days + 1);
vm.prank(buyer);
escrow.claimArbiterTimeout(id);
// check: guarantor 0, buyer and seller equal
}This is how the rules from access control and the deal lifecycle are verified: who may act and when, and what happens on timeout.
Invariants
An invariant test checks a property that must hold after any sequence of calls. Foundry randomly calls the contract's functions and after every step checks the condition:
function invariant_solvency() public view {
assertGe(token.balanceOf(address(escrow)), escrow.reserved(address(token)));
}This is exactly the solvency invariant: the contract's balance is no less than its obligations (explanation).
Gas and coverage
forge test --gas-report # a table of gas use per function
forge snapshot # saves reference gas values
forge coverage # which lines are covered by testsThe results help to optimise gas and to see untested branches of code.
A local network and cast
anvil # a blockchain on localhost:8545, chain id 31337
cast sig "transfer(address,uint256)" # 0xa9059cbb - the selector
cast keccak "hello" # the keccak256 hash
cast call <address> "balanceOf(address)(uint256)" <wallet> --rpc-url http://127.0.0.1:8545anvil creates test accounts with ETH, on which it is convenient to rehearse the whole deal flow locally without spending real money. Selectors and hashes are the subject of the articles on ABI and hash functions.
Forking a real network
Tests can run on a copy of a real network (state is only read, nothing is sent):
forge test --fork-url $RPC_URLThis is how interaction with real tokens like USDC is checked - for example how they behave on address blocking.
Tips
- Test not only the "happy path" but also failures and boundary values.
- Write a fuzz test for every arithmetic function.
- One test, one idea, clear names:
test_..._reverts_when_.... - Run tests in CI on every change.
- Tests do not replace an audit but complement it.
What to read next
Learn how independent experts examine contracts: audits and bug bounties. Or how to cut gas use: gas optimization.
Keep reading
- 3 · Under the hood
Testing smart contracts: unit, fuzz, invariants and static analysis
How contracts holding money are verified: unit tests, fuzzing, solvency invariants, evil token mocks, Slither and Mythril - with an escrow built in Foundry.
5 min read - 3 · Under the hood
A tour of the Solidity language - types, functions, errors and events by example
A short practical tour of Solidity for those who can read code: contract structure, types, visibility, modifiers, errors and events. Examples from the escrow.
4 min read
Mentioned in
- Glossary - blockchain, wallets, smart contracts and escrow from A to Z
- A tour of the Solidity language - types, functions, errors and events by example
- ABI: how a function call turns into bytes - selectors, calldata and events
- ERC-20 from scratch - writing a token in Solidity and analysing every line
- Integer math and rounding in smart contracts
- Storage layout and variable packing in Solidity
- Gas optimization in Solidity - 12 techniques and when they apply
- Account abstraction (ERC-4337) - smart wallets, gas sponsorship and access recovery
- Testing smart contracts: unit, fuzz, invariants and static analysis
- Audits and bug bounties - how smart contracts are examined and how to read a report
Ready to try it?
Create your first deal in a couple of minutes. The money is locked in a smart contract and paid out by rules you can see in advance.

