paint-brush
Une enquête 2023 sur la sécurité des contrats intelligents pour la soliditépar@rareskills
6,103 lectures
6,103 lectures

Une enquête 2023 sur la sécurité des contrats intelligents pour la solidité

par RareSkills53m2023/05/16
Read on Terminal Reader

Trop long; Pour lire

Un problème de sécurité dans Solidity se résume à des contrats intelligents qui ne se comportent pas comme ils le devraient. Cela peut appartenir à quatre grandes catégories : Fonds volés Fonds bloqués ou gelés dans un contrat Les gens reçoivent moins de récompenses que prévu (les récompenses sont retardées ou réduites) Il n'est pas possible de dresser une liste complète de tout ce qui peut mal tourner.
featured image - Une enquête 2023 sur la sécurité des contrats intelligents pour la solidité
RareSkills HackerNoon profile picture
0-item
1-item
2-item


Une liste de vulnérabilités de solidité

Cet article sert de mini-cours sur la sécurité des contrats intelligents et fournit une liste complète des problèmes et des vulnérabilités qui ont tendance à se reproduire dans les contrats intelligents Solidity. C'est le genre de problèmes qui peuvent survenir lors d'un audit de qualité.


Un problème de sécurité dans Solidity se résume à des contrats intelligents qui ne se comportent pas comme ils le devraient.


Cela peut appartenir à quatre grandes catégories :

  • Des fonds volés

  • Des fonds bloqués ou gelés dans un contrat

  • Les gens reçoivent moins de récompenses que prévu (les récompenses sont retardées ou réduites)

  • Les gens reçoivent plus de récompenses que prévu (conduisant à l'inflation et à la dévaluation)


Il n'est pas possible de dresser une liste exhaustive de tout ce qui peut mal tourner. Cependant, tout comme l'ingénierie logicielle traditionnelle a des thèmes communs de vulnérabilités telles que l'injection SQL, les dépassements de mémoire tampon et les scripts intersites, les contrats intelligents ont des anti-modèles récurrents qui peuvent être documentés.


Considérez ce guide comme une référence. Il n'est pas possible de discuter de tout le concept en détail sans en faire un livre (avertissement juste : cet article contient plus de 10 000 mots, alors n'hésitez pas à le mettre en signet et à le lire par morceaux). Cependant, il sert de liste de ce qu'il faut rechercher et de ce qu'il faut étudier. Si un sujet ne vous semble pas familier, cela devrait servir d'indicateur qu'il vaut la peine de consacrer du temps à la pratique de l'identification de cette classe de vulnérabilité.

Conditions préalables

Cet article suppose une maîtrise de base de Solidity . Si vous débutez avec Solidity, veuillez consulter notre tutoriel Solidity gratuit .

Réentrance

Nous avons beaucoup écrit sur la réentrance des contrats intelligents , nous ne le répéterons donc pas ici. Mais voici un petit résumé :


Chaque fois qu'un contrat intelligent appelle la fonction d'un autre contrat intelligent, lui envoie de l'Ether ou lui transfère un jeton, il existe une possibilité de réentrée.


  • Lorsqu'Ether est transféré, la fonction de repli ou de réception du contrat récepteur est appelée. Cela donne le contrôle au récepteur.
  • Certains protocoles de jeton avertissent le contrat intelligent récepteur qu'il a reçu le jeton en appelant une fonction prédéterminée. Cela transmet le flux de contrôle à cette fonction.
  • Lorsqu'un contrat attaquant reçoit le contrôle, il n'a pas besoin d'appeler la même fonction qui a transmis le contrôle. Il pourrait appeler une fonction différente dans le contrat intelligent de la victime (réentrance interfonctionnelle) ou même un contrat différent (réentrance intercontrat)
  • La réentrance en lecture seule se produit lorsqu'une fonction d'affichage est accessible alors que le contrat est dans un état intermédiaire.


Bien que la réentrance soit probablement la vulnérabilité de contrat intelligent la plus connue, elle ne représente qu'un petit pourcentage des piratages qui se produisent dans la nature. Le chercheur en sécurité Pascal Caversaccio (pcaveraccio) tient à jour une liste github des attaques par réentrance . En avril 2023, 46 attaques de réentrance ont été documentées dans ce référentiel.

Contrôle d'accès

Cela semble être une simple erreur, mais oublier d'imposer des restrictions sur qui peut appeler une fonction sensible (comme retirer de l'éther ou changer de propriétaire) se produit étonnamment souvent.


Même si un modificateur est en place, il y a eu des cas où le modificateur n'a pas été implémenté correctement, comme dans l'exemple ci-dessous où l'instruction require est manquante.

 // DO NOT USE! modifier onlyMinter { minters[msg.sender] == true_; }

Ce code ci-dessus est un exemple réel de cet audit : https://code4rena.com/reports/2023-01-rabbithole/#h-01-bad-implementation-in-minter-access-control-for-rabbitholereceipt-and- rabbitholetickets-contrats


Voici une autre façon dont le contrôle d'accès peut mal tourner

 function claimAirdrop(bytes32 calldata proof[]) { bool verified = MerkleProof.verifyCalldata(proof, merkleRoot, keccak256(abi.encode(msg.sender))); require(verified, "not verified"); require(alreadyClaimed[msg.sender], "already claimed"); _transfer(msg.sender, AIRDROP_AMOUNT); }

Dans ce cas, « alreadyClamed » n'est jamais défini sur true, de sorte que le demandeur peut appeler la fonction plusieurs fois.

Exemple concret : Trader bot exploité

Un exemple assez récent de contrôle d'accès insuffisant était une fonction non protégée pour recevoir des prêts flash par un bot commercial (qui s'appelait 0xbad, car l'adresse commençait par cette séquence). Il a accumulé plus d'un million de dollars de bénéfices jusqu'au jour où un attaquant a remarqué que n'importe quelle adresse pouvait appeler la fonction de réception de flashloan, pas seulement le fournisseur de flashloan.


Comme c'est généralement le cas avec les bots de trading, le code de contrat intelligent pour exécuter les transactions n'a pas été vérifié, mais l'attaquant a quand même découvert la faiblesse. Plus d'informations dans la couverture des nouvelles de rekt .

Validation d'entrée incorrecte

Si le contrôle d'accès consiste à contrôler qui appelle une fonction, la validation des entrées consiste à contrôler avec quoi ils appellent le contrat.


Cela revient généralement à oublier de mettre en place les instructions requises appropriées.

