Attaque de réentrance : prévention en Solidity

Les attaques de reentrancy font partie des exploits de smart contracts les plus anciens — et encore parmi les plus rentables — en DeFi. Elles sont simples à comprendre, faciles à introduire par inadvertance et dévastatrices lorsqu’elles touchent des chemins comptables critiques comme les retraits, les liquidations et les claims de rewards. Même des équipes expérimentées livrent des vulnérabilités de reentrancy, car le bug se cache souvent dans des appels externes « normaux » : envoyer de l’ETH, transférer des tokens, appeler un vault ou interagir avec un AMM.

Cet article explique comment fonctionne une attaque de reentrancy, à quoi ressemble la « smart contract reentrancy » dans des systèmes réels, pourquoi elle reste pertinente après The DAO, et comment mettre en place exactement la prévention de la reentrancy en Solidity avec des patterns éprouvés (Checks-Effects-Interactions, pull payments, ReentrancyGuard et une conception prudente des appels externes). Nous ferons aussi le lien entre la reentrancy et des sujets voisins comme les attaques par flash loan, les risques liés à delegatecall, et les bonnes pratiques modernes de Solidity.

Chez Soken (soken.io), nous avons audité des centaines de contrats en production dans le lending, le staking, les bridges et la gouvernance. La reentrancy reste une cause racine récurrente ou un facteur aggravant — en particulier lorsque des protocoles intègrent de nouveaux tokens, hooks, callbacks ou primitives DeFi composables.


Qu’est-ce qu’une attaque de reentrancy (et pourquoi est-ce si dangereux) ?

Une attaque de reentrancy se produit lorsqu’un contrat effectue un appel externe avant d’avoir terminé ses propres mises à jour d’état, ce qui permet à la partie appelée de rappeler (« re-enter ») la fonction vulnérable et de répéter une action, comme retirer des fonds plusieurs fois.

La reentrancy est dangereuse car Ethereum est synchrone : lorsque votre contrat appelle un autre contrat, l’exécution bascule immédiatement vers ce contrat externe — avant la fin de votre fonction. Si votre contrat n’a pas encore mis à jour les soldes ou les flags de verrouillage, un attaquant peut exploiter cet état « entre-deux ».

Quotable (40–60 words):
A reentrancy attack exploits a contract that performs an external call before completing internal accounting. Because EVM calls are synchronous, the callee can re-enter the vulnerable function while state still reflects the “old” balances. This enables repeated withdrawals, double-claims, or bypassed limits—often draining TVL in one transaction.

Le modèle mental classique : « J’ai appelé l’extérieur trop tôt »

Un flux typiquement vulnérable ressemble à ceci :

  1. L’utilisateur appelle withdraw()
  2. Le contrat envoie de l’ETH/des tokens à l’utilisateur (appel externe)
  3. Le contrat met à jour balances[msg.sender] (trop tard)
  4. La fonction fallback/receive de l’attaquant re-entre dans withdraw() avant que l’étape 3 ne s’exécute

Même si Solidity et les outils se sont améliorés, la reentrancy apparaît encore dans : - les transferts d’ETH via call - les tokens ERC777 avec hooks - les safe transfers ERC721/1155 (onERC721Received, onERC1155Received) - les intégrations de vaults (ERC4626), callbacks de staking, distributeurs de rewards - les patterns inter-contrats « comptabilité + payout » - des flux DeFi complexes où des appels externes se produisent en plein milieu de la fonction


Comment une vulnérabilité de reentrancy fonctionne-t-elle réellement en Solidity ?

Une vulnérabilité de reentrancy existe lorsqu’une fonction (1) dispose d’un chemin appelable de l’extérieur et (2) effectue une interaction externe avant les changements d’état finaux, permettant une re-entrée dans une fonction sensible avec un état périmé.

En pratique, l’exploit repose sur le control flow : vous pensez envoyer de la valeur « à la fin », mais le contrat externe récupère le contrôle et peut tout faire — y compris vous rappeler.

Quotable (40–60 words):
A reentrancy vulnerability arises when a Solidity function makes an external call—sending ETH, transferring tokens, or calling another protocol—before it finalizes internal state. The attacker uses a fallback, token hook, or callback to re-enter the function while balances and limits remain unchanged, repeating a withdrawal or claim.

Exemple vulnérable : retrait d’ETH (le bug « style DAO »)

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

contract VulnerableBank {
    mapping(address => uint256) public balance;

    function deposit() external payable {
        balance[msg.sender] += msg.value;
    }

    function withdraw(uint256 amount) external {
        require(balance[msg.sender] >= amount, "insufficient");

        // INTERACTION first (vulnerable)
        (bool ok, ) = msg.sender.call{value: amount}("");
        require(ok, "send failed");

        // EFFECTS last (too late)
        balance[msg.sender] -= amount;
    }
}

