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.

Three tool icons on a workbench, a hammer, a fishing line and an anvil, each connected to a small blockchain icon.
Figure 7.1 — forge, cast and anvil.

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:8545

Fork 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.