Comparaison·9 min de lecture·Par Solingo

Medusa vs Echidna — Duel du Fuzzing Smart Contract

Deux fuzzers property-based. Deux philosophies différentes. Lequel utiliser ?

# Medusa vs Echidna — Duel du Fuzzing Smart Contract

Fuzzing = laisser un bot casser votre code. Deux outils, deux approches.

Qu'est-ce que le Fuzzing ?

Fuzzing = tester avec des inputs random pour trouver des edge cases.

// Vous écrivez :

function transfer(address to, uint256 amount) external {

balances[msg.sender] -= amount;

balances[to] += amount;

}

// Fuzzer teste :

transfer(0x0, 0)

transfer(0x0, type(uint256).max)

transfer(msg.sender, 1)

transfer(attacker, totalSupply + 1) // ← trouve l'underflow !

// ... des millions de combinaisons

Property-based testing : définir des invariants qui doivent toujours tenir.

// Invariant : totalSupply = sum(balances)

function invariant_totalSupply() public view returns (bool) {

uint256 sum = 0;

for (uint i = 0; i < users.length; i++) {

sum += balances[users[i]];

}

return sum == totalSupply;

}

// Fuzzer appelle des fonctions random, puis vérifie l'invariant

// Si invariant casse → bug trouvé

Echidna

Echidna = fuzzer Haskell, par Trail of Bits (2018).

Installation

# Via Docker (recommandé)

docker pull trailofbits/echidna

# Ou binary

wget https://github.com/crytic/echidna/releases/download/v2.2.1/echidna-2.2.1-Linux.tar.gz

tar -xzf echidna-2.2.1-Linux.tar.gz

sudo mv echidna /usr/local/bin/

Setup

// EchidnaTest.sol

contract EchidnaTest {

Token token;

constructor() {

token = new Token();

}

// Echidna teste cette fonction avec inputs random

function test_transfer(address to, uint256 amount) public {

token.transfer(to, amount);

}

// Invariant : commence par "echidna_"

function echidna_total_supply() public view returns (bool) {

return token.totalSupply() == 1000000;

}

}

Config

# echidna.yaml

testMode: assertion # ou "property"

testLimit: 50000 # Nombre de tests

timeout: 600 # Secondes

coverage: true # Coverage-guided

shrinkLimit: 5000 # Shrinking iterations

deployer: "0x10000"

sender: ["0x10000", "0x20000", "0x30000"] # Addresses à utiliser

Run

echidna EchidnaTest.sol --config echidna.yaml

# Output :

# echidna_total_supply: passed! (50000 tests)

# ou

# echidna_total_supply: failed!

# Call sequence:

# 1. transfer(0x20000, 1000001) ← Bug trouvé

Medusa

Medusa = fuzzer Go, par Trail of Bits (2023). Successeur d'Echidna.

Installation

# Via Go

go install github.com/crytic/medusa@latest

# Ou binary

wget https://github.com/crytic/medusa/releases/download/v0.1.3/medusa-linux-x64

chmod +x medusa-linux-x64

sudo mv medusa-linux-x64 /usr/local/bin/medusa

Setup

// MedusaTest.sol

contract MedusaTest {

Token token;

constructor() {

token = new Token();

}

// Medusa teste cette fonction

function fuzz_transfer(address to, uint256 amount) public {

token.transfer(to, amount);

}

// Property : préfixe "property_"

function property_total_supply() public view returns (bool) {

return token.totalSupply() == 1000000;

}

}

Config

{

"fuzzing": {

"workers": 10,

"testLimit": 50000,

"timeout": 600,

"coverage": {

"enabled": true

}

},

"compilation": {

"platform": "crytic-compile"

}

}

Run

medusa fuzz --config medusa.json

# Output :

# [PASSED] property_total_supply

# ou

# [FAILED] property_total_supply

# Counterexample:

# fuzz_transfer(0x20000, 1000001)

Comparaison Technique

1. Vitesse

Benchmark : fuzzing Uniswap V2 pair (50k tests)

Echidna : 45 secondes (single-threaded)

Medusa : 12 secondes (10 workers parallel)

Winner : Medusa (3.75× faster)

Medusa = Go, parallèle. Echidna = Haskell, single-thread (legacy).

2. Coverage Guidance

// Contract avec branch difficile

function vulnerableFunction(uint256 x) public {

if (x == 0x123456789ABCDEF) { // Hard to hit

// Bug here

selfdestruct(payable(msg.sender));

}

}

// Echidna : coverage-guided

// - Mutate inputs pour maximiser code coverage

// - Trouve la valeur magique en ~10k tests

// Medusa : coverage-guided + optimistic

// - Essaye des valeurs "intéressantes" (0, max, min)

// - Trouve en ~5k tests

Winner : Medusa (meilleure stratégie).

3. Shrinking

Quand un bug est trouvé, le fuzzer "shrink" la sequence pour la simplifier.