Voici un exemple rudimentaire :

 contract UnsafeBank { mapping(address => uint256) public balances; // allow depositing on other's behalf function deposit(address for) public payable { balances += msg.value; } function withdraw(address from, uint256 amount) public { require(balances[from] <= amount, "insufficient balance"); balances[from] -= amount; msg.sender.call{value: amout}(""); } }

Le contrat ci-dessus vérifie que vous ne retirez pas plus que ce que vous avez sur votre compte, mais il ne vous empêche pas de retirer d'un compte arbitraire.

Exemple concret : Sushiswap

Sushiswap a connu un piratage de ce type en raison d'un des paramètres d'une fonction externe non désinfecté.

Pirater Sushiswap

Quelle est la différence entre un contrôle d'accès inapproprié et une validation d'entrée incorrecte ?

Un contrôle d'accès incorrect signifie que msg.sender n'a pas de restrictions adéquates. Une mauvaise validation des entrées signifie que les arguments de la fonction ne sont pas suffisamment filtrés. Il y a aussi un inverse à cet anti-modèle : placer trop de restriction sur un appel de fonction.

Restriction de fonction excessive

Une validation excessive signifie probablement que les fonds ne seront pas volés, mais cela pourrait signifier que les fonds seront bloqués dans le contrat. Avoir trop de garanties en place n'est pas une bonne chose non plus.

Exemple concret : Akutars NFT

L'un des incidents les plus médiatisés a été l'Akutars NFT, qui s'est soldé par 34 millions de dollars d'Eth coincés dans le contrat intelligent et non rétractables.


Le contrat avait un mécanisme bien intentionné pour empêcher le propriétaire du contrat de se retirer jusqu'à ce que tous les remboursements de payer au-dessus du prix d'enchère néerlandais aient été donnés. Mais en raison d'un bogue documenté dans le fil Twitter lié ci-dessous, le propriétaire n'a pas pu retirer les fonds.

Vulnérabilité Akutars NFT

Trouver le bon équilibre

Sushiswap a donné trop de pouvoir aux utilisateurs non fiables, et le NFT d'Akutars a donné trop peu de pouvoir à l'administrateur. Lors de la conception de contrats intelligents, un jugement subjectif sur la liberté que chaque classe d'utilisateurs doit être faite, et cette décision ne peut pas être laissée à des tests et à des outils automatisés. Il existe des compromis importants avec la décentralisation, la sécurité et l'expérience utilisateur qui doivent être pris en compte.


Pour le programmeur de contrats intelligents, écrire explicitement ce que les utilisateurs doivent et ne doivent pas pouvoir faire avec certaines fonctions est une partie importante du processus de développement.

Nous reviendrons sur le sujet des administrateurs surpuissants plus tard.

La sécurité se résume souvent à gérer la façon dont l'argent sort du contrat

Comme indiqué dans l'introduction, il existe quatre principales façons de pirater les contrats intelligents :


  • Argent volé
  • Argent gelé
  • Récompenses insuffisantes
  • Récompenses excessives


"L'argent" signifie ici tout ce qui a de la valeur, comme les jetons, pas seulement la crypto-monnaie. Lors du codage ou de l'audit d'un contrat intelligent, le développeur doit être conscient de la manière dont la valeur doit entrer et sortir du contrat. Les problèmes énumérés ci-dessus sont les principales façons dont les contrats intelligents sont piratés, mais il existe de nombreuses autres causes profondes qui peuvent se transformer en problèmes majeurs, qui sont documentés ci-dessous.

Double vote ou usurpation d'expéditeur de msg.

L'utilisation de jetons vanille ERC20 ou de NFT comme tickets pour peser le vote n'est pas sûre car les attaquants peuvent voter avec une adresse, transférer les jetons vers une autre adresse et voter à nouveau à partir de cette adresse.

Voici un exemple minimal :

 // A malicious voter can simply transfer their tokens to // another address and vote again. contract UnsafeBallot { uint256 public proposal1VoteCount; uint256 public proposal2VoteCount; IERC20 immutable private governanceToken; constructor(IERC20 _governanceToken) { governanceToken = _governanceToken; } function voteFor1() external notAlreadyVoted { proposal1VoteCount += governanceToken.balanceOf(msg.sender); } function voteFor2() external notAlreadyVoted { proposal2VoteCount += governanceToken.balanceOf(msg.sender); } // prevent the same address from voting twice, // however the attacker can simply // transfer to a new address modifier notAlreadyVoted { require(!alreadyVoted[msg.sender], "already voted"); _; alreadyVoted[msg.sender] = true; } }

Pour empêcher cette attaque, ERC20 Snapshot ou ERC20 Votes doivent être utilisés. En prenant un instantané d'un moment dans le passé, les soldes de jetons actuels ne peuvent pas être manipulés pour obtenir un pouvoir de vote illicite.

Attaques de gouvernance de Flashloan

Cependant, l'utilisation d'un jeton ERC20 avec une capacité d'instantané ou de vote ne résout pas complètement le problème si quelqu'un peut prendre un prêt flash pour augmenter temporairement son solde, puis prendre un instantané de son solde dans la même transaction. Si cet instantané est utilisé pour voter, ils disposeront d'un nombre déraisonnablement élevé de votes.


Un flashloan prête une grande quantité d'éther ou de jeton à une adresse, mais revient si l'argent n'est pas remboursé dans la même transaction.

 contract SimpleFlashloan { function borrowERC20Tokens() public { uint256 before = token.balanceOf(address(this)); // send tokens to the borrower token.transfer(msg.sender, amount); // hand control back to the borrower to // let them do something IBorrower(msg.sender).onFlashLoan(); // require that the tokens got returned require(token.balanceOf(address(this) >= before); } }

Un attaquant peut utiliser un prêt flash pour gagner soudainement beaucoup de votes pour faire basculer des propositions en sa faveur et/ou faire quelque chose de malveillant.

Attaques sur les prix des prêts flash

Il s'agit sans doute de l'attaque la plus courante (ou du moins la plus médiatisée) contre DeFi, représentant des centaines de millions de dollars perdus. Voici une liste de ceux de haut niveau.


Le prix d'un actif sur la blockchain est souvent calculé comme le taux de change actuel entre les actifs. Par exemple, si un contrat négocie actuellement 1 USDC pour 100 k9coin, alors vous pourriez dire que k9coin a un prix de 0,01 USDC. Cependant, les prix évoluent généralement en réponse aux pressions d'achat et de vente, et les prêts flash peuvent créer une pression d'achat et de vente massive.


Lorsqu'il interroge un autre contrat intelligent sur le prix d'un actif, le développeur doit être très prudent car il suppose que le contrat intelligent qu'il appelle est à l'abri de la manipulation de prêt flash.

Contourner le contrôle du contrat

Vous pouvez "vérifier" si une adresse est un contrat intelligent en regardant sa taille de bytecode. Les comptes appartenant à des tiers (portefeuilles ordinaires) n'ont pas de bytecode. Voici quelques façons de le faire

 import "@openzeppelin/contracts/utils/Address.sol" contract CheckIfContract { using Address for address; function addressIsContractV1(address _a) { return _a.code.length == 0; } function addressIsContractV2(address _a) { // use the openzeppelin libraryreturn _a.isContract(); } }


Cependant, cela a quelques limites

  • Si un contrat effectue un appel externe à partir d'un constructeur, sa taille de bytecode apparente sera nulle car le code de déploiement du contrat intelligent n'a pas encore renvoyé le code d'exécution
  • L'espace peut être vide maintenant, mais un attaquant peut savoir qu'il peut y déployer un contrat intelligent à l'avenir en utilisant create2


En général, vérifier si une adresse est un contrat est généralement (mais pas toujours) un antimodèle. Les portefeuilles multisignatures sont eux-mêmes des contrats intelligents, et faire tout ce qui pourrait casser les portefeuilles multisignatures brise la composabilité.


L'exception à cela est de vérifier si la cible est un contrat intelligent avant d'appeler un crochet de transfert. Plus à ce sujet plus tard.

tx.origine

Il y a rarement une bonne raison d'utiliser tx.origin. Si tx.origin est utilisé pour identifier l'expéditeur, une attaque man-in-the-middle est alors possible. Si l'utilisateur est amené à appeler un contrat intelligent malveillant, alors le contrat intelligent peut utiliser toute l'autorité dont dispose tx.origin pour faire des ravages.


Considérez cet exercice suivant et les commentaires au-dessus du code.

 contract Phish { function phishingFunction() public { // this fails, because this contract does not have approval/allowance token.transferFrom(msg.sender, address(this), token.balanceOf(msg.sender)); // this also fails, because this creates approval for the contract,// not the wallet calling this phishing function token.approve(address(this), type(uint256).max); } }


Cela ne signifie pas que vous pouvez appeler des contrats intelligents arbitraires en toute sécurité. Mais il existe une couche de sécurité intégrée à la plupart des protocoles qui sera contournée si tx.origin est utilisé pour l'authentification.

Parfois, vous pouvez voir un code qui ressemble à ceci :

 require(msg.sender == tx.origin, "no contracts");


Lorsqu'un contrat intelligent appelle un autre contrat intelligent, msg.sender sera le contrat intelligent et tx.origin sera le portefeuille de l'utilisateur, donnant ainsi une indication fiable que l'appel entrant provient d'un contrat intelligent. Cela est vrai même si l'appel provient du constructeur.


La plupart du temps, ce modèle de conception n'est pas une bonne idée. Les portefeuilles multisignatures et les portefeuilles de l'EIP 4337 ne pourront pas interagir avec une fonction contenant ce code. Ce modèle peut généralement être observé dans les monnaies NFT, où il est raisonnable de s'attendre à ce que la plupart des utilisateurs utilisent un portefeuille traditionnel. Mais à mesure que l'abstraction de compte devient plus populaire, ce modèle gênera plus qu'il n'aidera.

Chagrin de gaz ou déni de service

Une attaque chagrine signifie que le pirate essaie de "causer du chagrin" à d'autres personnes, même s'ils n'en tirent aucun profit économique.


Un contrat intelligent peut utiliser de manière malveillante tout le gaz qui lui est transmis en entrant dans une boucle infinie. Considérez l'exemple suivant :

 contract Mal { fallback() external payable { // infinite loop uses up all the gas while (true) { } } }


Si un autre contrat distribue ether à une liste d'adresses comme suit :

 contract Distribute { funtion distribute(uint256 total) public nonReentrant { for (uint i; i < addresses.length; ) { (bool ok, ) addresses.call{value: total / addresses.length}(""); // ignore ok, if it reverts we move on // traditional gas saving trick for for loops unchecked { ++i; } } } }


Ensuite, la fonction reviendra lorsqu'elle enverra de l'éther à Mal. L'appel dans le code ci-dessus transmet 63/64 du gaz disponible, il n'y aura donc probablement pas assez de gaz pour terminer l'opération avec seulement 1/64 du gaz restant.


Un contrat intelligent peut renvoyer une grande matrice de mémoire qui consomme beaucoup de gaz

Considérez l'exemple suivant

 function largeReturn() public { // result might be extremely long! (book ok, bytes memory result) = otherContract.call(abi.encodeWithSignature("foo()")); require(ok, "call failed"); }

Les matrices de mémoire utilisent une quantité quadratique de gaz après 724 octets, de sorte qu'une taille de données de retour soigneusement choisie peut perturber l'appelant.


Même si la variable result n'est pas utilisée, elle est toujours copiée en mémoire. Si vous souhaitez limiter la taille de retour à un certain montant, vous pouvez utiliser assembly

 function largeReturn() public { assembly { let ok := call(gas(), destinationAddress, value, dataOffset, dataSize, 0x00, 0x00); // nothing is copied to memory until you // use returndatacopy() } }

La suppression de tableaux auxquels d'autres peuvent ajouter est également un vecteur de déni de service

Bien que l'effacement du stockage soit une opération économe en gaz, il a tout de même un coût net. Si un tableau devient trop long, il devient impossible de le supprimer. Voici un exemple minimal

 contract VulnerableArray { address[] public stuff; function addSomething(address something) public { stuff.push(something); } // if stuff is too long, this will become undeletable due to // the gas cost function deleteEverything() public onlyOwner { delete stuff; } }

ERC777, ERC721 et ERC1155 peuvent également être des vecteurs de chagrin

Si un contrat intelligent transfère des jetons qui ont des hooks de transfert, un attaquant peut configurer un contrat qui n'accepte pas le jeton (il n'a pas de fonction onReceive ou programme la fonction pour revenir en arrière). Cela rendra le jeton non transférable et entraînera l'annulation de toute la transaction.


Avant d'utiliser safeTransfer ou transfer, considérez la possibilité que le destinataire force la transaction à revenir.

 contract Mal is IERC721Receiver, IERC1155Receiver, IERC777Receiver { // this will intercept any transfer hook fallback() external payable { // infinite loop uses up all the gaswhile (true) { } } // we could also selectively deny transactions function onERC721Received(address operator, address from, uint256 tokenId, bytes calldata data ) external returns (bytes4) { if (wakeUpChooseViolence()) { revert(); } else { return IERC721Receiver.onERC721Received.selector; } } }

Aléatoire non sécurisé

Il n'est actuellement pas possible de générer de l'aléatoire en toute sécurité avec une seule transaction sur la blockchain. Les chaînes de blocs doivent être entièrement déterministes, sinon les nœuds distribués ne pourraient pas parvenir à un consensus sur l'état. Parce qu'ils sont entièrement déterministes, tout nombre "aléatoire" peut être prédit. La fonction de lancer de dés suivante peut être exploitée.


 contract UnsafeDice { function randomness() internal returns (uint256) { return keccak256(abi.encode(msg.sender, tx.origin, block.timestamp, tx.gasprice, blockhash(block.number - 1); } // our dice can land on one of {0,1,2,3,4,5}function rollDice() public payable { require(msg.value == 1 ether); if (randomness() % 6) == 5) { msg.sender.call{value: 2 ether}(""); } } } contract ExploitDice { function randomness() internal returns (uint256) { return keccak256(abi.encode(msg.sender, tx.origin, block.timestamp, tx.gasprice, blockhash(block.number - 1); } function betSafely(IUnsafeDice game) public payable { if (randomness % 6) == 5)) { game.betSafely{value: 1 ether}() } // else don't do anything } }


Peu importe la façon dont vous générez le caractère aléatoire, car un attaquant peut le reproduire exactement. Ajouter plus de sources "d'entropie" telles que le msg.sender, l'horodatage, etc. n'aura aucun effet car le contrat intelligent peut le mesurer en deux.

Utilisation de l'Oracle Chainlink Randomness

Chainlink est une solution populaire pour obtenir des nombres aléatoires sécurisés. Il le fait en deux étapes. Tout d'abord, les contrats intelligents envoient une demande de caractère aléatoire à l'oracle, puis quelques blocs plus tard, l'oracle répond avec un nombre aléatoire.


Puisqu'un attaquant ne peut pas prédire l'avenir, il ne peut pas prédire le nombre aléatoire.

Sauf si le contrat intelligent utilise mal l'oracle.


  • Le contrat intelligent demandant le caractère aléatoire ne doit rien faire tant que le nombre aléatoire n'est pas renvoyé. Sinon, un attaquant peut surveiller le mempool pour l'oracle renvoyant le caractère aléatoire et lancer l'oracle, sachant quel sera le nombre aléatoire.
  • Les oracles aléatoires eux-mêmes pourraient essayer de manipuler votre application. Ils ne peuvent pas choisir des nombres aléatoires sans consensus des autres nœuds, mais ils peuvent retenir et réorganiser des nombres aléatoires si votre application en demande plusieurs en même temps.
  • La finalité n'est pas instantanée sur Ethereum ou la plupart des autres chaînes EVM. Ce n'est pas parce qu'un bloc est le plus récent qu'il ne le restera pas nécessairement. C'est ce qu'on appelle une "réorganisation en chaîne". En fait, la chaîne peut modifier plus que le bloc final. C'est ce qu'on appelle la "profondeur de réorganisation". Etherscan signale des réorganisations pour diverses chaînes, par exemple les réorganisations Ethereum et les réorganisations Polygon. Les réorganisations peuvent être aussi profondes que 30 blocs ou plus sur Polygon, donc attendre moins de blocs peut rendre l'application vulnérable (cela peut changer lorsque le zk-evm deviendra le consensus standard sur Polygon, car la finalité correspondra à celle d'Ethereum mais c'est une prédiction future , pas un fait sur le présent).

Obtenir des données obsolètes à partir d'un prix Oracle

Il n'y a pas de SLA (accord de niveau de service) pour que Chainlink maintienne ses oracles de prix à jour dans un certain délai. Lorsque la chaîne est gravement encombrée (comme lorsque la menthe Yuga Labs Otherside a submergé Ethereum au point qu'aucune transaction n'est en cours), les mises à jour des prix peuvent être retardées.


Un contrat intelligent qui utilise un oracle de prix doit vérifier explicitement que les données ne sont pas obsolètes, c'est-à-dire qu'elles ont été mises à jour récemment dans un certain seuil. Sinon, il ne peut pas prendre de décision fiable en ce qui concerne les prix.


Il y a une complication supplémentaire que si le prix ne change pas au-delà d'un seuil d'écart , l'oracle peut ne pas mettre à jour le prix pour économiser de l'essence, ce qui pourrait affecter le seuil de temps considéré comme « obsolète ».


Il est important de comprendre le SLA d'un oracle sur lequel repose un contrat intelligent.

S'appuyer sur un seul oracle

Peu importe à quel point un oracle semble sécurisé, une attaque peut être découverte à l'avenir. La seule défense contre cela est d'utiliser plusieurs oracles indépendants.

Les oracles en général sont difficiles à comprendre

La blockchain peut être assez sécurisée, mais mettre des données sur la chaîne en premier lieu nécessite une sorte d'opération hors chaîne qui renonce à toutes les garanties de sécurité fournies par les blockchains. Même si les oracles restent honnêtes, leur source de données peut être manipulée. Par exemple, un oracle peut rapporter de manière fiable les prix d'un échange centralisé, mais ceux-ci peuvent être manipulés avec d'importants ordres d'achat et de vente. De même, les oracles qui dépendent des données des capteurs ou de certaines API web2 sont soumis aux vecteurs de piratage traditionnels.


Une bonne architecture de contrat intelligent évite complètement l'utilisation d'oracles dans la mesure du possible.

Comptabilité mixte

Considérez le contrat suivant

 contract MixedAccounting { uint256 myBalance; function deposit() public payable { myBalance = myBalance + msg.value; } function myBalanceIntrospect() public view returns (uint256) { return address(this).balance; } function myBalanceVariable() public view returns (uint256) { return myBalance; } function notAlwaysTrue() public view returns (bool) { return myBalanceIntrospect() == myBalanceVariable(); } }


Le contrat ci-dessus n'a pas de fonction de réception ou de repli, donc le transfert direct d'Ether vers celui-ci sera annulé. Cependant, un contrat peut lui envoyer de force Ether avec autodestruction.


Dans ce cas, myBalanceIntrospect() sera supérieur à myBalanceVariable(). La méthode de comptabilité Ether est correcte, mais si vous utilisez les deux, le contrat peut avoir un comportement incohérent.


Il en va de même pour les jetons ERC20.

 contract MixedAccountingERC20 { IERC20 token; uint256 myTokenBalance; function deposit(uint256 amount) public { token.transferFrom(msg.sender, address(this), amount); myTokenBalance = myTokenBalance + amount; } function myBalanceIntrospect() public view returns (uint256) { return token.balanceOf(address(this)); } function myBalanceVariable() public view returns (uint256) { return myTokenBalance; } function notAlwaysTrue() public view returns (bool) { return myBalanceIntrospect() == myBalanceVariable(); } }

Encore une fois, nous ne pouvons pas supposer que myBalanceIntrospect() et myBalanceVariable() renverront toujours la même valeur. Il est possible de transférer directement les jetons ERC20 vers MixedAccountingERC20, en contournant la fonction de dépôt et en ne mettant pas à jour la variable myTokenBalance.


Lors de la vérification des soldes avec introspection, des contrôles d'égalité stricts doivent être évités car le solde peut être modifié à volonté par une personne extérieure.

Traiter les preuves cryptographiques comme des mots de passe

Ce n'est pas une bizarrerie de Solidity, mais plutôt un malentendu parmi les développeurs sur la façon d'utiliser la cryptographie pour donner aux adresses des privilèges spéciaux. Le code suivant n'est pas sécurisé

 contract InsecureMerkleRoot { bytes32 merkleRoot; function airdrop(bytes[] calldata proof, bytes32 leaf) external { require(MerkleProof.verifyCalldata(proof, merkleRoot, leaf), "not verified"); require(!alreadyClaimed[leaf], "already claimed airdrop"); alreadyClaimed[leaf] = true; mint(msg.sender, AIRDROP_AMOUNT); } }


Ce code n'est pas sécurisé pour trois raisons :

  1. Toute personne connaissant les adresses sélectionnées pour le largage peut recréer l'arbre Merkle et créer une preuve valide.
  2. La feuille n'est pas hachée. Un attaquant peut soumettre une feuille égale à la racine merkle et contourner l'instruction require.
  3. Même si les deux problèmes ci-dessus sont résolus, une fois que quelqu'un soumet une preuve valide, il peut être anticipé.


Les preuves cryptographiques (arbres Merkle, signatures, etc.) doivent être liées à msg.sender, qu'un attaquant ne peut pas manipuler sans acquérir la clé privée.

La solidité n'est pas convertie à la taille uint finale

 function limitedMultiply(uint8 a, uint8 b) public pure returns (uint256 product) { product = a * b; }

Bien que product soit une variable uint256 , le résultat de la multiplication ne peut pas être supérieur à 255 ou le code sera inversé.


Ce problème peut être atténué en convertissant individuellement chaque variable.

 function unlimitedMultiply(uint8 a, uint8 b) public pure returns (uint256 product) { product = uint256(a) * uint256(b); }

Une situation comme celle-ci peut se produire lors de la multiplication d'entiers emballés dans une structure. Vous devez en tenir compte lors de la multiplication de petites valeurs qui ont été emballées dans une structure

 struct Packed { uint8 time; uint16 rewardRate } //... Packed p; p.time * p.rewardRate; // this might revert!

Le downcasting de solidité ne revient pas en cas de débordement

La solidité ne vérifie pas s'il est sûr de convertir un entier en un plus petit. À moins qu'une logique métier ne garantisse que le downcasting est sûr, une bibliothèque comme SafeCast doit être utilisée.

 function test(int256 value) public pure returns (int8) { return int8(value + 1); // overflows and does not revert }

Les écritures dans les pointeurs de stockage n'enregistrent pas les nouvelles données.

Le code semble copier les données de myArray[1] vers myArray[0], mais ce n'est pas le cas. Si vous commentez la dernière ligne de la fonction, le compilateur dira que la fonction doit être transformée en fonction d'affichage. L'écriture sur foo n'écrit pas dans le stockage sous-jacent.


 contract DoesNotWrite { struct Foo { uint256 bar; } Foo[] public myArray; function moveToSlot0() external { Foo storage foo = myArray[0]; foo = myArray[1]; // myArray[0] is unchanged // we do this to make the function a state // changing operation // and silence the compiler warning myArray[1] = Foo({bar: 100}); } }

Donc, n'écrivez pas dans les pointeurs de stockage.

La suppression de structures contenant des types de données dynamiques ne supprime pas les données dynamiques

Si un mappage (ou un tableau dynamique) se trouve dans une structure et que la structure est supprimée, le mappage ou le tableau ne sera pas supprimé.


À l'exception de la suppression d'une baie, le mot-clé delete ne peut supprimer qu'un emplacement de stockage. Si l'emplacement de stockage contient des références à d'autres emplacements de stockage, ceux-ci ne seront pas supprimés.

 contract NestedDelete { mapping(uint256 => Foo) buzz; struct Foo { mapping(uint256 => uint256) bar; } Foo foo; function addToFoo(uint256 i) external { buzz[i].bar[5] = 6; } function getFromFoo(uint256 i) external view returns (uint256) { return buzz[i].bar[5]; } function deleteFoo(uint256 i) external { // internal map still holds the data in the // mapping and array delete buzz[i]; } }


Faisons maintenant la séquence de transaction suivante

  1. addToFoo(1)
  2. getFromFoo(1) renvoie 6
  3. supprimerFoo(1)
  4. getFromFoo(1) renvoie toujours 6 !


N'oubliez pas que les cartes ne sont jamais "vides" dans Solidity. Ainsi, si quelqu'un accède à un élément qui a été supprimé, la transaction ne sera pas annulée mais renverra à la place la valeur zéro pour ce type de données.

Problèmes de jeton ERC20

Si vous ne traitez que des jetons ERC20 de confiance, la plupart de ces problèmes ne s'appliquent pas. Cependant, lors de l'interaction avec un jeton ERC20 arbitraire ou partiellement non fiable, voici quelques points à surveiller.

ERC20 : Commission de transfert

Lorsque vous traitez avec des jetons non fiables, vous ne devez pas supposer que votre solde augmente nécessairement du montant. Il est possible pour un jeton ERC20 d'implémenter sa fonction de transfert comme suit :

 contract ERC20 { // internally called by transfer() and transferFrom() // balance and approval checks happen in the caller function _transfer(address from, address to, uint256 amount) internal returns (bool) { fee = amount * 100 / 99; balanceOf[from] -= to; balanceOf[to] += (amount - fee); balanceOf[TREASURY] += fee; emit Transfer(msg.sender, to, (amount - fee)); return true; } }


Ce jeton applique une taxe de 1 % sur chaque transaction. Donc, si un contrat intelligent interagit avec le jeton comme suit, nous obtiendrons soit des retours inattendus, soit de l'argent volé.

 contract Stake { mapping(address => uint256) public balancesInContract; function stake(uint256 amount) public { token.transferFrom(msg.sender, address(this), amount); balancesInContract[msg.sender] += amount; // THIS IS WRONG! } function unstake() public { uint256 toSend = balancesInContract[msg.sender]; delete balancesInContract[msg.sender]; // this could revert because toSend is 1% greater than// the amount in the contract. Otherwise, 1% will be "stolen"// from other depositors. token.transfer(msg.sender, toSend); } }

ERC20 : rebaser les jetons

Le jeton de rebasage a été popularisé par le jeton sOhm d' Olympus DAO et le jeton AMPL d'Ampleforth . Coingecko tient à jour une liste de jetons ERC20 de rebasage.


Lorsqu'un jeton change de base, l'offre totale change et le solde de chacun augmente ou diminue en fonction de la direction de la rebase.


Le code suivant est susceptible de se casser lorsqu'il s'agit d'un jeton de rebasage

 contract WillBreak { mapping(address => uint256) public balanceHeld; IERC20 private rebasingToken function deposit(uint256 amount) external { balanceHeld[msg.sender] = amount; rebasingToken.transferFrom(msg.sender, address(this), amount); } function withdraw() external { amount = balanceHeld[msg.sender]; delete balanceHeld[msg.sender]; // ERROR, amount might exceed the amount // actually held by the contract rebasingToken.transfer(msg.sender, amount); } }


La solution de nombreux contrats consiste simplement à interdire le changement de base des jetons. Cependant, on pourrait modifier le code ci-dessus pour vérifier balanceOf(address(this)) avant de transférer le solde du compte à l'expéditeur. Ensuite, cela fonctionnerait toujours même si l'équilibre change.

ERC20 : ERC777 en tenue ERC20

ERC20, s'ils sont implémentés conformément à la norme, les jetons ERC20 n'ont pas de crochets de transfert, et donc transfer et transferFrom n'ont pas de problème de réentrance.


Les jetons avec crochets de transfert présentent des avantages significatifs, c'est pourquoi toutes les normes NFT les implémentent et pourquoi ERC777 a été finalisé. Cependant, cela a causé suffisamment de confusion pour qu'Openzeppelin déconseille la bibliothèque ERC777.


Si vous voulez que votre protocole soit compatible avec les jetons qui se comportent comme des jetons ERC20 mais qui ont des hooks de transfert, il suffit alors de traiter les fonctions transfer et transferFrom comme si elles émettaient un appel de fonction au récepteur.


Cette réentrée ERC777 est arrivée à Uniswap (Openzeppelin a documenté l'exploit ici si vous êtes curieux).

ERC20 : tous les jetons ERC20 ne renvoient pas la valeur true

La spécification ERC20 stipule qu'un jeton ERC20 doit renvoyer la valeur true lorsqu'un transfert réussit. Étant donné que la plupart des implémentations ERC20 ne peuvent échouer que si l'allocation est insuffisante ou si le montant transféré est trop important, la plupart des développeurs ont pris l'habitude d'ignorer la valeur de retour des jetons ERC20 et de supposer qu'un transfert échoué sera annulé.


Franchement, cela n'a aucune conséquence si vous ne travaillez qu'avec un jeton ERC20 de confiance dont vous connaissez le comportement. Mais lorsqu'il s'agit de jetons ERC20 arbitraires, cette variance de comportement doit être prise en compte.


Il existe une attente implicite dans de nombreux contrats selon laquelle les transferts échoués doivent toujours revenir, et non renvoyer faux, car la plupart des jetons ERC20 n'ont pas de mécanisme pour renvoyer faux, ce qui a entraîné beaucoup de confusion.


Pour compliquer davantage cette question, certains jetons ERC20 ne suivent pas le protocole de retour vrai, notamment Tether. Certains jetons reviennent en cas d'échec du transfert, ce qui entraînera la remontée du retour jusqu'à l'appelant. Ainsi, certaines bibliothèques enveloppent les appels de transfert de jeton ERC20 pour intercepter le retour et renvoyer un booléen à la place.


Voici quelques implémentations

Openzeppelin SafeTransfer

Solady SafeTransfer (considérablement plus économe en gaz)

ERC20 : Empoisonnement d'adresse

Il ne s'agit pas d'une vulnérabilité de contrat intelligent, mais nous le mentionnons ici par souci d'exhaustivité.

Le transfert de zéro jeton ERC20 est autorisé par la spécification. Cela peut prêter à confusion pour les applications frontales et tromper les utilisateurs sur la personne à qui ils ont récemment envoyé des jetons. Metamask a plus à ce sujet dans ce fil .

ERC20 : tout simplement robuste

(Dans le langage web3, "robuste" signifie "avoir le tapis retiré sous vous".)

Rien n'empêche quelqu'un d'ajouter une fonction à un jeton ERC20 qui lui permet de créer, de transférer et de graver des jetons à volonté - ou de s'autodétruire ou de se mettre à niveau. Donc, fondamentalement, il y a une limite à la façon dont un jeton ERC20 peut être "non fiable".


Bogues logiques dans les protocoles de prêt

Lorsque l'on considère comment les protocoles DeFi basés sur le prêt et l'emprunt peuvent se casser, il est utile de penser aux bogues qui se propagent au niveau du logiciel et affectent le niveau de la logique métier. Il y a beaucoup d'étapes pour former et conclure un contrat obligataire. Voici quelques vecteurs d'attaque à considérer.

Manières dont les prêteurs perdent

  • Bugs qui permettent de réduire le principal dû (éventuellement à zéro) sans effectuer de paiement.
  • La garantie de l'acheteur ne peut être liquidée lorsque le prêt n'est pas remboursé ou que la garantie tombe en dessous du seuil.
  • Si le protocole dispose d'un mécanisme de transfert de propriété de la dette, cela pourrait être un vecteur de vol d'obligations auprès des prêteurs.
  • La date d'échéance du principal du prêt ou des paiements est incorrectement reportée à une date ultérieure.

Manières dont les emprunteurs perdent

  • Un bogue où le remboursement du principal n'entraîne pas de réduction du principal.
  • Une attaque de bogue ou de chagrin empêche l'utilisateur d'effectuer le paiement.
  • Le principal ou le taux d'intérêt est augmenté de manière illégitime.
  • La manipulation d'Oracle conduit à dévaluer la garantie.
  • La date d'échéance du principal du prêt ou des paiements est incorrectement déplacée à une date antérieure.


Si la garantie est retirée du protocole, le prêteur et l'emprunteur sont perdants, car l'emprunteur n'a aucune incitation à rembourser le prêt et l'emprunteur perd le principal.


Comme on peut le voir ci-dessus, il y a beaucoup plus de niveaux pour qu'un protocole DeFi soit "piraté" qu'un tas d'argent drainé du protocole (le genre d'événements qui font généralement l'actualité). C'est un domaine où les exercices de sécurité CTF (capture the flag) peuvent être trompeurs. Bien que le vol des fonds du protocole soit le résultat le plus catastrophique, ce n'est en aucun cas le seul contre lequel se défendre.

Valeurs de retour non cochées

Il existe deux manières d'appeler un contrat intelligent externe : 1) appeler la fonction avec une définition d'interface ; 2) en utilisant la méthode .call. Ceci est illustré ci-dessous

 contract A { uint256 public x; function setx(uint256 _x) external { require(_x > 10, "x must be bigger than 10"); x = _x; } } interface IA { function setx(uint256 _x) external; } contract B { function setXV1(IA a, uint256 _x) external { a.setx(_x); } function setXV2(address a, uint256 _x) external { (bool success, ) = a.call(abi.encodeWithSignature("setx(uint256)", _x)); // success is not checked! } }

Dans le contrat B, setXV2 peut échouer silencieusement si _x est inférieur à 10. Lorsqu'une fonction est appelée via la méthode .call, l'appelé peut revenir en arrière, mais le parent ne le fera pas. La valeur de success doit être vérifiée et le comportement du code doit se ramifier en conséquence.

Variables privées

Les variables privées sont toujours visibles sur la blockchain, les informations sensibles ne doivent donc jamais y être stockées. S'ils n'étaient pas accessibles, comment les validateurs pourraient-ils traiter les transactions qui dépendent de leurs valeurs ? Les variables privées ne peuvent pas être lues à partir d'un contrat Solidity extérieur, mais elles peuvent être lues hors chaîne à l'aide d'un client Ethereum.


Pour lire une variable, vous devez connaître son emplacement de stockage. Dans l'exemple suivant, l'emplacement de stockage de myPrivateVar est 0.

 // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract PrivateVarExample { uint256 private myPrivateVar; constructor(uint256 _initialValue) { myPrivateVar = _initialValue; } }

Voici le code javascript pour lire la variable privée du smart contract déployé

 const Web3 = require("web3"); const PRIVATE_VAR_EXAMPLE_ADDRESS = "0x123..."; // Replace with your contract address async function readPrivateVar() { const web3 = new Web3("http://localhost:8545"); // Replace with your provider's URL // Read storage slot 0 (where 'myPrivateVar' is stored) const storageSlot = 0; const privateVarValue = await web3.eth.getStorageAt( PRIVATE_VAR_EXAMPLE_ADDRESS, storageSlot ); console.log("Value of private variable 'myPrivateVar':", web3.utils.hexToNumberString(privateVarValue)); } readPrivateVar();

Appel de délégué non sécurisé

Delegatecall ne doit jamais être utilisé avec des contrats non approuvés car il transfère tout le contrôle au délégué appelé. Dans cet exemple, le contrat non approuvé vole tout l'éther du contrat.

 contract UntrustedDelegateCall { constructor() payable { require(msg.value == 1 ether); } function doDelegateCall(address _delegate, bytes calldata data) public { (bool ok, ) = _delegate.delegatecall(data); require(ok, "delegatecall failed"); } } contract StealEther { function steal() public { // you could also selfdestruct here // if you really wanted to be mean (bool ok,) = tx.origin.call{value: address(this).balance}(""); require(ok); } function attack(address victim) public { UntrustedDelegateCall(victim).doDelegateCall( address(this), abi.encodeWithSignature("steal()")); } }

Mettre à niveau les bugs liés aux proxys

Nous ne pouvons pas rendre justice à ce sujet dans une seule section. La plupart des bogues de mise à niveau peuvent généralement être évités en utilisant le plugin hardhat d'Openzeppelin et en lisant les problèmes contre lesquels il protège. ( https://docs.openzeppelin.com/upgrades-plugins/1.x/ ).


En bref, voici les problèmes liés aux mises à niveau des contrats intelligents :

  • l'autodestruction et l'appel délégué ne doivent pas être utilisés dans les contrats d'implémentation
  • il faut veiller à ce que les variables de stockage ne s'écrasent jamais lors des mises à niveau
  • l'appel de bibliothèques externes doit être évité dans les contrats d'implémentation car il n'est pas possible de prédire comment ils affecteront l'accès au stockage
  • le déployeur ne doit jamais négliger d'appeler la fonction d'initialisation
  • ne pas inclure une variable d'écart dans les contrats de base pour éviter les collisions de stockage lorsque de nouvelles variables sont ajoutées au contrat de base (ceci est géré automatiquement par le plugin hardhat)
  • les valeurs des variables immuables ne sont pas conservées entre les mises à jour
  • faire quoi que ce soit dans le constructeur est fortement déconseillé car les futures mises à niveau devraient exécuter une logique de constructeur identique pour maintenir la compatibilité.

Administrateurs surpuissants

Ce n'est pas parce qu'un contrat a un propriétaire ou un administrateur que son pouvoir doit être illimité. Considérez un NFT. Il est raisonnable que seul le propriétaire retire les revenus de la vente NFT, mais le fait de pouvoir suspendre le contrat (transferts en bloc) pourrait faire des ravages si les clés privées du propriétaire étaient compromises. En règle générale, les privilèges d'administrateur doivent être aussi minimes que possible afin de minimiser les risques inutiles.


En parlant de propriété contractuelle…

Utilisez Ownable2Step au lieu de Ownable

Ce n'est techniquement pas une vulnérabilité, mais OpenZeppelin peut entraîner la perte de la propriété du contrat si la propriété est transférée à une adresse inexistante. Ownable2step demande au destinataire de confirmer la propriété. Cela garantit contre l'envoi accidentel de la propriété à une adresse mal saisie.

Erreurs d'arrondi

La solidité n'a pas de flottants, les erreurs d'arrondi sont donc inévitables. Le concepteur doit être conscient de savoir si la bonne chose à faire est d'arrondir vers le haut ou vers le bas, et en faveur de qui l'arrondi doit être.


La division doit toujours être effectuée en dernier. Le code suivant effectue une conversion incorrecte entre les stablecoins qui ont un nombre différent de décimales. Le mécanisme d'échange suivant permet à un utilisateur de prendre gratuitement une petite quantité d'USDC (qui a 6 décimales) lors de l'échange contre dai (qui a 18 décimales). La variable daiToTake arrondira à zéro, ne prenant rien de l'utilisateur en échange d'un usdcAmount différent de zéro.

 contract Exchange { uint256 private constant CONVERSION = 1e12; function swapDAIForUSDC(uint256 usdcAmount) external pure returns (uint256 a) { uint256 daiToTake = usdcAmount / CONVERSION; conductSwap(daiToTake, usdcAmount); } }

Avant-gardiste

Faire de l'avant dans le contexte d'Etheruem (et de chaînes similaires) signifie observer une transaction en attente et exécuter une autre transaction avant celle-ci en payant un prix du gaz plus élevé. Autrement dit, l'attaquant a "couru devant" la transaction. Si la transaction est un commerce rentable, il est logique de copier la transaction exactement, sauf à payer un prix du gaz plus élevé. Ce phénomène est parfois appelé MEV, ce qui signifie valeur extractible mineure, mais parfois valeur extractible maximale dans d'autres contextes. Les producteurs de blocs ont un pouvoir illimité pour réorganiser les transactions et insérer les leurs, et historiquement, les producteurs de blocs étaient des mineurs avant qu'Ethereum ne passe à la preuve de participation, d'où le nom.

Frontrunning : Retrait non protégé

Le retrait d'Ether d'un contrat intelligent peut être considéré comme un "commerce rentable". Vous exécutez une transaction sans frais (à part le gaz) et vous vous retrouvez avec plus de crypto-monnaie que vous n'en aviez au départ.

 contract UnprotectedWithdraw { constructor() payable { require(msg.value == 1 ether, "must create with 1 eth"); } function unsafeWithdraw() external { (bool ok, ) = msg.sender.call{value: address(this).value}(""); require(ok, "transfer failed"). } }


Si vous déployez ce contrat et essayez de vous retirer, un bot précurseur remarquera votre appel à "unsafeWithdraw" dans le mempool et le copiera pour obtenir l'Ether en premier.

Frontrunning : ERC4626 Inflation attack, une combinaison d'erreurs de frontrunning et d'arrondi

Nous avons écrit en profondeur sur l'attaque d'inflation ERC-4626 dans notre tutoriel ERC4626 . Mais l'essentiel est qu'un contrat ERC4626 distribue des jetons «partagés» en fonction du pourcentage d '«actifs» qu'un commerçant apporte.


En gros, cela fonctionne comme suit :

 function getShares(...) external { // code shares_received = assets_contributed / total_assets; // more code }

Bien sûr, personne n'apportera d'actifs et ne récupérera aucune action, mais ils ne peuvent pas prédire que cela se produira si quelqu'un peut prendre les devants pour obtenir les actions.


Par exemple, ils apportent 200 actifs alors que le pool en compte 20, ils s'attendent à obtenir 100 actions. Mais si quelqu'un lance la transaction pour déposer 200 actifs, alors la formule sera 200/220, qui arrondit à zéro, ce qui fait que la victime perd des actifs et ne récupère aucune action.

Avant-garde : homologation ERC20

Il est préférable d'illustrer cela avec un exemple réel plutôt que de le décrire dans l'abstrait


  1. Supposons qu'Alice approuve Eve pour 100 jetons. (Eve est toujours la personne maléfique, pas Bob, donc nous garderons la convention).
  2. Alice change d'avis et envoie une transaction pour changer l'approbation d'Eve à 50.
  3. Avant que la transaction pour changer l'approbation à 50 ne soit incluse dans le bloc, elle se trouve dans le mempool où Eve peut la voir.
  4. Eve envoie une transaction pour réclamer ses 100 jetons afin d'obtenir l'approbation de 50.
  5. L'approbation pour 50 passe par
  6. Eve récupère les 50 jetons.


Maintenant, Eve a 150 jetons au lieu de 100 ou 50. La solution à cela est de mettre l'approbation à zéro avant de l'augmenter ou de la diminuer, lorsqu'il s'agit d'approbations non fiables.

Avant-garde : attaques en sandwich

Le prix d'un actif évolue en réponse aux pressions d'achat et de vente. Si une commande importante se trouve dans le mempool, les commerçants sont incités à copier la commande mais avec un prix du gaz plus élevé. De cette façon, ils achètent l'actif, laissent la commande importante faire monter le prix, puis vendent immédiatement. L'ordre de vente est parfois appelé "backrunning". L'ordre de vente peut être effectué en plaçant un ordre de vente avec un prix du gaz inférieur afin que la séquence ressemble à ceci


  1. acheter en avant-première
  2. gros achat
  3. vendre


La principale défense contre cette attaque est de fournir un paramètre de "glissement". Si l'« achat initial » lui-même fait monter le prix au-delà d'un certain seuil, l'ordre « achat important » reviendra, faisant échouer le favori sur la transaction.


C'est ce qu'on appelle un sandwich, parce que le gros achat est encadré par l'achat en amont et la vente en aval. Cette attaque fonctionne également avec des ordres de vente importants, juste dans la direction opposée.

En savoir plus sur le frontrun

Le frontrun est un sujet énorme. Flashbots a fait des recherches approfondies sur le sujet et a publié plusieurs outils et articles de recherche pour aider à minimiser ses externalités négatives.


La question de savoir si le frontrunning peut être "conçu" avec une architecture de blockchain appropriée est un sujet de débat qui n'a pas été définitivement réglé. Les deux articles suivants sont des classiques indémodables sur le sujet :


Ethereum est une forêt sombre

Fuyant la sombre forêt

Lié aux signatures

Les signatures numériques ont deux usages dans le cadre des smart contracts :

  • permettre aux adresses d'autoriser certaines transactions sur la blockchain sans effectuer de transaction réelle
  • prouver à un contrat intelligent que l'expéditeur a le pouvoir de faire quelque chose, selon une adresse prédéfinie

Voici un exemple d'utilisation de signatures numériques en toute sécurité pour donner à un utilisateur le privilège de frapper un NFT :

 import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; contract NFT is ERC721("name", "symbol") { function mint(bytes calldata signature) external { address recovered = keccak256(abi.encode(msg.sender)).toEthSignedMessageHash().recover(signature); require(recovered == authorizer, "signature does not match"); } }

Un exemple classique est la fonctionnalité d'approbation dans ERC20. Pour approuver une adresse pour retirer un certain nombre de jetons de notre compte, nous devons effectuer une transaction Ethereum réelle, qui coûte du gaz.


Il est parfois plus efficace de transmettre une signature numérique au destinataire hors chaîne, puis le destinataire fournit la signature au contrat intelligent pour prouver qu'il était autorisé à effectuer la transaction.


ERC20Permit permet des approbations avec une signature numérique. La fonction est décrite comme suit

 function permit(address owner, address spender, uint256 amount, uint256 deadline, uint8 v, bytes32 r, bytes32 s ) public

Plutôt que d'envoyer une transaction d'approbation réelle, le propriétaire peut "signer" l'approbation pour le dépensier (avec une date limite). Le dépensier approuvé peut ensuite appeler la fonction d'autorisation avec les paramètres fournis.

Anatomie d'une signature

Vous verrez fréquemment les variables v, r et s. Ils sont représentés solidement avec les types de données uint8, bytes32 et bytes32 respectivement. Parfois, les signatures sont représentées sous la forme d'un tableau de 65 octets qui contient toutes ces valeurs concaténées sous la forme abi.encodePacked(r, s, v);


Les deux autres composants essentiels d'une signature sont le hachage du message (32 octets) et l'adresse de signature. La séquence ressemble à ceci


  1. Une clé privée (privKey) est utilisée pour générer une adresse publique (ethAddress)

  2. Un contrat intelligent stocke ethAddress à l'avance

  3. Un utilisateur hors chaîne hache un message et signe le hachage. Cela produit la paire msgHash et la signature (r, s, v)

  4. Le contrat intelligent reçoit un message, le hache pour produire msgHash, puis le combine avec (r, s, v) pour voir quelle adresse sort.

  5. Si l'adresse correspond à ethAddress, la signature est valide (sous certaines hypothèses que nous verrons bientôt !)


Les contrats intelligents utilisent le contrat précompilé ecrecover à l'étape 4 pour faire ce que nous avons appelé la combinaison et récupérer l'adresse.


Il y a beaucoup d'étapes dans ce processus où les choses peuvent aller de travers.

Signatures : ecrecover renvoie l'adresse (0) et ne revient pas lorsque l'adresse est invalide

Cela peut conduire à une vulnérabilité si une variable non initialisée est comparée à la sortie d'ecrecover.

Ce code est vulnérable

 contract InsecureContract { address signer; // defaults to address(0) // who lets us give the beneficiary the airdrop without them// spending gas function airdrop(address who, uint256 amount, uint8 v, bytes32 r, bytes32 s) external { // ecrecover returns address(0) if the signature is invalid require(signer == ecrecover(keccak256(abi.encode(who, amount)), v, r, s), "invalid signature"); mint(msg.sender, AIRDROP_AMOUNT); } }

Relecture des signatures

La relecture de la signature se produit lorsqu'un contrat ne suit pas si une signature a été utilisée précédemment. Dans le code suivant, nous corrigeons le problème précédent, mais il n'est toujours pas sécurisé.

 contract InsecureContract { address signer; function airdrop(address who, uint256 amount, uint8 v, bytes32 r, bytes32 s) external { address recovered == ecrecover(keccak256(abi.encode(who, amount)), v, r, s); require(recovered != address(0), "invalid signature"); require(recovered == signer, "recovered signature not equal signer"); mint(msg.sender, amount); } }

Les gens peuvent réclamer le largage autant de fois qu'ils le souhaitent !


On pourrait ajouter les lignes suivantes

 bytes memory signature = abi.encodePacked(v, r, s); require(!used[signature], "signature already used"); // mapping(bytes => bool); used[signature] = true;

Hélas, le code n'est toujours pas sécurisé !

Malléabilité de la signature

Étant donné une signature valide, un attaquant peut faire quelques opérations arithmétiques rapides pour en dériver une autre. L'attaquant peut alors « rejouer » cette signature modifiée. Mais d'abord, fournissons un code qui montre que nous pouvons commencer avec une signature valide, la modifier et montrer que la nouvelle signature passe toujours.

 contract Malleable { // v = 28 // r = 0xf8479d94c011613baeffe9239e4ff65e2adbac744c34217ca7d51378e72c5204 // s = 0x57af17590a914b759c45aaeabaf513d5ef72d7da1bdd19d9f2e1bc371ece5b86 // m = 0x0000000000000000000000000000000000000000000000000000000000000003 function foo(bytes calldata msg, uint8 v, bytes32 r, bytes32 s) public pure returns (address, address){ bytes32 h = keccak256(msg); address a = ecrecover(h, v, r, s); // The following is math magic to invert the // signature and create a valid one // flip s bytes32 s2 = bytes32(uint256(0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141) - uint256(s)); // invert v uint8 v2; require(v == 27 || v == 28, "invalid v"); v2 = v == 27 ? 28 : 27; address b = ecrecover(h, v2, r, s2); assert(a == b); // different signatures, same address!; return (a, b); } }

En tant que tel, notre exemple en cours d'exécution est toujours vulnérable. Une fois que quelqu'un présente une signature valide, sa signature d'image miroir peut être produite et contourner le contrôle de signature utilisé.

 contract InsecureContract { address signer; function airdrop(address who, uint256 amount, uint8 v, bytes32 r, bytes32 s) external { address recovered == ecrecover(keccak256(abi.encode(who, amount)), v, r, s); require(recovered != address(0), "invalid signature"); require(recovered == signer, "recovered signature not equal signer"); bytes memory signature = abi.encodePacked(v, r, s); require(!used[signature], "signature already used"); // this can be bypassed used[signature] = true; mint(msg.sender, amount); } }

Signatures sécurisées

Vous voulez probablement un code de signature sécurisé à ce stade, n'est-ce pas ? Nous vous renvoyons à notre tutoriel sur la création de signatures en solidité et leur test en fonderie.


Mais voici la liste de contrôle.


  • Utilisez la bibliothèque d'openzeppelin pour empêcher les attaques de malléabilité et récupérer à zéro problème
  • N'utilisez pas les signatures comme mot de passe. Les messages doivent contenir des informations que les attaquants ne peuvent pas facilement réutiliser (par exemple msg.sender)
  • Hachez ce que vous signez en chaîne
  • Utilisez un nonce pour empêcher les attaques de rejeu. Mieux encore, suivez EIP712 afin que les utilisateurs puissent voir ce qu'ils signent et vous pouvez empêcher la réutilisation des signatures entre les contrats et les différentes chaînes.

Les signatures peuvent être falsifiées ou fabriquées sans protections appropriées

L'attaque ci-dessus peut être généralisée davantage si le hachage n'est pas effectué sur la chaîne. Dans les exemples ci-dessus, le hachage a été effectué dans le contrat intelligent, de sorte que les exemples ci-dessus ne sont pas vulnérables à l'exploit suivant.


Regardons le code pour récupérer les signatures

 // this code is vulnerable! function recoverSigner(bytes32 hash, uint8 v, bytes32 r, bytes32 s) public returns (address signer) { require(signer == ecrecover(hash, v, r, s), "signer does not match"); // more actions }


L'utilisateur fournit à la fois le hachage et les signatures. Si l'attaquant a déjà vu une signature valide du signataire, il peut simplement réutiliser le hachage et la signature d'un autre message.

C'est pourquoi il est très important de hacher le message dans le contrat intelligent , et non hors chaîne.

Pour voir cet exploit en action, consultez le CTF que nous avons publié sur Twitter.


Défi original :

Partie 1 : https://twitter.com/RareSkills_io/status/1650869999266037760

Partie 2 : https://twitter.com/RareSkills_io/status/1650897671543197701

Solutions:

https://twitter.com/RareSkills_io/status/1651527648676573185 https://twitter.com/RareSkills_io/status/1651224817465540611

Les signatures comme identifiants

Les signatures ne doivent pas être utilisées pour identifier les utilisateurs. En raison de leur malléabilité, ils ne peuvent pas être considérés comme uniques. Msg.sender a des garanties d'unicité beaucoup plus fortes.

Certaines versions du compilateur Solidity ont des bogues

Voir un exercice de sécurité que nous avons organisé sur Twitter ici . Lors de l'audit d'une base de code, vérifiez la version de Solidity par rapport aux annonces de publication sur la page Solidity pour voir si un bogue peut être présent.

En supposant que les contrats intelligents sont immuables

Les contrats intelligents peuvent être mis à niveau avec le modèle proxy (ou plus rarement, le modèle métamorphique). Les contrats intelligents ne doivent pas compter sur la fonctionnalité d'un contrat intelligent arbitraire pour rester inchangés.

Transfer() et send() peuvent rompre avec les portefeuilles multi-signatures

Les fonctions de solidité transfer et send ne doivent pas être utilisées. Ils limitent intentionnellement la quantité de gaz acheminée avec la transaction à 2 300, ce qui entraînera une panne de gaz pour la plupart des opérations.


Le portefeuille multi-signatures gnosis safe couramment utilisé prend en charge le transfert de l'appel vers une autre adresse dans la fonction de secours . Si quelqu'un utilise le transfert ou l'envoi pour envoyer Ether au portefeuille multisig, la fonction de secours pourrait manquer de gaz et le transfert échouerait. Une capture d'écran de la fonction de secours de gnosis safe est fournie ci-dessous. Le lecteur peut clairement voir qu'il y a plus qu'assez d'opérations pour utiliser le gaz 2300.


Si vous avez besoin d'interagir avec un contrat qui utilise le transfert et l'envoi, consultez notre article sur les transactions de liste d'accès Ethereum qui vous permet de réduire le coût du gaz des opérations de stockage et d'accès au contrat.

Le débordement arithmétique est-il toujours d'actualité ?

Solidity 0.8.0 a intégré une protection contre les débordements et les débordements. Ainsi, à moins qu'un bloc non coché soit présent ou qu'un code de bas niveau dans Yul soit utilisé, il n'y a aucun risque de débordement. En tant que telles, les bibliothèques SafeMath ne doivent pas être utilisées car elles gaspillent du gaz lors des vérifications supplémentaires.

Qu'en est-il de block.timestamp ?

Certains documents de la littérature indiquent que block.timestamp est un vecteur de vulnérabilité car les mineurs peuvent le manipuler. Cela s'applique généralement à l'utilisation d'horodatages comme source de caractère aléatoire, ce qui ne devrait pas être fait de toute façon comme documenté précédemment. Après la fusion, Ethereum met à jour l'horodatage à des intervalles d'exactement 12 secondes (ou multiples de 12 secondes). Cependant, mesurer le temps avec une granularité de second niveau est un anti-modèle. À l'échelle d'une minute, il existe un risque considérable d'erreur si un validateur manque son emplacement de bloc et qu'un intervalle de 24 secondes dans la production de blocs se produit.

Corner Cases, Edge Cases et Off By One Errors

Les cas d'angle ne peuvent pas être facilement définis, mais une fois que vous en avez vu suffisamment, vous commencez à développer une intuition pour eux. Une affaire de coin peut être quelque chose comme quelqu'un essayant de réclamer une récompense, mais n'ayant rien jalonné. C'est valable, nous devrions juste leur donner zéro récompense. De même, nous souhaitons généralement répartir les récompenses de manière égale, mais que se passe-t-il s'il n'y a qu'un seul destinataire, et techniquement, aucune division ne devrait se produire ?

Cas d'angle : Exemple 1

Cet exemple a été tiré du fil Twitter d'Akshay Srivastav et modifié.

Considérons le cas où quelqu'un peut mener une action privilégiée si un ensemble d'adresses privilégiées lui fournit une signature.

 contract VulnerableMultisigAuthorization { struct Authorization { bytes signature; address authorizer; bytes32 hashOfAction; // more fields } // more codef unction takeAction(Authorization[] calldata auths, bytes calldata action) public { // logic for avoiding replay attacks for (uint256 i; i < auths.length; ++i) { require(validateSignature(auths[i].signature, auths[i].authorizer), "invalid signature"); require(authorizers[auths[i].authorizer], "address is not an authorizer"); } doTheAction(action) } }

Si l'une des signatures n'est pas valide ou si les signatures ne correspondent pas à une adresse valide, le retour se produira. Mais que se passe-t-il si le tableau est vide ? Dans ce cas, il sautera jusqu'à doTheAction sans avoir besoin de signatures.

Off-By-One : Exemple 2

 contract ProportionalRewards { mapping(address => uint256) originalId; address[] stakers; function stake(uint256 id) public { nft.transferFrom(msg.sender, address(this), id); stakers.append(msg.sender); } function unstake(uint256 id) public { require(originalId[id] == msg.sender, "not the owner"); removeFromArray(msg.sender, stakers); sendRewards(msg.sender, totalRewardsSinceLastclaim() / stakers.length()); nft.transferFrom(address(this), msg.sender, id); } }

Bien que le code ci-dessus ne montre pas toutes les implémentations de fonctions, même si les fonctions se comportent comme leurs noms le décrivent, il y a toujours un bogue. Peux tu le repérer? Voici une image pour vous donner un peu d'espace pour ne pas voir la réponse avant de faire défiler vers le bas.

Les fonctions removeFromArray et sendRewards sont dans le mauvais ordre. S'il n'y a qu'un seul utilisateur dans le tableau des jalonneurs, il y aura une erreur de division par zéro et l'utilisateur ne pourra pas retirer son NFT. De plus, les récompenses ne sont probablement pas réparties comme l'auteur le souhaite. S'il y avait quatre jalons d'origine et qu'une personne se retire, elle recevra un tiers des récompenses puisque la longueur du tableau est de 3 au moment du retrait.

Cas particulier Exemple 3 : Erreur de calcul de la récompense financière composée

Prenons un exemple réel qui, selon certaines estimations, a causé plus de 100 millions de dollars de dommages. Ne vous inquiétez pas si vous ne comprenez pas parfaitement le protocole Compound, nous nous concentrerons uniquement sur les parties pertinentes. (De plus, le protocole Compound est l'un des protocoles les plus importants et les plus conséquents de l'histoire de DeFi, nous l'enseignons dans notre bootcamp DeFi , donc si c'est votre première impression du protocole, ne vous y trompez pas).


Quoi qu'il en soit, le but de Compound est de récompenser les utilisateurs pour avoir prêté leur crypto-monnaie inactive à d'autres commerçants qui pourraient en avoir l'utilité. Les prêteurs sont payés à la fois en intérêts et en jetons COMP (les emprunteurs pourraient réclamer une récompense en jetons COMP, mais nous ne nous concentrerons pas là-dessus pour le moment).

Le Compound Comptroller est un contrat de proxy qui délègue les appels aux implémentations qui peuvent être définies par la Compound Governance.


Lors de la proposition de gouvernance 62 du 30 septembre 2021, le contrat de mise en œuvre a été défini sur un contrat de mise en œuvre présentant la vulnérabilité. Le jour même de sa mise en ligne, il a été observé sur Twitter que certaines transactions réclamaient des récompenses COMP malgré le jalonnement de zéro jeton.

La fonction vulnérabledistribueSupplierComp()


Voici le code d'origine


Le bogue, ironiquement, est dans le commentaire TODO. "Ne distribuez pas le fournisseur COMP si l'utilisateur n'est pas sur le marché des fournisseurs." Mais il n'y a pas de vérification dans le code pour cela. Tant que l'utilisateur détient un jeton de jalonnement dans son portefeuille (CToken(cToken).balanceOf(supplier);), alors

La proposition 64 a corrigé le bogue le 9 octobre 2021.


Bien que cela puisse être considéré comme un bogue de validation des entrées, les utilisateurs n'ont rien soumis de malveillant. Si quelqu'un essaie de réclamer une récompense pour ne rien miser, le calcul correct devrait être zéro. On peut soutenir qu'il s'agit davantage d'une logique métier ou d'une erreur de cas de coin.

Hacks du monde réel

Les hacks DeFi qui se produisent souvent dans le monde réel ne rentrent pas dans les belles catégories ci-dessus.

Gel du portefeuille Pairity (novembre 2017)

Le portefeuille de parité n'était pas destiné à être utilisé directement. Il s'agissait d'une implémentation de référence vers laquelle les clones de contrats intelligents pointaient. La mise en œuvre permettait aux clones de s'autodétruire si nécessaire, mais cela nécessitait que tous les propriétaires de portefeuilles l'approuvent.

 // throw unless the contract is not yet initialized.modifier only_uninitialized { if (m_numOwners > 0) throw; _; } function initWallet(address[] _owners, uint _required, uint _daylimit) only_uninitialized { initDaylimit(_daylimit); initMultiowned(_owners, _required); }

Les propriétaires de portefeuille sont déclarés

 // kills the contract sending everything to `_to`.function kill(address _to) onlymanyowners(sha3(msg.data)) external { suicide(_to); }

Certaines publications décrivent cela comme une « autodestruction non protégée », c'est-à-dire une défaillance du contrôle d'accès, mais ce n'est pas tout à fait exact. Le problème était que la fonction initWallet n'était pas appelée sur le contrat d'implémentation et cela permettait à quelqu'un d'appeler lui-même la fonction initWallet et d'en devenir le propriétaire. Cela leur a donné le pouvoir d'appeler la fonction kill. La cause première était que l'implémentation n'a pas été initialisée. Par conséquent, le bogue a été introduit non pas en raison d'un code de solidité défectueux, mais en raison d'un processus de déploiement défectueux.

Badger DAO Hack (décembre 2021)

Aucun code Solidity n'a été exploité dans ce hack. Au lieu de cela, les attaquants obtiennent la clé API Cloudflare et injectent un script dans l'interface du site Web qui modifie les transactions des utilisateurs pour diriger les retraits vers l'adresse de l'attaquant. Lire la suite dans cet article .

Vecteurs d'attaque pour les portefeuilles

Clés privées avec un caractère aléatoire insuffisant

La motivation pour découvrir des adresses avec beaucoup de zéros non significatifs est qu'elles sont plus économes en gaz à utiliser. Une transaction Ethereum est facturée 4 gaz pour un octet zéro dans les données de transaction et 16 gaz pour un octet non nul.


En tant que tel, Wintermute a été piraté car il utilisait l'adresse grossière ( writeup ). Voici l'écriture de 1inch sur la façon dont le générateur d'adresses grossières a été compromis.


Le portefeuille de confiance avait une vulnérabilité similaire documentée dans cet article ( https://blog.ledger.com/Funds-of-every-wallet-created-with-the-Trust-Wallet-browser-extension-could-have-been- volé/ )


Notez que cela ne s'applique pas aux contrats intelligents avec des zéros non significatifs découverts en modifiant le sel dans create2, car les contrats intelligents n'ont pas de clés privées.

Nonces réutilisés ou insuffisamment aléatoires.

Les points « r » et « s » sur la signature de la courbe elliptique sont générés comme suit

 r = k * G (mod N) s = k^-1 * (h + r * privateKey) (mod N)


G, r, s, h, un N sont tous connus publiquement. Si "k" devient public, alors "privateKey" est la seule variable inconnue et peut être résolue. Pour cette raison, les portefeuilles doivent générer k de manière parfaitement aléatoire et ne jamais le réutiliser. Si le caractère aléatoire n'est pas parfaitement aléatoire, alors k peut être déduit.


La génération aléatoire non sécurisée dans la bibliothèque Java a laissé de nombreux portefeuilles Bitcoin Android vulnérables en 2013. (Bitcoin utilise le même algorithme de signature qu'Ethereum.)


( https://arstechnica.com/information-technology/2013/08/all-android-created-bitcoin-wallets-vulnerable-to-theft/ ).

La plupart des vulnérabilités sont spécifiques à l'application

S'entraîner à reconnaître rapidement les anti-modèles de cette liste fera de vous un programmeur de contrats intelligents plus efficace, mais la plupart des bogues de contrats intelligents sont dus à une inadéquation entre la logique métier prévue et ce que le code fait réellement.


Autres domaines où des bogues peuvent survenir :

  • mauvaises incitations symboliques
  • d'une erreur
  • des erreurs typographiques
  • administrateurs ou utilisateurs se faisant voler leurs clés privées

De nombreuses vulnérabilités auraient pu être détectées avec des tests unitaires

Les tests unitaires de contrats intelligents sont sans doute les garanties les plus élémentaires pour les contrats intelligents, mais un nombre choquant de contrats intelligents en manquent ou ont une couverture de test insuffisante .

Mais les tests unitaires ont tendance à ne tester que le "chemin heureux" (comportement attendu/conçu) des contrats. Pour tester les cas surprenants, des méthodologies de test supplémentaires doivent être appliquées.


Avant qu'un contrat intelligent ne soit envoyé pour audit, les étapes suivantes doivent d'abord être effectuées :

  • Analyse statique avec des outils tels que Slither pour s'assurer que les erreurs de base n'ont pas été manquées
  • Couverture à 100 % des lignes et des succursales grâce aux tests unitaires
  • Tests de mutation pour s'assurer que les tests unitaires ont des déclarations d'assertion robustes
  • Tests fuzz, en particulier pour l'arithmétique
  • Test invariant pour les propriétés avec état
  • Vérification formelle le cas échéant


Pour ceux qui ne connaissent pas certaines des méthodologies ici, Patrick Collins de Cyfrin Audits a une introduction humoristique au fuzzing avec et sans état dans sa vidéo .


Les outils pour accomplir ces tâches deviennent rapidement plus répandus et plus faciles à utiliser.

Davantage de ressources

Certains auteurs ont compilé une liste des hacks DeFi précédents dans ces Repos :


Secureum a été largement utilisé pour étudier et pratiquer la sécurité, mais gardez à l'esprit que le dépôt n'a pas été substantiellement mis à jour depuis 2 ans


Vous pouvez vous entraîner à exploiter les vulnérabilités de solidité avec notre référentiel Solidity Riddles .


DamnVulnerableDeFi est un wargame classique que chaque développeur devrait pratiquer


Capture The Ether et Ethernaut sont des classiques, mais gardez à l'esprit que certains problèmes sont irréalistes ou enseignent des concepts Solidity obsolètes


Certaines entreprises de sécurité participatives réputées disposent d'une liste utile d'audits antérieurs à étudier.

Devenir un auditeur de contrat intelligent

Si vous ne maîtrisez pas Solidity, vous ne pourrez en aucun cas auditer les contrats intelligents Ethereum.


Il n'existe aucune certification reconnue par l'industrie pour devenir un auditeur de contrat intelligent. N'importe qui peut créer un site Web et des profils de médias sociaux prétendant être un auditeur de solidité et commencer à vendre des services, et beaucoup l'ont fait. Par conséquent, soyez prudent et obtenez des références avant d'en embaucher un.

Pour devenir un auditeur de contrat intelligent, vous devez être nettement meilleur que le développeur de solidité moyen pour repérer les bogues. En tant que telle, la « feuille de route » pour devenir auditeur n'est rien de plus que des mois et des mois de pratique incessante et délibérée jusqu'à ce que vous soyez un meilleur attrapeur de bogues de contrat intelligent que la plupart.


Si vous manquez de détermination pour surpasser vos pairs dans l'identification des vulnérabilités, il est peu probable que vous remarquiez les problèmes critiques avant que les criminels hautement qualifiés et motivés ne le fassent.

Vérité froide sur vos chances de succès pour devenir un auditeur de sécurité des contrats intelligents

L'audit des contrats intelligents a récemment été perçu comme un domaine souhaitable dans lequel travailler en raison de la perception qu'il est lucratif. En effet, certains paiements de primes de bogue ont dépassé 1 million de dollars, mais c'est l'exception extrêmement rare, pas la norme.


Code4rena a un classement public des paiements des concurrents dans leurs concours d'audit, ce qui nous donne des données sur les taux de réussite.


Il y a 1171 noms sur le tableau, pourtant

  • Seuls 29 concurrents ont plus de 100 000 $ de revenus à vie (2,4 %)
  • Seulement 57 ont plus de 50 000 $ de revenus à vie (4,9 %)
  • Seuls 170 ont plus de 10 000 $ de revenus à vie (14,5 %)


Considérez également ceci, lorsque Openzeppelin a ouvert une candidature pour une bourse de recherche en sécurité (pas un emploi, une sélection et une formation préalables à l'emploi), ils ont reçu plus de 300 candidatures pour sélectionner moins de 10 candidats, dont encore moins obtiendraient un plein emploi du temps.

Demande d'emploi d'auditeur de contrat OpenZeppelin Smart

C'est un taux d'admission inférieur à Harvard.


L'audit intelligent des contrats est un jeu compétitif à somme nulle. Il n'y a qu'un nombre limité de projets à auditer, un nombre limité de budgets pour la sécurité et un nombre limité de bogues à trouver. Si vous commencez à étudier la sécurité maintenant, il y a des dizaines d'individus et d'équipes très motivés qui ont une longueur d'avance sur vous. La plupart des projets sont prêts à payer une prime pour un auditeur réputé plutôt que pour un nouvel auditeur non testé.


Dans cet article, nous avons répertorié au moins 20 catégories différentes de vulnérabilités. Si vous avez passé une semaine à les maîtriser (ce qui est quelque peu optimiste), vous commencez seulement à comprendre ce qui est commun aux auditeurs expérimentés. Nous n'avons pas couvert l'optimisation du gaz ou la tokenomics dans cet article, qui sont tous deux des sujets importants à comprendre pour un auditeur. Faites le calcul et vous verrez que ce n'est pas un court voyage.


Cela dit, la communauté est généralement amicale et utile aux nouveaux arrivants et les trucs et astuces abondent. Mais pour ceux qui lisent cet article dans l'espoir de faire carrière dans la sécurité des contrats intelligents, il est important de comprendre clairement que les chances d'obtenir une carrière lucrative ne sont pas en votre faveur. Le succès n'est pas le résultat par défaut.


Cela peut être fait bien sûr, et pas mal de gens sont passés de l'absence de Solidity à une carrière lucrative dans l'audit. Il est sans doute plus facile d'obtenir un emploi d'auditeur de contrats intelligents dans un délai de deux ans que d'être admis à la faculté de droit et de réussir l'examen du barreau. Il a certainement plus d'avantages par rapport à beaucoup d'autres choix de carrière.


Mais il faudra néanmoins une persévérance herculéenne de votre part pour maîtriser la montagne de connaissances en évolution rapide qui vous attend et affiner votre intuition pour repérer les bugs.

Cela ne veut pas dire que l'apprentissage de la sécurité des contrats intelligents n'est pas une poursuite valable. C'est absolument le cas. Mais si vous approchez du terrain avec des signes dollar dans les yeux, gardez vos attentes sous contrôle.

Conclusion

Il est important d'être conscient des anti-modèles connus. Cependant, la plupart des bogues du monde réel sont spécifiques à l'application. L'identification de l'une ou l'autre catégorie de vulnérabilités nécessite une pratique continue et délibérée.


Apprenez la sécurité des contrats intelligents et bien d'autres sujets de développement Ethereum grâce à notre formation sur la solidité à la pointe de l'industrie.


L'image principale de cet article a été générée parle générateur d'images AI de HackerNoon via l'invite "un robot protégeant un ordinateur".