7Block Labs
Blockchain Development

ByAUJay

GetFoundry.sh, Echidna --corpus-dir Flag, and Building a Testing Strategy for Blockchain With Foundry and Echidna

A Handy Guide for Decision-Makers: Fast and Reliable Smart Contract Testing with Foundry and Echidna

If you’re a decision-maker looking to set up a smart contract testing stack that works quickly and reliably, you’ve come to the right place. This playbook walks you through the process of combining Foundry and Echidna, ensuring you have everything you need at your fingertips.

Installation Steps Made Easy

First up, let’s nail down the installation process. We’ll break it down into clear, manageable steps so you can get everything up and running without a hitch.

Unpacking Echidna’s --corpus-dir

Next, we’ll dive into the real value of Echidna’s --corpus-dir. Understanding this feature is crucial for optimizing your testing efforts, so we’ll explain what it does and how to leverage it effectively.

CI Patterns You Can Use

Continuous Integration (CI) is the name of the game for teams wanting to stay agile. We’ll share practical CI patterns that are perfect for integrating your smart contract testing into your workflow.

Ready-to-Use Configs

Lastly, we’ll provide some concrete configurations that your team can adopt right away. This way, you won’t have to start from scratch--you can just plug and play!

Let’s jump into the details and get your smart contract testing setup thriving!


Why these two tools belong in the same test strategy

  • Foundry is your go-to toolkit for super-fast unit, fuzz, and invariant tests in Solidity. Plus, it offers reproducible local chains and coverage. Think of it as the essential daily tool for developers. Check it out here.
  • Echidna takes things up a notch with coverage-guided, sequence-based fuzzing and persistent corpora. The awesome corpus feature lets you scale your efforts beyond just one-off runs, so you can keep using those “hard-won” inputs across different machines and over time. Learn more about it here.

Below, we’ll walk you through setting up Foundry the right way using GetFoundry.sh. We’ll also dive into how Echidna’s --corpus-dir functions in real-world scenarios, and how to combine these tools into a sleek, modern pipeline with great results.


GetFoundry.sh: the right way to install and pin your toolchain

Foundry's official installer, known as foundryup, can be easily obtained through GetFoundry.sh. If you're setting this up on a new machine, here’s what you need to do:

# 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 legit. When you use foundryup to install a release, it checks the binary hashes with the attestations from GitHub artifacts. If you want to double-check things yourself, you can easily do that with GitHub's CLI:
    gh attestation verify --owner foundry-rs $(which forge). (getfoundry.sh)
  • If you’re looking to experiment with Tempo’s Foundry fork, just run: foundryup -n tempo. This is super handy for specific requirements, but make sure to stick with one distribution when it comes to your CI. (getfoundry.sh)
  • Uninstalling is a breeze: just delete your ~/.foundry directory and the PATH entry. (getfoundry.sh)
  • If you prefer Docker for a clean CI setup, pull the latest version with: docker pull ghcr.io/foundry-rs/foundry:latest. (getfoundry.sh)

Pro tip for stability in large teams

  • Make sure to pin to stable in CI. Nightlies can change rapidly; for instance, a coverage regression popped up on August 27, 2025, when switching from 1.3.2‑stable to a nightly. Think of nightlies as something you opt into, and be sure to clearly control 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

When you're working on a project, it's super helpful to set up profiles that focus on boosting your speed while developing locally and ensuring thoroughness during Continuous Integration (CI).

Keep in mind that defaults can change over time, so always refer to the documentation as your go-to source for the most accurate option names and defaults. This way, you'll stay on top of any updates!

# 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"
  • When we talk about depth, we're referring to the number of calls made during a run. A maximum of 500 is usually a good upper limit for continuous integration on more complex protocols. You can find the defaults and environment variables detailed in the testing reference. (getfoundry.sh)
  • If you want to tweak the configuration for specific tests, you can do so right in the Solidity comments. This way, you can bump up the runs/depth for those resource-heavy tests without dragging down the entire suite:
    /// forge-config: ci.fuzz.runs = 2000 (Just make sure to apply this only to the tests that really need it.) (getfoundry.sh)