Echidna trouve :
  • mint(user1, 1000)
  • transfer(user2, 500)
  • burn(user1, 600) ← Bug ici
  • Shrinking :

    • Essaye de retirer step 1 → bug persiste ?
    • Essaye de réduire amounts → bug avec burn(user1, 1) ?
    • Minimal : burn(user1, 1)

    Echidna shrinkLimit : 5000 iterations

    Medusa shrinkLimit : configurable (défaut 10000)

    Winner : Draw (qualité similaire)

    4. Intégration Foundry

    // Foundry invariant tests (natif)
    

    contract InvariantTest is Test {

    Token token;

    function setUp() public {

    token = new Token();

    }

    function invariant_totalSupply() public {

    assertEq(token.totalSupply(), 1000000);

    }

    }

    // Run :

    forge test --fuzz-runs 50000

    // Echidna/Medusa peuvent aussi lire les Foundry invariants

    echidna . --contract InvariantTest

    medusa fuzz --foundry-project .

    Winner : Medusa (meilleure intégration Foundry).

    Bugs Trouvés (Historique)

    Echidna

    - 2019 : Aragon bug (governance vote manipulation)
    
    • 2020 : Balancer bug (pool drain via reentrancy)
    • 2021 : Compound bug (collateral calculation overflow)

    Medusa

    - 2023 : Morpho bug (liquidation threshold bypass)
    
    • 2024 : Curve V2 bug (price oracle manipulation)
    • 2025 : [Votre protocol] (à venir)

    Winner : Echidna (plus ancien, plus de track record). Medusa rattrape vite.

    Quand Utiliser Lequel ?

    Utiliser Echidna Si

    ✅ Legacy codebase (déjà setup Echidna)
    

    ✅ Vous voulez le fuzzer le plus battle-tested

    ✅ Pas de contrainte de vitesse

    ✅ Vous aimez Haskell (lol)

    Utiliser Medusa Si

    ✅ Nouveau projet (2024+)
    

    ✅ Vous voulez la vitesse (CI/CD rapide)

    ✅ Multi-contract fuzzing complexe

    ✅ Foundry project (meilleure intégration)

    Exemple Complet : Vault Fuzzing

    // Vault.sol
    

    contract Vault {

    mapping(address => uint256) public balances;

    uint256 public totalDeposits;

    function deposit() external payable {

    balances[msg.sender] += msg.value;

    totalDeposits += msg.value;

    }

    function withdraw(uint256 amount) external {

    require(balances[msg.sender] >= amount);

    balances[msg.sender] -= amount;

    totalDeposits -= amount;

    payable(msg.sender).transfer(amount);

    }

    }

    // VaultFuzzTest.sol

    contract VaultFuzzTest {

    Vault vault;

    address[] users;

    constructor() {

    vault = new Vault();

    users.push(address(0x10000));

    users.push(address(0x20000));

    }

    function fuzz_deposit(uint256 userIndex, uint256 amount) public {

    address user = users[userIndex % users.length];

    vm.deal(user, amount);

    vm.prank(user);

    vault.deposit{value: amount}();

    }

    function fuzz_withdraw(uint256 userIndex, uint256 amount) public {

    address user = users[userIndex % users.length];

    vm.prank(user);

    try vault.withdraw(amount) {} catch {}

    }

    // Invariant : vault balance = totalDeposits

    function property_balance() public view returns (bool) {

    return address(vault).balance == vault.totalDeposits();

    }

    // Invariant : sum(balances) = totalDeposits

    function property_accounting() public view returns (bool) {

    uint256 sum = 0;

    for (uint i = 0; i < users.length; i++) {

    sum += vault.balances(users[i]);

    }

    return sum == vault.totalDeposits();

    }

    }

    Test avec Medusa

    medusa fuzz --target VaultFuzzTest --test-limit 100000
    
    

    # Output :

    # [PASSED] property_balance (100000 tests)

    # [PASSED] property_accounting (100000 tests)

    # Coverage : 98%

    Test avec Echidna

    echidna VaultFuzzTest.sol --config echidna.yaml --test-limit 100000
    
    

    # Output :

    # property_balance: passed! (100000 tests)

    # property_accounting: passed! (100000 tests)

    # Coverage : 96%

    Verdict

    2026 : Medusa gagne

    • Plus rapide (parallèle)
    • Meilleure intégration Foundry
    • Développement actif (Echidna en maintenance)

    Mais : Echidna reste valide si vous avez déjà un setup fonctionnel.

    Conseil Final

    Fuzzing = dernière ligne de défense, pas la première.

    1. Unit tests (Foundry)        → couvrir happy paths
    
  • Invariant tests (Foundry) → propriétés de base
  • Fuzzing (Medusa/Echidna) → edge cases obscurs
  • Audit (humain) → logique business
  • Bug bounty → incentives externes
  • Tous ensemble = defense in depth.

    Lancez Medusa sur votre code aujourd'hui. Vous serez surpris par ce qu'il trouve.

    Prêt à mettre en pratique ?

    Applique ces concepts avec des exercices interactifs sur Solingo.

    Commencer gratuitement