ChainScore Labs
All Guides

Fuzz Testing Smart Contracts with Foundry

LABS

Fuzz Testing Smart Contracts with Foundry

Chainscore © 2025

Core Concepts of Fuzz Testing

Foundational principles of automated property-based testing for smart contract security.

Property-Based Testing

Property-based testing defines invariant properties a contract must always uphold, rather than testing specific inputs. The fuzzer generates random data to try and violate these properties.

  • Properties are assertions like "token supply never decreases" or "user balance never exceeds total supply".
  • Foundry's forge test runs these property checks across thousands of random scenarios.
  • This matters because it systematically uncovers edge cases that manual example-based testing misses.

Invariant Assertions

Invariant assertions are the core predicates written in Solidity that the fuzzer aims to disprove. They represent the system's fundamental safety rules.

  • Common invariants include access control checks, state machine validity, and financial integrity (e.g., totalSupply == sum(balances)).
  • In Foundry, these are defined in a test contract using assert or custom error reverts.
  • This matters as a formal, executable specification of correct contract behavior under any condition.

Fuzz Input Generation

Fuzz input generation is the process of automatically creating random function arguments and state sequences to test against invariants.

  • Foundry's fuzzer can generate random addresses, integers, bytes, and arrays within configurable bounds.
  • It uses strategies like edge-case biasing (zero, max values) and corpus collection from previous runs.
  • This matters because it efficiently explores the vast input space to find breaking sequences developers wouldn't consider.

Minimization & Reproduction

Minimization is the process of reducing a failing test case to its simplest form. Reproduction allows deterministic re-running of that failure.

  • When Foundry finds a counterexample, it shrinks the input to a minimal sequence that still breaks the invariant.
  • The test runner outputs a seed and calldata to replay the exact failure via forge test --match-test --seed.
  • This matters for efficient debugging, providing developers with a clear, actionable failing case.

Stateful Fuzzing

Stateful fuzzing tests sequences of contract interactions, not just single function calls. It simulates multi-transaction user behavior.

  • The fuzzer generates random sequences of calls (e.g., deposit, transfer, withdraw) to a test contract's helper functions.
  • Invariants are checked between each step in the sequence.
  • This matters for finding complex vulnerabilities that only emerge from specific interaction patterns over time.

Differential Fuzzing

Differential fuzzing compares the behavior of two implementations given the same random inputs to detect discrepancies.

  • A common use case is fuzzing a new contract upgrade against the legacy version to ensure identical logic.
  • Another is testing a custom implementation against a known-correct reference standard (e.g., an ERC20).
  • This matters for ensuring functional equivalence and catching subtle logic bugs in complex mathematical operations.

Setting Up Foundry for Fuzz Testing

Process overview

1

Install Foundry and Initialize a Project

Set up the Foundry development environment and create a new project structure.

Detailed Instructions

Begin by installing the Foundry toolkit using the foundryup installer. This provides the forge, cast, anvil, and chisel command-line tools. Open a terminal and run the installation command. Once installed, create a new directory for your project and initialize it with forge init. This command generates the standard Foundry project structure, including src/ for your contracts, test/ for your test files, script/ for deployment scripts, and a foundry.toml configuration file.

  • Sub-step 1: Run curl -L https://foundry.paradigm.xyz | bash and then foundryup.
  • Sub-step 2: Create a project directory: mkdir my-fuzz-project && cd my-fuzz-project.
  • Sub-step 3: Initialize the project: forge init --force to generate all necessary folders and files.
bash
# Example installation and initialization commands curl -L https://foundry.paradigm.xyz | bash foundryup forge init my-fuzz-project

Tip: Use the --force flag with forge init if the directory is not empty to overwrite existing configuration files.

2

Configure the Foundry.toml File for Fuzzing

Optimize the project configuration for efficient and effective fuzz testing.

Detailed Instructions