Invariant test harness patterns that scale

  • Group function targets: When you're setting up your test contract, make sure to use selectors to direct the fuzzer towards state-advancing calls. It's a good idea to leave out view and pure getters from your targets.
  • Use vm.assume: Instead of sticking with “fail_on_revert = true” for everything, leverage vm.assume to slice away impossible paths. It helps cut down on those annoying false negatives.
  • Keep regressions easy to reproduce: To make your life easier, enable failure persistence. You can then rerun tests by using forge test --rerun or by referencing the input file that Forge recorded. You can find all the options and details on failure replay right alongside the coverage and display flags. Check it out here!

Coverage that CI can gate on

Generate LCOV for Platforms like Codecov/GitLab

If you’re looking to generate LCOV reports for platforms like Codecov or GitLab, you’re in the right place! Here’s a step-by-step guide to help you through the process.

Step 1: Install LCOV

First things first, make sure you have LCOV installed on your system. If you haven’t installed it yet, you can do so by running:

sudo apt-get install lcov

This command works great for Debian-based systems. If you’re on a different OS, check out the LCOV installation guide for more details.

Step 2: Create a Coverage Report

Once LCOV is installed, you’ll want to generate a coverage report from your test suite. Let’s use lcov command to do that:

lcov --capture --directory coverage --output-file coverage.info

Replace coverage with the path where your coverage data is stored. This command will create a file called coverage.info.

Step 3: Filter the Report

It's a good idea to clean up your report by removing unnecessary files or directories (like your test files). You can filter them out with:

lcov --remove coverage.info '*tests*' --output-file coverage.info

This command will exclude any files or directories with "tests" in their names from your report.

Step 4: Generate the HTML Report

Now that your coverage data is clean and ready to go, let’s turn it into a nice HTML report. This makes it easier to visualize:

genhtml coverage.info --output-directory out

The out directory will contain your HTML report. Open the index.html file in your browser to see the results!

Step 5: Upload to Codecov or GitLab

For Codecov

To upload your report to Codecov, you’ll typically use a bash command like this:

