ByAUJay
GetFoundry.sh, Echidna --corpus-dir Flag, and Building a Testing Strategy for Blockchain With Foundry and Echidna
A practical, up-to-date playbook for decision‑makers who want a fast, reliable smart contract testing stack by combining Foundry and Echidna. We cover precise install steps, the real value of Echidna’s --corpus-dir, CI patterns, and concrete configs that teams can adopt immediately.
Why these two tools belong in the same test strategy
- Foundry gives you lightning‑fast unit, fuzz, and invariant tests in Solidity, plus reproducible local chains and coverage. It’s the day‑to‑day developer harness. (getfoundry.sh)
- Echidna adds coverage‑guided, sequence‑based fuzzing with persistent corpora. The corpus feature is the lever that lets you scale beyond single runs and reuse “hard‑won” inputs across machines and time. (secure-contracts.com)
Below we show how to set up Foundry correctly (GetFoundry.sh), how Echidna’s --corpus-dir works in practice, and how to stitch them into a modern, high‑signal pipeline.
GetFoundry.sh: the right way to install and pin your toolchain
Foundry’s official installer is foundryup, delivered via GetFoundry.sh. On a new machine:
# Install foundryup curl -L https://foundry.paradigm.xyz | bash # Install the stable toolchain (forge, cast, anvil, chisel) foundryup # If you need the nightly toolchain explicitly foundryup --install nightly
- Binaries are attested. When foundryup installs a release, it checks binary hashes against GitHub artifact attestations. You can also verify manually with GitHub’s CLI:
. (getfoundry.sh)gh attestation verify --owner foundry-rs $(which forge) - If you must test Tempo’s Foundry fork:
. (Great for specific needs, but pin your CI to one distribution.) (getfoundry.sh)foundryup -n tempo - Uninstalling is simple: remove your
directory and PATH entry. (getfoundry.sh)~/.foundry - Prefer Docker for hermetic CI:
. (getfoundry.sh)docker pull ghcr.io/foundry-rs/foundry:latest
Pro tip for stability in large teams
- Pin to stable in CI. Nightlies evolve quickly; for example, a coverage regression was reported on August 27, 2025 when moving from 1.3.2‑stable to a nightly. Treat nightlies as opt‑in and explicitly gate their rollout. (github.com)
Foundry testing you’ll actually use in production
Essential foundry.toml settings for fuzz and invariants
Use profiles for developer speed locally and thoroughness in CI. Defaults evolve—use the docs as source of truth for option names and defaults.
# foundry.toml [profile.default] # Show gas samples in reports (helpful when fuzzing) gas_reports = ["*"] [fuzz] # Heuristic fuzzing controls, pulled by invariant if not overridden runs = 256 dictionary_weight = 40 include_storage = true include_push_bytes = true [invariant] # CI-friendly depth and runs; tune per project complexity runs = 512 depth = 500 fail_on_revert = false dictionary_weight = 80 shrink_run_limit = 5000 max_assume_rejects = 65536 gas_report_samples = 256 # Persist failing sequences so you can rerun and fix deterministically # failure_persist_dir = "artifacts/fuzz-failures" # failure_persist_file = "failures"
- depth is how many calls per run; 500 is a sensible upper bound for CI on non‑trivial protocols. Defaults and environment variables are documented in the testing reference. (getfoundry.sh)
- You can inline per‑test config directly in Solidity comments to selectively increase runs/depth for expensive tests without slowing the whole suite:
(Apply this only to the hot tests.) (getfoundry.sh)/// forge-config: ci.fuzz.runs = 2000
Invariant test harness patterns that scale
- Group function targets: use selectors in your test contract to focus the fuzzer on state‑advancing calls; keep view/pure getters out of targets.
- Use vm.assume to prune impossible paths instead of relying on “fail_on_revert = true” globally; it reduces false negatives.
- Keep regressions easy to reproduce: turn on failure persistence and rerun with
or by pointing at the input file recorded by Forge. Options and failure replay are documented alongside coverage and display flags. (getfoundry.sh)forge test --rerun
Coverage that CI can gate on
Generate LCOV for platforms like Codecov/GitLab:
forge coverage --report lcov --lcov-version 1
- You can also output summary, debug, or even bytecode‑level coverage; choose LCOV for tooling, summary for developer hints, and debug for triage. (getfoundry.sh)
Echidna’s --corpus-dir: what it buys you and how to use it
Echidna’s corpus is a set of transaction sequences that increased coverage. Persisting it means:
- Faster subsequent runs (doesn’t start from scratch).
- Reproducibility and transferability across laptops/CI containers.
- The ability to seed future campaigns with “rare” sequences from prior work.
You can set the directory in config or via CLI:
# echidna.yaml testMode: assertion # or property coverage: true # enabled by default; keep it on corpusDir: "echidna-corpora/my-protocol" testLimit: 100000 # sequences to generate seqLen: 100 # tx per sequence (tune per protocol) sender: ["0x10000", "0x20000", "0x30000"] # Call into ancillary contracts too (renamed from multi-abi): allContracts: true
Run either with a single file or with your Foundry project root (via crytic‑compile):
# From your Foundry repo root echidna . --contract MyHarness --config echidna.yaml # or override on CLI if you prefer: echidna . --contract MyHarness --test-mode assertion --corpus-dir echidna-corpora/my-protocol
- corpusDir in config maps to the CLI flag --corpus-dir; both are supported on current Echidna versions. (secure-contracts.com)
- Working with Foundry repos is first‑class via crytic‑compile; you can simply run
. (github.com)echidna . - multi-abi was renamed to allContracts in Echidna 2.1.0; both may work today, but use allContracts going forward. (github.com)
Seeding and curating your corpus
- Start by collecting coverage in a “neutral” run, then copy/modify sequences to craft seeds for deep edges. The official guide shows how to duplicate a covered path file and tweak arguments so a property fails immediately on the next run—this is a great way to confirm assumptions and produce regression seeds. (secure-contracts.com)
- Store the corpus directory as a CI artifact (keep per‑branch corpora separate) and pull it on subsequent runs for a warm start. GitHub Actions wrappers expose corpus‑dir directly to make this straightforward. (github.com)
Targeting, filtering, and addressing complex protocols
- Use filterFunctions + filterBlacklist to denylist or allowlist methods during campaigns, reducing noise and drifting state. (blog.trailofbits.com)
- In assertion mode, you can assert inside multi‑call sequences—handy for invariants that need mid‑transaction checks. The Crytic properties repo has production‑grade configs for ERC‑20/721 showing assertion mode, senders, and corpus usage. (github.com)
On‑chain and multi‑contract testing
- allContracts lets Echidna call functions in contracts your harness deployed (e.g., token approvals before protocol calls). Prefer this over trying to mock “user‑side” flows manually. (secure-contracts.com)
- You can fetch code via RPC or Etherscan to test on real deployments by setting rpcUrl/rpcBlock and ETHERSCAN_API_KEY in config; this is useful for regression hunting across historical states. (secure-contracts.com)
A concrete, step‑by‑step testing strategy (that teams can adopt this quarter)
- Pin and verify your toolchain
- Install Foundry with foundryup; pin stable in CI. Verify
provenance with GitHub attestations. Keep a “bump‑nightly” job off the main path to surface regressions early. (getfoundry.sh)forge
- Establish fast unit/fuzz feedback with Foundry
- Write unit tests and lightweight fuzz tests in Solidity. Start with
, increase runs for high‑risk modules. Persist and replay failures ([fuzz].runs = 256
) to keep feedback tight. (getfoundry.sh)--rerun
- Add invariant campaigns where logic spans multiple calls
- For protocols with state machines (AMMs, lending), use invariant tests with
,runs=512
in CI. Inline per‑test overrides in Solidity for your heaviest suites rather than raising global defaults. (getfoundry.sh)depth≈500
- Bring in Echidna for coverage‑guided, sequence fuzzing
- Add an Echidna harness contract (either your protocol or a thin wrapper) and a config with
. Run both property mode (corpusDir
functions return bool) and assertion mode (assert inside flows). Keep the corpus inechidna_*
. (secure-contracts.com)echidna-corpora/$APP/$SUITE
- Reuse and evolve corpora
- Upload the corpus to CI artifacts; warm‑start future jobs using
. Maintain a short allowlist of seeds you always inject (e.g., “liquidity added then reentrancy gadget invoked”). The official docs demonstrate seeding and modifying covered paths; treat seeds like first‑class test assets. (secure-contracts.com)--corpus-dir
- Convert Echidna crashes into Foundry tests automatically
- Use crytic/fuzz-utils to generate a Forge test from a failing Echidna sequence. This shifts discovery from “one‑off fuzz bug” to “permanent regression test” in your normal suite:
. (github.com)fuzz-utils generate --corpus-dir echidna-corpora/my-protocol -c MyHarness
- Track coverage and gate merges
- Run
and enforce minimum thresholds on changed lines. When you need precise formats for downstream tooling, pick the LCOV tracefile version explicitly withforge coverage --report lcov
. (getfoundry.sh)--lcov-version
- Scale fuzzing when needed
- For large campaigns, shard Echidna by function allowlists or by seqLen ranges and point all shards at the same corpus directory in CI (read/write from a shared artifact bucket). Tools like echidna‑parade can orchestrate multi‑core runs and automatically manage corpora across workers. (github.com)
- Standardize containers for reproducibility
- Echidna’s official Docker image includes Foundry, Slither, and solc‑select. Using this single image avoids “works‑on‑my‑machine” mismatches in CI and lets you run
,echidna
, andforge
side‑by‑side. (github.com)slither
Minimal, real configs you can copy‑paste
1) foundry.toml with CI profile
[profile.default] src = "src" test = "test" libs = ["lib"] optimizer = true optimizer_runs = 500 [fuzz] runs = 256 dictionary_weight = 40 [invariant] runs = 512 depth = 500 dictionary_weight = 80 shrink_run_limit = 5000 [profile.ci.fuzz] runs = 1000 [profile.ci.invariant] runs = 1000 depth = 600
- depth/runs defaults and semantics are documented in the Foundry testing reference; tune conservatively and bump as hot spots stabilize. (getfoundry.sh)
2) Echidna harness and config
Solidity (excerpt):
// SPDX-License-Identifier: MIT pragma solidity ^0.8.24; contract Vault { mapping(address => uint256) public balances; function deposit() external payable { balances[msg.sender] += msg.value; } function withdraw(uint256 amt) external { require(balances[msg.sender] >= amt, "insufficient"); balances[msg.sender] -= amt; (bool ok,) = msg.sender.call{value: amt}(""); require(ok, "send failed"); } // Property-style invariant: total balance never negative function echidna_balance_never_negative() public returns (bool) { // The mapping is unsigned; we still guard for logical underflow bugs // (e.g., incorrect decrement paths discovered by Echidna sequences). return address(this).balance >= 0; } // Assertion-style: withdraw never increases contract balance function test_no_withdraw_increase(uint256 amt) public { uint256 beforeBal = address(this).balance; if (amt > 0 && balances[msg.sender] >= amt) { withdraw(amt); assert(address(this).balance <= beforeBal); } } }
echidna.yaml:
testMode: assertion coverage: true corpusDir: "echidna-corpora/vault" testLimit: 120000 seqLen: 120 sender: ["0x10000", "0x20000", "0x30000"] allContracts: true # Optional: denylist getters to bias toward state-changing calls filterBlacklist: true filterFunctions: - "Vault.balances(address)"
- corpusDir is persisted between runs; the CLI accepts
to override per job. (secure-contracts.com)--corpus-dir - filterFunctions/filterBlacklist are a practical way to exclude view-only noise. (blog.trailofbits.com)
3) GitHub Actions: warm‑start Echidna and publish corpus
name: fuzz on: [push, pull_request] jobs: echidna: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 # Restore previous corpus (if any) - name: Download last corpus artifact uses: actions/download-artifact@v4 with: name: vault-corpus path: echidna-corpora/vault continue-on-error: true # Run Echidna via the official action wrapper - name: Echidna uses: crytic/echidna-action@v2 with: files: . contract: Vault config: echidna.yaml format: json corpus-dir: echidna-corpora/vault # Publish updated corpus - name: Upload corpus uses: actions/upload-artifact@v4 if: always() with: name: vault-corpus path: echidna-corpora/vault
- The action’s inputs support corpus-dir directly, and JSON output is CI‑friendly. (github.com)
4) Converting crashes into Forge tests
When Echidna finds a failing sequence, preserve it in your Forge suite:
pipx install git+https://github.com/crytic/fuzz-utils fuzz-utils generate --corpus-dir echidna-corpora/vault -c Vault # This writes a Foundry test under ./test that replays the exact failure
- This path “locks in” bugs found by fuzzing as regression tests your team runs on every PR. (github.com)
Tuning tips we’ve seen pay off
- Use allContracts for realistic flows (e.g., token approvals before protocol interaction). This was previously called multi‑abi; use the newer config going forward. (secure-contracts.com)
- Shard long campaigns by seqLen ranges (e.g., 50–100, 100–200) and merge corpora; most deep bugs show up only after a specific prefix that coverage will preserve. (secure-contracts.com)
- For coverage in Forge, pick the right report target and tracefile version to satisfy your CI parser; LCOV v1 is widely compatible. (getfoundry.sh)
- Keep an eye on nightly changes in Forge’s coverage engine; pin stable in CI and move nightlies through a canary branch first. A 2025 regression illustrates why discipline matters. (github.com)
- Run Echidna in the official container when you want uniformity across solc/slither/foundry without hand‑rolling images. (github.com)
- If your team prefers “turn the dial to 11” fuzzing on a workstation, echidna‑parade orchestrates many Echidna processes and manages a shared corpus for you. (github.com)
What to expect when you implement this
- Faster feedback: Forge tests run in seconds; most PRs get immediate signal. (getfoundry.sh)
- Deeper bug discovery: Echidna’s coverage‑guided, sequence‑based fuzzing finds edge‑case interleavings you won’t hit with single‑call fuzz tests. The corpus keeps your discovery compounding. (secure-contracts.com)
- Reproducibility: Attested Foundry binaries, pinned versions, and Dockerized Echidna mean your CI and dev laptops behave the same. (getfoundry.sh)
Final checklist
- Install Foundry via GetFoundry.sh, verify binaries, pin stable in CI. (getfoundry.sh)
- Add foundry.toml with sensible defaults; inline config on the few tests that need more runs/depth. (getfoundry.sh)
- Turn on Forge coverage and gate merges with LCOV. (getfoundry.sh)
- Add Echidna with corpusDir and assertion/property tests; store corpora as artifacts. (secure-contracts.com)
- Convert Echidna failures into Forge regression tests with fuzz‑utils. (github.com)
- Use allContracts for realistic multi‑contract flows and seed corpora for critical scenarios. (secure-contracts.com)
If you adopt only these six steps, you’ll move from ad‑hoc testing to a high‑signal, reproducible, and continuously improving security posture—without slowing your developers down.
Like what you're reading? Let's build together.
Get a free 30‑minute consultation with our engineering team.

