Skip to main content

Fuzzing Template

A practical template for fuzzing smart contracts using Foundry.

Overview

Fuzzing (or property-based testing) automatically generates random inputs to test your contracts against invariants.

Project Structure

fuzzing-template/
├── src/
│ ├── Token.sol
│ └── Vault.sol
├── test/
│ ├── Token.t.sol
│ ├── TokenFuzz.t.sol
│ └── VaultInvariant.t.sol
└── foundry.toml

Configuration

foundry.toml

[profile.default]
src = "src"
out = "out"
libs = ["lib"]

[fuzz]
runs = 1000
max_test_rejects = 100000

[invariant]
runs = 256
depth = 15
fail_on_revert = true

Fuzzing Tests

Basic Fuzzing

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "forge-std/Test.sol";
import "../src/Token.sol";

contract TokenFuzzTest is Test {
Token token;

function setUp() public {
token = new Token("Test", "TST", 1000000);
}

/// forge-config: default.fuzz.runs = 10000
function testFuzzTransfer(address to, uint256 amount) public {
// Assume valid inputs
vm.assume(to != address(0));
vm.assume(to != address(this));
vm.assume(amount <= token.balanceOf(address(this)));

uint256 balanceBefore = token.balanceOf(address(this));

token.transfer(to, amount);

// Invariants
assertEq(
token.balanceOf(address(this)),
balanceBefore - amount
);
assertEq(token.balanceOf(to), amount);
}
}

Invariant Testing

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "forge-std/Test.sol";
import "../src/Vault.sol";

contract VaultHandler is Test {
Vault public vault;
address[] public actors;

constructor(Vault _vault) {
vault = _vault;
}

function deposit(uint256 actorIndex, uint256 amount) public {
actorIndex = bound(actorIndex, 0, actors.length - 1);
amount = bound(amount, 0, 1000 ether);

address actor = actors[actorIndex];
vm.deal(actor, amount);

vm.prank(actor);
vault.deposit{value: amount}();
}

function withdraw(uint256 actorIndex, uint256 amount) public {
actorIndex = bound(actorIndex, 0, actors.length - 1);
address actor = actors[actorIndex];

uint256 balance = vault.balanceOf(actor);
amount = bound(amount, 0, balance);

vm.prank(actor);
vault.withdraw(amount);
}
}

contract VaultInvariantTest is Test {
Vault vault;
VaultHandler handler;

function setUp() public {
vault = new Vault();
handler = new VaultHandler(vault);

// Add actors
handler.actors().push(address(0x1));
handler.actors().push(address(0x2));
handler.actors().push(address(0x3));

// Target handler for invariant testing
targetContract(address(handler));
}

/// forge-config: default.invariant.runs = 256
/// forge-config: default.invariant.depth = 15
function invariant_totalSupplyEqualsBalance() public {
assertEq(
vault.totalSupply(),
address(vault).balance
);
}

function invariant_userBalancesLessThanTotal() public {
for (uint i = 0; i < handler.actors().length; i++) {
address actor = handler.actors(i);
assertLe(
vault.balanceOf(actor),
vault.totalSupply()
);
}
}
}

Best Practices

1. Use bound() for Range Constraints

function testFuzz(uint256 x) public {
x = bound(x, 1, 100); // Constrain to [1, 100]
// ...
}

2. Use vm.assume() for Preconditions

function testFuzz(address user) public {
vm.assume(user != address(0));
vm.assume(user.code.length == 0);
// ...
}

3. Create Handler Contracts

Handler contracts wrap your protocol and provide bounded random actions for invariant testing.

4. Define Clear Invariants

Good invariants:

  • Total supply equals sum of balances
  • Contract balance is greater than or equal to total deposits
  • User balance is less than or equal to total supply

5. Use Ghost Variables

Track additional state in your handler:

contract Handler {
uint256 public ghost_depositSum;
uint256 public ghost_withdrawSum;

function deposit(uint256 amount) public {
// ...
ghost_depositSum += amount;
}
}

Running Fuzzing Tests

# Run fuzzing tests
forge test --match-contract Fuzz

# Run invariant tests
forge test --match-contract Invariant

# Verbose output
forge test --match-contract Invariant -vvvv

# With gas reporting
forge test --match-contract Fuzz --gas-report

Interpreting Results

Success

Test result: ok. 1000 passed

Failure

Failing test:
testFuzzTransfer(address,uint256)

Counterexample:
to=0x0000000000000000000000000000000000000000
amount=1

Advanced Techniques

Dictionary-based Fuzzing

address[] public dictionary = [
address(0),
address(this),
address(token)
];

function testFuzz(uint256 index) public {
address target = dictionary[bound(index, 0, dictionary.length - 1)];
// ...
}

Stateful Fuzzing

Maintain state across fuzz runs to test complex scenarios.

Resources

📢 Share this article