Passer au contenu principal
Change page

Sécurité des contrats intelligents

Dernière modification: @demaupeoucorentin(opens in a new tab), 20 février 2024

Les contrats intelligents sont extrêmement flexibles et capables de contrôler de grandes quantités de valeur et de données, tout en exécutant une logique immuable basée sur le code déployé sur la blockchain. Cela a créé un écosystème dynamique d’applications sans tiers de confiance et décentralisées qui offrent de nombreux avantages par rapport aux systèmes existants. Ils représentent également des opportunités pour les attaquants qui cherchent à tirer profit de vulnérabilités dans les contrats intelligents.

Les blockchains publiques, comme Ethereum, compliquent encore davantage la question de la sécurisation des contrats intelligents. Le code de contrat déployé ne peut généralement pas être modifié pour corriger des défauts de sécurité, et les actifs volés sur des contrats intelligents sont extrêmement difficiles à suivre et la plupart du temps irrécupérables en raison de l’immuabilité.

Bien que les chiffres varient, on estime que le montant total de la valeur volée ou perdue en raison de défauts de sécurité dans les contrats intelligents est d'au moins 1 milliard de dollars. Cela inclut des incidents de haut niveau, tels que le hack de DAO(opens in a new tab) (3,6 millions d'ETH volés, d'une valeur de plus de 1 milliard de dollars aux prix actuels), le hack du portefeuille multi-sig Parity(opens in a new tab) (30 millions de dollars volés par les hackeurs), et le problème du portefeuille gelé Parity(opens in a new tab) (plus de 300 millions de dollars en ETH verrouillés pour toujours).

Les problèmes susmentionnés rendent impératif pour les développeurs d'investir des efforts dans la construction de contrats intelligents sécurisés, robustes et résistants. La sécurité des contrats intelligents est une affaire sérieuse, que chaque développeur ferait bien d’apprendre. Ce guide couvrira les considérations de sécurité des développeurs Ethereum et explorera les ressources pour améliorer la sécurité des contrats intelligents.

Prérequis

Assurez-vous de vous familiariser avec les fondamentaux du développement de contrats intelligent avant de vous attaquer à la sécurité.

Lignes directrices pour la construction de contrats intelligents sécurisés Ethereum

1. Concevoir des contrôles d'accès appropriés

Dans les contrats intelligents, les fonctions marquées publiques ou externes peuvent être appelées par n'importe quel compte externe (EOA) ou compte de contrat. Il est nécessaire de spécifier une visibilité publique des fonctions si vous voulez que les autres interagissent avec votre contrat. Les fonctions marquées privées ne peuvent cependant être appelées que par des fonctions au sein du contrat intelligent, et non par des comptes externes. Donner à chaque participant au réseau un accès aux fonctions du contrat peut causer des problèmes, surtout si cela signifie que n'importe qui peut effectuer des opérations sensibles (par exemple, frapper de nouveaux jetons).

Pour éviter l'utilisation non autorisée de fonctions de contrats intelligents, il est nécessaire de mettre en place des contrôles d'accès sécurisés. Les mécanismes de contrôle d'accès restreignent la capacité d'utiliser certaines fonctions dans un contrat intelligent à des entités approuvées, comme les comptes responsables de la gestion du contrat. Le modèle Ownable et le contrôle d'accès basé sur les rôles sont deux pratiques utiles pour implémenter le contrôle d'accès dans les contrats intelligents :

Modèle Ownable

Dans le modèle Ownable, une adresse est définie comme « propriétaire » du contrat au cours du processus de création du contrat. Les fonctions protégées sont assignées avec un modificateur OnlyOwner , qui assure que le contrat authentifie l'identité de l'adresse d'appel avant d'exécuter la fonction. Les appels à des fonctions protégées à partir d'autres adresses en dehors du propriétaire du contrat s'annulent toujours, empêchant l'accès non désiré.

Contrôle d'accès basé sur les rôles

L'enregistrement d'une seule adresse en tant que Owner dans un contrat intelligent introduit un risque de centralisation et représente un point de défaillance unique. Si les clés de compte du propriétaire sont compromises, des attaquants peuvent attaquer le contrat détenu. C'est pourquoi utiliser un modèle de contrôle d'accès basé sur des rôles avec plusieurs comptes administratifs peut être une meilleure solution.

Dans le cadre du contrôle d'accès basé sur les rôles, l'accès aux fonctions sensibles est réparti entre un ensemble de participants de confiance. Par exemple, un compte peut être responsable de la frappe des jetons, tandis qu'un autre compte peut effectuer des mises à niveau ou interrompre le contrat. Décentraliser le contrôle d'accès de cette façon élimine les points de défaillance uniques et réduit les hypothèses de confiance pour les utilisateurs.

Utilisation de portefeuilles multi-signature

Une autre approche pour implémenter un contrôle d'accès sécurisé est d'utiliser un compte multi-signature pour gérer un contrat. Contrairement à un EOA habituel, les comptes multi-signature sont détenus par plusieurs entités et nécessitent les signatures d'un nombre minimum de comptes — disons de 3 sur 5 — pour exécuter des transactions.

L'utilisation d'un portefeuille multi-signature pour le contrôle d'accès introduit une couche de sécurité supplémentaire dans la mesure où les actions sur le contrat cible nécessitent le consentement de plusieurs parties. Ceci est particulièrement utile si l'utilisation du modèle Ownable est nécessaire, car il rend plus difficile pour un attaquant ou un initié malhonnête de manipuler des fonctions sensibles du contrat à des fins malveillantes.

2. Utiliser les commandes require(), assert() et revert() pour protéger les opérations de contrat

