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.

설치로 이동

소스 정보

저장소
ccashwell/evm-cortex
최근 소스 활동
2026년 4월 10일 16:31
감지된 SKILL.md 언어
영어
스타
131
포크
18

설치 방법

기본적으로 소스를 먼저 확인하는 Prompt가 선택됩니다. 직접 명령으로 전환하거나 로컬 사본을 다운로드할 수도 있습니다.

소스 파일 검토

설치 여부를 결정하기 전에 SKILL.md와 SkillsMP에 표시된 보조 파일을 읽어 보세요.

SKILL.md 표시 중

SKILL.md
소스 지침 · 읽기 전용 미리보기
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
GitHub에서 보기