The foundry.toml file controls the behavior of the forge test runner. For fuzzing, you must adjust key parameters under the [fuzz] and [profile.default] sections. The fuzz runs setting defines how many random inputs are generated for each test. The fuzz seed allows for deterministic test reproduction. Increase the fuzz max test rejections to handle complex preconditions. Under the default profile, you can set the gas limit and enable FFI if your tests require external calls.

  • Sub-step 1: Open foundry.toml in your project root.
  • Sub-step 2: Add or modify the [fuzz] section, setting runs = 10_000 and seed = "12345".
  • Sub-step 3: In [profile.default], ensure gas_limit = 30000000 and ffi = true if needed.
toml
# Example foundry.toml configuration [fuzz] runs = 10000 seed = "12345" max_test_rejections = 65536 [profile.default] gas_limit = 30000000 ffi = true

Tip: A higher runs value increases coverage but slows test execution. Start with 10,000 for a balance.

3

Write a Basic Smart Contract with Invariants

Create a contract with clear internal state rules that can be tested via fuzzing.

Detailed Instructions

Fuzz tests validate invariants—properties that should always hold true. Write a simple contract, such as a token or vault, that maintains explicit invariants. For a Token contract, key invariants include: total supply consistency, non-negative balances, and allowance sums. Use the invariant_ prefix in your test function names later to target these properties. The contract should have functions that modify state (like transfer or mint) which the fuzzer will call with random inputs to try and break the defined rules.

  • Sub-step 1: In src/, create Token.sol with totalSupply, balanceOf mapping, and a transfer function.
  • Sub-step 2: Implement logic that maintains the invariant totalSupply == sum(balances).
  • Sub-step 3: Add a mint function that increases both a user's balance and the total supply.
solidity
// Example invariant-heavy contract snippet contract Token { mapping(address => uint256) public balanceOf; uint256 public totalSupply; function mint(address to, uint256 amount) external { balanceOf[to] += amount; totalSupply += amount; // Invariant: totalSupply must always equal sum of balances } }

Tip: Design invariants that are simple, atomic, and fundamental to the contract's correctness.

4

Create and Run Invariant Test Contracts

Implement test contracts that define system invariants and execute the fuzzer.

Detailed Instructions

In the test/ directory, create a test contract that extends forge-std's Test contract. Define a setUp() function to deploy your target contract and initialize a handler contract that will be called by the fuzzer. Write functions prefixed with invariant_ that assert a condition which must never be false, such as invariant_totalSupplyConsistency(). The fuzzer will randomly call functions on the handler, which in turn call the target contract, attempting to violate the invariant. Use forge test --match-contract to run specific invariant tests.

  • Sub-step 1: Create test/Invariant.t.sol and import forge-std/Test.sol.
  • Sub-step 2: In setUp(), deploy the Token contract and a Handler contract.
  • Sub-step 3: Write function invariant_totalSupplyEqualsSumBalances() public that asserts the condition.
solidity
// Example invariant test structure import "forge-std/Test.sol"; import "../src/Token.sol"; contract InvariantTest is Test { Token token; function setUp() public { token = new Token(); } function invariant_totalSupplyConsistency() public view { // This property should never be false during fuzzing assert(token.totalSupply() >= 0); } }

Tip: Run your invariant tests with forge test --match-test invariant to execute only the fuzzing suite.

5

Analyze Fuzzing Results and Interpret Counterexamples

Examine test output to identify broken invariants and understand the failing sequence.

Detailed Instructions

When an invariant test fails, Foundry provides a detailed counterexample. This includes the seed used, the sequence of function calls that broke the invariant, and the state of all variables. Analyze the call trace to understand the exact path that led to the violation. The output will show the specific arguments passed to each function. Use this information to debug your contract logic or refine your invariant definitions. You can replay the exact failing sequence using the provided seed by running forge test --match-contract YourTest --ffi --seed <seed_value>.

  • Sub-step 1: Run forge test --match-contract InvariantTest -vvv for verbose output on failure.
  • Sub-step 2: Locate the Failing seed and sequence of Calls in the console output.
  • Sub-step 3: Reproduce the failure deterministically using the command forge test --seed 12345.
bash
# Example command to run tests and see detailed output on failure forge test --match-contract InvariantTest -vvv

