CoinEx Research : Introduction aux vulnérabilités et attaques courantes dans les contrats intelligents
Qu’est-ce qu’un contrat intelligent ?
Ethereum a deux types de comptes courants : les comptes appartenant à des entités externes (EOA) et les comptes de contrats intelligents (SCA).
Les EOA sont très similaires aux comptes financiers électroniques que nous utilisons couramment pour stocker des fonds et interagir avec les applications. Par exemple, les utilisateurs déposent de la monnaie fiduciaire via PayPal et interagissent avec divers sites Web, magasins et applications pour les paiements. Les mineurs de DeFi stockent généralement les cryptos dans leur EOA, interagissent avec les dApps DeFi et déposent des fonds dans les dApps pour réaliser des bénéfices. Cependant, les EOA présentent une particularité que ne possèdent pas les comptes financiers électroniques : les utilisateurs doivent faire vérifier leur contrôle sur les EOA via la détention des clés privées — pas vos clés, pas vos coins.
Les SCA sont également un type de compte qui est essentiellement associé à un segment de bytecode exécutable (également appelé contrat intelligent). Le contrat intelligent décrit diverses logiques métier et sert de backend pour les dApps. Cependant, malgré des contraintes plus importantes que les langages de développement Turing complets traditionnels, les contrats intelligents quasi Turing-complets se sont malgré tout révélés vulnérables à de nombreuses attaques, infligeant de rudes revers à l’industrie de la blockchain.
Attaques courantes de contrats intelligents
1. Attaque par réentrée
L’attaque la plus courante et la plus notoire est l’attaque par réentrée, qui a été à l’origine du fork Ethereum qui a conduit à la création d’Ethereum Classic. En 2016, des pirates ont exécuté une attaque par réentrée sur le contrat The DAO, volant 3 600 000 ETH d’une valeur de plus de 150 millions de dollars à l’époque. Cette attaque, survenue au début d’Ethereum, a dévasté l’écosystème et brisé la confiance des investisseurs, conduisant finalement à une bifurcation.
Logique spécifique
Voici un exemple pour vous aider à mieux comprendre le principe de l’attaque par réentrée. La banque B a précédemment prêté de l’argent à la banque A. Un jour, la banque B lance un virement vers la banque A, demandant le transfert de tout l’argent vers la banque B. Le cheminement normal est le suivant :
Étape 1 : la banque B demande le retrait de fonds
Étape 2 : la banque A transfère les fonds à la banque B
Étape 3 : la banque A confirme le succès du virement à la banque B
Étape 4 : la banque A met à jour le solde du compte de la banque B.
Cependant, si la banque B crée une faille après l’étape 2 et continue de demander tout l’argent à la banque A sans confirmation à l’étape 3, le solde du compte de la banque A auprès de la banque B restera inchangé. Cet appel récursif videra tous les actifs de la banque A.
Contrats intelligents associés
Le contrat de la banque A comprend deux fonctions :
● deposit(): une fonction de dépôt qui dépose de l’argent dans la banque A et met à jour le solde de l’utilisateur ;
● withdraw(): une fonction de retrait qui permet aux utilisateurs de retirer tous leurs fonds de la banque A.
Le contrat d’attaque de la banque B implique principalement une boucle qui déclenche la fonction de rappel receive(), qui à son tour appelle la fonction withdraw() du contrat de la banque pour drainer les actifs de la banque A via une séquence de 1 dépôt, 1 retrait et appels de fonction de receive() callback, et met enfin à jour le solde de B dans A. Il comprend deux fonctions :
● receive() : une fonction de callback déclenchée lors de la réception d’ETH, qui appelle récursivement la fonction withdraw() du contrat de la Bank pour effectuer des retraits.
● attack(): il appelle d’abord la fonction deposit() du contrat de la Bank pour actualiser le solde, puis la fonction withdraw()pour lancer le premier retrait, et déclenche la fonction de receive() callback pour appeler récursivement withdraw()afin de drainer les actifs du contrat de la Bank.
Solution
Mise en œuvre d’un verrou de réentrée
Un verrou de réentrée est un modificateur utilisé pour empêcher la réentrée, garantissant qu’un appel doit terminer son exécution avant de pouvoir être invoqué à nouveau. Par exemple, comme l’attaque de la banque B nécessite d’appeler la fonction withdraw() du contrat de la Bank plusieurs fois, elle échouera avec la mise en œuvre d’un verrou de réentrée.
Comment l’utiliser
2. Utilisation abusive de tx.origin
La fonction principale de tx.origin dans un contrat intelligent est de récupérer le compte d’origine qui a initié la transaction. Ici, nous allons discuter de deux variables courantes dans les contrats intelligents : msg.senderet tx.origin. msg.sender récupère le compte appelant directement le contrat intelligent, tandis que dans le monde de la blockchain, en raison des appels imbriqués et mutuels de différents contrats intelligents (tels que DeFi Lego), tx.origin est nécessaire pour obtenir le compte d’origine qui a initié la transaction. Une vulnérabilité survient lorsque les développeurs dApp ne vérifient que la sécurité de tx.origin dans le code, négligeant la vérification de sécurité des attaquants déployant des contrats intermédiaires pour contourner tx.origin et lancer des attaques.
Logique spécifique
Voici un exemple pour vous plonger dans le scénario d’attaque courant. Bill a un portefeuille intelligent qui vérifie si Bill est à l’origine d’un transfert. Une fois, Bill a créé un NFT sur un site Web d’hameçonnage. Cela a permis au site Web d’obtenir l’identité de Bill et d’initier un transfert depuis son portefeuille intelligent en utilisant son identité, entraînant des pertes d’actifs. Dans des circonstances normales, les utilisateurs sont moins susceptibles de tomber dans ce piège, mais lorsqu’ils interagissent avec des dApps à l’aide d’un portefeuille, ils oublient souvent de vérifier les invites d’interaction. Par exemple, si les deux impliquent la fonction Mint(), les utilisateurs négligents peuvent facilement tomber dans un piège d’hameçonnage. La logique métier au sein du site Web d’hameçonnage est truffée de pièges, il est donc important de vérifier les invites d’interaction pour les erreurs lors des interactions régulières.
Contrat de portefeuille intelligent
Le contrat de portefeuille intelligent comprend une fonction :
● transfer(): une fonction de retrait qui ne peut être initiée que par le propriétaire du portefeuille, qui dans ce cas est Bill.
Contrat d’attaque par hameçonnage
Dans un contrat d’attaque par hameçonnage, Mint() incite les utilisateurs à transférer des fonds vers l’adresse d’un pirate informatique. Il comprend une fonction :
● Mint() : une fois appelée, la fonction d’hameçonnage exécute en interne transfer() du contrat Wallet. Étant donné que l’initiateur d’origine est l’utilisateur lui-même (dans cet exemple, Bill), la vérification require(tx.origin == owner, “Not owner”); ne posera pas de problème. Cependant, l’adresse cible du transfert a déjà été falsifiée vers l’adresse du pirate informatique, ce qui a entraîné un vol de fonds.
Solutions
1. Utilisez msg.sender au lieu de tx.origin
Peu importe le nombre d’appels de contrat impliqués (Contrat A → Contrat B →…→ contrat cible), ne vérifiez que msg.sender, c’est-à-dire l’appelant direct, pour éviter les attaques causées par des contrats intermédiaires malveillants.
2. Vérifiez tx.origin == msg.sender
Cette méthode peut éloigner les contrats malveillants, cette approche requiert que les développeurs considèrent leur propre contexte business puisqu’elle enferme de fait tous les autres appels de contrats externes.
3. Attaque du générateur de nombres aléatoires (RNG)
Cela nous ramène à la tendance des dApp de jeu ou de paris vers 2018 et 2019. En règle générale, les développeurs utilisent certaines seeds dans des contrats intelligents pour générer des nombres aléatoires afin de sélectionner les gagnants lors des tirages au sort. Les seeds courantes incluent block.number,block.timestamp, blockhash et keccak256. Cependant, les mineurs peuvent contrôler entièrement ces seeds, de sorte que dans certains cas, les mineurs malveillants peuvent manipuler les variables pour en tirer des avantages.
Contrats de dés courants
Le contrat Dice comprend une fonction :
● Bet(): une fonction de pari où les utilisateurs saisissent un numéro de pari et paient un ETH. Un nombre aléatoire est généré avec plusieurs seeds, et si le numéro de pari correspond au nombre aléatoire, l’utilisateur remporte la totalité de la cagnotte.
Contrat d’attaque du mineur
Les mineurs peuvent gagner tant qu’ils précalculent le numéro aléatoire gagnant et l’exécutent dans le même bloc. Cela inclut une fonction :
● attack(): une fonction d’attaque de pari, où le mineur précalcule le numéro aléatoire gagnant. Puisqu’il est exécuté dans le même bloc, blockhash(block.number — 1) et block.timestamp dans le même bloc sont les mêmes. Ensuite, le mineur appelle Bet() du contrat Dice pour terminer l’attaque.
Solution
Utiliser des nombres aléatoires hors chaîne fournis par des projets d’oracle
Grâce aux services fournis par des projets d’oracle tels que Chainlink, des nombres aléatoires on-chain sont injectés dans des contrats on-chain pour garantir l’aléatoire et la sécurité. Cependant, les projets d’oracle comportent également des risques de centralisation, ce qui nécessite des services d’oracle plus matures.
4. Attaque par rejeu
Une attaque par rejeu consiste à relancer une transaction en utilisant une signature précédemment utilisée pour voler des fonds. L’une des attaques par rejeu les plus connues de ces dernières années a été le vol de 20 millions de tokens $OP au teneur de marché Wintermute sur Optimism, qui était une attaque par rejeu inter-chaînes. Étant donné que le compte de portefeuille multi-signatures de Wintermute n’était temporairement déployé que sur le réseau principal Ethereum, le pirate informatique a utilisé la signature de la transaction pour le déploiement par Wintermute d’une adresse multi-signatures sur Ethereum pour réexécuter la même transaction sur la chaîne Optimism, prenant ainsi le contrôle du portefeuille multi-signatures sur Optimism. Un compte de portefeuille multi-signatures est essentiellement un compte de contrat intelligent, ce qui démontre également une différence significative entre SCA et EOA. Pour un EOA, un utilisateur normal n’a besoin que d’une clé privée pour contrôler toutes les adresses sur Ethereum et les chaînes compatibles EVM (les chaînes d’adresses sont exactement les mêmes), tandis qu’un SCA n’est efficace que sur une seule chaîne après avoir été déployé.
Logique spécifique
Ici, nous fournissons un exemple d’attaque par rejeu typique (attaque par rejeu sur la même chaîne). Bill a un portefeuille intelligent qui l’oblige à saisir sa signature électronique avant que chaque transaction puisse être exécutée. Maintenant que la pirate informatique Lucy a volé la signature électronique de Bill, elle peut lancer un nombre illimité de transactions pour vider le portefeuille intelligent de Bill.
Exemple
Un contrat avec des vulnérabilités se compose de trois fonctions :
checkSig(): fonction de vérification ECDSA, garantissant que le résultat de la vérification est le signerinitialement défini.
● getMsgHash() : fonction de génération de hash, qui combine to et amount pour former un hash.
● transfer(): fonction de transfert, permettant aux utilisateurs de retirer des fonds du pool de liquidités. En raison de l’absence de restrictions sur la signature, la même signature peut être réutilisée, ce qui permet aux pirates de voler continuellement des fonds.
Incluez nonce dans la combinaison de signatures pour empêcher les attaques par rejeu. Le principe du paramètre est le suivant :
● nonce : il décrit la variable du nombre de transactions d’un EOA dans le réseau blockchain. Il a un ordre et un caractère unique. À chaque transaction supplémentaire, la valeur nonce augmente de 1. Le réseau blockchain vérifiera si le nonce de la transaction est cohérent avec le nonce actuel du compte. Par conséquent, un pirate informatique échouerait s’il utilisait une signature utilisée car la valeur nonce dans la combinaison de signatures est inférieure à la valeur nonce actuelle de l’EOA.
5. Attaque par déni de service (DoS)
L’attaque par déni de service (DoS) n’est pas une nouveauté dans le monde traditionnel du Web2. Il fait référence à toute interférence avec un serveur, telle que l’envoi d’une grande quantité d’informations indésirables ou perturbatrices, entravant ou détruisant complètement la disponibilité. De même, les contrats intelligents sont en proie à de telles attaques, qui visent essentiellement à faire fonctionner le contrat intelligent.
Logique spécifique
Voyons un exemple. Le projet A organise une offre publique pour le token de protocole, où tous les utilisateurs peuvent contribuer des fonds au pool de liquidités (contrat intelligent) pour acheter des quotas sur la base du premier arrivé, premier servi, et les fonds excédentaires seront restitués aux participants. La pirate informatique Alice exploite le contrat d’attaque pour participer à l’offre publique. Une fois que le pool de liquidités tente de restituer des fonds au contrat d’attaque d’Alice, une attaque DoS sera déclenchée, empêchant ainsi l’action de retour de se réaliser. En conséquence, une grande quantité de fonds est bloquée dans le contrat intelligent.
Exemple
Le contrat d’offre publique comprend deux fonctions :
● deposit(): fonction de dépôt, enregistrant l’adresse du déposant et le montant contribué.
● refund(): fonction de remboursement, avec laquelle l’équipe du projet restitue les fonds aux investisseurs.
Contrat d’attaque DoS
Le contrat d’attaque DoS comprend une fonction :
● attack(): bien qu’il s’agisse d’une fonction d’attaque, elle ne présente aucun problème. Le problème principal réside dans la fonction de rappel de paiement receive() intégrée au contrat Hacker, qui inclut un jugement des exceptions. Tout contrat externe transférant des fonds au contrat Hacker déclenchera une exception via revert(), empêchant ainsi l’opération de se terminer.
Solutions
1. Évitez que les fonctionnalités critiques ne se bloquent lors de l’appel de contrats externes
Supprimez require(success, “Refund Fail!”); de la fonction refund() ci-dessus du contrat PublicSale, en vous assurant que l’opération de remboursement peut se poursuivre même si un remboursement à une seule adresse échoue.
2. Découplage
Dans la fonction refund() ci-dessus du contrat PublicSale, autorisez les utilisateurs à demander eux-mêmes des remboursements plutôt que de distribuer les remboursements, minimisant ainsi les interactions inutiles avec les contrats externes.
6. Attaque permit
Dans une attaque permit, le compte A fournit à l’avance la signature d’une partie désignée, puis le compte B, après avoir obtenu la signature, peut effectuer des transferts de tokens autorisés pour voler une certaine quantité de tokens. Ici, nous discutons principalement de deux fonctions courantes d’autorisation de tokens dans les contrats intelligents : approve() et permit().
Dans le contrat ERC20 commun, le compte A peut appeler approve() pour autoriser un certain montant de tokens pour le compte B, permettant à ce dernier de transférer ces tokens du premier. De plus, permit() a été introduit dans les contrats ERC20 dans l’EIP-2612, et Uniswap a publié une nouvelle norme d’autorisation de tokens, Permit2, en novembre 2022.
Logique spécifique
Voici un exemple. Un jour, Bill naviguait sur un site Web d’actualités sur la blockchain lorsqu’une fenêtre contextuelle de signature Metamask est soudainement apparue. Étant donné que de nombreux sites Web ou applications de blockchain utilisent des signatures pour vérifier les connexions des utilisateurs, Bill n’y a pas trop pensé et a terminé la signature directement. Cinq minutes plus tard, ses actifs Metamask ont été drainés. Bill a ensuite découvert dans l’explorateur de blockchain qu’une adresse inconnue avait lancé une transaction permit(), suivie d’une transaction transferFrom() qui a vidé son portefeuille.
Exemple
Les deux fonctions sont les suivantes :
● approve(): une fonction d’autorisation standard où le compte A autorise un certain montant de fonds au compte B.
● permit(): une fonction d’autorisation de signature où le compte B soumet et termine la vérification de signature pour obtenir le montant autorisé du compte A. Les paramètres incluent le owner accordant l’autorisation, le spender autorisé, le amount autorisé, la deadline de signature et les données de signature du propriétaire vrets.
Solutions
1. Faites attention à chaque signature dans les interactions en chaîne
Malgré les mesures prises par certains portefeuilles pour décoder et afficher les informations de signature d’autorisation approve(), ils ne fournissent pratiquement aucun avertissement concernant l’hameçonnage de signature permit(), ce qui augmente le risque d’attaques. Par conséquent, il est fortement recommandé d’inspecter rigoureusement chaque signature inconnue pour s’assurer qu’elle vise la fonction permit().
2. Séparez le portefeuille pour une interaction régulière du portefeuille stockant les actifs
Ceci est extrêmement important pour les utilisateurs de crypto, en particulier les chasseurs d’airdrop, car ils interagissent avec d’innombrables dApps ou sites Web chaque jour et sont sujets aux pièges. Ne stocker qu’une petite quantité de fonds dans un portefeuille pour une interaction régulière peut maintenir les pertes dans une fourchette gérable.
7. Attaque Honeypot
Dans l’industrie de la blockchain, une attaque honeypot fait référence à un type de contrats de tokens malveillants déployés par des équipes de projet. Le contrat n’accorde à l’équipe du projet que l’autorisation de vendre, tandis que les utilisateurs réguliers ne peuvent qu’acheter au lieu de vendre, subissant ainsi des pertes.
Logique spécifique
Voici un exemple. Dans une annonce sur Telegram, le projet A informe les utilisateurs que le token a été déployé sur le réseau principal et qu’il est disponible pour le trading. Comme le token ne peut être acheté que et ne peut pas être vendu, le prix n’a cessé d’augmenter au début, et les utilisateurs qui craignent de passer à côté continuent d’acheter. Après un certain temps, lorsque les utilisateurs se trouvent incapables de vendre, l’équipe du projet saisit l’opportunité et se débarrasse des tokens, faisant chuter le prix.
Exemple
Fonction principale :
● _beforeTokenTransfer(): une fonction interne appelée lors des transferts de tokens, qui ne peut réussir que lorsqu’elle est appelée par le propriétaire ; les appels d’autres comptes échoueront.
Solution
Utiliser des outils d’analyse de sécurité
a. TokenSniffer pour les tokens Ethereum
b. AveCheck pour les tokens sur d’autres chaînes
c. Sites Web du marché avec des outils de détection intégrés comme Dextools
Évitez de trader des tokens avec des scores faibles.
8. Attaque Front-Running
Le front-running a émergé à l’origine sur les marchés financiers traditionnels, où l’asymétrie d’information permettait aux intermédiaires financiers de réaliser des bénéfices en prenant des mesures rapides basées sur des informations spécifiques du secteur. Dans l’industrie de la blockchain, le front-running découle principalement du front-running en chaîne, qui consiste à manipuler les mineurs pour qu’ils donnent la priorité à l’emballage de leurs propres transactions sur la chaîne afin de réaliser des bénéfices.
Dans le domaine de la blockchain, les mineurs peuvent réaliser des bénéfices en manipulant les transactions qu’ils regroupent en blocs, par exemple en excluant certaines transactions et en réorganisant les transactions. Ce profit peut être mesuré à l’aide de la Miner Extractable Value (MEV). Avant qu’une transaction d’un utilisateur ne soit ajoutée au réseau principal Ethereum, la majorité des transactions sont regroupées dans le mempool. Les mineurs recherchent les transactions avec des prix du gaz plus élevés dans ce mempool et les regroupent en priorité pour maximiser leurs gains. En général, les transactions avec des prix du gaz plus élevés sont plus facilement regroupées par les mineurs. Pendant ce temps, certains bots MEV parcourent également le mempool à la recherche de transactions rentables.
Logique spécifique
Voici un exemple. Bill découvre un nouveau token chaud avec des fluctuations de prix importantes. Pour assurer le succès des transactions de tokens sur Uniswap, Bill définit une fourchette de glissement exceptionnellement large. Malheureusement, le bot MEV d’Alice détecte cette transaction dans le mempool et augmente rapidement les frais de gaz, en lançant une transaction d’achat avant celle de Bill et en insérant une transaction de vente après celle de Bill dans le même bloc. Après la confirmation du bloc, cela entraîne des pertes de glissement importantes pour Bill, tandis qu’Alice profite d’une opération d’arbitrage consistant à acheter à bas prix et à vendre à un prix élevé.
Exemple
La fonction est la suivante :
● solve(): une fonction de devinette où n’importe qui peut soumettre une réponse, et si la réponse soumise correspond à la réponse cible, le soumissionnaire peut recevoir 10 éthers.
processus :
1. Bill trouve la bonne réponse.
2. Alice surveille le mempool, attendant que quelqu’un soumette la bonne réponse.
3. Bill appelle solve() pour soumettre la réponse et fixe le prix du gaz à 100 Gwei.
4. Alice voit la transaction envoyée par Bill et découvre la réponse. Elle fixe un prix du gaz plus élevé que celui de Bill, 200 Gwei, et appelle solve().
5. La transaction d’Alice est emballée par le mineur avant celle de Bill.
6. Alice remporte une récompense de 10 éthers.
Solution
Les trois fonctions principales sont les suivantes :
● commitSolution() : une fonction pour soumettre les résultats, en plaçant la solution de réponse soumise par l’utilisateur solutionHash, l’heure de soumission commitTime et l’état r revealed dans la structure Commit.
● getMySolution(): une fonction pour obtenir les résultats, permettant aux utilisateurs de voir leurs réponses soumises et les informations associées, y compris la solution de réponse soumise par l’utilisateur solutionHash, l’heure de soumission commitTime et l’état revealed.
● revealSolution(): une fonction pour réclamer des récompenses pour avoir deviné le puzzle, permettant aux utilisateurs de réclamer des récompenses après avoir fourni la réponse et le mot de passe qu’ils ont défini.
processus :
1. Bill trouve la bonne réponse.
2. Bill appelle commitSolution() pour soumettre la bonne réponse.
3. Dans le bloc suivant, Bill appelle revealSolution(), en fournissant la réponse et le mot de passe qu’il a défini pour réclamer la récompense.
Dans commitSolution(), Bill soumet une chaîne cryptée, en gardant les données en texte brut soumises uniquement pour lui-même. Dans cette étape, l’heure du bloc de soumission commitTime est également enregistrée. Ensuite, dans revealSolution(), l’heure du bloc est vérifiée pour empêcher le front-running dans le même bloc. Étant donné que l’appel de revealSolution() nécessite la soumission de la réponse en texte brut, cette étape vise à empêcher d’autres personnes de contourner commitSolution() et d’appeler directement revealSolution().Après une vérification réussie, la récompense sera distribuée si la réponse est vérifiée correcte.
Conclusion
Les contrats intelligents jouent un rôle crucial dans la technologie de la blockchain et offrent de nombreux avantages. Premièrement, ils permettent une exécution décentralisée et automatisée, garantissant la sécurité et la fiabilité des transactions sans tiers. Deuxièmement, les contrats intelligents réduisent les étapes intermédiaires et les coûts, améliorant ainsi l’efficacité des transactions.
Malgré tous ces avantages, les contrats intelligents sont également exposés à des risques d’attaques qui entraînent des pertes financières pour les utilisateurs. À ce titre, certaines habitudes sont essentielles pour les utilisateurs de la chaîne. Premièrement, les utilisateurs doivent toujours choisir avec soin les dApps avec lesquelles ils interagissent et examiner attentivement le code du contrat et les règles associées. En outre, ils doivent régulièrement mettre à jour et utiliser des portefeuilles sécurisés et des outils d’interaction avec les contrats afin de réduire le risque d’attaques de pirates informatiques. En outre, il est conseillé de stocker leurs fonds sur plusieurs adresses afin de minimiser les pertes potentielles dues aux attaques de contrats.
Pour les acteurs du secteur, il est tout aussi important de garantir la sécurité et la stabilité des contrats intelligents. La première priorité devrait être de renforcer l’audit des contrats intelligents afin d’identifier et de rectifier les vulnérabilités potentielles et les risques de sécurité. Deuxièmement, les acteurs du secteur doivent se tenir informés des derniers développements de la blockchain en matière d’attaques de contrats et prendre des mesures de sécurité en conséquence. Enfin et surtout, ils doivent également renforcer l’éducation des utilisateurs et la sensibilisation à la sécurité en ce qui concerne l’utilisation correcte des contrats intelligents.
En conclusion, grâce aux efforts concertés des utilisateurs et des acteurs du secteur, les risques de sécurité posés par les contrats intelligents peuvent être considérablement atténués. Les utilisateurs doivent toujours sélectionner soigneusement les contrats et protéger leurs actifs personnels, tandis que les acteurs du secteur doivent intensifier l’audit des contrats, se tenir au courant des avancées technologiques et renforcer l’éducation des utilisateurs et la sensibilisation à la sécurité. Ensemble, nous favoriserons le développement sûr et fiable des contrats intelligents.
Références :
Solidity by Example
https://solidity-by-example.org/
Blockchain Know-how of SlowMist
Chainlink — Top 10 DeFi Security Best Practices
https://blog.chain.link/defi-security-best-practices/#post-title
WTF — Solidity 104 Contract Security
https://www.wtf.academy/solidity-104/
Vulnerabilities in DeFi Smart Contracts in 4 Categories with 38 Scenarios
https://www.weiyangx.com/381670.html
OpenZeppelin