Blockchain security isn't optional.
Protect your smart contracts and DeFi protocols with Three Sigma, a trusted security partner in blockchain audits, smart contract vulnerability assessments, and Web3 security.
Get a Quote Today
Introduction
We wrapped up Part 6 by stress-testing single calls with fuzzing; this final chapter raises the bar to stateful sequences. Invariant testing lets you declare rules, like “totalSupply always equals balances”, and have Forge hammer your protocol until either the rule breaks or your confidence soars.
What Is Invariant Testing in Foundry?
Invariant tests are stateful fuzz tests that assert “rules” which must always hold true, even after any sequence of contract calls. In practice, you write test functions with the prefix invariant
, and Forge will generate random sequences of transactions (calls) on your contracts to try to violate those invariants. For example, an ERC20 invariant might be “totalSupply == sum of all balances” or “no one can create tokens from thin air.” Foundry will then make random calls (and fuzz inputs) and after each call check those conditions.
“Invariant testing allows for a set of invariant expressions to be tested against randomized sequences of pre-defined function calls… Invariant testing is a powerful tool to expose incorrect logic in protocols.”
How Invariant Testing Works
You define invariants as functions with the invariant_
prefix. For example:

Under the hood, Foundry will deploy your contracts, then run many “runs” of random calls up to a certain “depth”. Each run is a sequence of up to depth
random transactions. After each transaction, all invariant_
functions are checked against the new state. If any assertion fails, Forge reports a counterexample sequence. You control runs
and depth
via configuration (default 256 runs, depth 15).
Key Parameters
- runs = how many random sequences to try.
- depth = how many calls in each sequence. Higher values increase confidence but take more time. You can set these in
foundry.toml
under[invariant]
or via CLI flags, e.g.:

(The docs note you can also split invariants into multiple “jobs” to run in parallel.)
Test Structure and Setup
Invariant tests live alongside unit tests. For example, if you have handler contracts or multiple contracts, you bundle them in your test suite. Forge will detect invariant_
functions and run the special invariant engine. At the end of each run, an optional afterInvariant()
function (if defined) executes for cleanup or logging.
Common Patterns
Often, one writes a handler contract that wraps the system under test to provide simplified functions for fuzzing. For instance, a handler might expose function hDeposit(uint256 x)
and function hWithdraw(uint256 y)
which call the real contract. Then invariants assert global properties (e.g. “sum of balances = funds available”). Foundry will repeatedly call random sequences like hDeposit
, hWithdraw
, etc.
Pitfalls to Watch
- Non-determinism: Invariants create new EVM forks for each invariant function, so invariants are not checked on the same state unless grouped. If you need multiple conditions jointly, group them in one function.
- Reverts: By default Forge does not fail on reverts during invariant runs (unless you set
fail_on_revert=true
). Instead, reverts simply terminate that run. You can configure this behavior. - Complex state: For very complex stateful systems, you may need to carefully bound inputs or assumptions (like with fuzzing) to keep invariants meaningful and tests efficient.
Why Invariant Testing Matters in Web3 Security
Invariant testing in Foundry is particularly valuable for protocols with invariants (e.g. AMMs, lending pools). To catch “false assumptions and incorrect logic in edge cases and highly complex protocol states”. By specifying what must always hold (e.g. conservation of tokens, accounting equations), you let Forge try to break your contract in ways deterministic tests might not catch.
Example Setup
In practice, a full invariant test might look like:

Running forge test --ffi
(if needed) or simply forge test
will run the invariant engine automatically. If Forge finds a sequence of calls that breaks invariant_TotalSupplyEqualsBalances
, it will report the steps needed.
Conclusion
In summary, invariant tests are the ultimate “fuzz” for protocol-wide properties. Define as many invariants as possible, configure runs/depth to suit your CI budget, and let Forge exhaustively explore call sequences. This powerful feature goes beyond unit/fuzz tests, helping ensure that your smart contracts behave correctly under any valid use sequence.
Sources
Foundry documentation and community materials provide extensive guidance on cheatcodes and testing. Key references include the official Foundry Book for cheatcodes and advanced testing, as well as community articles comparing tools. All example code above follows the Forge Standard Library (forge-std
) conventions.