Contrat attaquant qui re-entre via receive()

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

interface IVulnerableBank {
    function deposit() external payable;
    function withdraw(uint256 amount) external;
}

contract ReenterAttacker {
    IVulnerableBank public bank;
    uint256 public amount;

    constructor(address _bank) {
        bank = IVulnerableBank(_bank);
    }

    function attack() external payable {
        require(msg.value > 0, "need ETH");
        amount = msg.value;

        bank.deposit{value: msg.value}();
        bank.withdraw(amount);
    }

    receive() external payable {
        // Re-enter as long as the bank still has ETH
        if (address(bank).balance >= amount) {
            bank.withdraw(amount);
        }
    }
}

Ce qui se passe : la banque envoie de l’ETH, la fonction receive() de l’attaquant s’exécute, et withdraw() est rappelée avant l’exécution de balance[msg.sender] -= amount. L’attaquant vide la banque.

« Mais j’utilise des transferts ERC20 — suis-je à l’abri ? »

Pas nécessairement. transfer() d’ERC20 n’effectue généralement pas de callback, mais : - certains tokens peuvent être non standards (hooks type ERC777 ou patterns proxy) - votre code peut appeler un routeur externe, un vault ou un contrat de staking - les safe transfers ERC721/1155 appellent des hooks côté receiver - vous pouvez utiliser call ou des appels externes arbitraires dans la même fonction


Quels sont les principaux types de smart contract reentrancy (et où apparaissent-ils en DeFi) ?

Il existe plusieurs formes de smart contract reentrancy, notamment la re-entrée directe dans une fonction, la reentrancy inter-fonctions, et la read-only reentrancy — chacune étant pertinente avec la composabilité moderne de la DeFi.

Quotable (40–60 words):
Reentrancy is not only “withdraw twice.” Modern protocols face direct reentrancy (same function), cross-function reentrancy (re-entering a different function that shares state), and read-only reentrancy (manipulating intermediate state for pricing or checks). Any external call before final accounting can enable one of these variants.

Variantes courantes de reentrancy

  1. Reentrancy mono-fonction (classique)
    Re-entrer dans withdraw() à répétition.

  2. Reentrancy inter-fonctions (cross-function)
    withdraw() appelle l’extérieur, l’attaquant re-entre dans claimRewards() ou borrow() qui s’appuie sur un état partagé, partiellement mis à jour.

  3. Read-only reentrancy (lectures dépendantes de l’état)
    Même si vous n’écrivez pas d’état après l’appel, un attaquant peut exploiter une incohérence temporaire pour influencer :

  4. les calculs de price-per-share
  5. les vérifications de collateral
  6. la logique dépendante d’oracles
  7. les validations du type “isHealthy”

  8. Reentrancy via token-hook/callback

  9. ERC777 tokensReceived
  10. ERC721 onERC721Received
  11. ERC1155 onERC1155Received
  12. hooks custom dans des tokens upgradeables/proxy

Impact réel : incidents de reentrancy à connaître

  • The DAO (2016) : ~3.6M ETH drainés via reentrancy dans une fonction de split ; a déclenché le hard fork d’Ethereum.
  • Cream Finance (Oct 2021) : exploit majeur (~$130M rapportés) impliquant des mécaniques DeFi complexes ; même si ce n’était pas un « pur style DAO », cela montre comment la composabilité et les interactions externes amplifient le risque.
  • Divers incidents sur marketplaces NFT / staking (2021–2023) : reentrancy via hooks de réception ERC721 et logique de payout, causant à répétition des double-withdrawals et des erreurs de comptabilité lorsque les callbacks n’étaient pas pris en compte.

(La reentrancy est aussi souvent couplée à des flash loans : les attaquants empruntent du capital, manipulent l’état, re-entrent dans des fonctions critiques et remboursent dans une seule transaction — transformant un exploit « qui nécessite du capital » en un exploit sans capital.)


Prévention de la reentrancy en Solidity : les patterns qui fonctionnent vraiment

Une prévention efficace de la reentrancy en Solidity repose sur : (1) minimiser les appels externes, (2) ordonner correctement la logique, et (3) imposer des verrous explicites de reentrancy sur les chemins sensibles.

Quotable (40–60 words):
The most reliable reentrancy prevention combines three layers: apply Checks-Effects-Interactions so state updates happen before external calls; prefer pull payments over push payouts; and protect critical functions with a reentrancy guard (mutex). Also treat token callbacks and “safe” transfers as external calls with re-entry risk.

1) Checks-Effects-Interactions (CEI)

Corrigez l’exemple précédent en mettant à jour l’état avant d’envoyer l’ETH :

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

