Skip to main content

fuzzing-patterns

Use when writing fuzz tests for Solidity contracts. Covers property-based testing, input constraining with vm.assume/vm.bound, stateful vs stateless fuzzing, configuring runs, seed corpus, and interpreting counterexamples.

Jump to install

Source facts

Repository
ccashwell/evm-cortex
Last source activity
April 10, 2026 at 16:31
Detected SKILL.md language
English
Stars
131
Forks
18

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
fuzzing-patterns
description
Use when writing fuzz tests for Solidity contracts. Covers property-based testing, input constraining with vm.assume/vm.bound, stateful vs stateless fuzzing, configuring runs, seed corpus, and interpreting counterexamples.
# Fuzz Testing Patterns ## Stateless vs Stateful Fuzzing | Type | How It Works | Use Case | |------|-------------|----------| | **Stateless** | Random inputs per test, fresh state each run | Individual function properties | | **Stateful** | Random sequences of calls, accumulated state | System-level invariants | Stateless = `function testFuzz_*(uint256 x)` — Foundry randomizes `x` each run. Stateful = invariant tests with handlers (see `invariant-testing` skill). ## Basic Fuzz Test ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import {Test} from "forge-std/Test.sol"; import {Vault} from "../src/Vault.sol"; contract VaultFuzzTest is Test { Vault vault; function setUp() public { vault = new Vault(address(token)); deal(address(token), address(this), type(uint128).max); token.approve(address(vault), type(uint256).max); } // Property: deposit then redeem never yields more than deposited function testFuzz_depositRedeemNoProfit(uint256 assets) public { assets = bound(assets, 1, type(uint128).max); uint256 shares = vault.deposit(assets, address(this)); uint256 received = vault.redeem(shares, address(this), address(this)); assertLe(received, assets, "profit from round-trip"); } // Property: transfer preserves total supply function testFuzz_transferPreservesSupply( address to, uint256 mintAmount, uint256 transferAmount ) public { vm.assume(to != address(0) && to != address(this)); mintAmount = bound(mintAmount, 1, type(uint128).max); transferAmount = bound(transferAmount, 0, mintAmount); vault.deposit(mintAmount, address(this)); uint256 supplyBefore = vault.totalSupply(); vault.transfer(to, transferAmount); uint256 supplyAfter = vault.totalSupply(); assertEq(supplyBefore, supplyAfter, "supply changed on transfer"); } } ``` ## Input Constraining ### `bound()` vs `vm.assume()` ```solidity // PREFER bound() — maps input to valid range without discarding runs function testFuzz_bounded(uint256 x) public { x = bound(x, 1e18, 1_000_000e18); // x is always in [1e18, 1_000_000e18] } // AVOID vm.assume() for wide ranges — discards too many inputs function testFuzz_assumed(uint256 x) public { vm.assume(x > 0 && x < 1000); // 99.99% of inputs discarded! // Only use for conditions that are hard to express with bound() } ``` ### When to Use `vm.assume()` ```solidity // Valid: exclude specific addresses function testFuzz_transfer(address to, uint256 amount) public { vm.assume(to != address(0)); vm.assume(to != address(vault)); vm.assume(to.code.length == 0); // no contracts amount = bound(amount, 1, balanceOf(address(this))); // ... } ``` ## Fuzz Configuration In `foundry.toml`: ```toml [fuzz] runs = 1000 # number of random inputs per test (default: 256) max_test_rejects = 65536 # max vm.assume rejections before fail seed = "0x1234" # fixed seed for reproducibility dictionary_weight = 40 # % of inputs from extracted constants [invariant] runs = 256 depth = 64 fail_on_revert = false ``` ## Property Categories ### Algebraic Properties ```solidity // Commutativity: f(a, b) == f(b, a) function testFuzz_addCommutative(uint256 a, uint256 b) public { assertEq(target.add(a, b), target.add(b, a)); } // Associativity: f(f(a, b), c) == f(a, f(b, c)) // Identity: f(a, 0) == a // Idempotency: f(f(a)) == f(a) ``` ### Roundtrip Properties ```solidity // encode then decode returns original function testFuzz_encodeDecodeRoundtrip(uint256 value) public { bytes memory encoded = target.encode(value); uint256 decoded = target.decode(encoded); assertEq(decoded, value); } // deposit then withdraw returns (approximately) original function testFuzz_depositWithdrawRoundtrip(uint256 amount) public { amount = bound(amount, 1e18, 1_000_000e18); uint256 shares = vault.deposit(amount, address(this)); uint256 returned = vault.redeem(shares, address(this), address(this)); assertApproxEqAbs(returned, amount, 1); // within 1 wei } ``` ### Monotonicity Properties ```solidity // More input -> more output (or equal) function testFuzz_depositMonotonic(uint256 a, uint256 b) public { a = bound(a, 1e18, 1_000_000e18); b = bound(b, a, 1_000_000e18); // b >= a uint256 sharesA = vault.previewDeposit(a); uint256 sharesB = vault.previewDeposit(b); assertGe(sharesB, sharesA, "more deposit should give more shares"); } ``` ### Boundary Properties ```solidity // No overflow at max values function testFuzz_noOverflow(uint256 a, uint256 b) public { a = bound(a, 0, type(uint128).max); b = bound(b, 0, type(uint128).max); // Should not revert with overflow target.safeMultiply(a, b); } ``` ## Interpreting Counterexamples When a fuzz test fails, Foundry shows the failing input: ``` [FAIL. Reason: profit from round-trip] Counterexample: calldata=0x..., args=[115792089237316195423570985008687907853269984665640564039457584007913129639935] ``` Steps: 1. Copy the failing args into a concrete test 2. Add `console2.log()` to trace execution 3. Check if it's a real bug or a test constraint issue 4. If real: fix the bug, keep the counterexample as a regression test 5. If false positive: tighten `bound()` constraints ```solidity // Regression test from fuzzer counterexample function test_regression_overflowOnMaxDeposit() public { uint256 amount = type(uint256).max; vm.expectRevert(); vault.deposit(amount, address(this)); } ``` ## Checklist - [ ] Use `bound()` over `vm.assume()` to minimize discarded runs - [ ] Properties categorized: algebraic, roundtrip, monotonicity, boundary - [ ] Fuzz runs set to 1000+ for CI, 256 for local development - [ ] Counterexamples from failures preserved as regression tests - [ ] Fixed seed used for reproducible CI results - [ ] Separate fuzz tests from unit tests (naming convention `testFuzz_`) - [ ] Edge values tested explicitly alongside fuzzing (0, 1, max) - [ ] Properties verified against known-correct reference implementation
View on GitHub