Foundry
Foundry is the modern Rust-based Ethereum toolchain: `forge` for build and test, `cast` for chain interaction, `anvil` for a local node. Tests are written in Solidity — same language as your contracts — which removes a layer of impedance vs Hardhats TypeScript tests and makes fuzz/invariant testing first-class.
Forge tests, fuzz tests, and a cast call
EXAMPLE
// src/Token.sol — same contract as a Hardhat example, simpler to test in Solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
contract Token {
string public name = 'DemoToken';
uint256 public totalSupply;
mapping(address => uint256) public balanceOf;
event Transfer(address indexed from, address indexed to, uint256 value);
constructor(uint256 initialSupply) {
totalSupply = initialSupply;
balanceOf[msg.sender] = initialSupply;
emit Transfer(address(0), msg.sender, initialSupply);
}
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;
}
}
// test/Token.t.sol
pragma solidity ^0.8.24;
import 'forge-std/Test.sol';
import { Token } from '../src/Token.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_initialSupplyOnDeployer() public {
assertEq(token.balanceOf(address(this)), 1_000 ether);
}
function test_transferEmitsAndUpdates() public {
vm.expectEmit(true, true, false, true);
emit Token.Transfer(address(this), alice, 10 ether);
token.transfer(alice, 10 ether);
assertEq(token.balanceOf(alice), 10 ether);
}
// FUZZ TEST: 'amount' is generated for each run; vm.assume filters
// pathological cases. Forge runs hundreds of cases by default.
function testFuzz_transferConservesSupply(uint96 amount) public {
vm.assume(amount <= 1_000 ether);
token.transfer(alice, amount);
assertEq(
token.balanceOf(address(this)) + token.balanceOf(alice),
1_000 ether,
'supply must be conserved'
);
}
function test_RevertWhen_InsufficientBalance() public {
vm.prank(alice); // act as alice for this call
vm.expectRevert('insufficient');
token.transfer(bob, 1 ether);
}
}
// Run it
// forge test
// forge test --gas-report
// forge test --match-test testFuzz -vvv
// Local node + interact via cast
// anvil & # starts a local chain on :8545
// forge create src/Token.sol:Token --constructor-args 1000000000000000000000 \\
// --rpc-url http://localhost:8545 --private-key 0xac0974...
// cast call $ADDR 'balanceOf(address)(uint256)' $DEPLOYER --rpc-url http://localhost:8545
// cast send $ADDR 'transfer(address,uint256)' $ALICE 10000000000000000000 \\
// --private-key 0xac0974... --rpc-url http://localhost:8545
Why it matters
Foundrys fuzzer is the single biggest reason to learn it. testFuzz_* functions catch invariants you would never test by hand — supply conservation, monotonicity, decimal precision bugs — and they run in seconds. Add them to every contract that handles money.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
// foundry.toml [profile.default] src = 'src' out = 'out' libs = ['lib'] # Commands: # forge test -vvv # forge build # forge create --rpc-url $RPC --private-key $PK src/MyToken.sol:MyTokenTry it Yourself »
Discussion
Loading…