bash <(curl -s https://codecov.io/bash) -f coverage.info

Make sure your Codecov token is set up in your environment if you’re working in a CI/CD pipeline.

For GitLab

If you're using GitLab, it’s pretty straightforward. Just add the following lines to your .gitlab-ci.yml file:

coverage: '/^\d+\.\d+%/'
reporting:
  coverage_report:
    coverage_format: lcov
    path: coverage.info

With this configuration, GitLab will automatically detect your coverage reports.

Conclusion

And that’s it! You’ve successfully generated and uploaded an LCOV report for Codecov or GitLab. If you run into any hiccups along the way, feel free to reach out. Happy coding!

forge coverage --report lcov --lcov-version 1
  • You can mix it up with summary, debug, or even bytecode-level coverage. If you're into tooling, go for LCOV. For some helpful tips for developers, choose summary, and if you need to troubleshoot, debug is the way to go. Check it out here: (getfoundry.sh)

Echidna’s --corpus-dir: what it buys you and how to use it

Echidna’s corpus is like a treasure chest of transaction sequences that really boosts coverage. Keeping it around means a few great things:

  • Quicker runs down the line (no need to start from square one).
  • You can replicate and share it between laptops or CI containers with ease.
  • It gives you the chance to kick off future campaigns using those “rare” sequences from past efforts.

You can choose to set the directory either in the config file or through the command line interface (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

You can either run it with just a single file or go for your Foundry project root by using 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
  • The corpusDir in the config is basically the same as the CLI flag --corpus-dir; you'll find both are supported in the latest Echidna versions. (secure-contracts.com)
  • If you're diving into Foundry repos, you’re in for a treat with crytic-compile--just run echidna . and you’re good to go! (github.com)
  • Heads up! The multi-abi option was changed to allContracts in Echidna 2.1.0; while both might still work at the moment, it’s best to stick with allContracts moving forward. (github.com)

Seeding and curating your corpus

  • Kick things off by gathering coverage in a “neutral” run. After that, take those sequences and tweak them a bit to create some seeds for deep edges. The official guide walks you through how to copy a covered path file and adjust the arguments so that a property fails right away on your next run--this is an awesome way to test your assumptions and generate those regression seeds. (secure-contracts.com)
  • Make sure to save your corpus directory as a CI artifact, and it’s a good idea to keep corpora for each branch separate. Then, you can pull it in during future runs to get a warm start. GitHub Actions wrappers make it super easy to access the corpus-dir directly. (github.com)

Targeting, filtering, and addressing complex protocols

  • Check out using filterFunctions and filterBlacklist to manage which methods are allowed or denied during your campaigns. This helps cut down on unnecessary noise and keeps your state from drifting. You can dive deeper into this over at Trail of Bits.
  • When you’re in assertion mode, you can actually assert within multi‑call sequences. This is super useful for making sure certain checks are done mid-transaction. If you're looking for some solid examples, the Crytic properties repository has got production-grade configs for ERC‑20 and ERC‑721 that highlight assertion mode, senders, and how corpus is used. Check it out on GitHub.

On‑chain and multi‑contract testing

  • The allContracts feature allows Echidna to interact with functions in the contracts that your harness has deployed (think token approvals before protocol calls). It's a better option than trying to mock those pesky “user-side” flows yourself. You can check out more about it here.
  • If you want to test your code on actual deployments, you can grab it via RPC or Etherscan. Just set rpcUrl/rpcBlock and ETHERSCAN_API_KEY in your config. This is super handy for hunting down regressions across historical states. For more details, head over here.

A concrete, step‑by‑step testing strategy (that teams can adopt this quarter)

  1. Pin and verify your toolchain
  • Kick things off by installing Foundry using foundryup. Make sure to pin the stable version in your CI. To verify the provenance of forge, check out the GitHub attestations. It’s a good idea to set up a “bump‑nightly” job outside of the main workflow to catch any regressions early on. (getfoundry.sh)
  1. Set Up Quick Unit/Fuzz Feedback with Foundry
  • Create your unit tests and simple fuzz tests in Solidity. Kick things off with [fuzz].runs = 256, and bump it up for those high-risk modules. Make sure to save and replay failures (--rerun) to stay on top of feedback. Check it out here: (getfoundry.sh)
  1. Include invariant campaigns that cover multiple calls
  • For protocols that use state machines (like AMMs and lending), go ahead and use invariant tests with runs=512 and depth≈500 in your CI. It’s a good idea to add inline per-test overrides in Solidity for your most demanding test suites instead of bumping up the global defaults. Check it out here: (getfoundry.sh)
  1. Get Echidna rolling for coverage-guided sequence fuzzing
  • Start by adding an Echidna harness contract. You can either use your protocol directly or create a simple wrapper around it. Don't forget to set up a configuration file with the corpusDir. You'll want to run it in both property mode (where echidna_* functions return a boolean) and assertion mode (where you have asserts inside your flows). Keep your corpus organized in echidna-corpora/$APP/$SUITE. For more details, check out secure-contracts.com.

5) Reuse and Evolve Corpora

  • Go ahead and upload your corpus to CI artifacts. When you're ready to kick off future jobs, just use --corpus-dir to give them a warm start. It’s a good idea to keep a small allowlist of seeds that you always want to include--think of phrases like “liquidity added then reentrancy gadget invoked.” The official docs show you how to seed and tweak the covered paths, so treat those seeds like they're top-notch test assets. Check it out here: (secure-contracts.com)

6) Turn Echidna Crashes into Foundry Tests Automatically

  • Leverage crytic/fuzz-utils to create a Forge test based on a failing Echidna sequence. This way, you’re not just spotting a “one-off fuzz bug,” but you’re embedding that knowledge into your regular test suite as a “permanent regression test.” Just run this command:
    fuzz-utils generate --corpus-dir echidna-corpora/my-protocol -c MyHarness
    Check it out on GitHub!