Comme mentionné, n'importe qui peut appeler des fonctions publiques de votre contrat intelligent une fois qu'il est déployé sur la blockchain. Comme vous ne pouvez pas savoir à l'avance comment les comptes externes interagiront avec un contrat, il est idéal de mettre en œuvre des protections internes contre les opérations problématiques avant le déploiement. Vous pouvez imposer un comportement correct dans les contrats intelligents en utilisant les fonctions require(), assert(), et revert() pour déclencher des exceptions et annuler les changements d'état si l'exécution ne répond pas à certaines exigences.

require() : require sont définis en début de fonction et cela garantit que des conditions prédéfinies sont remplies avant l'exécution de la fonction appelée. Une instruction require peut être utilisée pour valider les entrées utilisateur, vérifier les variables d'état, ou authentifier l'identité du compte appelant avant d'exécuter la fonction.

assert(): assert() est utilisée pour détecter les erreurs internes et vérifier les violations des « invariants » dans votre code. Un invariant est une assertion logique à propos de l’état d’un contrat qui devrait être vrai pour toutes les exécutions de fonctions. Un exemple d'invariant est la quantité maximale totale ou le solde d'un contrat de jeton. L'utilisation de la fonction assert() garantit que votre contrat n'atteint jamais un état vulnérable, et si c'est le cas malgré tout que toutes les modifications apportées aux variables d'état sont annulées.

revert() : revert() peut être utilisé dans une instruction if-else qui déclenche une exception si la condition demandée n'est pas satisfaite. L'exemple de contrat ci-dessous utilise revert() pour proteger l'exécution des fonctions :

1pragma solidity ^0.8.4;
2
3contract VendingMachine {
4 address owner;
5 error Unauthorized();
6 function buy(uint amount) public payable {
7 if (amount > msg.value / 2 ether)
8 revert("Not enough Ether provided.");
9 // Perform the purchase.
10 }
11 function withdraw() public {
12 if (msg.sender != owner)
13 revert Unauthorized();
14
15 payable(msg.sender).transfer(address(this).balance);
16 }
17}
Afficher tout

3. Tester les contrats intelligents et vérifier la justesse du code

L'immuabilité du code exécuté dans la Machine Virtuelle Ethereum signifie que les contrats intelligents exigent un plus haut niveau d'évaluation de la qualité pendant la phase de développement. Tester votre contrat de manière intensive et l'observer pour déceler tout résultat inattendu améliorera considérablement la sécurité et protégera vos utilisateurs sur le long terme.

La méthode habituelle est d'écrire de petits tests unitaires à l'aide de données fictives que le contrat devrait recevoir de la part des utilisateurs. Le test unitaire est bon pour tester la fonctionnalité de certaines fonctions et pour s'assurer qu'un contrat intelligent fonctionne comme prévu.

Malheureusement, les tests unitaires sont peu efficaces pour améliorer la sécurité des contrats intelligents lorsqu'ils sont utilisés isolément. Un test unitaire peut prouver qu'une fonction s'exécute correctement pour les données simulées, mais les tests unitaires sont seulement aussi efficaces que les tests écrits. Il est donc difficile de détecter les cas et les vulnérabilités marginaux manqués qui pourraient nuire à la sécurité de votre contrat intelligent.

Une meilleure approche est de combiner les tests unitaires avec des tests fondés sur les propriétés effectués en utilisant l'analyse statique et dynamique. L'analyse statique repose sur des représentations de bas niveau, tels que des graphiques de flux de contrôle(opens in a new tab) et des arbres de syntaxe abstraite(opens in a new tab) pour analyser les états de programme et les chemins d'exécution accessibles. D'autre part, les techniques d'analyse dynamique, telles que le fuzzing, exécutent du code de contrat avec des valeurs d'entrées aléatoires pour détecter les opérations qui violent les propriétés de sécurité.

La vérification formelle est une autre technique de vérification des propriétés de sécurité dans les contrats intelligents. Contrairement aux tests réguliers, la vérification formelle peut prouver de façon concluante l'absence d'erreurs dans un contrat intelligent. Ceci est réalisé en créant une spécification formelle qui permet de saisir les propriétés de sécurité désirées et de prouver qu'un modèle formel des contrats adhère à cette spécification.

4. Demander une revue indépendante de votre code

Après avoir testé votre contrat, il est bon de demander à d'autres de vérifier le code source pour tout problème de sécurité. Les tests ne décèleront pas toutes les failles d'un contrat intelligent, mais obtenir un examen indépendant augmente la possibilité de détecter les vulnérabilités.

Audits

Demander un audit des contrats intelligents est une façon de procéder à un examen indépendant du code. Les vérificateurs jouent un rôle important en veillant à ce que les contrats intelligents soient sécurisés et exempts de défauts de qualité et d'erreurs de conception.

Cela dit, évitez de considérer les audits comme un remède miracle. Les audits de contrats intelligents ne saisiront pas chaque bogue et sont principalement conçus pour fournir une série de revues complémentaires, qui peut aider à détecter les problèmes qui auront échappé aux développeurs lors du développement et du test initial. Suivez également les bonnes pratiques pour travailler avec les auditeurs(opens in a new tab), comme documenter le code correctement et ajouter de commentaires en ligne, pour maximiser les avantages d'un audit des contrats intelligents.

Chasse à la prime

La mise en place d'un programme de prime de bogues est une autre approche pour implémenter des examens de code externes. Une prime de bogue est une récompense financière donnée aux individus (généralement des hackers whitehat) qui découvrent des vulnérabilités dans une application.