Tip: The -vvv flag provides the most verbose output, showing the exact call sequence and intermediate state changes.

Types of Fuzz Tests and Invariants

Understanding Fuzzing and Invariants

Fuzz testing is an automated technique that feeds random, unexpected, or invalid data ("fuzz") into a program to find vulnerabilities. In smart contracts, this means generating random transaction sequences and function inputs. An invariant is a logical property of your system that should always hold true, no matter what actions users take. For example, in a lending protocol like Aave, the total amount of borrowed assets should never exceed the total supplied assets.

Key Concepts

  • Stateful Fuzzing: Tests sequences of interactions (e.g., deposit, borrow, repay) to find bugs in complex state transitions.
  • Stateless Fuzzing: Tests individual functions in isolation with random inputs.
  • Property-Based Testing: You define the invariant property, and the fuzzer tries to break it by generating random scenarios.

Simple Example

In a basic ERC-20 token contract, a core invariant is that the sum of all user balances must equal the totalSupply(). A fuzz test would randomly mint, transfer, and burn tokens, checking this sum after every operation.

Advanced Fuzzing Patterns and Configuration

Process overview

1

Configure Foundry.toml for Targeted Fuzzing

Set up project-level configuration to direct fuzzer effort.

Detailed Instructions

Adjust the [fuzz] section in your foundry.toml file to optimize the fuzzing campaign. The fuzz dictionary and runs parameters are critical for coverage.

  • Sub-step 1: Set runs = 100000 for a more thorough exploration of the state space.
  • Sub-step 2: Enable dictionary_weight = 40 to bias the fuzzer towards using known values from your contract's storage and constants.
  • Sub-step 3: Configure include_storage = true and include_push_bytes = true to automatically populate the dictionary with on-chain data.
toml
[fuzz] runs = 100000 dictionary_weight = 40 include_storage = true include_push_bytes = true

Tip: For invariant tests, consider setting a lower runs value (e.g., 5000) for faster iteration, then increase it for final validation.

2

Implement Custom Fuzz Inputs with `targetSelector`

Guide the fuzzer to specific functions using targeted selectors.

Detailed Instructions

Use the targetSelector cheatcode to focus fuzzing on a subset of your contract's functions. This is essential for differential fuzzing or testing complex interactions where random entry points are inefficient.

  • Sub-step 1: In your test's setUp() function, call vm.targetSelector(bytes4 selector).
  • Sub-step 2: Calculate the function selector, e.g., bytes4(keccak256("swapExactTokensForTokens(uint256,uint256,address[],address,uint256)")).
  • Sub-step 3: The fuzzer will now primarily call functions matching this selector, generating inputs for their specific parameters.