7) Track Coverage and Gate Merges

  • To track coverage, run forge coverage --report lcov and set minimum thresholds for any changed lines. If you need specific formats for tools later on, make sure to specify the LCOV tracefile version you want using --lcov-version. Check out more info at (getfoundry.sh).

8) Scale fuzzing when needed

  • When you're running large campaigns, consider sharding Echidna based on function allowlists or seqLen ranges. Just point all those shards to the same corpus directory in CI, so they can read from and write to a shared artifact bucket. Tools like echidna-parade can help you set up multi-core runs and easily manage corpora across different workers.

9) Standardize containers for reproducibility

  • Echidna's official Docker image comes packed with Foundry, Slither, and solc‑select. By using this one image, you can steer clear of those annoying “works‑on‑my‑machine” problems in CI and easily run echidna, forge, and slither all at once. Check it out on GitHub!

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
  • You can find the defaults and semantics for depth/runs in the Foundry testing reference. It’s best to adjust these settings carefully and increase them as any hot spots calm down. Check it out here: (getfoundry.sh)

2) Echidna harness and config

Solidity (excerpt)

Solidity is a programming language specifically designed for writing smart contracts on blockchain platforms, particularly Ethereum. If you're diving into the world of blockchain, you'll definitely want to get a handle on Solidity.

Why Use Solidity?

  • Smart Contracts: At its core, Solidity allows developers to create smart contracts that automatically execute transactions when certain conditions are met.
  • Ethereum: It's the go-to language for Ethereum, which is the most popular platform for decentralized applications (dApps).
  • Strongly Typed: Solidity is statically typed, meaning you need to define the types of variables before using them, which can help catch bugs early.

Key Features

  • Inheritance: Solidity supports inheritance, allowing developers to create complex contract structures and reduce code duplication.
  • User-Defined Types: You can create custom types to better suit your application's needs.
  • Libraries: Reusable libraries make it easier to share common code across different smart contracts.

Getting Started

To kick things off, you'll want to set up your environment:

  1. Install Node.js: You'll need Node.js. You can download it from Node.js official site.
  2. Download Truffle: This is a development framework for Ethereum. You can install it using npm:
    npm install -g truffle
  3. Create a New Project: Once you’ve got Truffle set up, you can create a new project by running:
    truffle init

Learn More

If you’re really interested in digging deeper into Solidity, check out the official documentation at Solidity Documentation.

Example Code

Here’s a quick example of a simple smart contract written in Solidity:

pragma solidity ^0.8.0;

contract SimpleStorage {
    uint256 number;

    function store(uint256 _number) public {
        number = _number;
    }

    function retrieve() public view returns (uint256){
        return number;
    }
}

This code snippet sets up a contract that can store and retrieve a number, just to give you a taste of how Solidity works.

Conclusion

So, there you have it! Solidity is a powerful tool for anyone looking to get into blockchain development. With its unique features and strong community support, you'll find plenty of resources to help you master it. Happy coding!

// 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);
        }
    }
}
type: echidna
name: Echidna
description: The echidna, a unique egg-laying mammal found in Australia and New Guinea, has spiny fur and a long snout. It's part of the monotreme group that includes the platypus, and it's known for its adorable appearance and quirky behavior.
habitat:
  - forests
  - grasslands
  - scrublands
diet:
  primary:
    - ants
    - termites
  secondary:
    - fruits
    - insects
behavior:
  - nocturnal
  - solitary
  - excellent burrowers
reproduction:
  mating_season: July to September
  gestation_period: 22 days
  egg_laying: 1-3 eggs
  young_development: 
    - hatchling stage: 10 days post-laying
    - nursing: 3 months