Lorsqu'elle est utilisée correctement, la primes de bogues incitent les membres de la communauté hacker à inspecter votre code pour trouver des défauts critiques. Un exemple réel est le « bogue d'argent infini » qui aurait permis à un attaquant de créer un nombre illimité d'Ether sur Optimisme(opens in a new tab), un protocole Couche 2 fonctionnant sur Ethereum. Heureusement, un hacker whitehat a découvert le défaut(opens in a new tab) et l'a notifié à l'équipe, gagnant une grosse prime ce faisant(opens in a new tab).

Une stratégie utile est de définir le paiement d'un programme de prime de bogues proportionnellement au montant des fonds mis en jeu. Décrit comme la «mise à l'échelle de la prime de bogue(opens in a new tab)», cette approche fournit des incitations financières pour les individus à divulguer de manière responsable des vulnérabilités au lieu de les exploiter.

5. Suivre les bonnes pratiques lors du développement de contrats intelligents

L’existence d’audits et de primes de bogue n'exclut pas votre responsabilité d’écrire un code de haute qualité. Une bonne sécurité du contrat intelligent commence en suivant des processus de conception et de développement adéquats :

  • Stocker tout le code dans un système de contrôle de version, tel que git

  • Effectuer toutes les modifications de code via des pulls requests

  • Assurez-vous que les pulls requests ont au moins un réviseur indépendant — si vous travaillez en solo sur un projet, envisagez de trouver d'autres développeurs et d'échanger mutuellement vos avis sur le code

  • Utilisez un environnement de développement pour tester, compiler, déployer des contrats intelligents

  • Exécutez votre code sur des outils d'analyse de code basiques, tels que Mythril et Slither. Idéalement, vous devriez le faire avant de fusionner chaque pull request et comparer les différences de sortie

  • Assurez-vous que votre code est compilé sans erreurs, et que le compilateur Solidity n'émet aucun avertissement

  • Documentez correctement votre code (en utilisant NatSpec(opens in a new tab)) et décrivez les détails sur l'architecture du contrat dans un langage facile à comprendre. Cela facilitera l'audit et l'examen de votre code pour les autres.

6. Mettre en œuvre des plans de relance robustes en cas de catastrophe

La conception de contrôles d'accès sécurisés, la mise en œuvre de modificateurs de fonction et d'autres suggestions peuvent améliorer la sécurité des contrats intelligents, mais elles ne peuvent pas exclure la possibilité d'exploits malveillants. Pour élaborer des contrats intelligents sécurisés, il faut se « préparer à l'échec » et disposer d'un plan de repli pour répondre efficacement aux attaques. Un plan de reprise après sinistre adéquat intègre tout ou partie des éléments suivants :

Mise à niveau du contrat

Bien que les contrats intelligents Ethereum soient immuables par défaut, il est possible d'obtenir un certain degré de mutabilité en utilisant des modèles de mise à niveau. La mise à niveau des contrats est nécessaire dans les cas où une faille critique rend votre ancien contrat inutilisable et où le déploiement d'une nouvelle logique est l'option la plus réalisable.

Les mécanismes de mise à niveau des contrats fonctionnent différemment, mais le « modèle proxy » est l'une des approches les plus populaires pour la mise à niveau des contrats intelligents. Les modèles de proxy divisent l'état et la logique d'une application entre deux contrats. Le premier contrat (appelé « contrat mandataire ») stocke les variables d'état (par exemple, les soldes des utilisateurs), tandis que le second contrat (appelé « contrat logique ») contient le code d'exécution des fonctions du contrat.

Les comptes interagissent avec le contrat du mandataire, qui envoie tous les appels de fonction au contrat logique en utilisant l'appel de bas niveau delegatecall()(opens in a new tab). Contrairement à un appel de message ordinaire, delegatecall() garantit que le code exécuté à l'adresse du contrat logique est exécuté dans le contexte du contrat appelant. Cela signifie que le contrat logique écrira toujours dans le stockage du proxy (au lieu de son propre stockage) et les valeurs originales des msg.sender et msg.value sont préservées.

La délégation des appels au contrat logique nécessite de stocker son adresse dans le stockage du contrat de procuration. Par conséquent, la mise à niveau de la logique du contrat consiste simplement à déployer un autre contrat logique et à stocker la nouvelle adresse dans le contrat de procuration. Comme les appels ultérieurs au contrat de procuration sont automatiquement acheminés vers le nouveau contrat logique, vous aurez « mis à niveau » le contrat sans modifier réellement le code.

En savoir plus sur la mise à niveau des contrats.

Arrêts d'urgence

Comme nous l'avons mentionné, les audits et les tests approfondis ne peuvent pas découvrir tous les bugs d'un contrat intelligent. Si une vulnérabilité apparaît dans votre code après le déploiement, il est impossible de la corriger puisque vous ne pouvez pas modifier le code exécuté à l'adresse du contrat. De plus, les mécanismes de mise à niveau ( par exemple, les modèles de procuration) peuvent prendre du temps à se mettre en œuvre (ils nécessitent souvent l'approbation de différentes parties), ce qui ne fait que donner plus de temps aux attaquants pour causer plus de dommages.

L'option nucléaire consiste à mettre en œuvre une fonction « d'arrêt d'urgence » qui bloque les appels aux fonctions vulnérables dans un contrat. Les arrêts d'urgence comprennent généralement les composants suivants :

  1. Une variable booléenne globale indiquant si le contrat intelligent est dans un état arrêté ou non. Cette variable est définie à false lors de la mise en place du contrat, mais elle deviendra vraie une fois le contrat arrêté.

  2. Les fonctions qui font référence à la variable booléenne dans leur exécution. Ces fonctions sont accessibles lorsque le contrat intelligent n'est pas arrêté, et deviennent inaccessibles lorsque la fonction d'arrêt d'urgence est déclenchée.

  3. Une entité qui a accès à la fonction d'arrêt d'urgence, qui définit la variable booléenne à true. Pour éviter les actions malveillantes, les appels à cette fonction peuvent être limités à une adresse de confiance (par exemple, le propriétaire du contrat).