solidity
function setUp() public { // Focus fuzzing only on the 'swap' function bytes4 swapSelector = bytes4(keccak256("swapExactTokensForTokens(uint256,uint256,address[],address,uint256)")); vm.targetSelector(swapSelector); }

Tip: You can call targetSelector multiple times to build a set of allowed functions, or use vm.targetContract(address) to focus on a specific contract in a multi-contract setup.

3

Apply Differential Fuzzing Against a Reference Implementation

Test a new implementation against a known-good contract.

Detailed Instructions

Differential fuzzing compares the outputs of two systems given the same inputs. Deploy both your new Vault and a reference VaultReference contract.

  • Sub-step 1: In the test's setUp(), deploy both contract implementations.
  • Sub-step 2: In a forge test function with the fuzz modifier, perform the same state-changing operation on both contracts (e.g., deposit(amount)).
  • Sub-step 3: Assert that key state variables (like user balance or total supply) match between the two contracts after the operation.
solidity
function testFuzz_depositMatchesReference(uint96 amount) public { // Act on both systems vault.deposit(amount); referenceVault.deposit(amount); // Assert state equivalence assertEq(vault.balanceOf(address(this)), referenceVault.balanceOf(address(this))); assertEq(vault.totalSupply(), referenceVault.totalSupply()); }

Tip: Use vm.assume() to constrain fuzz inputs (e.g., vm.assume(amount <= type(uint96).max)) to avoid trivial reverts and focus on meaningful test cases.

4

Use `vm.skip()` to Prune Uninteresting Test Paths

Improve fuzzing efficiency by skipping invalid input ranges.

Detailed Instructions

The vm.skip(bool condition) cheatcode allows you to halt execution of the current fuzz run if a condition is met, saving time. This is useful for input validation pre-checks.

  • Sub-step 1: At the start of your fuzz test, identify inputs that would cause the contract to revert immediately with a custom error or require.
  • Sub-step 2: Use vm.skip() with the condition that triggers this revert. For example, skip if a fuzzed address is the zero address.
  • Sub-step 3: The fuzzer will discard this run and generate a new one, focusing computational effort on valid, interesting scenarios.
solidity
function testFuzz_transferToNonZeroAddress(address to, uint256 amount) public { // Skip runs where 'to' is address(0), as transfer will revert vm.skip(to == address(0)); // Also skip if the amount exceeds the sender's balance for a cleaner test vm.skip(amount > balanceOf[sender]); // Proceed with the core test logic... bool success = token.transferFrom(sender, to, amount); assertTrue(success); }

Tip: Combine vm.skip() with vm.assume() for complex preconditions; assume reverts and consumes gas, while skip is cheaper and simply aborts the run.

5

Analyze and Interpret Fuzzing Campaign Reports

Extract insights from Foundry's fuzzer output to improve tests.

Detailed Instructions

After running forge test --match-contract FuzzTest --fuzz-runs 50000, analyze the console output. Key metrics include unique reverts, total counterexamples, and code coverage.

  • Sub-step 1: Look for the "Fuzzing" section in the output. Note the number of runs completed and any failing counterexample with calldata.
  • Sub-step 2: Run forge coverage --report debug to generate a coverage report. Identify lines or branches in your src/ that were never executed during fuzzing.
  • Sub-step 3: For each unique revert reason (e.g., "InsufficientBalance"), determine if it represents a legitimate bug or an expected guard. If expected, consider using vm.expectRevert() to make the test more precise.
bash
# Example forge test output snippet [PASS] testFuzz_withdraw(uint256) (runs: 50000, μ: 92506, ~: 102831) [FAIL. Reason: InsufficientBalance] testFuzz_deposit(uint256) (runs: 50000, μ: 92506, ~: 102831) Counterexample: calldata=0x...0000000000000000000000000000000000000000000000000000000000000000 (args: [0])

Tip: A high number of unique reverts may indicate your fuzz inputs are too unconstrained. Use vm.assume or vm.skip to refine the input domain.

Fuzzing vs. Unit Testing and Formal Verification

A comparison of core testing methodologies for smart contract security and correctness.

MethodologyUnit TestingFuzz TestingFormal Verification

Primary Goal

Verify specific, predetermined logic paths.

Discover edge cases and invalid inputs via random data.

Mathematically prove correctness against a specification.

Input Generation

Manual, developer-defined test cases.

Automated, pseudo-random generation (e.g., Foundry fuzzer).

Symbolic execution exploring all possible program states.

Coverage Type

Statement and branch coverage for known paths.

High-volume input space exploration for unknown paths.

Complete logical path coverage for specified properties.

Execution Speed

Fast (milliseconds per test).

Moderate to Slow (seconds to minutes per fuzz run).

Very Slow (minutes to hours for complex proofs).

Bug Detection

Logic errors in expected scenarios.

Overflow/underflow, unexpected reverts, invariant violations.

Formal specification violations, logical contradictions.

Tool Example

Foundry's forge test, Hardhat, Truffle.

Foundry's invariant testing, Echidna, Harvey.

Certora Prover, SMTChecker, KEVM.

Resource Intensity

Low. Requires writing and maintaining test suites.

Medium. Requires defining invariants and configuring runs.

Very High. Requires deep expertise to write formal specs.

Best For

Functional correctness of core features.

Stress testing and uncovering hidden assumptions.

Mission-critical contracts where absolute guarantees are needed.

Debugging and Analyzing Fuzz Test Failures

Methodical process for diagnosing and understanding the root cause of invariant violations discovered during fuzzing.

1

Reproduce the Failing Test Case

Isolate and run the specific failing scenario to confirm the issue.

Detailed Instructions

When a fuzz test fails, Foundry outputs the specific calldata seed that triggered the failure. Use the -m flag with the test function name and the --ffi seed to reproduce it deterministically. First, run the test with forge test --match-test testFunctionName -vvv to see the detailed logs and the failing seed. Then, execute forge test --match-test testFunctionName --ffi <FAILING_SEED> to run only that specific scenario. This confirms the failure is reproducible and not a fluke. Check the console for the reverted reason and the state of storage variables printed in the trace.

Tip: The -vvvv flag provides a full stack trace, showing the exact line where the revert occurred within your contract or test.

2

Analyze the Execution Trace

Examine the detailed call and state trace to pinpoint the erroneous operation.

Detailed Instructions

Foundry's verbose traces are essential for debugging. After reproducing the failure, analyze the output with -vvv or higher. Focus on the sequence of contract calls and storage changes. Look for unexpected state transitions or violated pre-conditions. Key details include:

  • Call Sequence: Identify which external call or internal function triggered the revert.
  • Storage Logs: Check STORAGE changes for variables like balance, allowance, or custom mapping states. An unexpected final state indicates a logic flaw.
  • Event Logs: Missing or incorrect events can signal that a critical function path was not executed.

Cross-reference this trace against your invariant to understand which condition was broken.

3

Inspect the Fuzzer's Input Values

Understand the specific random inputs that led to the edge case failure.

Detailed Instructions

The failing seed corresponds to specific fuzz input values. Foundry logs these values (e.g., uint256 a, address sender). Decode these to understand the edge case. For example, a failure might occur when an input is zero, at a maximum uint256 value, or a specific address like the zero address. Write a simple test in your setUp() to log these values during the failing run. Analyze how these inputs flow through your contract's logic. Common issues include arithmetic overflow/underflow, incorrect access control for certain addresses, or logic that doesn't handle boundary values.

solidity
// Example of logging a fuzz input in a test function testFuzz(uint256 amount, address recipient) public { console.log("Fuzz Input - amount:", amount, "recipient:", recipient); // ... test logic }
4

Simplify and Isolate the Bug

Reduce the test case to a minimal reproducible example to clarify the core issue.

Detailed Instructions

Once you have the inputs and trace, create a new, focused test that isolates the bug. Comment out unrelated setup and assertions. Your goal is to write the smallest possible test that still fails. This often involves:

  • Hardcoding the failing fuzz inputs directly into a test function (not a testFuzz).
  • Reducing the number of interacting contracts or calls to the essential path.
  • Checking intermediate values with console.log to see where calculations diverge from expectations.

This process transforms a complex, randomly-generated failure into a clear, deterministic bug report. It separates the invariant violation from any noise in the broader test setup.

5

Formulate and Apply the Fix

Correct the underlying smart contract logic or adjust the invariant based on your analysis.

Detailed Instructions

Based on the isolated bug, decide whether the issue is in the contract implementation or the test invariant. If the contract is wrong, apply a targeted fix, such as adding a missing check for zero address, using SafeMath libraries, or correcting a state update. If the invariant is too strict (i.e., the contract behavior is correct but your test assumption was wrong), refine the invariant property. After making changes, re-run the specific failing fuzz case to verify the fix. Finally, run the full fuzz test suite (forge test) to ensure no regressions were introduced.

solidity
// Example fix: Adding a zero-address check function transfer(address to, uint256 amount) public { require(to != address(0), "Transfer to zero address"); // Fix applied balances[msg.sender] -= amount; balances[to] += amount; }
SECTION-FAQ

Frequently Asked Questions on Fuzz Testing

Ready to Start Building?

Let's bring your Web3 vision to life.

From concept to deployment, ChainScore helps you architect, build, and scale secure blockchain solutions.