2026-03-25·10 min read

What is smart contract fuzzing?

What is smart contract fuzzing?

Smart contract fuzzing is an automated testing technique that throws thousands — sometimes millions — of random or semi-random inputs at your contracts to find states that violate your security assumptions. Instead of writing test cases for scenarios you can imagine, you define properties that should always hold and let the fuzzer discover the scenarios you didn't imagine.

If you've ever shipped code that passed every unit test and still got exploited, you already know why this matters.

<a href="/request-audit" class="cta-button">Get expert fuzzing for your protocol</a>

The core idea

Traditional testing is example-based. You write a test that deposits 100 tokens, withdraws 50, and checks the balance. If that specific scenario works, the test passes. But what about depositing 0 tokens? What about depositing type(uint256).max? What about depositing, then having someone else deposit, then withdrawing in the same block?

Fuzzing flips this around. Instead of specifying inputs, you specify properties, things that should always be true, and let a fuzzer generate the inputs automatically.

// Unit test: one scenario, one check
function test_depositWithdraw() public {
    vault.deposit(100e18);
    vault.withdraw(100e18);
    assertEq(vault.balanceOf(address(this)), 0);
}

// Fuzz test: thousands of scenarios, same property
function testFuzz_depositWithdraw(uint256 amount) public {
    amount = bound(amount, 1, type(uint128).max);
    vault.deposit(amount);
    vault.withdraw(vault.balanceOf(address(this)));
    assertEq(vault.balanceOf(address(this)), 0);
}

The first test checks one path. The second test checks every amount the fuzzer can think of. When the fuzzer finds an amount that breaks the property, you've got a real bug and a concrete reproducer to debug it with.

How fuzzing works for smart contracts

Smart contract fuzzers operate in a loop:

  1. Generate a transaction (or sequence of transactions) with random parameters: function selector, arguments, msg.sender, msg.value
  2. Execute the transaction against a local EVM instance
  3. Check whether any defined property was violated
  4. Mutate. If the transaction hit new code paths, keep it in the corpus and mutate it to explore further

This loop runs thousands of times per second. The fuzzer tracks which lines of code and which branches each transaction exercises. When it finds a transaction that reaches new code, it saves that input and generates variations of it. This is coverage-guided fuzzing. The fuzzer doesn't just spray random data, it learns which inputs are interesting and focuses on those.

Over time, the fuzzer builds up a corpus of inputs that exercise most of your contract's reachable code. Each new run starts from the corpus and tries to push further. This means fuzzing gets more effective the longer you run it.

Stateless vs stateful fuzzing

There are two flavors of smart contract fuzzing, and the distinction matters a lot.

Stateless fuzzing

Stateless fuzzing generates random inputs for a single function call. Each test invocation is independent, and the contract resets between calls. Foundry's testFuzz_ prefix does this by default.

// Stateless: each call is independent
function testFuzz_sqrt(uint256 x) public {
    uint256 result = Math.sqrt(x);
    assert(result * result <= x);
    assert((result + 1) * (result + 1) > x);
}

This is great for pure math functions, input validation, and simple state transitions. It's fast, easy to write, and catches a surprising number of edge-case bugs.

Stateful fuzzing

Stateful fuzzing generates sequences of transactions that build on each other. The fuzzer calls deposit(), then borrow(), then withdraw(), with different senders, amounts, and orderings, and checks properties after each step. The contract's state persists across the sequence.

// Stateful: sequences of calls, state persists
// The fuzzer generates random call sequences against target functions
// and checks invariant properties after each call

// Target functions the fuzzer can call:
function handler_deposit(uint256 amount) public {
    amount = bound(amount, 1, token.balanceOf(address(this)));
    vault.deposit(amount, address(this));
}

function handler_withdraw(uint256 shares) public {
    shares = bound(shares, 1, vault.balanceOf(address(this)));
    vault.withdraw(shares, address(this), address(this));
}

// Property checked after every call in the sequence:
function invariant_solvency() public view returns (bool) {
    return token.balanceOf(address(vault)) >= vault.totalAssets();
}

Most real smart contract bugs are stateful. They require a specific sequence of operations to trigger: a deposit followed by a price change followed by a liquidation, for instance. Stateful fuzzing is how you find these. Tools like Echidna and Medusa were built specifically for stateful fuzzing of smart contracts.

The tools

Four tools dominate smart contract fuzzing today. We've written a detailed comparison, but here's the short version.

Foundry

Foundry's built-in fuzzer handles both stateless and basic stateful fuzzing inside forge test. Zero setup beyond what your project already has. It's the fastest way to start fuzzing, and the right tool for quick property checks during development. The limitation is that it doesn't persist a corpus between runs, so each test starts from scratch.