Une fois que le contrat a activé l'arrêt d'urgence, certaines fonctions ne seront pas appelables. Pour ce faire, les fonctions de sélection sont enveloppées dans un modificateur qui fait référence à la variable globale. Voici un exemple(opens in a new tab) décrivant une implémentation de ce modèle dans les contrats :

1// Ce code n'a pas fait l'objet d'un audit professionnel et ne fait aucune promesse quant à sa sécurité ou son exactitude. Utilisez-le à vos risques et périls.
2
3contract EmergencyStop {
4
5 bool isStopped = false;
6
7 modifier stoppedInEmergency {
8 require(!isStopped);
9 _;
10 }
11
12 modifier onlyWhenStopped {
13 require(isStopped);
14 _;
15 }
16
17 modifier onlyAuthorized {
18 // Check for authorization of msg.sender here
19 _;
20 }
21
22 function stopContract() public onlyAuthorized {
23 isStopped = true;
24 }
25
26 function resumeContract() public onlyAuthorized {
27 isStopped = false;
28 }
29
30 function deposit() public payable stoppedInEmergency {
31 // Deposit logic happening here
32 }
33
34 function emergencyWithdraw() public onlyWhenStopped {
35 // Emergency withdraw happening here
36 }
37}
Afficher tout
Copier

Cet exemple montre les caractéristiques de base des arrêts d'urgence :

  • isStopped est un booléen qui évalue à false en début et à true lorsque le contrat entre en mode d'urgence.

  • Les modificateurs de fonction onlyWhenStopped et stoppedInEmergency vérifient la variable isStopped. stoppedInEmergency est utilisé pour piloter des fonctions qui doivent être inaccessibles lorsque le contrat est vulnérable (par exemple : deposit()). Les appels à ces fonctions seront tout simplement annulés.

onlyWhenStopped est utilisé pour des fonctions qui doivent être appelables pendant une urgence (par exemple, emergencyWithdraw()). De telles fonctions peuvent aider à résoudre la situation, d’où leur exclusion de la liste des « fonctions restreintes ».

L'utilisation d'une fonctionnalité d'arrêt d'urgence constitue un palliatif efficace pour faire face aux vulnérabilités graves de votre contrat intelligent. Cependant, les utilisateurs doivent faire confiance aux développeurs pour qu'ils ne l'activent pas pour des raisons intéressées. À cette fin, décentraliser le contrôle de l'arrêt d'urgence soit en le soumettant à un mécanisme de vote en chaîne, un timelock, ou à l'approbation d'un portefeuille multisig sont des solutions possibles.

Suivi des événements

Les événements(opens in a new tab) vous permettent de suivre les appels vers les fonctions des contrats intelligents et de surveiller les changements apportés aux variables d'état. Il est idéal de programmer votre contrat intelligent pour qu'il émette un événement chaque fois qu'une partie prend une mesure critique en matière de sécurité (par exemple, retirer des fonds).

Le log des événements et leur surveillance hors chaîne fournissent un aperçu des opérations contractuelles et aide à la découverte plus rapide des actions malveillantes. Cela signifie que votre équipe peut réagir plus rapidement aux hacks et prendre des mesures pour atténuer l'impact sur les utilisateurs, tels que suspendre les fonctions ou effectuer une mise à niveau.

Vous pouvez également opter pour un outil de surveillance en vente libre qui transmet automatiquement les alertes lorsque quelqu'un interagit avec vos contrats. Ces outils vous permettent de créer des alertes personnalisées basées sur différents déclencheurs, comme le volume de la transaction, la fréquence des appels de fonctions, ou les fonctions spécifiques impliquées. Par exemple, vous pouvez programmer une alerte qui arrive lorsque le montant retiré en une seule opération dépasse un seuil particulier.

7. Concevoir des systèmes de gouvernance sécurisés

Vous voudrez peut-être décentraliser votre application en transférant le contrôle des contrats intelligents de base aux membres de la communauté. Dans ce cas, le système de contrats intelligents comprendra un module de gouvernance, un mécanisme qui permet aux membres de la communauté d'approuver des actions administratives via un système de gouvernance en chaîne. Par exemple, une proposition de mise à niveau d'un contrat de procuration vers une nouvelle implémentation peut être votée par les détenteurs de jetons.

Une gouvernance décentralisée peut être bénéfique, en particulier parce qu'elle aligne les intérêts des développeurs et des utilisateurs finaux. Néanmoins, les mécanismes de gouvernance des contrats intelligents peuvent introduire de nouveaux risques s'ils sont mal mis en œuvre. Un scénario plausible est si un attaquant acquiert un énorme pouvoir de vote (mesuré en nombre de jetons conservés) en prenant un crédit flash et en poussant une proposition malveillante.

Une façon de prévenir les problèmes liés à la gouvernance sur la chaîne est d'utiliser un timelock(opens in a new tab). Un timelock empêche un contrat intelligent d'exécuter certaines actions jusqu'à ce qu'un certain temps passe. D'autres stratégies incluent l'assignation d'une « pondération de vote » à chaque jeton en fonction de la durée d'enfermement de chaque jeton, ou mesurant le pouvoir de vote d'une adresse à une période historique (par exemple, 2-3 blocs dans le passé) au lieu du bloc actuel. Les deux méthodes réduisent la possibilité d’amasser rapidement le pouvoir de vote pour basculer sur les votes en chaîne.