contract CEIBank {
    mapping(address => uint256) public balance;

    function deposit() external payable {
        balance[msg.sender] += msg.value;
    }

    function withdraw(uint256 amount) external {
        require(balance[msg.sender] >= amount, "insufficient");

        // EFFECTS first
        balance[msg.sender] -= amount;

        // INTERACTION last
        (bool ok, ) = msg.sender.call{value: amount}("");
        require(ok, "send failed");
    }
}

Attention : CEI est nécessaire mais pas toujours suffisant face à la reentrancy inter-fonctions si d’autres fonctions peuvent être re-entrées et s’appuient sur un état partagé.

2) Utiliser un Reentrancy Guard (mutex)

Le ReentrancyGuard d’OpenZeppelin est une mitigation standard, en particulier pour les chemins de withdrawal/claim/borrow.

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

import "@openzeppelin/contracts/security/ReentrancyGuard.sol";

contract GuardedBank is ReentrancyGuard {
    mapping(address => uint256) public balance;

    function deposit() external payable {
        balance[msg.sender] += msg.value;
    }

    function withdraw(uint256 amount) external nonReentrant {
        require(balance[msg.sender] >= amount, "insufficient");
        balance[msg.sender] -= amount;

        (bool ok, ) = msg.sender.call{value: amount}("");
        require(ok, "send failed");
    }
}

Important : appliquez nonReentrant à tous les points d’entrée qui touchent le même état sensible (par ex. withdraw, claim, exit, liquidate), ou structurez soigneusement les fonctions internes pour éviter les conflits de guard.

3) Préférer les pull payments aux push payments

Au lieu de « payer » à l’intérieur d’une fonction complexe, vous pouvez créditer un utilisateur et lui permettre de retirer séparément.

  • Avantages : moins d’appels externes dans la logique métier ; surface d’attaque réduite
  • Inconvénients : ajoute une étape UX ; nécessite une comptabilité rigoureuse et un withdraw protégé

4) Considérer les « token transfers » comme des appels externes dans les threat models

Même si ERC20 est généralement sûr, votre intégration peut ne pas l’être : - transfert de tokens ERC777 - appel d’un vault qui émet des shares - appel d’un router/aggregator - utilisation de safeTransferFrom pour des NFTs (callbacks !)

5) Éviter les appels externes arbitraires dans les flux privilégiés

Si la gouvernance ou un admin peut configurer des contrats cibles/callbacks, vous pouvez créer par accident des chemins de reentrancy qui contournent vos hypothèses. C’est encore plus dangereux combiné aux delegatecall risks in Solidity (où le contexte de storage est partagé).


Comparaison : CEI vs ReentrancyGuard vs Pull Payments (quoi utiliser et quand)

La meilleure approche dépend de l’objectif de la fonction, des besoins de composabilité et des contraintes UX. La plupart des protocoles en production utilisent plusieurs couches.

Quotable (40–60 words):
No single mitigation fits every protocol. Checks-Effects-Interactions prevents the most common “send then update” mistakes, but cross-function reentrancy may still exist. Reentrancy guards add a hard mutex for sensitive paths. Pull payments reduce external calls in core logic, trading off UX and requiring robust withdrawal accounting.

Mitigation What it does Strengths Weaknesses Best for
Checks-Effects-Interactions (CEI) Met à jour l’état avant les appels externes Simple, peu coûteux, bonne base Ne bloque pas toujours la reentrancy inter-fonctions ou read-only La plupart des fonctions qui modifient l’état avec appels externes
nonReentrant guard (mutex) Bloque la re-entrée pendant l’exécution Fort, explicite, largement utilisé Peut compliquer les appels internes ; doit couvrir tous les points d’entrée pertinents Retraits, claims, borrows, liquidations
Pull payments Crédite des soldes ; les utilisateurs retirent plus tard Minimise les appels externes dans la logique cœur Transaction supplémentaire pour l’utilisateur ; nécessite toujours un withdraw protégé Rewards, distributions de fees, remboursements
Restricted external calls Limite quels contrats peuvent être appelés Réduit les callbacks inattendus Composabilité réduite ; overhead de gouvernance Outils admin, hooks d’upgrade, exécution de stratégies
Careful token/NFT handling Traite les hooks comme vecteurs de re-entrée Évite les surprises liées aux callbacks Exige une connaissance approfondie des intégrations Marketplaces NFT, staking avec ERC777/4626

Checklist d’audit : comment trouver et tester la reentrancy dans des protocoles réels

On prévient la reentrancy dès la conception, mais on la prouve via des tests, des revues adversariales et des audits ciblés sur chaque chemin d’interaction externe.

Quotable (40–60 words):
To detect reentrancy, identify every external call (ETH sends, token transfers, router/vault interactions, NFT safe transfers), then trace whether state updates and invariants are finalized before the call. Test with malicious receiver contracts and fuzzing. Pay special attention to cross-function shared state and callback-enabled tokens.

