Fuzzing the Foundations: A Deep Dive into Smart Contract Invariant Testing with Foundry
Beyond Unit Tests: An Introduction to Fuzz & Invariant Testing for Smart Contracts
After completing a comprehensive suite of unit tests, many developers feel confident in their project’s security. However, traditional unit tests are deterministic; they only verify scenarios you have already anticipated. The true test of a smart contract lies in its ability to withstand unexpected, malicious, and random interactions. This is where fuzz testing becomes an indispensable tool.
This article will introduce the fundamentals of fuzz testing, explain the crucial difference between stateless and stateful (invariant) fuzzing, and demonstrate why it is so vital for projects handling significant financial value, like stablecoin protocols.

The Core Concept of Fuzz & Invariant Testing
Fuzz testing is a dynamic and powerful testing methodology that aims to discover vulnerabilities you never thought of. Unlike traditional tests, which use known inputs to check for a predictable output, fuzzing feeds your contract with vast amounts of random data to see if it breaks.
It is often called Invariant Testing because its core purpose is to validate the contract’s invariants—the properties that must hold true under any circumstances. Instead of testing a single function, the fuzzing engine continuously sends random transactions to the contract and checks if these invariants are ever violated.
How It Works:
- Define Invariants: You must first define what an “invariant” is for your system. These are critical, never-changing properties. For example:
- In a lending protocol, the total collateral value must always be greater than or equal to the total debt value.
- In an ERC20 token, the
totalSupply
must always equal the sum of all user balances. - In an NFT contract, there can only ever be one owner for each unique
tokenId
.
- Randomized Input: Testing frameworks like Foundry automatically generate random parameters for your contract’s functions. For instance, it might randomly select a function like
mint
,transfer
, orburn
, and then generate random values for the_to
address and_amount
. - Continuous Testing: The framework then executes a massive, continuous sequence of these random transactions. After each transaction, it automatically calls your predefined invariant functions to check if they still hold true.
- Reporting a Failure: If an invariant is ever broken (e.g.,
totalSupply
no longer matches the sum of balances), the test will immediately halt. The framework will then provide a complete, reproducible transaction sequence that led to the failure. This sequence is a golden ticket for developers, as it pinpoints the exact series of random inputs that revealed the bug.
Stateless vs. Stateful Fuzz Testing
It’s important to distinguish between two types of fuzzing that Foundry handles differently:
Stateless Fuzz Testing
A stateless fuzz test runs each time with a clean, independent execution environment. The state of a previous test is discarded before the next one begins. This method is ideal for testing a single function’s behavior and its boundary conditions, such as checking for overflow errors or unexpected reverts.
- Goal: Test the behavior of a single function in isolation.
- State: The contract state is reset for every run.
- Example: A function
testFuzz_add(uint256 a, uint256 b)
checks ifa + b
behaves as expected. The test runs many times, each time with randoma
andb
values, but it doesn’t care about the state left by the previous run.
Stateful Fuzz Testing (Invariant Testing)
Stateful fuzz testing, or invariant testing, is a more powerful approach that maintains the contract’s state across multiple random transactions. It simulates a real-world, chaotic environment where a series of seemingly harmless actions can lead to a catastrophic failure.
- Goal: Test the contract’s core properties across a sequence of complex transactions.
- State: The contract’s state persists from one test run to the next.
- Example: A test might simulate a user
deposit
, followed by another user’swithdraw
, and then a malicious user’sborrow
. After each step, it checks the invarianttotalCollateralValue >= totalDebtValue
to see if the protocol remains solvent.
In Foundry, you write stateless
fuzz tests with function names like testFuzz_...
, while stateful
invariant tests use functions starting with invariant_...
.
A Practical Example with Foundry
Let’s use Foundry to demonstrate how a stateful fuzz test can find a subtle bug in a simple token contract.
1. The Vulnerable Contract (MyToken.sol
)
This contract has a critical bug in its burn
function:
1 |
|
The burn
function should decrease the total supply, but it incorrectly increases the extraSupply
variable, which will cause totalSupply
to not match the sum of all user balances over time.
2. The Invariant Test Script (MyToken.t.sol
)
This Foundry test script is designed to catch the bug by continuously checking if the sum of all token balances equals the total supply.
1 |
|
Foundry will automatically execute the testFuzz_...
function with random inputs and, after each execution, it will call invariant_totalSupplyEqualsBalanceSum()
to check if the contract’s state is still valid. The test will eventually fail when a burn
call leads to an inconsistent state.
3. Running the Test and Finding the Bug
To run this invariant test, you would use the following command:
forge test --mt MyTokenTest
Foundry will then:
- Start randomly calling
testFuzz_...
functions. - After each call, it automatically runs
invariant_totalSupplyEqualsBalanceSum()
. - When the
burn
function is called with a random amount, thetotalSupply()
will no longer match the sum of balances, and the test will fail. - Foundry will provide a detailed report showing the exact sequence of transactions that broke the invariant, allowing you to quickly locate and fix the
burn
function’s bug.
Conclusion
In the world of Web3, Fuzz (Invariant) testing is a critical line of defense. It’s not about verifying “does this function work correctly?” but rather “under any possible circumstance, do the core properties of my contract remain unchanged?” This makes it an essential part of any robust smart contract security audit, ensuring your protocol is resilient enough for the unpredictable nature of the blockchain.