Echidna
A property-based fuzzer for Ethereum smart contracts that uses grammar-based input generation to find violations of user-defined invariants.
Echidna is a smart contract fuzzer developed by Trail of Bits that specializes in property-based testing for Ethereum contracts. Unlike random fuzzers that just look for crashes, Echidna tests user-defined properties (invariants) by generating sequences of transactions and checking if any sequence can violate the specified properties. It uses grammar-based fuzzing and coverage-guided mutation to efficiently explore contract state spaces, making it one of the most powerful tools for finding logic bugs in DeFi protocols.
How Echidna Works
Echidna operates in three phases:
- Property definition: You write Solidity functions that return
trueif an invariant holds - Transaction generation: Echidna generates random sequences of function calls with random parameters
- Property checking: After each sequence, Echidna checks if any property returns
false
1contract TokenTest {2 Token token;34 constructor() {5 token = new Token();6 }78 // Echidna property: must always return true9 function echidna_total_supply_constant() public view returns (bool) {10 return token.totalSupply() == 1000000;11 }1213 // Echidna will call transfer() with random addresses and amounts14 function transfer(address to, uint256 amount) public {15 token.transfer(to, amount);16 }17}
If Echidna finds a sequence of calls that makes echidna_total_supply_constant return false, it reports the exact sequence as a counterexample.
Installation and Usage
1# Install via pip (recommended)2pip install echidna34# Or via Docker5docker pull trailofbits/echidna67# Run on a contract8echidna contract.sol --contract TestContract910# With config file11echidna . --config echidna.yaml
Basic config file (echidna.yaml):
1testMode: property2testLimit: 500003seqLen: 1004deployer: "0x10000"5sender: ["0x10000", "0x20000", "0x30000"]
Writing Echidna Properties
Echidna recognizes properties by naming convention:
1contract EchidnaTest {2 // Properties: functions starting with echidna_ that return bool3 function echidna_balance_never_negative() public view returns (bool) {4 // Solidity uint256 can't be negative, but you might check invariants like:5 return address(this).balance >= minimumBalance;6 }78 // Or check protocol invariants9 function echidna_pool_solvent() public view returns (bool) {10 return pool.totalAssets() >= pool.totalLiabilities();11 }12}
Property types:
echidna_*: Must always returntrueechidna_revert_*: Must always revert (for testing access control)
Echidna vs Other Fuzzers
| Feature | Echidna | Foundry Fuzz | Medusa |
|---|---|---|---|
| Language | Haskell | Rust | Go |
| Input generation | Grammar-based | Random + mutation | Grammar + parallel |
| Sequence testing | Native | Via invariant tests | Native |
| Speed | Medium | Fast | Fast |
| Corpus management | Built-in | Basic | Built-in |
| Shrinking | Advanced | Basic | Good |
Echidna's strength is its sophisticated shrinking—when it finds a bug, it minimizes the counterexample to the shortest possible sequence.
Stateful Invariant Testing
Echidna excels at stateful testing where bugs emerge from specific sequences:
1contract LendingTest {2 Lending lending;34 // Setup initial state5 constructor() {6 lending = new Lending();7 lending.deposit{value: 100 ether}();8 }910 // Actions Echidna can take11 function deposit(uint256 amount) public {12 lending.deposit{value: amount}();13 }1415 function borrow(uint256 amount) public {16 lending.borrow(amount);17 }1819 function repay(uint256 amount) public {20 lending.repay{value: amount}();21 }2223 // Invariant: protocol should never be insolvent24 function echidna_solvent() public view returns (bool) {25 return address(lending).balance >= lending.totalBorrowed();26 }27}
Echidna might find a sequence like:
deposit(1 ether)borrow(0.9 ether)deposit(0.1 ether)borrow(0.5 ether)← breaks solvency!
Advanced Features
Coverage-Guided Fuzzing
Echidna tracks code coverage and prioritizes inputs that explore new paths:
1coverage: true2corpusDir: "corpus"
The corpus directory saves interesting inputs for future runs, improving efficiency over time.
Assertion Mode
Test for specific assertions instead of properties:
1testMode: assertion
1function testWithdraw(uint256 amount) public {2 uint256 balanceBefore = token.balanceOf(address(this));3 vault.withdraw(amount);4 assert(token.balanceOf(address(this)) == balanceBefore + amount);5}
Optimization Mode
Find inputs that maximize or minimize a value:
1testMode: optimization
1function echidna_optimize_gas() public returns (int256) {2 // Echidna will find inputs that maximize this value3 return int256(gasleft());4}
Real-World Example: Finding a Reentrancy Bug
1contract VulnerableVault {2 mapping(address => uint256) public balances;34 function deposit() external payable {5 balances[msg.sender] += msg.value;6 }78 function withdraw() external {9 uint256 amount = balances[msg.sender];10 (bool success,) = msg.sender.call{value: amount}("");11 require(success);12 balances[msg.sender] = 0; // State update after external call!13 }14}1516contract EchidnaAttack {17 VulnerableVault vault;1819 constructor() {20 vault = new VulnerableVault();21 }2223 function deposit() public payable {24 vault.deposit{value: msg.value}();25 }2627 function attack() public {28 vault.withdraw();29 }3031 receive() external payable {32 if (address(vault).balance > 0) {33 vault.withdraw(); // Reentrant call34 }35 }3637 // This property will fail due to reentrancy38 function echidna_vault_solvent() public view returns (bool) {39 return address(vault).balance >= vault.balances(address(this));40 }41}
Echidna discovers the attack sequence: deposit() → attack() → reentrancy drains funds.
Integration with CI/CD
1# GitHub Actions example2- name: Run Echidna3 uses: crytic/echidna-action@v24 with:5 files: .6 contract: TestContract7 config: echidna.yaml
Best Practices
- Start simple: Begin with basic invariants, add complexity as needed
- Bound inputs: Use
require()to constrain parameters to realistic ranges - Multiple senders: Configure multiple sender addresses to test access control
- Long sequences: Increase
seqLenfor complex state machine bugs - Save corpus: Persist the corpus directory between runs for faster testing
- Combine with Medusa: Medusa offers parallel fuzzing that complements Echidna
Limitations
EVM-only: Echidna works on EVM bytecode, not other chains natively.
Property specification: You must know what properties to test—Echidna won't discover invariants for you.
External dependencies: Mocking external contracts requires setup work.
Time-dependent bugs: Testing time-based logic requires careful configuration of block timestamps.
Echidna is a cornerstone of smart contract security testing, used by leading security firms and protocols to find bugs before deployment. Combined with static analysis and manual review, it forms a robust security testing pipeline.
Articles Using This Term
Learn more about Echidna in these articles:
Related Terms
Fuzzing
Automated testing technique using randomly generated inputs to discover edge cases and vulnerabilities in smart contracts.
Invariant
A property or condition that must always hold true throughout a smart contract's execution, used as a basis for testing and formal verification.
Invariant Testing
Property-based testing approach verifying that critical protocol conditions remain true across all possible execution paths.
Medusa
A parallelized smart contract fuzzer based on Echidna's approach, offering faster execution through concurrent testing across multiple workers.
Foundry
Fast, portable Ethereum development framework written in Rust, featuring advanced testing and debugging capabilities.
Need expert guidance on Echidna?
Our team at Zealynx has deep expertise in blockchain security and DeFi protocols. Whether you need an audit or consultation, we're here to help.
Get a Quote