Checklist pratique (orientée ingénieurs)

  • Cartographier tous les appels externes
  • call, transfer, send
  • token transfer/transferFrom
  • safeTransferFrom (ERC721/1155)
  • interactions vault/router/bridge
  • Repérer l’état partagé touché par plusieurs fonctions
  • balances, shares, debt, indices de rewards, snapshots
  • Vérifier l’ordre d’exécution
  • les mises à jour d’état critiques sont-elles faites avant l’appel externe ?
  • Ajouter des invariants
  • total assets == somme des balances (ou bornée)
  • cohérence shares * price-per-share
  • monotonie de la reward debt
  • Simuler des attaquants
  • receiver malveillant qui re-entre
  • token malveillant avec hooks
  • attaque financée par flash loan qui amplifie l’impact

Exemple : piège de reentrancy inter-fonctions (pattern)

Si withdraw() appelle l’extérieur, et que claimRewards() peut être re-entré et suppose un état « post-withdraw », vous pouvez obtenir : - double claims de rewards - contournement de cooldowns - comptabilité de reward debt cassée

Une mitigation courante consiste à : - centraliser les mises à jour de comptabilité dans une fonction interne - appliquer nonReentrant de manière cohérente sur tous les points d’entrée mutateurs d’état - repousser les interactions externes à la fin de l’exécution

Comment Soken audite typiquement la reentrancy (à quoi s’attendre)

Dans les audits Soken, nous : - construisons un external call graph et annotons les « fenêtres de re-entrée » - vérifions la conformité CEI et la couverture des mutex - testons les intégrations contre des standards avec callbacks (ERC777, ERC721/1155 safe transfers, ERC4626 vaults) - tentons des PoCs d’exploit sous Foundry pour valider la sévérité et confirmer les correctifs

C’est particulièrement important dans des designs DeFi exposés à flash-loan-attacks-defi, où les attaquants peuvent augmenter la taille des positions et pousser des cas limites dans une seule transaction atomique.


Conclusion : la reentrancy est évitable — si vous concevez pour la composabilité

La reentrancy reste un risque majeur pour les smart contracts, car la DeFi est composable par défaut : le « contrat externe » que vous appelez peut être un utilisateur, un token avec hooks, un vault, un router ou un callback contrôlé par un attaquant. La solution n’est presque jamais un seul « truc » — c’est de l’ingénierie disciplinée : ordre CEI, points d’entrée protégés, minimisation des appels externes et tests adversariaux.

Si vous déployez du staking, du lending, de la gouvernance, des bridges, ou tout contrat qui déplace des assets, vous devez considérer la reentrancy comme une contrainte de design dès le premier jour — pas comme un simple warning de lint à la fin.

Soken (soken.io) aide les équipes à renforcer des systèmes en production via smart contract auditing & penetration testing et des DeFi security reviews sur des vaults, du staking, de la gouvernance et des intégrations complexes. Si vous souhaitez une revue centrée sur la reentrancy (avec PoCs et tests Foundry), contactez-nous sur soken.io pour planifier un audit et livrer en toute confiance.

Frequently Asked Questions

Qu’est-ce qu’une attaque de réentrance dans les smart contracts ?

Une attaque de réentrance survient lorsqu’un contrat effectue un appel externe avant de mettre à jour son propre état. Le contrat de l’attaquant peut alors ré-entrer dans la fonction vulnérable et la répéter. Cela peut vider des fonds via des logiques de retrait ou de claim en exploitant des soldes incohérents dans la même transaction.

Comment identifier une vulnérabilité de réentrance dans du code Solidity ?

Repérez les appels externes (envoi d’ETH, appels à des tokens, AMM, vaults) effectués avant des mises à jour d’état critiques comme la décrémentation d’un solde ou l’incrémentation d’un nonce. Signalez aussi les callbacks (hooks ERC777, fonctions fallback/receive). Si une fonction peut être réexécutée en cours d’exécution, elle peut être réentrante.

Quelles sont les meilleures pratiques pour prévenir la réentrance en Solidity ?

Appliquez Checks-Effects-Interactions : validez les entrées, mettez à jour l’état interne, puis interagissez avec l’externe. Préférez les paiements “pull” aux transferts “push”. Ajoutez un verrou anti-réentrance (ex. OpenZeppelin ReentrancyGuard) sur les fonctions sensibles. Réduisez les appels externes dans la comptabilité et concevez les interfaces pour éviter les callbacks.

OpenZeppelin ReentrancyGuard empêche-t-il totalement les attaques de réentrance ?

ReentrancyGuard bloque de nombreux schémas de ré-entrée sur un même contrat, mais ne constitue pas une garantie de sécurité totale. La réentrance inter-fonctions, les callbacks de protocoles externes et une comptabilité incorrecte peuvent encore causer des pertes. Combinez un guard avec l’ordre CEI, des patterns pull-payment et une conception stricte des appels externes.