← Back to Blog

Echidna Smart Contract Fuzzer: A Practical Security Testing Guide

2026-04-18 echidna fuzzing solidity security testing property testing invariants 2026

Echidna is the open-source property-based fuzzer by Trail of Bits that tests smart contracts by exploring thousands of input combinations to find violations of your invariants. Unlike static analysis, which reads code patterns, Echidna executes your contract repeatedly with randomized and evolved inputs, discovering edge cases and business logic flaws that static tools cannot detect.

If Slither finds "this function has no access control," Echidna finds "when users call these functions in this sequence, the protocol invariant breaks." This makes Echidna invaluable for testing DeFi protocols, AMMs, lending markets, and vaults where the real risk lies in broken assumptions, not simple coding errors.

This guide covers installation, writing your first invariant, testing vulnerable ERC-20 and vault contracts, understanding Echidna's output, and comparing it with Foundry's fuzzing capabilities.


What Echidna Is

Echidna is a property-based fuzzer designed specifically for Solidity. It doesn't look for crashes — it looks for property violations.

Key concepts:

What Echidna excels at:

What Echidna doesn't do:


Installation

Echidna is written in Haskell and distributed as a prebuilt binary. The easiest approach is Docker.

Docker ensures consistency across environments and avoids Haskell dependency hassles.

docker pull trailofbits/echidna:latest

# Run Echidna on a contract
docker run -it -v $(pwd):/src trailofbits/echidna /src/contracts/MyVault.sol

Option 2: Direct Binary (macOS / Linux)

Download the prebuilt binary from the Echidna releases page:

# macOS (Intel)
wget https://github.com/crytic/echidna/releases/download/v2.2.7/echidna-2.2.7-macos.tar.gz
tar xzf echidna-2.2.7-macos.tar.gz
sudo mv echidna /usr/local/bin/

# Linux x86
wget https://github.com/crytic/echidna/releases/download/v2.2.7/echidna-2.2.7-ubuntu-20.04.tar.gz
tar xzf echidna-2.2.7-ubuntu-20.04.tar.gz
sudo mv echidna /usr/local/bin/

# Verify installation
echidna --version

Option 3: Build from Source (Cargo)

Requires Rust and Cargo. Slower but gives you the latest development version:

git clone https://github.com/crytic/echidna.git
cd echidna
cargo build --release
./target/release/echidna --version

Writing Your First Invariant

Echidna invariants are Solidity functions that return bool. Echidna calls your contract repeatedly and checks that every invariant function always returns true. If an invariant ever returns false, Echidna has found a violation.

Invariant Function Pattern

// Invariant functions follow this pattern:
// - Name starts with "echidna_"
// - Take NO parameters
// - Return bool
// - Always return true (violations return false)

contract MyContract {
    uint256 public balance;

    // This is an invariant
    function echidna_balance_never_negative() public view returns (bool) {
        return balance >= 0;
    }
}

Echidna calls echidna_balance_never_negative() after every transaction. If it ever returns false, Echidna stops and reports a violation.

Basic Example: Simple Token

// SPDX-License-Identifier: MIT
pragma solidity 0.8.20;

contract SimpleToken {
    mapping(address => uint256) public balances;
    uint256 public totalSupply;

    constructor(uint256 initialSupply) {
        balances[msg.sender] = initialSupply;
        totalSupply = initialSupply;
    }

    function transfer(address to, uint256 amount) public {
        require(balances[msg.sender] >= amount, "insufficient balance");
        balances[msg.sender] -= amount;
        balances[to] += amount;
    }

    function mint(uint256 amount) public {
        balances[msg.sender] += amount;
        totalSupply += amount;
    }

    // Invariant: total supply should never exceed max
    function echidna_total_supply_bounded() public view returns (bool) {
        return totalSupply <= type(uint256).max;
    }

    // Invariant: no account balance should exceed total supply
    function echidna_no_balance_exceeds_supply() public view returns (bool) {
        return balances[msg.sender] <= totalSupply;
    }
}

When you run Echidna on this contract, it will randomly call transfer() and mint() with various amounts and addresses, checking that the invariants always hold.


Testing a Vulnerable ERC-20

Let's write a realistic example: an ERC-20 token with a hidden bug in how it tracks total supply.

Vulnerable Contract

// SPDX-License-Identifier: MIT
pragma solidity 0.8.20;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";

contract VulnerableToken is ERC20 {
    constructor() ERC20("BadToken", "BAD") {
        _mint(msg.sender, 1000e18);
    }

    // VULNERABLE: allows burning without updating total supply properly
    function burn(uint256 amount) public {
        balanceOf[msg.sender] -= amount;
        // BUG: forgot to decrement totalSupply!
        // totalSupply -= amount;
    }

    // Invariant: sum of all balances should equal totalSupply
    function echidna_total_supply_equals_sum_of_balances() public view returns (bool) {
        // In a real test, this would iterate over all possible accounts
        // For this example, we simulate two key accounts
        return balanceOf[msg.sender] + balanceOf[address(this)] <= totalSupply;
    }
}

