Skip to main content

Someone wanted to learn this too, so Grasp built them a personal learning path.

You can keep exploring this one or create your own

Fuzz Testing with Foundry

Hello! Welcome to your eighth lesson in the "Modern Development & Testing Toolchain" module.

Introduction

In our last few lessons, we've built a robust toolkit for writing targeted unit tests. We've covered the "happy path" for state changes, the "unhappy path" for reverts and errors, and finally, how to verify event emissions for off-chain communication. These tests are essential, but they share a common limitation: they only test the specific scenarios you can imagine and write by hand.

What about the edge cases you didn't think of? The "unknown unknowns" that can lead to critical vulnerabilities? This is where fuzz testing comes in. It's a powerful, automated technique for hardening your contracts against unexpected inputs.

Learning Outcome: By the end of this lesson, you will be able to implement fuzz testing with Foundry to discover edge cases in contract logic.

Today, we will explore:

  • The concept of fuzz testing and why it's a step up from traditional unit testing.
  • How to write and run stateless fuzz tests in Foundry.
  • How to constrain random inputs using vm.assume and bound for more efficient testing.
  • How to configure fuzz test runs.
  • The difference between stateless fuzzing and stateful fuzzing (invariant testing).

1. Beyond Unit Tests: The "Why" of Fuzzing

Fuzz testing, also known as property-based testing, flips the traditional testing model on its head. Instead of writing a test for a single input (e.g., assert(myFunction(5) == 10)), you define a property that should hold true for all valid inputs (e.g., assert(myFunction(x) > x)). The testing framework then automatically generates hundreds or thousands of random inputs (x) to try and find one that breaks your property.

This video from Patrick Collins provides an excellent conceptual introduction to fuzzing and why it's so critical for security.

Fuzz & Invariant Tests | The secret to finding CRITICAL vulnerabilities faster

This video explains the core idea behind fuzz testing and how it uncovers bugs that manual unit tests might miss.

Please watch from 00:30 to 02:09. Focus on: The definition of fuzzing as supplying random data to a system. The concept of an 'invariant' or a property that should always hold true. The example of how a simple unit test can pass, while a fuzz test would catch the hidden bug.

As the video highlights, it would be impossible to write a unit test for every possible uint256 input. Fuzzing programmatically searches the input space for you, making it an indispensable tool for finding edge cases related to arithmetic (overflows/underflows), boundary conditions, and complex logical branches.

2. Stateless Fuzz Testing with Foundry

Foundry has first-class support for fuzzing and makes it incredibly simple to get started. Any test function that accepts one or more parameters is automatically treated as a fuzz test. Foundry will generate random values for those parameters and run the test multiple times.

If a test run fails, Foundry stops and reports a counterexample: the specific set of inputs that caused the failure.

2.1. A Practical Example: mostSignificantBit

Let's consider a function that is non-trivial to test manually. The following video uses a mostSignificantBit function, which employs a binary search algorithm to find the position of the highest '1' in a number's binary representation. This complexity is a perfect candidate for fuzzing.

The video demonstrates a powerful pattern for property-based testing: comparing a complex, optimized implementation against a simple, slower, but known-correct reference implementation.

Fuzz | Testing with Foundry

This video from Smart Contract Programmer walks through creating a fuzz test for a complex function.

Watch from 00:39 to 05:12. Pay attention to: The mostSignificantBit contract being tested. The creation of a simple, known-correct _mostSignificantBit helper function inside the test contract. How the manual test is converted into a fuzz test simply by adding a uint256 x parameter to the test function. The property being tested: contract.mostSignificantBit(x) == helper.mostSignificantBit(x).

Here’s what the fuzz test from the video looks like:

// test/Fuzz.t.sol

// ... setup and helper function ...

function testFuzz_mostSignificantBit(uint256 x) public {
    // Call the contract's implementation (the one we are testing)
    uint256 result = bits.mostSignificantBit(x);

    // Call our simple, trusted reference implementation
    uint256 expected = _mostSignificantBit(x);

    // Assert that the property holds true: the results must be equal
    assertEq(result, expected);
}

When you run forge test, Foundry will execute this function with hundreds of random values for x. If it finds any x for which the optimized function gives a different result than the simple one, it will report a failure and the counterexample x.

2.2. Interpreting Fuzz Statistics

When a fuzz test passes, Foundry provides some useful statistics.

Example of fuzz test statistics in the terminal.

The video you just watched explains these stats.

Fuzz | Testing with Foundry

This final clip from the same video explains the statistics you see after a successful fuzz run.

Watch from 07:32 to 09:07. Understand the meaning of: runs: The number of different inputs tested. μ (mu): The mean (average) gas cost across all runs. ~ (tilde): The median gas cost across all runs.

