Sécurité et wallets · 6 min · démo interactive
L'attaque par réentrance rejouée en direct
Deux lignes dans le mauvais ordre ont coûté environ 3,6 millions d'ETH et scindé Ethereum en deux. Rejoue l'attaque, puis inverse ces lignes.
L’idée en une phrase
Une réentrance se produit quand un contrat envoie de l'argent avant de mettre à jour ses comptes, ce qui permet au destinataire de le rappeler pour se faire payer une seconde fois.
Imagine un guichetier qui te remet tes billets, puis se tourne pour noter le retrait. Tu demandes un nouveau retrait avant qu'il n'écrive. Ton solde affiche toujours le même montant, donc il paie encore. Sur Ethereum, ce tour de main est possible parce qu'un envoi d'ETH à un contrat exécute le code de ce contrat.
La démo
Étapes de l'exécution
- Les étapes apparaissent ici.
Sans JavaScript, voici le déroulé chiffré. La banque détient 20 ETH de ses clients. L'attaquant dépose 2 ETH et retire : la banque lui envoie 2 ETH sans avoir encore mis son solde à zéro. Il rappelle withdraw() pendant la réception et reçoit 2 ETH de plus, et ainsi de suite. Après 11 retraits imbriqués, la banque est vide : l'attaquant repart avec 22 ETH pour 2 ETH de mise.
Ce n'est pas une animation écrite d'avance. Les trois contrats sont du vrai bytecode, exécuté avec de vrais appels imbriqués dans notre mini-EVM pédagogique, un sous-ensemble limité d'Ethereum, et non un nœud complet. La banque est un cas d'école : elle reproduit le mécanisme de The DAO, pas son code ni ses montants. Le gas reste indicatif.
Pourquoi ça compte
En juin 2016, The DAO, un fonds géré par contrat qui avait levé des millions d'ETH auprès du public, se vide en direct. Un attaquant retire ses parts en boucle grâce à ce schéma et détourne environ 3,6 millions d'ETH vers un contrat fils, où les règles du projet bloquent les fonds pendant plusieurs semaines.
Ce délai ouvre un débat sans précédent. Le code avait-il raison, parce que « le code fait loi » ? La communauté choisit majoritairement une modification exceptionnelle du protocole : un hard fork, activé en juillet 2016, qui transfère les fonds vers un contrat de remboursement. Une minorité refuse et continue la chaîne d'origine, qui devient Ethereum Classic. Une erreur d'ordre dans quelques lignes a ainsi produit deux blockchains.
Le code
Voici la même attaque en code exécutable. Passe CORRIGE à true : l'ordre des deux opérations s'inverse et l'attaque échoue. Modifie aussi le montant de la mise.
Clique sur Lancer pour exécuter ce code dans ton navigateur.
Ce qui se passe vraiment
Voici l'équivalent simplifié en Solidity de la version vulnérable. Le bytecode de la démo a été assemblé à la main. Compilée avec solc et rejouée dans une EVM de référence, cette source donne les mêmes soldes finaux que notre version.
function withdraw() external {
uint256 b = balanceOf[msg.sender];
require(b > 0);
(bool ok, ) = msg.sender.call{value: b}(""); // 1. envoie
require(ok);
balanceOf[msg.sender] = 0; // 2. met à jour, trop tard
}
L'attaquant est un contrat dont la fonction de réception rappelle withdraw(), tant que la banque peut payer :
receive() external payable {
if (address(banque).balance >= mise) banque.withdraw();
}
Regarde les étapes de la version vulnérable. Après chaque ligne « La banque envoie », l'attaquant rappelle aussitôt, et la profondeur d'appel grimpe de deux crans à chaque aller-retour. Le compteur « solde dans le registre » ne bouge pas : il reste à la mise de départ pendant que le solde réel fond. C'est l'instant précis de la faille : pendant l'appel externe, l'état du contrat est incohérent et un tiers a la main.
Le graphique montre le solde réel chuter jusqu'à zéro, puis un plateau. Ce plateau, c'est le bas de la liste : quand la banque est vide, les appels se dénouent en cascade et chacun écrit enfin zéro dans le registre. Aucun REVERT, aucune erreur : l'EVM a exécuté exactement le programme qu'on lui a donné.
Choisis maintenant « Corrigé ». Le solde interne passe à zéro avant l'envoi. L'attaquant rappelle toujours withdraw(), mais require(b > 0) échoue et cet appel seul est annulé. Il récupère sa mise, rien de plus. Cette règle porte un nom, checks-effects-interactions :
- Checks : vérifier les conditions.
- Effects : écrire les changements d'état.
- Interactions : appeler l'extérieur en dernier.
Le correctif tient en un déplacement de ligne, sans ajout de vérification.
Dans la vraie vie
The DAO n'avait pas exactement cette banque. Sa fonction de sortie transférait les fonds avant de remettre à zéro le solde du participant, et la récursion passait par ce chemin : le schéma reste le même. Le hard fork a été activé au bloc 1 920 000, et Ethereum Classic poursuit depuis la chaîne non modifiée. Ce précédent reste discuté, car il montre qu'une communauté peut réécrire un résultat quand l'enjeu est assez grand.
Les protections courantes se complètent. Le schéma checks-effects-interactions reste la première défense. Un verrou de réentrance, comme le ReentrancyGuard d'OpenZeppelin, bloque tout rappel pendant l'exécution. Le modèle de retrait différé, ou pull payment, sépare l'enregistrement de la créance et son paiement. Un audit, enfin, cherche ce schéma de façon systématique.
Le risque n'a pas disparu avec Solidity 0.8. Les vérifications automatiques de débordement peuvent bloquer certaines variantes : si le code fait balanceOf[msg.sender] -= b après l'envoi, le dénouement échoue et annule tout. Ce n'est qu'un effet de bord utile, notre banque le montre : avec une simple mise à zéro, l'attaque passe. En juillet 2023, un défaut du compilateur Vyper a cassé des verrous de réentrance dans des pools Curve, ce qui a permis des vols. Le schéma resurgit aussi dans les AMM et chez les oracles de prix qui lisent leur état.
Ce qui peut mal tourner
- Réentrance sur une seule fonction. Le cas de la démo : un appel externe avant la mise à jour de l'état.
- Réentrance entre fonctions.
withdrawest protégée, mais pendant l'envoi, l'attaquant appelletransfer, qui lit le même solde périmé. Il faut raisonner sur tout le contrat. - Réentrance en lecture seule. Un autre protocole lit un prix ou un solde au milieu d'une opération incomplète. Le contrat lui-même ne perd rien, c'est son voisin qui est trompé.
- Rappels cachés. Certaines normes appellent le destinataire, comme
safeTransferFromd'ERC-721 ou les crochets de tokens ERC-777. Tout appel externe est une interaction, même s'il ne ressemble pas à un paiement. - Se fier à la limite de 2 300 gas.
transferetsendla fixent, mais le coût des instructions a déjà changé par le passé. Ce n'est pas une garantie durable. - Déposer dans un contrat non audité. Pour un utilisateur, aucune action ne corrige ce défaut : un contrat vulnérable peut se vider sans que tu signes quoi que ce soit.
Teste-toi
Contenu éducatif. Pas un conseil en investissement. Les crypto-actifs comportent un risque de perte en capital. Pour la démarche éditoriale, voir à propos. Mise à jour : 31 octobre 2026.