When Echidna runs this, it will call burn(), then check the invariant. It will discover that burning breaks the invariant because the balance decreased but total supply didn't.

Echidna output (when it finds the bug):

Invariant "echidna_total_supply_equals_sum_of_balances" violated!

Counterexample sequence:
  1. burn(500000000000000000)  [called by: 0x0000...]

Property violation found.

Testing a DeFi Vault: ERC-4626

ERC-4626 vaults are complex — shares represent claims on assets, and the exchange rate must be protected. Let's write invariants for a vault:

// SPDX-License-Identifier: MIT
pragma solidity 0.8.20;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/token/ERC4626/ERC4626.sol";

contract VaultForTesting is ERC4626 {
    constructor(IERC20 asset) 
        ERC4626(asset) 
        ERC20("Vault", "vTKN") 
    {}

    // Invariant 1: shares should never exceed total supply
    function echidna_shares_never_exceed_total_supply() public view returns (bool) {
        return totalSupply() >= 0;  // Always true, but demonstrates the pattern
    }

    // Invariant 2: price per share should never decrease over time (for yield-bearing vaults)
    // Note: this requires state tracking across multiple Echidna runs
    uint256 private lastPricePerShare = 1e18;

    function echidna_price_per_share_never_decreases() public returns (bool) {
        uint256 assets = totalAssets();
        uint256 supply = totalSupply();

        if (supply == 0) {
            lastPricePerShare = 1e18;
            return true;
        }

        uint256 currentPrice = (assets * 1e18) / supply;
        bool result = currentPrice >= lastPricePerShare;
        lastPricePerShare = currentPrice;
        return result;
    }

    // Invariant 3: actual assets >= expected assets (vault can't over-promise)
    function echidna_assets_backed() public view returns (bool) {
        return asset.balanceOf(address(this)) >= totalAssets();
    }

    // Invariant 4: redeeming shares should always return non-zero assets (unless shares are zero)
    function echidna_redeem_returns_assets() public view returns (bool) {
        uint256 assets = totalAssets();
        uint256 supply = totalSupply();
        return supply == 0 || assets > 0;
    }
}

These invariants protect against:
1. Share minting bugs that exceed actual backing
2. Oracle price reversals that shouldn't happen
3. Asset miscounting
4. Redemption dead-ends where shares exist but can't be redeemed


Echidna Configuration

Echidna's behavior is controlled by .echidna.yaml in your project root.

Basic Configuration

# Number of fuzzing transactions to generate per property
testLimit: 10000

# Maximum sequence length (number of function calls per test)
seqLen: 30

# Fuzzing mode: "normal" (stateful) or "assertion" (transaction-level assertions)
testMode: "normal"

# Corpus directory for reproducible fuzzing
corpus: ".echidna-corpus"

# Seed for reproducibility (set to number for reproducible runs)
seed: 12345

# Only fuzz these functions (optional)
filterFunctions: 
  - "transfer"
  - "approve"
  - "burn"

# Contract to test (if multiple contracts in file)
contractName: "VaultForTesting"

# Solc version
solcVersion: "0.8.20"

# Gas limit per transaction
txGasLimit: 4000000

# Time limit in seconds
timeout: 30

# Verbosity: "normal", "verbose", or "quiet"
verbosity: "normal"

Advanced Configuration for DeFi

# For testing protocols with state evolution
testLimit: 50000
seqLen: 100

# Keep generated test cases that violate properties
corpus: ".echidna-corpus"

# Only test state-changing functions
filterFunctions:
  - "deposit"
  - "withdraw"
  - "transfer"

# Test all invariants
allProperties: true

# Increase mutation pressure
seed: null  # Random seed each run

# For protocols where transaction ordering matters
testMode: "normal"

# Don't simplify counterexamples (useful for understanding transaction sequences)
simplifyFailingTests: false

Reading Echidna Output

Successful Run (No Violations)

Echidna completed. No violations found.

All properties satisfied (5):
  * echidna_total_supply_equals_sum_of_balances
  * echidna_no_overflow_in_transfers
  * echidna_price_per_share_monotonic
  * echidna_shares_backed_by_assets
  * echidna_cannot_mint_arbitrarily

Transactions executed: 10000
Unique sequences explored: 5432
Max sequence length: 30

Property Violation

Invariant "echidna_price_per_share_monotonic" violated!

Counterexample (minimized):
  1. deposit(address(0x1234...), 1000000000000000000)
  2. approve(address(0x5678...), 500000000000000000)
  3. transfer(address(0xabcd...), 500000000000000000)