3. Controlling the Fuzz: assume and bound

Throwing completely random data isn't always efficient. Sometimes, you need to guide the fuzzer by constraining the inputs to a meaningful range. Foundry provides two primary ways to do this.

Foundry Cheatcodes Part 6: Fuzzing, Forks, and Edge- ...

This article from Three Sigma's blog provides a concise explanation of how to narrow the conditions for your fuzz tests.

Read the section titled 'Narrowing conditions'. It introduces the vm.assume cheatcode and the bound function.

Let's see these in action with the Smart Contract Programmer video.

Fuzz | Testing with Foundry

This segment demonstrates how to use vm.assume and bound in a test.

Watch from 05:12 to 07:32. Note the difference in usage: vm.assume(condition): Skips the test run if the condition is false. x = bound(x, min, max): Modifies the input x to fit within the specified range.

Summary of assume vs. bound:

  • vm.assume(x > 0): Use this to filter out values. If the condition is false, Foundry discards the input and tries a new one. Be careful not to make your assumptions too restrictive, or the fuzzer might struggle to find valid inputs, a situation known as "max rejects."
  • x = bound(x, 1 ether, 100 ether): Use this to constrain values to a specific range. This is generally more efficient than using assume for ranges (e.g., vm.assume(x >= 1 ether && x <= 100 ether)), as it doesn't discard any runs. The bound function is part of Foundry's standard library, not a cheat code.

4. Configuring Fuzz Runs

By default, Foundry performs 256 runs for each fuzz test. You can easily change this to increase test coverage.

1. Command-Line Flag:
You can specify the number of runs for a single command using the --fuzz-runs flag.

# Run fuzz tests with 1000 inputs
forge test --fuzz-runs 1000

2. Configuration File (foundry.toml):
For a project-wide setting, it's better to add it to your foundry.toml file. This ensures your entire team and your CI pipeline use the same configuration.

# foundry.toml

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

# See more config options https://github.com/foundry-rs/foundry/tree/master/config

[fuzz]
runs = 1024

This configuration sets the default number of fuzz runs to 1024 for the project.

5. Stateless vs. Stateful Fuzzing (Invariant Testing)

So far, we've only discussed stateless fuzzing. Each of the 256+ runs is independent, starting with a fresh contract state. This is perfect for testing the properties of a single function call.

However, some of the most complex bugs emerge from a sequence of transactions that corrupt a contract's state over time. To find these, we need stateful fuzzing, which Foundry calls invariant testing.

In an invariant test, Foundry:

  1. Maintains the contract's state across runs.
  2. Calls any public/external function on your contract in a random sequence, with random inputs.
  3. After each call, it checks that your defined "invariants" (properties about the contract's state) still hold true.

This is a more advanced technique, but it's crucial to understand the distinction.

Fuzz & Invariant Tests | The secret to finding CRITICAL vulnerabilities faster

Patrick Collins explains the difference between stateless and stateful fuzzing and clarifies Foundry's specific terminology.

Watch from 04:15 to 05:33 and then from 07:28 to 07:56. This will clarify: The conceptual difference: stateless discards the state, while stateful preserves it. Foundry's terminology: 'fuzzing' refers to stateless tests, and 'invariant testing' refers to stateful tests.

A classic invariant for an ERC-20 token contract would be: "The total supply of the token must always equal the sum of all individual balances." An invariant test would call transfer, mint, burn, etc., in random sequences to see if any combination could ever break that core property.

We will dive deeper into writing invariant tests in a later module, but for now, recognizing the two types of fuzzing is a key takeaway.

Conclusion

You've now added one of the most powerful tools in the smart contract security arsenal to your skillset. By moving beyond hand-picked examples and letting the machine explore the edge cases, you dramatically increase the robustness and security of your code.

Key Takeaways:

  • Fuzz Testing (or Property-Based Testing) automates the discovery of edge cases by checking if a property holds true for many random inputs.
  • In Foundry, any test function with parameters is a stateless fuzz test. A failure produces a counterexample.
  • Use vm.assume(condition) to filter out unwanted inputs and bound(var, min, max) to constrain inputs to a range.
  • You can configure the number of test iterations via the --fuzz-runs CLI flag or permanently in foundry.toml.
  • Stateless fuzzing tests individual function calls in isolation, while stateful fuzzing (invariant testing) tests sequences of function calls on a persistent state.

Next Steps:

We have now covered unit tests (state, reverts, events) and property-based tests (fuzzing). These focus on a single contract in isolation. However, most dApps are systems of multiple, interacting contracts. In our next lesson, we will learn how to write integration tests to ensure these multi-contract systems work together as expected.

Can't find a good explanation? Sign up and we'll make it for you

Sign up