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

Building on the one-liner cheats from Part 5, we now hand the wheel to randomness. Today’s focus is Forge’s built-in fuzzing: you’ll see how parameterized tests uncover edge-cases automatically, plus tips for bounds, assumptions, and fork-based fuzz runs.

Forge’s built-in fuzzing runs tests with randomized inputs, enabling property-based testing of smart contracts. Any forge test function with parameters is treated as a fuzz test: for example:

image

In this testFuzz_ function, Forge will automatically generate many values for amount to try finding edge cases. It “runs any test that takes at least one parameter as a property-based test”. In practice, Forge will run hundreds of random cases, looking for any input that violates an assertion. If it finds a failure, it reports the concrete counterexample.

Test structure: Fuzz tests look similar to unit tests. Prefixing the function name (often testFuzz_ by convention) isn’t strictly required by Forge (it runs any parametric test), but it clarifies intent. The body sets up the scenario using the input parameters, then makes assertions. In the example above, we test that withdrawing returns exactly the deposited amount for any amount. Running this might reveal a bug if, say, extreme values cause an overflow or revert.

Fork testing:

Unit-level fuzzing is powerful, yet sometimes you must interact with real main-net contracts: reading an ERC-20 balance of USDC, invoking Uniswap pools, or verifying upgrade logic against a live proxy. Foundry’s answer is the forking cheat-codes. A fork is a complete in-memory copy of another chain at a given block; you create one with vm.createFork and activate it with vm.selectFork. The moment a fork is selected every subsequent call, log and storage read goes through that remote state while writes remain local to your test.

image

createFork is overloaded. The simplest form takes an RPC URL or alias and snapshots the latest block. A second variant lets you pin the fork at an explicit block height.

image

A third overload accepts a transaction hash; Foundry rolls the fork to the block that mined that hash, replays every prior transaction in that block, and positions execution at the post-state. This is useful when you want to replay the exact environment of a known exploit transaction.

Narrowing conditions: Sometimes you only want to fuzz a subset of inputs. Forge provides vm.assume(bool cond) for this purpose: if the condition is false, Forge discards that random input and tries another. For example, to skip zero values you could write vm.assume(v != 0); require(v != 0);. The cheatsheet warns to use assume sparingly (broad conditions slow down fuzzing by rejecting many tries). Another strategy is to use bound() (from Forge Std) to constrain inputs to a range instead of rejecting them. For instance:

image

You can trigger fuzzing exactly the same way you trigger ordinary tests, just run forge test. Forge sees the parameterised function signatures and automatically switches to its fuzzer. To increase coverage or speed things up you pass the usual CLI flags, for example:

image

The --match-test filter focuses on the fuzz case you care about, --fuzz-runs sets how many random inputs Forge will attempt (default is 256), and -vv bumps verbosity so you can watch each counter-example, gas reading and mean/median report scroll by in real time.

Pitfalls & tips:

  • Max rejects: If too many inputs are assumed away, the fuzzer might hit the reject limit. You can configure this in foundry.toml or with fuzz.max_test_rejects.
  • Statefulness: Each fuzz invocation runs in a fresh EVM state (same as normal tests). Ensure your test setup (like setUp()) is idempotent.
  • Repeatability: Forge reports the seed/counterexample so you can reproduce a failing case.
  • Coverage: Because Forge runs hundreds of fuzz cases, it dramatically increases code coverage. This is especially valuable for security-critical code; Paradigm reports that v1.0 improved fuzz execution 2× faster, allowing more cases in CI.

Conclusion

In summary, Foundry’s fuzzing automates edge-case exploration. Use parameterized testFuzz_ functions, leverage vm.assume or bound() for constraints, and interpret failures as concrete examples of edge inputs. This complements unit tests by catching what human-written cases might miss.

Simeon Cholakov
Simeon Cholakov

Security Researcher