Lesson 19 / 25
Foundry: forge, cast and anvil
Build, test and script contracts with Foundry.
A fast, Solidity-native toolkit
Foundry is a popular Rust-based toolkit for Solidity development. forge builds, tests and deploys: tests are written in Solidity in test/, in contracts inheriting forge-std/Test.sol, with functions starting test (and testFuzz style parameters for fuzzing). Cheatcodes on the vm object control the environment: vm.prank(addr) sets msg.sender for the next call, vm.deal sets balances, vm.warp changes block.timestamp, vm.roll changes block numbers, vm.expectRevert and vm.expectEmit assert reverts and events, and makeAddr creates labelled addresses. forge test -vvvv shows detailed traces, forge coverage reports coverage and forge snapshot tracks gas. Mainnet forking (--fork-url) runs tests against real deployed contracts and state. cast is a command-line Swiss army knife for calling contracts, sending transactions, decoding calldata and converting units. anvil runs a local Ethereum node with funded test accounts. Solidity scripts (forge script) deploy contracts reproducibly. Dependencies are installed as git submodules or via Soldeer, and import remappings map paths such as @openzeppelin/.
The Foundry toolkit
forge builds and tests, cast talks to chains, anvil runs a local node.
Foundry workflow and a unit test with cheatcodes
Time travel, pranks and event assertions.
// test/PiggyBank.t.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {Test} from "forge-std/Test.sol";
import {PiggyBank} from "../src/PiggyBank.sol";
contract PiggyBankTest is Test {
PiggyBank bank;
address owner = makeAddr("owner");
address saver = makeAddr("saver");
event Deposited(address indexed from, uint256 amount);
function setUp() public {
vm.prank(owner);
bank = new PiggyBank(7 days);
vm.deal(saver, 5 ether);
}
function test_DepositEmitsEvent() public {
vm.expectEmit(true, false, false, true);
emit Deposited(saver, 1 ether);
vm.prank(saver);
bank.deposit{value: 1 ether}();
}
function test_CannotWithdrawBeforeUnlock() public {
uint256 unlock = bank.unlockTime(); // read before prank/expectRevert
vm.prank(owner);
vm.expectRevert(abi.encodeWithSelector(PiggyBank.Locked.selector, unlock));
bank.withdrawAll();
}
function test_WithdrawAfterUnlock() public {
vm.prank(saver);
bank.deposit{value: 2 ether}();
vm.warp(block.timestamp + 7 days); // time travel
vm.prank(owner);
bank.withdrawAll();
assertEq(owner.balance, 2 ether);
}
}
// Shell:
// forge init piggy && cd piggy
// forge test -vvv
// anvil # local node with funded accounts
// cast balance <address> --rpc-url http://127.0.0.1:8545Fork tests for integrations
If your contract talks to a real DEX, oracle or token, fork mainnet or a testnet in your tests. Mocks rarely reproduce the quirks of the real contracts you integrate with.
Quick check: What does vm.prank(addr) do in a Foundry test?
- Makes the next call come from addr, setting msg.sender
- Deletes an address
- Gives addr ether
- Advances time
Answer
Makes the next call come from addr, setting msg.sender — prank changes msg.sender for the next external call.