Aller au contenu principal
BlockchainMar 28, 2026

Deep EVM #19 : Test par propriétés pour smart contracts — Fuzzing avec Foundry

OS
Open Soft Team

Engineering Team

Au-delà des tests unitaires

Les tests unitaires vérifient des scénarios spécifiques, mais les smart contracts font face à des entrées adversariales de n’importe quelle adresse, avec n’importe quelle valeur, dans n’importe quel ordre. Le test par propriétés inverse le paradigme : au lieu de spécifier les sorties attendues pour des entrées spécifiques, vous définissez des propriétés qui doivent tenir pour TOUTES les entrées.

Fuzzing avec Foundry

Foundry intègre un fuzzer qui génère automatiquement des entrées aléatoires :

function testFuzz_transferPreservesSupply(
    address from,
    address to,
    uint256 amount
) public {
    // Setup
    vm.assume(from != to);
    vm.assume(amount <= token.balanceOf(from));

    uint256 supplyBefore = token.totalSupply();

    // Action
    vm.prank(from);
    token.transfer(to, amount);

    // Propriété : la supply totale ne change jamais
    assertEq(token.totalSupply(), supplyBefore);
}

Le préfixe testFuzz_ indique à Foundry de générer des entrées aléatoires pour les paramètres. Par défaut, 256 exécutions ; configurable dans foundry.toml.

Test d’invariants

Les tests d’invariants vont plus loin : Foundry appelle des fonctions aléatoires dans un ordre aléatoire et vérifie que les invariants tiennent après chaque séquence.

contract TokenInvariantTest is Test {
    Token token;
    Handler handler;

    function setUp() public {
        token = new Token();
        handler = new Handler(token);
        targetContract(address(handler));
    }

    function invariant_supplyEqualsBalances() public {
        uint256 totalBalances = token.balanceOf(alice) +
                                token.balanceOf(bob) +
                                token.balanceOf(charlie);
        assertEq(token.totalSupply(), totalBalances);
    }

    function invariant_noNegativeBalances() public {
        assertGe(token.balanceOf(alice), 0);
        assertGe(token.balanceOf(bob), 0);
    }
}

Le pattern Handler

Le Handler guide le fuzzer vers des séquences d’appels valides :

contract Handler is Test {
    Token token;

    constructor(Token _token) {
        token = _token;
    }

    function transfer(uint256 toSeed, uint256 amount) public {
        address to = actors[toSeed % actors.length];
        amount = bound(amount, 0, token.balanceOf(msg.sender));
        vm.prank(msg.sender);
        token.transfer(to, amount);
    }
}

Test différentiel Huff vs Solidity

Le pattern le plus puissant pour valider du Huff : vérifier que votre implémentation Huff produit des résultats identiques à une référence Solidity pour des millions d’entrées aléatoires.

function testFuzz_differentialAdd(
    uint256 a,
    uint256 b
) public {
    // Huff
    (bool huffOk, bytes memory huffData) = huffImpl.call(
        abi.encodeWithSignature("add(uint256,uint256)", a, b)
    );

    // Solidity (vérité terrain)
    (bool solOk, bytes memory solData) = address(solImpl).call(
        abi.encodeWithSignature("add(uint256,uint256)", a, b)
    );

    assertEq(huffOk, solOk);
    if (huffOk && solOk) {
        assertEq(huffData, solData);
    }
}

Avec 10 000 exécutions de fuzz, vous testez effectivement 10 000 entrées aléatoires. Si les deux implémentations divergent, Foundry affiche l’entrée problématique.

Stratégies de fuzzing avancées

  1. Fuzzing guidé par dictionnaire — Fournissez des valeurs intéressantes : 0, 1, MAX_UINT, frontières de type
  2. Fuzzing de séquences — Testez des séquences d’appels, pas juste des appels isolés
  3. Fuzzing avec état — Maintenez un état fantôme (ghost state) et vérifiez la cohérence
  4. Minimisation de contre-exemples — Foundry réduit automatiquement les entrées qui échouent

Conclusion

Le test par propriétés et le fuzzing sont des outils essentiels pour les smart contracts. Ils découvrent des bugs que les tests unitaires manquent en explorant un espace d’entrées exponentiellement plus grand. Pour les contrats Huff et Yul, le test différentiel contre une référence Solidity est la stratégie la plus efficace.