Tests
Smart-contract bugs are immutable once deployed. The defence: test everything — unit tests, fuzz tests, fork tests against mainnet state, invariant tests that try thousands of input sequences. Foundry is the modern toolchain; Hardhat is the older alternative. Test until the tests are boring, then add one more class of test.
Foundry testing patterns: unit, fuzz, invariant, fork
EXAMPLE
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
// ============================================================
// Contract under test
// ============================================================
contract Token {
string public name = 'Demo';
uint256 public totalSupply;
mapping(address => uint256) public balanceOf;
event Transfer(address indexed from, address indexed to, uint256 value);
constructor(uint256 supply) {
totalSupply = supply;
balanceOf[msg.sender] = supply;
emit Transfer(address(0), msg.sender, supply);
}
function transfer(address to, uint256 amount) external returns (bool) {
require(balanceOf[msg.sender] >= amount, 'insufficient');
unchecked { balanceOf[msg.sender] -= amount; }
balanceOf[to] += amount;
emit Transfer(msg.sender, to, amount);
return true;
}
}
// ============================================================
// 1) Unit tests
// ============================================================
import 'forge-std/Test.sol';
contract TokenTest is Test {
Token token;
address alice = makeAddr('alice');
address bob = makeAddr('bob');
function setUp() public {
token = new Token(1_000 ether);
}
function test_initialSupply() public {
assertEq(token.balanceOf(address(this)), 1_000 ether);
}
function test_transferEmitsEvent() public {
vm.expectEmit(true, true, false, true);
emit Token.Transfer(address(this), alice, 10 ether);
token.transfer(alice, 10 ether);
}
function test_RevertWhen_Insufficient() public {
vm.prank(alice);
vm.expectRevert('insufficient');
token.transfer(bob, 1 ether);
}
}
// ============================================================
// 2) Fuzz tests — Foundry generates random inputs
// ============================================================
contract TokenFuzzTest is Test {
Token token;
function setUp() public { token = new Token(1_000 ether); }
function testFuzz_transferConservesSupply(uint96 amount) public {
vm.assume(amount <= 1_000 ether);
address to = makeAddr('to');
token.transfer(to, amount);
assertEq(token.balanceOf(address(this)) + token.balanceOf(to), 1_000 ether);
}
}
// ============================================================
// 3) Invariant tests — random sequences of arbitrary actions
// ============================================================
import 'forge-std/StdInvariant.sol';
contract Handler is Test {
Token public token;
address[] public actors;
constructor(Token t) {
token = t;
for (uint160 i = 1; i <= 5; i++) actors.push(address(i));
}
function transfer(uint256 actorSeed, uint256 toSeed, uint256 amount) external {
address from = actors[actorSeed % actors.length];
address to = actors[toSeed % actors.length];
amount = bound(amount, 0, token.balanceOf(from));
vm.prank(from);
token.transfer(to, amount);
}
}
contract TokenInvariantTest is StdInvariant, Test {
Token token; Handler handler;
function setUp() public {
token = new Token(1_000 ether);
handler = new Handler(token);
targetContract(address(handler));
token.transfer(address(handler), 1_000 ether); // seed
}
// Foundry runs thousands of random handler call sequences and checks the invariant.
function invariant_supplyConserved() public {
uint256 sum;
for (uint160 i = 1; i <= 5; i++) sum += token.balanceOf(address(i));
assertEq(sum, 1_000 ether);
}
}
// ============================================================
// 4) Fork tests — real mainnet state as your test fixture
// ============================================================
contract ForkTest is Test {
address constant USDC = 0xA0b86991c6218B36c1d19D4a2e9Eb0cE3606eB48;
address constant whale = 0x55FE002aefF02F77364de339a1292923A15844B8;
function setUp() public {
// Set FOUNDRY_ETH_RPC_URL or pass --fork-url
vm.createSelectFork(vm.rpcUrl('mainnet'), 19_500_000);
}
function test_whaleHasUSDC() public {
IERC20 usdc = IERC20(USDC);
assertGt(usdc.balanceOf(whale), 0);
}
}
interface IERC20 {
function balanceOf(address) external view returns (uint256);
function transfer(address, uint256) external returns (bool);
}
// ============================================================
// 5) Gas snapshots — track perf regressions
// ============================================================
// forge snapshot writes .gas-snapshot
// forge snapshot --check fails CI if gas regresses
// ============================================================
// Run
// forge test # all tests
// forge test --match-test test_initialSupply -vvv
// forge test --gas-report
// forge coverage
// ============================================================
Why it matters
Invariant testing is the headline feature of Foundry. Define a few invariants ("supply is conserved", "no negative balance"), give it a handler that exercises every public function, and Foundry generates thousands of action sequences that try to break the invariant. The bugs it finds are exactly the kind that would have shipped to mainnet.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
// Foundry test — Solidity native
import { Test } from "forge-std/Test.sol";
import { MyToken } from "src/MyToken.sol";
contract MyTokenTest is Test {
MyToken token;
function setUp() public { token = new MyToken(); }
function testTotalSupply() public view {
assertEq(token.totalSupply(), 1_000_000 ether);
}
}
Try it Yourself »
Exercise
Foundry CLI to run the tests.
forge
Four letters.
Discussion
Loading…