En savoir plus sur la conception de systèmes de gouvernance sécurisée(opens in a new tab) et de mécanismes de vote différents dans les DAO(opens in a new tab).

8. Réduire la complexité du code à un minimum

Les développeurs de logiciels traditionnels sont familiers avec le principe KISS (« keep it simple, stupid ») qui recommande de ne pas introduire de complexité inutile dans la conception de logiciels. Cela fait suite à la pensée de longue date selon laquelle « les systèmes complexes échouent de manière complexe » et sont plus susceptibles d’être confrontés à des erreurs coûteuses.

Garder les choses simples est particulièrement important lors de la rédaction de contrats intelligents, étant donné que les contrats intelligents contrôlent potentiellement de grandes quantités de valeur. Une astuce pour atteindre la simplicité lors de l'écriture de contrats intelligents est de réutiliser des bibliothèques existantes, telles que les contrats OpenZeppelin(opens in a new tab), lorsque cela est possible. Parce que ces bibliothèques ont été largement vérifiées et testées par les développeurs, leur utilisation réduit les chances d'introduire des bogues en écrivant de nouvelles fonctionnalités à partir de zéro.

Un autre conseil commun est d'écrire de petites fonctions et de garder les contrats modulaires en divisant la logique commerciale entre plusieurs contrats. Non seulement l'écriture de code plus simple réduit la surface d'attaque dans un contrat intelligent, mais il est également plus facile de raisonner sur la justesse du système global et de détecter les éventuelles erreurs de conception plus tôt.

9. Protéger contre les vulnérabilités communes des contrats intelligents

Réentrance

L’EVM ne permet pas la simultanéité, ce qui signifie que deux contrats impliqués dans un appel de message ne peuvent pas être exécutés simultanément. Un appel externe met en pause l'exécution et la mémoire du contrat d'appel jusqu'à ce que l'appel revienne, à partir duquel l'exécution du point se déroule normalement. Ce processus peut être décrit formellement comme le transfert du flux de contrôle(opens in a new tab) vers un autre contrat.

Bien que la plupart du temps inoffensifs, le transfert de flux de contrôle vers des contrats non approuvés peut causer des problèmes, tels que la réentrance. Une attaque par réentrance survient lorsqu'un contrat malveillant rappelle un contrat vulnérable avant que l'invocation de la fonction d'origine ne soit terminée. Ce type d'attaque est mieux expliqué avec un exemple.

Considérez un simple contrat intelligent (« Victim ») qui permet à quiconque de déposer et de retirer de l'Ether :

1// This contract is vulnerable. Do not use in production
2
3contract Victim {
4 mapping (address => uint256) public balances;
5
6 function deposit() external payable {
7 balances[msg.sender] += msg.value;
8 }
9
10 function withdraw() external {
11 uint256 amount = balances[msg.sender];
12 (bool success, ) = msg.sender.call.value(amount)("");
13 require(success);
14 balances[msg.sender] = 0;
15 }
16}
Afficher tout
Copier

Ce contrat expose une fonction withdraw() pour permettre aux utilisateurs de retirer de l'ETH précédemment déposé dans le contrat. Lors du traitement d'un retrait, le contrat effectue les opérations suivantes :

  1. Vérifie le solde ETH de l'utilisateur
  2. Envoie des fonds à l'adresse d'appel
  3. Réinitialise son solde à 0, empêchant les retraits supplémentaires de l'utilisateur

La fonction withdraw() dans le contrat Victim suit un modèle « checks-interactions-effects ». Il vérifie si les conditions nécessaires à l'exécution sont satisfaites (c.-à-d. l'utilisateur a un solde ETH positif) et effectue l'interaction _ en envoyant l'ETH à l'adresse de l'appelant, avant d'appliquer les _effets de la transaction (c.-à-d., réduisant le solde de l’utilisateur).

Si la fonction withdraw() est appelée depuis un compte externe (Externally Orné Account, dit EOA), la fonction s'exécute comme attendu : msg.sender.call.value() envoie l'ETH à l'appelant. Cependant, si msg.sender est un compte de contrat intelligent qui appelle withdraw(), l'envoie de fonds en utilisant msg.sender.call.value() déclenchera également le code stocké à cette adresse pour l'exécuter.

Imaginez qu'il s'agisse du code déployé à l'adresse du contrat:

1 contract Attacker {
2 function beginAttack() external payable {
3 Victim(victim_address).deposit.value(1 ether)();
4 Victim(victim_address).withdraw();
5 }
6
7 function() external payable {
8 if (gasleft() > 40000) {
9 Victim(victim_address).withdraw();
10 }
11 }
12}
Afficher tout
Copier

Ce contrat est conçu pour faire trois choses :

  1. Accepter un dépôt depuis un autre compte (probablement l’EOA de l’attaquant)
  2. Dépose 1 ETH dans le contrat Victim
  3. Retirer 1 ETH stocké dans le contrat intelligent

Il n'y a rien de mal ici, excepté que l'Attacker a une autre fonction qui appelle withdraw() dans Victim à nouveau si le gaz restant du msg.sender.call.value entrant est supérieur à 40 000. Cela donne à l'Attacker la possibilité de rentrer Victim et de retirer plus de fonds avant que la première invocation de withdraw soit terminée. Le cycle ressemble à ceci:

