À des fins éducatives uniquement ; ceci ne constitue pas un conseil en investissement. Une faille de réentrance peut causer une perte rapide et irréversible des actifs d’un contrat, et aucune défense ou aucun audit isolé ne prouve qu’un contrat est sûr.
Réponse directe
Une attaque par réentrance peut se produire lorsque la logique d’un contrat effectue un appel externe et que le destinataire le rappelle avant la fin de l’exécution initiale et le rétablissement de ses invariants. Si d’anciens soldes, parts, droits ou prix restent visibles, le callback peut répéter une action ou en influencer une autre à partir d’un état incohérent.
Le callback n’a pas besoin de rentrer dans la même fonction ni de transférer la monnaie native. Les hooks de jetons, callbacks de réception NFT, appels de coffre ou de stratégie et callbacks de prêt flash peuvent transférer le contrôle à du code non fiable. La réentrance peut traverser fonctions, contrats et modules ; la réentrance en lecture seule peut aussi amener un autre protocole à consommer une valeur transitoire.
Fonctionnement
Une exploitation typique suit cette séquence :
- L’attaquant entre dans une fonction qui modifie l’état et passe ses contrôles initiaux.
- Le contrat vulnérable appelle une adresse, un jeton ou un protocole avant de terminer sa comptabilité.
- Le code contrôlé par l’attaquant entre dans le contrat d’origine ou un contrat connecté pendant que l’ancien état reste visible.
- L’appel imbriqué répète un effet ou modifie l’état partagé, puis l’exécution revient avec une invariante rompue.
Le transfert de contrôle externe peut être explicite, comme un appel bas niveau, ou caché derrière un transfert de jetons, un hook de réception, un mint sûr, un adaptateur de stratégie ou une interface d’appel arbitraire. Un revert annule normalement l’arbre d’appels concerné, mais l’attaquant peut construire une séquence imbriquée réussie qui revient normalement après avoir extrait de la valeur ou corrompu la comptabilité.
Les défenses doivent se superposer :
- Appliquer vérifications-effets-interactions : valider d’abord, enregistrer tous les effets internes pertinents, puis interagir à l’extérieur en dernier.
- Utiliser une protection sur chaque point d’entrée partageant l’invariante protégée, pas seulement sur la fonction contenant l’appel évident.
- Préférer les retraits initiés par le bénéficiaire lorsque c’est adapté et réduire les appels aux jetons, destinataires, hooks, proxys et intégrations non fiables.
- Définir les invariantes entre contrats et tester les callbacks malveillants, chemins interfonctions, multicalls imbriqués et consommateurs en lecture seule.
- Associer la revue d’implémentation à la surveillance, la pause et la réponse aux incidents lorsque la conception le permet.
Exemple
Considérons un coffre qui envoie de la valeur et n’efface le solde de l’utilisateur qu’après l’appel :
function withdraw() external {
uint256 amount = balances[msg.sender];
require(amount > 0, "empty balance");
(bool ok, ) = msg.sender.call{value: amount}("");
require(ok, "transfer failed");
balances[msg.sender] = 0;
}Le destinataire obtient le contrôle alors que balances[msg.sender] contient encore l’ancien montant. Sa fonction de réception peut rappeler withdraw(), passer le même contrôle et demander un autre transfert. Mettre à jour le solde avant l’appel ferme cette fenêtre ; une protection peut refuser l’entrée imbriquée. Aucun changement ne prouve la sûreté de tout le système, car un autre point d’entrée ou contrat connecté peut exposer la même invariante inachevée.
Risques
- Une protection couvre une fonction tandis qu’une autre expose le même état.
- L’ordre local des appels est correct, mais une invariante entre contrats reste incohérente pendant le callback.
- Un jeton, destinataire, callback de prêt flash ou adaptateur de stratégie exécute du code externe de manière inattendue.
- Une fonction view publie un prix ou taux transitoire qu’un autre protocole utilise dans la même transaction.
- Une mise à niveau, un module ou un changement de disposition du stockage contourne ou corrompt le verrou initial.
- Les tests couvrent le retrait récursif mais omettent les chemins interfonctions, intercontrats et en lecture seule.
Pour l’utilisateur, un badge d’audit ou une protection contre la réentrance atteste d’un contrôle, pas d’une garantie. Les mises à niveau, intégrations et actions d’urgence privilégiées peuvent changer la surface d’attaque. Limitez les autorisations et l’exposition, vérifiez l’implémentation déployée et ne supposez pas que les pertes pourront être annulées.
Idées reçues
- La réentrance consiste seulement à répéter une fonction de retrait. Elle peut entrer dans une autre fonction ou un autre contrat, ou exposer des données incohérentes à un lecteur.
- Seuls les transferts de monnaie native déclenchent des callbacks. Les normes de jetons, hooks de réception et intégrations peuvent aussi exécuter du code externe.
- Une protection ou vérifications-effets-interactions rend le contrat sûr. Sa portée, tous les points d’entrée partagés et les invariantes entre contrats doivent encore être examinés.
- Un audit réussi exclut la réentrance. Les audits sont limités ; des changements ultérieurs et intégrations non examinées peuvent créer de nouveaux chemins.
Sujets connexes
- Contrat intelligent
- Audit de contrat intelligent
- Machine virtuelle Ethereum
- ERC-1155
- Norme de coffre ERC-4626
Sources
- Considérations de sécurité - Solidity Documentation (consulté: 2026-08-21)
- Sécurité des contrats intelligents - ethereum.org (consulté: 2026-08-21)
- ReentrancyGuard - OpenZeppelin Documentation (consulté: 2026-08-21)
- SC08:2026 Attaques par réentrance - OWASP Smart Contract Security (consulté: 2026-08-21)