WritingFuzzing Smart Contracts with Foundry and Echidna: A Practical Guide — Clixo
6 min readfuzzing, foundry, echidna, smart-contract-testing, solidity

Fuzzing Smart Contracts with Foundry and Echidna: A Practical Guide

Learn how to fuzz smart contracts using Foundry and Echidna to find edge-case bugs before auditors do. Covers invariant writing, setup, and interpreting results.

Unit tests verify that your code behaves correctly for the specific inputs you thought to test. Fuzzing finds the inputs you did not think to test — often the ones that break everything. For smart contracts, where a single missed edge case can mean drained user funds, property-based fuzzing is not a nice-to-have. It is a core part of any serious testing strategy.

This guide covers how to fuzz smart contracts using Foundry's built-in fuzzer and Echidna, when to use each, and how to write invariants that actually find bugs.

What Fuzzing Does for Smart Contracts

A fuzzer generates large numbers of random or semi-random inputs and runs them against your code. Unlike unit tests, which execute specific scenarios, a fuzzer explores the input space broadly, looking for inputs that cause an assertion to fail or violate a defined property.

For smart contracts, fuzzing is most valuable when paired with invariants — properties about your system that should always be true regardless of what sequence of operations was performed. Examples:

  • The total supply of a token should always equal the sum of all account balances
  • The contract's ETH balance should always be at least as large as the sum of all pending withdrawals
  • After any sequence of deposits and withdrawals, no user's balance should be negative
  • A user who has not called deposit should have a balance of zero

When the fuzzer finds a sequence of inputs that violates one of these invariants, you have found a bug — often one that no unit test would have caught.

Fuzzing with Foundry

Foundry includes two fuzzing modes: standard property-based fuzzing for individual functions and invariant testing for stateful, multi-call scenarios.

Standard Fuzz Tests

In a standard Foundry fuzz test, the test function accepts parameters that Foundry will randomly generate across many runs. The fuzz_runs configuration controls how many random inputs are tried (default is 256; increase to 10,000+ for critical paths).

A simple example pattern:

function testFuzz_depositAndWithdraw(uint256 amount) public {
    vm.assume(amount > 0 && amount <= INITIAL_BALANCE);
    vault.deposit(amount);
    vault.withdraw(amount);
    assertEq(token.balanceOf(address(this)), INITIAL_BALANCE);
}

The vm.assume cheatcode filters inputs that would be invalid for the test's purpose. Use it sparingly — overly restrictive assumptions shrink the input space and reduce coverage.

Invariant Tests

Invariant tests run a sequence of random function calls against your contract and then check that defined invariant functions still hold after each call sequence. This is stateful fuzzing — the fuzzer builds up system state across multiple calls, which is how it finds bugs in protocols where a single function call cannot cause harm but a specific sequence can.

The structure:

  • A handler contract wraps your target contract and exposes the functions the fuzzer can call, with appropriate setup
  • An invariant function (prefixed with invariant_) defines a property that should always hold
  • Foundry randomly selects from the available handler functions, executes them in sequence, and checks the invariant after each call

Foundry's invariant testing finds a much wider range of bugs than unit tests alone, particularly in protocols with multiple interacting state variables.

Fuzzing with Echidna

Echidna is a dedicated property-based fuzzer for Ethereum contracts from Trail of Bits. It offers more advanced fuzzing strategies than Foundry and is preferred by many professional security researchers for deep fuzzing campaigns.

Echidna works by deploying your contract and running campaigns of transactions, checking that invariant functions (prefixed with echidna_) return true after every transaction sequence.

Key advantages Echidna brings over Foundry's fuzzer:

  • Corpus collection: Echidna saves transaction sequences that produce interesting state transitions and uses them as seeds for future campaigns, making it more likely to find bugs that require specific state setups
  • Coverage-guided fuzzing: Echidna tracks which code paths have been exercised and biases new inputs toward unexplored paths
  • Configurable strategies: Echidna supports multiple fuzzing strategies and allows fine-grained configuration of how inputs are generated

Echidna is worth using for protocols where the attack paths are complex — lending protocols, AMMs with multiple pools, yield optimizers with multi-step harvest logic.

Writing Invariants That Find Bugs

The quality of your fuzzing campaign is determined almost entirely by the quality of your invariants. A fuzzer can only find violations of properties you have defined.

Start with Accounting Invariants

The most important invariants to write first are about token and ETH accounting. Examples:

  • Sum of all user balances equals total supply
  • Token balance held by the contract equals what the accounting storage says it should hold
  • Total debt issued never exceeds total collateral value at the minimum acceptable collateral ratio

These are the invariants most directly tied to loss of funds. If the fuzzer violates any of these, you have found a real bug.

Add Authorization Invariants

Define properties about access control:

  • After any sequence of calls from an unauthorized address, the owner should not have changed
  • A non-admin caller cannot move protocol fees to an arbitrary address

Add State Transition Invariants

In protocols with defined states (paused, active, shutdown), verify that invalid state transitions cannot happen:

  • When the protocol is paused, no deposits should be accepted
  • When a loan is liquidated, its debt balance should be zero

Interpret Counterexamples Carefully

When a fuzzer finds a violation, it produces a sequence of calls that triggered it. Read this sequence carefully — the violation may reveal a root cause that is several layers removed from the failing invariant. A balance invariant violation might trace back to a reentrancy path, an integer truncation, or a fee calculation error.

Integrating Fuzzing into CI

Running fuzzing in CI ensures that invariants are checked on every pull request, not just before major releases. Foundry's fuzz tests run automatically with forge test. The number of fuzz runs in CI can be lower than in a dedicated fuzzing campaign — the goal in CI is fast feedback, while deep campaigns run longer offline.

For Echidna, a lower testLimit in CI and a higher limit for release-gate campaigns is a practical approach.

Fuzzing is what separates protocols that have been tested from protocols that have been adversarially tested. Security researchers and auditors who submit codebases with comprehensive invariant suites get more focused, higher-value reviews. The bugs the fuzzer found are no longer distracting the auditor from the bugs the fuzzer did not find.

Clixo builds DeFi and Web3 protocols with invariant testing as part of the standard engineering process. If you want a team that writes the tests that find the bugs, talk to us.