1- L'EOA de l'attaquant appelle `Attacker.beginAttack()` avec 1 ETH
2- `Attaquant.beginAttack()` dépose 1 ETH dans `Victim`
3- `Attacker` appelle `withdraw() dans `Victim`
4- `Victim` vérifie le solde de `Attacker` (1 ETH)
5- `Victim` envoie 1 ETH à `Attacker` (qui déclenche la fonction par défaut)
6- `Attacker` appelle `Victim.withdraw()` à nouveau (notez que `Victim` n'a pas réduit le solde de `Attacker` à partir du premier retrait)
7- `Victim` vérifie le solde de `Attacker` (qui est toujours 1 ETH car il n'a pas appliqué les effets du premier appel)
8- `Victim` envoie 1 ETH à `Attacker` (qui déclenche la fonction par défaut et permet à `Attacker` de réintroduire la fonction `withdraw`)
9- Le processus se répète jusqu'à ce que `Attacker` soit épuisé, à quel point `msg.sender.call.value` retourne sans déclencher de retraits supplémentaires
10- `Victim` applique enfin les résultats de la première transaction (et de celles subséquentes) à son état, donc le solde de `Attacker` est fixé à 0
Afficher tout
Copier

Le résumé est que, comme le solde de l'appelant n'est pas défini à 0 jusqu'à ce que l'exécution de la fonction soit terminée, les invocations suivantes réussiront et permettront à l'appelant de retirer son solde plusieurs fois. Ce type d'attaque peut être utilisé pour drainer un contrat intelligent de ses fonds, comme ce qui s'est passé dans le hack DAO 2016(opens in a new tab). Les attaques par réentrance sont toujours un problème critique pour les contrats intelligents aujourd'hui, comme le montre les listes publiques des exploits de réentrance(opens in a new tab).

Comment empêcher les attaques par réentrance

Une approche pour traiter la réentrance est de suivre le modèle de vérifications-effets-interactions(opens in a new tab). Ce modèle ordonne l'exécution de fonctions d'une manière que le code qui effectue les vérifications nécessaires avant de progresser avec l'exécution arrive en premier, suivi du code qui manipule l'état du contrat, avec du code qui interagit avec d'autres contrats ou EOA arrivant en dernier.

Le modèle de vérifications-effets-interactions est utilisé dans une version révisée du contrat Victim affichée ci-dessous :

1contract NoLongerAVictim {
2 function withdraw() external {
3 uint256 amount = balances[msg.sender];
4 balances[msg.sender] = 0;
5 (bool success, ) = msg.sender.call.value(amount)("");
6 require(success);
7 }
8}
Copier

Ce contrat effectue un check sur le solde de l'utilisateur, applique les effects de la fonction withdraw() (en réinitialisant le solde de l'utilisateur à 0), et procède à l’exécution de l'interaction (envoi de l’ETH à l’adresse de l’utilisateur). Cela garantit que le contrat met à jour son stockage avant l’appel externe, éliminant ainsi la condition de réentrance qui a permis la première attaque. Le contrat Attacker pourrait toujours être rappelé dans NoLongerAVictim, mais depuis que balances[msg.sender] a été réglé à 0, les retraits supplémentaires lanceront une erreur.

Une autre option est d'utiliser un verrou d'exclusion mutuelle (communément décrit comme un « mutex ») qui verrouille une partie de l'état d'un contrat jusqu'à ce qu'une invocation de fonction soit terminée. Ceci est implémenté en utilisant une variable booléenne qui est définie à true avant que la fonction ne s'exécute et retourne à false après que l'invocation ait été faite. Comme on le voit dans l'exemple ci-dessous, l'utilisation d'un mutex protège une fonction contre les appels récursifs alors que l'invocation originale est toujours en cours de traitement, empêchant ainsi efficacement la réentrance.

1pragma solidity ^0.7.0;
2
3contract MutexPattern {
4 bool locked = false;
5 mapping(address => uint256) public balances;
6
7 modifier noReentrancy() {
8 require(!locked, "Blocked from reentrancy.");
9 locked = true;
10 _;
11 locked = false;
12 }
13 // This function is protected by a mutex, so reentrant calls from within `msg.sender.call` cannot call `withdraw` again.
14 // The `return` statement evaluates to `true` but still evaluates the `locked = false` statement in the modifier
15 function withdraw(uint _amount) public payable noReentrancy returns(bool) {
16 require(balances[msg.sender] >= _amount, "No balance to withdraw.");
17
18 balances[msg.sender] -= _amount;
19 bool (success, ) = msg.sender.call{value: _amount}("");
20 require(success);
21
22 return true;
23 }
24}
Afficher tout
Copier

Vous pouvez également utiliser un système de « pull payments »(opens in a new tab) qui demande aux utilisateurs de retirer des fonds des contrats intelligents, au lieu d'un système de paiement « push payments » qui envoie des fonds à des comptes. Cela élimine la possibilité de déclencher par inadvertance du code à des adresses inconnues (et peut également prévenir certaines attaques par déni de service).

Soupassements et dépassements d'entier

Un dépassement d'entier se produit lorsque les résultats d'une opération arithmétique tombent en dehors de la plage de valeurs acceptable, le faisant passer à la valeur représentable la plus basse. Par exemple, un uint8 ne peut stocker que des valeurs allant jusqu'à 2^8-1=255. Les opérations arithmétiques qui aboutissent à des valeurs supérieures à 255 dépasseront et réinitialiseront uint à 0, similaire à la façon dont l'odomètre sur une voiture se réinitialise à 0 une fois qu'il atteint le kilométrage maximum (999999).

Les soupassements d'entier se produisent pour des raisons similaires : les résultats d'une opération arithmétique sont inférieurs à la fourchette acceptable. Disons que vous avez essayé de diminuer 0 dans un uint8, le résultat ne ferait que passer à la valeur représentative maximale (255).

Les dépassements d'entier et les soupassements peuvent entraîner des changements inattendus dans les variables d'état d'un contrat et entraîner une exécution non planifiée. Voici un exemple montrant comment un attaquant peut exploiter un dépassement arithmétique dans un contrat intelligent pour effectuer une opération invalide :

1pragma solidity ^0.7.6;
2
3// Ce contrat est conçu pour servir de coffre temporel.
4// L'utilisateur peut déposer dans ce contrat mais ne peut pas se retirer pendant au moins une semaine.
5// L'utilisateur peut également prolonger le temps d'attente au-delà de la période d'attente de 1 semaine.
6
7/*
81. Déployer TimeLock
92. Déployer l'Attaque avec l'adresse de TimeLock
103. Appeler Attaque.attack en envoyant 1 éther. Vous pourrez immédiatement
11 retirer votre éther.
12
13Que s'est-il passé ?
14L'attaque a causé un dépassement de TimeLock.lockTime et a pu entraîner un retrait
15avant la période d'attente d'une semaine.
16*/
17
18contract TimeLock {
19 mapping(address => uint) public balances;
20 mapping(address => uint) public lockTime;
21
22 function deposit() external payable {
23 balances[msg.sender] += msg.value;
24 lockTime[msg.sender] = block.timestamp + 1 weeks;
25 }
26
27 function increaseLockTime(uint _secondsToIncrease) public {
28 lockTime[msg.sender] += _secondsToIncrease;
29 }
30
31 function withdraw() public {
32 require(balances[msg.sender] > 0, "Insufficient funds");
33 require(block.timestamp > lockTime[msg.sender], "Lock time not expired");
34
35 uint amount = balances[msg.sender];
36 balances[msg.sender] = 0;
37
38 (bool sent, ) = msg.sender.call{value: amount}("");
39 require(sent, "Failed to send Ether");
40 }
41}
42
43contract Attack {
44 TimeLock timeLock;
45
46 constructor(TimeLock _timeLock) {
47 timeLock = TimeLock(_timeLock);
48 }
49
50 fallback() external payable {}
51
52 function attack() public payable {
53 timeLock.deposit{value: msg.value}();
54 /*
55 if t = current lock time then we need to find x such that
56 x + t = 2**256 = 0
57 so x = -t
58 2**256 = type(uint).max + 1
59 so x = type(uint).max + 1 - t
60 */
61 timeLock.increaseLockTime(
62 type(uint).max + 1 - timeLock.lockTime(address(this))
63 );
64 timeLock.withdraw();
65 }
66}
Afficher tout
Comment éviter les soupassements et dépassements d'entier

Depuis la version 0.8.0, le compilateur Solidity rejette le code qui entraîne des soupassements et dépassements d'entier. Cependant, les contrats compilés avec une version inférieure du compilateur devraient soit effectuer des vérifications sur des fonctions impliquant des opérations arithmétiques soit utiliser une bibliothèque (par ex., SafeMath(opens in a new tab)) qui vérifie le soupassement/dépassement.

Manipulation Oracle

Les Oracles sourcent des informations hors chaîne et les envoient en chaîne pour les contrats intelligents à utiliser. Avec des oracles, vous pouvez concevoir des contrats intelligents qui interagissent avec des systèmes hors chaîne, tels que les marchés de capitaux, en élargissant considérablement leur application.

Mais si l'oracle est corrompu et envoie des informations incorrectes en chaîne, les contrats intelligents s'exécuteront sur la base d'entrées erronées, ce qui peut causer des problèmes. C'est la base du « problème de l'oracle », qui concerne la tâche de s'assurer que les informations provenant d'un oracle de la blockchain sont exactes, à jour et en temps opportun.

Une préoccupation liée à la sécurité consiste à utiliser un oracle en chaîne, tel qu'un échange décentralisé, pour obtenir le prix au comptant d'un actif. Les plateformes de prêt dans l'industrie de la finance décentralisée (DeFi) le font souvent pour déterminer la valeur de la garantie d'un utilisateur pour déterminer le montant qu'il peut emprunter.

Les prix des DEX sont souvent exacts, en grande partie en raison du rétablissement de la parité sur les marchés. Cependant, ils sont ouverts à la manipulation, en particulier si l'oracle sur la chaîne calcule les prix des actifs en fonction des modèles de négociation historiques (comme c'est généralement le cas).

Par exemple, un attaquant pourrait artificiellement pomper le prix au comptant d'un actif en souscrivant un prêt flash juste avant d'interagir avec votre contrat de prêt. Interroger le DEX pour le prix de l’actif reviendrait à une valeur plus élevée que la normale (en raison de la forte demande de transfert de « l'ordre d’achat » de l’attaquant pour l’actif), leur permettant d'emprunter plus qu'ils ne le devraient. De telles « attaques de prêts flash » ont été utilisées pour exploiter la dépendance à l'égard de prix oracles parmi les applications DeFi, coûtant des millions de protocoles en fonds perdus.

Comment éviter la manipulation d'oracle

Le minimum requis pour éviter la manipulation d'oracle est d'utiliser un réseau oracle décentralisé qui interroge des informations provenant de sources multiples pour éviter des points de défaillance uniques. Dans la plupart des cas, les oracles décentralisés ont des incitations cryptoéconomiques intégrées pour encourager les noeuds d'oracle à signaler des informations correctes, les rendant plus sûres que les oracles centralisés.

Si vous comptez interroger un oracle sur le prix des actifs, envisagez d'utiliser un mécanisme qui implémente un prix moyen pondéré (« Time Weighted Average Price », dit TWAP). Un oracle TWAP(opens in a new tab) interroge le prix d'un actif à deux points différents dans le temps (que vous pouvez modifier) et calcule le prix au comptant en fonction de la moyenne obtenue. Le choix de périodes plus longues protège votre protocole contre la manipulation des prix car les larges ordres exécutés récemment ne peuvent pas affecter les prix des actifs.

Ressources de sécurité de contrats intelligents pour les développeurs

Outils pour analyser les contrats intelligents et vérifier la justesse du code

  • Outils de test et bibliothèques - Collection d'outils et de bibliothèques standards pour effectuer des tests unitaires, des analyses statiques et des analyses dynamiques des contrats intelligents.

  • Outils de vérification formels - Outils de vérification de l'exactitude fonctionnelle dans les contrats intelligents et de vérification des invariants.

  • Services d'audit de contrats intelligents - Liste des organisations fournissant des services d'audit de contrats intelligents pour les projets de développement d'Ethereum.

  • Plateformes de récompenses de bugs - Plateformes pour coordonner les récompenses de bugs et récompensant la divulgation responsable de vulnérabilités critiques dans les contrats intelligents.

  • Fork Checker(opens in a new tab) - : Il s'agit d'un outil gratuit en ligne pour la vérification de toutes les informations disponibles concernant un contrat issu du fork.

  • ABI Encoder(opens in a new tab) - : Il s'agit d'un service gratuit en ligne pour l'encodage des fonctions de contrat Solidity et de vos arguments de constructeur.

Outils de surveillance des contrats intelligents

Outils pour une administration sécurisée des contrats intelligents

  • OpenZeppelin Defender Admin(opens in a new tab) - Interface pour gérer l'administration des contrats intelligents, y compris les contrôles d'accès, les mises à jour et la pause.

  • Safe(opens in a new tab) - Portefeuille à contrat intelligent fonctionnant sur Ethereum qui exige qu'un nombre minimum de personnes approuvent une transaction avant qu'elle ne puisse avoir lieu (M-of-N).

  • Contrats OpenZeppelin(opens in a new tab) - Bibliothèques de contrats pour l'implémentation de fonctions administratives, y compris la propriété contractuelle, les mises à niveau, les contrôles d'accès, la gouvernance, les possibilités de pause, et plus encore.

Services d'audit pour contrat intelligent

  • Diligence ConsenSys(opens in a new tab) - Service d'audit de contrat intelligent pour aider les projets à travers l'écosystème de la blockchain à s'assurer que leurs protocoles sont prêts à être lancés et construits pour protéger les utilisateurs.

  • CertiK(opens in a new tab) - société de sécurité blockchain pionnière dans l'utilisation d'une technologie de vérification formelle de pointe sur les contrats intelligents et les réseaux blockchain.

  • Trail of Bits(opens in a new tab) - Entreprise de cybersécurité qui combine la recherche de sécurité avec une mentalité d'attaquant pour réduire le risque et fortifier le code.

  • PeckShield(opens in a new tab) - Société de sécurité blockchain offrant des produits et des services pour la sécurité, la confidentialité et la convivialité de l'ensemble de l'écosystème blockchain.

  • QuantStamp(opens in a new tab) - Service d'audit facilitant l'adoption courante de la technologie blockchain par les services de sécurité et d'évaluation des risques.

  • OpenZeppelin(opens in a new tab) - Société de sécurité de contrat intelligent fournissant des audits de sécurité pour les systèmes distribués.

  • Vérification de l'exécution(opens in a new tab) - Entreprise de sécurité spécialisée dans la modélisation et la vérification formelles des contrats intelligents.

  • Hacken(opens in a new tab) - Auditeur de cybersécurité Web3 apportant une approche à 360° à la sécurité de la blockchain.

  • Nethermind(opens in a new tab) - Des services offrant des audits Cairo et Solidity, utilisés comme garantie pour assurer l'intégrité des contrats intelligents et la sécurité des utilisateurs dans les écosystèmes Ethereum et Starknet.

  • HashEx(opens in a new tab) - Les rapports d'audit présentés par HashEx relatifs à la blockchain et aux contrats intelligents, visent à garantir la sécurité des cryptomonnaies, fournissant des services tels que le développement des contrats intelligents, le test de pénétration, ou le conseil blockchain.

  • Code4rena(opens in a new tab) - Une plateforme concurrentielle, réputée pour ses audits de sécurité, qui prête main forte aux experts garantissant la sécurité des smart-contracts, dans l'objectif commun d'œuvrer à la sécurisation du Web3.

Plateformes de récompense de bug

  • Immunefi(opens in a new tab) - Plateforme de récompense de bug pour les contrats intelligents et les projets DeFi, où les chercheurs en sécurité examinent le code, révèlent les vulnérabilités, sont payés et rendent les crypto-monnaies plus sûres.

  • HackerOne(opens in a new tab) - Coordination de vulnérabilité et plateforme de récompense de bogue qui relie les entreprises aux testeurs de pénétration et aux chercheurs en cybersécurité.

  • HackenProof(opens in a new tab) - Plateforme de récompense de bogue pour les projets de cryptomonnaies (DeFi, contrats intelligents, portefeuilles, CEX et bien plus encore), où les professionnels de la sécurité fournissent des services de triage et les chercheurs sont payés pour des rapports de bogues pertinents et vérifiés.

Publications de vulnérabilités connues de contrats intelligents et d'exploitations

Défis pour l'apprentissage de la sécurité des contrats intelligents

Meilleures pratiques pour sécuriser les contrats intelligents

Tutoriels sur la sécurité des contrats intelligents

Cet article vous a été utile ?