Echidna

Echidna is the original smart contract fuzzer. It builds transaction sequences incrementally and has the best sequence shrinking of any tool. When it finds a failing sequence of 100 calls, it reduces it to 4-5 calls that reproduce the bug. That makes debugging far easier. It's single-threaded, so it's slower than Medusa, but its directed exploration often finds deeper bugs.

Medusa

Medusa runs parallel workers that share coverage data. On an 8-core machine, it reaches broad coverage roughly 3x faster than Echidna. Its corpus is stored as human-readable JSON. It's the tool you run first when starting a new campaign, and it fits naturally into CI/CD pipelines where wall-clock time matters.

Halmos

Halmos isn't a fuzzer in the traditional sense. It's a symbolic execution engine. Instead of testing concrete inputs, it mathematically proves whether a property holds for all possible inputs within a bound. When you need guarantees about arithmetic correctness, not just statistical confidence, Halmos fills a gap that no fuzzer can.

Using them together

You don't have to pick one. The Chimera framework lets you write properties once and run them with Foundry, Echidna, and Medusa without changing your code. Our beginner's guide walks through the full setup.

What fuzzing catches

Fuzzing is particularly good at finding bugs that require unexpected inputs or sequences. Here are the categories we see most often in real engagements:

Arithmetic edge cases. Integer overflow/underflow, rounding errors that compound over time, division by zero with specific denominator values, precision loss in fixed-point math.

Accounting violations. Share-to-asset ratios that drift, total supply mismatches, solvency invariants that break after specific deposit/withdraw sequences. These are the category of bug that has drained the most value from DeFi protocols.

Access control gaps. Functions callable by unauthorized senders, privilege escalation through specific call sequences, missing modifiers that only matter in certain state configurations.

State machine violations. Protocols that reach states that should be unreachable: paused but still accepting deposits, liquidatable but not liquidatable, finalized but still mutable.

Cross-function interactions. Bugs that only appear when function A is called before function B, or when two users interact in a specific order. These are the vulnerabilities unit tests miss.

What fuzzing doesn't catch

Fuzzing isn't a silver bullet. It's bad at:

  • Business logic that needs human judgment. If the property is "the fee should be fair," no fuzzer can check that.
  • External integration bugs. Fuzzers test your contracts in isolation. Oracle manipulation, bridge failures, and cross-chain race conditions need different approaches.
  • Gas optimization issues. Fuzzers check correctness, not efficiency.
  • Issues requiring very specific preconditions. If a bug only triggers when the block timestamp is exactly a leap-second boundary, a random fuzzer is unlikely to hit it (though targeted property design can help).

A proper security review combines fuzzing with manual review, static analysis, and formal verification. Fuzzing handles the "find bugs in code I think is correct" part better than any other technique.

When to use fuzzing

The short answer: always, if your contracts handle value.

The more nuanced answer:

  • During development. Write properties as you write code. Foundry's built-in fuzzer gives you near-instant feedback. This catches bugs before they make it into a PR.
  • Before an audit. Running a fuzzing campaign before you send your code to auditors means the easy bugs are already found. Auditors can focus on the subtle stuff.
  • During an audit. At Recon, fuzzing is a core part of every engagement. We write custom invariant properties for each protocol and run multi-day campaigns with Echidna, Medusa, and Recon Pro.
  • After deployment. Continuous fuzzing in CI/CD catches regressions on every commit. A property that passed yesterday and fails today tells you exactly which change broke it.

How Recon uses fuzzing

Every Recon engagement starts with invariant property design. We identify the properties that your protocol must satisfy, like solvency, access control, and state machine correctness, then encode them as executable tests using the Chimera framework.

We then run these properties through multiple fuzzers in parallel. Medusa for fast broad coverage. Echidna for deep directed exploration. Halmos for mathematical proofs of critical arithmetic. The results feed back into property refinement. If coverage data shows unreached code paths, we write targeted properties to push the fuzzer into those areas.

This isn't a checkbox exercise. Fuzzing has caught critical and high-severity bugs in real engagements that manual review missed, the kind of bugs that would have drained protocols. We've written about several of these in our real vulnerability case studies.

Getting started

If you're new to fuzzing, start here:

  1. How to write your first invariant test. A hands-on tutorial with three properties for a simple vault
  2. From Zero to Fuzzing: Chimera beginner's guide. Full setup with Foundry, Echidna, and Medusa
  3. Smart contract fuzzing tools compared. Pick the right tool for your project

If you'd rather have experts build your invariant test suite and run the campaigns for you, request an audit with Recon. We'll find the bugs the fuzzers can find, and the ones that need a human eye too.

Related Posts

Related Glossary Terms

See fuzzing in action on your contracts