Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Property-based testing and fuzzing using Echidna for smart contracts. Includes invariant definition, corpus management, coverage analysis, and CI/CD integration for comprehensive security testing.
| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | 59% | 0% |
| case-03 | ✗→✓ | ▲ Improved | 13% | 0% |
| case-05 | ✗→✓ | ▲ Improved | -28% | 0% |
| case-07 | ✗→✓ | ▲ Improved | 37% | 0% |
| case-15 | ✗→✓ | ▲ Improved | 99% | 0% |
Property-based testing and fuzzing for smart contracts using Echidna, the premier smart contract fuzzer from Trail of Bits.
bash# Install via docker (recommended) docker pull ghcr.io/crytic/echidna/echidna:latest # Or download binary curl -LO https://github.com/crytic/echidna/releases/latest/download/echidna-Linux chmod +x echidna-Linux mv echidna-Linux /usr/local/bin/echidna # Verify echidna --version
solidity// contracts/Token.sol contract Token { mapping(address => uint256) public balances; uint256 public totalSupply; function transfer(address to, uint256 amount) external { require(balances[msg.sender] >= amount); balances[msg.sender] -= amount; balances[to] += amount; } } // contracts/TokenTest.sol contract TokenTest is Token { constructor() { balances[msg.sender] = 10000; totalSupply = 10000; } // Echidna property: name starts with echidna_ function echidna_totalSupply_constant() public view returns (bool) { return totalSupply == 10000; } function echidna_balance_under_total() public view returns (bool) { return balances[msg.sender] <= totalSupply; } }
soliditycontract TokenAssertions is Token { function transfer(address to, uint256 amount) external override { uint256 balanceBefore = balances[msg.sender] + balances[to]; super.transfer(to, amount); uint256 balanceAfter = balances[msg.sender] + balances[to]; // Assertion: conservation of tokens assert(balanceBefore == balanceAfter); } }
yaml# Test configuration testMode: property # property, assertion, exploration, overflow testLimit: 50000 seqLen: 100 shrinkLimit: 5000 # Contract configuration deployer: "0x10000" sender: ["0x10000", "0x20000", "0x30000"] psender: "0x10000" # Corpus configuration corpusDir: "corpus" coverage: true coverageFormats: ["html", "lcov"] # Filtering filterBlacklist: true filterFunctions: ["excludedFunction"] # Advanced codeSize: 0xffff gasLimit: 10000000 prefix: "echidna_" # Assertion mode specific checkAsserts: true # Workers workers: 4
bash# Basic run echidna contracts/TokenTest.sol --contract TokenTest # With config echidna contracts/TokenTest.sol --contract TokenTest --config echidna.yaml # Assertion mode echidna contracts/TokenTest.sol --contract TokenTest --test-mode assertion # Multi-ABI mode (test multiple contracts) echidna . --contract TokenTest --crytic-args "--compile-all"
soliditycontract TimeBased { uint256 public startTime; uint256 public lockedUntil; constructor() { startTime = block.timestamp; lockedUntil = block.timestamp + 1 days; } function withdraw() external { require(block.timestamp >= lockedUntil); // withdraw logic } function echidna_locked_before_time() public view returns (bool) { // Echidna can manipulate block.timestamp return block.timestamp < lockedUntil || true; // simplified } }
soliditycontract EchidnaTest { Token token; Staking staking; constructor() { token = new Token(); staking = new Staking(address(token)); } function stake(uint256 amount) public { token.approve(address(staking), amount); staking.stake(amount); } function echidna_staking_invariant() public view returns (bool) { return staking.totalStaked() <= token.totalSupply(); } }
soliditycontract AMMTest is AMM { function echidna_constant_product() public view returns (bool) { // k = x * y should be constant (or increase) uint256 currentK = reserveX * reserveY; return currentK >= initialK; } function echidna_no_free_tokens() public view returns (bool) { // Total tokens in pool >= total LP tokens value return reserveX + reserveY >= totalLPTokens; } function echidna_price_bounds() public view returns (bool) { // Price should be within reasonable bounds uint256 price = (reserveX * 1e18) / reserveY; return price > 0 && price < type(uint256).max / 1e18; } }
yaml# echidna.yaml coverage: true coverageFormats: ["html", "lcov", "txt"] corpusDir: "corpus"
bash# Run with coverage echidna contracts/Test.sol --contract Test --config echidna.yaml # View HTML coverage open corpus/covered.html
corpus/
├── coverage/
│ ├── covered.txt
│ └── covered.html
├── reproducers/
│ └── failing_test.txt
└── corpus/
└── sequence_1234.txtbash# Replay a failing sequence echidna contracts/Test.sol --contract Test --corpus-dir corpus --replay
yamlname: Echidna Fuzzing on: [push, pull_request] jobs: echidna: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Install Foundry uses: foundry-rs/foundry-toolchain@v1 - name: Compile contracts run: forge build - name: Run Echidna uses: crytic/echidna-action@v2 with: files: contracts/Test.sol contract: Test config: echidna.yaml test-limit: 10000
yaml# echidna-extended.yaml testLimit: 1000000 timeout: 86400 # 24 hours workers: 8
bash# Run extended campaign echidna . --contract Test --config echidna-extended.yaml
| Process | Purpose | |---------|---------| | smart-contract-fuzzing.js | Primary fuzzing | | invariant-testing.js | Invariant verification | | smart-contract-security-audit.js | Security testing | | amm-pool-development.js | DeFi invariants | | lending-protocol.js | Protocol invariants |
yamlgasLimit: 100000000 # Increase limit
bash# Use crytic-compile directly echidna . --crytic-args "--foundry-compile-all"
yamlworkers: 8 # Increase parallelism shrinkLimit: 1000 # Reduce shrinking
skills/slither-analysis/SKILL.md - Static analysisskills/mythril-symbolic/SKILL.md - Symbolic executionskills/foundry-framework/SKILL.md - Forge invariant testingagents/solidity-auditor/AGENT.md - Security auditorOther measured skills in the registry, with their headline benchmark lift.