Property: echidna_price_per_share_monotonic
Result: false at transaction #3

Final state:
  totalAssets: 1000000000000000000
  totalSupply: 1500000000000000000
  pricePerShare: 0.666...

Analysis: Price per share decreased from 1.0 to 0.666 after transfer

Key elements to understand:

Corpus Files

Echidna saves interesting test cases to .echidna-corpus/ for regression testing. These files contain transaction sequences that reproduce specific behavior.


Comparing Echidna with Foundry Fuzzing

Both Echidna and Foundry have fuzzing capabilities. Here's how they differ:

Feature Echidna Foundry
Fuzzing Type Stateful property-based Fuzz testing (assertion-based)
Sequence Length Up to 100+ transactions Single transaction per test
Setup Complex (requires invariant functions) Simple (uses existing fuzz tests)
Protocol Testing Excellent for invariants Good for unit-level fuzzing
CI/CD Integration Manual invocation, Docker-friendly Native Forge integration
Counterexample Quality Highly minimized sequences Transaction-level traces
Configuration YAML-based, detailed tuning Command-line flags or config
Performance Slower (stateful execution) Faster (single-transaction)
Output Clarity Property-focused Test assertion-focused

When to Use Each

Use Echidna when:
- Testing DeFi protocols where transaction sequences matter
- Validating invariants (e.g., "total supply always equals sum of balances")
- You need to find edge cases in state evolution
- Testing vaults, AMMs, lending markets, or complex token mechanics

Use Foundry when:
- Testing individual functions or views
- Running in CI/CD pipelines (simpler integration)
- Testing contracts that don't depend on complex state sequences
- You want fast feedback in development
- Testing contracts with external dependencies that are hard to simulate

Best practice: Use both. Static analysis (Slither) catches code smells, Foundry tests catch simple bugs quickly, and Echidna finds protocol-level invariant violations.


Running Echidna in Practice

Step 1: Create .echidna.yaml

In your project root:

testLimit: 10000
seqLen: 30
testMode: "normal"
contractName: "VaultForTesting"
solcVersion: "0.8.20"

Step 2: Add Invariants to Your Contract

Add echidna_* functions to your contract or a testing harness:

contract VaultHarness is VaultForTesting {
    function echidna_invariant_1() public view returns (bool) {
        return totalSupply() > 0 || totalAssets() == 0;
    }
}

Step 3: Run Echidna

# Using Docker
docker run -it -v $(pwd):/src trailofbits/echidna /src/contracts/VaultHarness.sol

# Using local binary
echidna contracts/VaultHarness.sol

Step 4: Review Results


ContractScan Coverage: What Echidna Finds

Echidna complements static analysis and AI-powered scanning. Here's what each tool catches:

Vulnerability Class Echidna Static Analysis (Slither) AI Analysis
Protocol invariant violations Yes No Sometimes
Reentrancy in complex sequences Yes Partial Yes
Token accounting bugs Yes No Yes
Vault share inflation/rounding Yes No Yes
Price oracle attacks No No Yes
Access control misconfiguration Partial Yes Yes
Integer overflow/underflow No Yes Yes
Uninitialized storage No Yes Yes
Missing transfer verification No Yes Yes
Business logic flaws Yes No Yes

Summary:
- Echidna: Finds bugs in how functions interact (stateful behavior)
- Slither: Finds bugs in code structure (static patterns)
- AI Analysis: Finds context-aware logical flaws and novel attack vectors

For maximum coverage, run all three. ContractScan integrates these tools to give you defense in depth.


Common Pitfalls and Solutions

Echidna runs forever: Infinite loops or unbounded state growth. Add bounds or use filterFunctions in .echidna.yaml to skip problematic functions.

Invariant always passes: Invariant too weak or trivial. Test actual properties: echidna_total_supply_consistent() should verify real relationships.

Can't generate counterexamples: Contract needs external state (oracles, timestamps). Mock dependencies in a test harness contract.


Next Steps

Echidna is a powerful tool, but it requires careful invariant design. Start with simple contracts and basic invariants, then scale to protocol-level testing.

Resources:
- Echidna Documentation
- Property-Based Testing Guide
- Smart Contract Security Testing

Try it now: Use ContractScan to analyze your contracts with Echidna, Slither, and AI-powered detection. Get instant feedback on protocol invariants, token accounting, and vault security.


Important Notes

This post is for informational and educational purposes only. It does not constitute financial, legal, or investment advice. The security analysis provided is based on available data and automated tools, which may not capture all potential vulnerabilities. Always conduct a professional audit before deploying smart contracts.

Scan your contract for this vulnerability
Free QuickScan — Unlimited quick scans. No signup required.. No signup required.
Scan a Contract →