conservation_status: Least Concern
fun_facts:
  - Echidnas can dig incredibly fast when they're trying to escape danger!
  - They don't have teeth but use their spiny tongue to slurp up ants and termites.
  - These little guys can live for over 50 years in the wild!
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)"
  • The corpusDir sticks around even after you finish your runs, but if you need to change it for a specific job, you can use the --corpus-dir option. Check out the details here.
  • If you're looking to cut out the clutter of view-only functions, the filterFunctions and filterBlacklist options are super handy. You can dive into more information about it here.

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 takes inputs straight from corpus-dir, and the JSON output is perfect for CI. Check it out on GitHub!

4) Converting crashes into Forge tests

When Echidna comes across a sequence that’s not working out, make sure to save 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
  • By using this approach, you "lock in" any bugs identified during fuzzing as regression tests that your team will run with every PR. (github.com)

Tuning tips we’ve seen pay off

  • Make sure to use allContracts for more realistic flows, like handling token approvals before diving into protocol interactions. This used to be called multi‑abi, so just a quick heads-up to use the newer config from now on. (secure-contracts.com)
  • When you’re dealing with long campaigns, it’s a good idea to break them up by seqLen ranges (like 50-100 or 100-200) and then merge the corpora. Most deep bugs tend to pop up after a certain prefix, which coverage will help maintain. (secure-contracts.com)
  • For coverage in Forge, don’t forget to choose the right report target and tracefile version that’ll suit your CI parser; LCOV v1 works great for compatibility. (getfoundry.sh)
  • Keep an eye on the nightly changes in Forge’s coverage engine. It’s smart to pin a stable version in CI and test the nightlies in a canary branch first. A regression from 2025 really highlights why sticking to this discipline is important. (github.com)
  • If you're looking for consistency across solc/slither/foundry, running Echidna in the official container is the way to go. It keeps things uniform without any hassle of creating your own images. (github.com)
  • For those teams who are all about “turning the dial to 11” on fuzzing from a workstation, echidna‑parade is perfect. It takes care of orchestrating multiple Echidna processes and manages a shared corpus for you. (github.com)

What to expect when you implement this

  • Quick Feedback: With Forge, your tests zoom by in just seconds, so most PRs get a quick response. Check it out here: (getfoundry.sh)
  • Better Bug Detection: Echidna uses coverage-guided, sequence-based fuzzing to uncover those tricky edge-case interleavings that single-call fuzz tests might miss. Plus, the corpus keeps building on your discoveries. Learn more at (secure-contracts.com)
  • Consistency Across Environments: Thanks to attested Foundry binaries, pinned versions, and a Dockerized Echidna setup, your CI and dev laptops will act just like one another. Get the details here: (getfoundry.sh)

Final checklist

  • First off, let's get Foundry installed using GetFoundry.sh. Make sure to check the binaries and pin the stable version in CI. You can find the details here.
  • Next, create a foundry.toml file with some sensible defaults. For those few tests that need a bit more depth, inline config will do the trick. More info here.
  • Don’t forget to enable Forge coverage and set up LCOV to gate your merges. Check it out here.
  • Let's add Echidna, including a corpusDir and some assertion/property tests. Make sure to store those corpora as artifacts. Details can be found here.
  • When you run into Echidna failures, convert them into Forge regression tests using fuzz‑utils. More on that here.
  • Lastly, utilize allContracts for more realistic multi-contract flows, and seed your corpora for those crucial scenarios. Check it out here.

By following these six simple steps, you can shift from random testing to a solid, reliable, and ever-improving security stance--while keeping your developers running at full speed.

Like what you're reading? Let's build together.

Get a free 30-minute consultation with our engineering team.

7BlockLabs

Full-stack blockchain product studio: DeFi, dApps, audits, integrations.

7Block Labs is a trading name of JAYANTH TECHNOLOGIES LIMITED.

Registered in England and Wales (Company No. 16589283).

Registered Office address: Office 13536, 182-184 High Street North, East Ham, London, E6 2JA.

© 2026 7BlockLabs. All rights reserved.