ERC-20 en sécurité
Introduction
L'un des grands avantages avec Ethereum est qu'il n'y a pas d'autorité centrale qui peut modifier ou annuler vos transactions. L'un des grands problèmes avec Ethereum est qu'il n'y a pas d'autorité centrale ayant le pouvoir d'annuler les erreurs des utilisateurs ou les transactions illicites. Dans cet article, vous apprendrez quelques-unes des erreurs courantes que commettent les utilisateurs avec les jetons ERC-20, ainsi que comment créer des contrats ERC-20 qui aident les utilisateurs à éviter ces erreurs, ou qui donnent à une autorité centrale certains pouvoirs (par exemple, geler des comptes).
Notez que bien que nous utiliserons le contrat de jeton ERC-20 d'OpenZeppelin(opens in a new tab), cet article n'explique pas en détail son fonctionnement. Vous pouvez trouver ces informations ici.
Si vous souhaitez consulter le code source complet :
- Ouvrez l'IDE Remix(opens in a new tab).
- Cliquez sur l'icône de clonage GitHub ().
- Clonez le référentiel GitHub
https://github.com/qbzzt/20220815-erc20-safety-rails
. - Ouvrez contrats > erc20-safety-rails.sol.
Création d'un contrat ERC-20
Avant de pouvoir ajouter la fonctionnalité de sécurité, nous avons besoin d'un contrat ERC-20. Dans cet article, nous utiliserons l'assistant de contrats OpenZeppelin(opens in a new tab). Ouvrez-le dans un autre navigateur et suivez ces instructions :
Sélectionnez ERC20.
Entrez ces paramètres :
Paramètre Valeur Nom SafetyRailsToken Symbole SAFE Pré-génère 1000 Fonctionnalités Aucune Contrôle d'accès Propriétaire Mise à jour Aucune Remontez et cliquez sur Ouvrir dans Remix (pour Remix) ou Télécharger pour utiliser un environnement différent. Je vais supposer que vous utilisez Remix, si vous utilisez autre chose, faites simplement les modifications appropriées.
Nous avons maintenant un contrat ERC-20 pleinement fonctionnel. Vous pouvez développer
.deps
>npm
pour voir le code importé.Compilez, déployez et jouez avec le contrat pour voir qu'il fonctionne comme un contrat ERC-20. Si vous devez apprendre à utiliser Remix, utilisez ce tutoriel(opens in a new tab).
Erreurs courantes
Les erreurs
Les utilisateurs envoient parfois des tokens à la mauvaise adresse. Bien que nous ne puissions pas lire dans leurs pensées pour savoir ce qu'ils voulaient faire, il existe deux types d'erreurs qui se produisent souvent et sont faciles à détecter :
Envoyer les jetons à l'adresse du contrat lui-même. Par exemple, le token OP d'Optimism(opens in a new tab) a réussi à accumuler plus de 120 000 tokens OP(opens in a new tab) en moins de deux mois. Cela représente une somme d'argent considérable que, vraisemblablement, des gens ont simplement perdue.
Envoyer les tokens à une adresse vide, une adresse qui ne correspond pas à un compte possédé extérieurement ou à un contrat intelligent. Bien que je n'aie pas de statistiques sur la fréquence à laquelle cela se produit, un incident aurait pu coûter 20 000 000 tokens(opens in a new tab).
Prévenir les transferts
Le contrat ERC-20 d'OpenZeppelin comprend un crochet, _beforeTokenTransfer
(opens in a new tab), qui est appelé avant qu'un token soit transféré. Par défaut, ce crochet ne fait rien, mais nous pouvons y ajouter notre propre fonctionnalité, comme des vérifications qui annulent le transfert s'il y a un problème.
Pour utiliser le crochet, ajoutez cette fonction après le constructeur :
1 function _beforeTokenTransfer(address from, address to, uint256 amount)2 internal virtual3 override(ERC20)4 {5 super._beforeTokenTransfer(from, to, amount);6 }Copier
Certaines parties de cette fonction peuvent vous être nouvelles si vous n'êtes pas très familiarisé avec Solidity :
1 internal virtualCopier
Le mot-clé virtual
signifie que tout comme nous avons hérité de la fonctionnalité d'ERC20
et avons surchargé cette fonction, d'autres contrats peuvent hériter de nous et surcharger aussi cette fonction.
1 override(ERC20)Copier
Nous devons explicitement spécifier que nous surchargeons(opens in a new tab) la définition du jeton ERC20 de _beforeTokenTransfer
. En général, les définitions explicites sont bien meilleures, du point de vue de la sécurité, que les définitions implicites : vous ne pouvez pas oublier que vous avez fait quelque chose si cela se trouve juste devant vous. C'est aussi la raison pour laquelle nous devons spécifier quelle surclasse de _beforeTokenTransfer
nous surchargeons.
1 super._beforeTokenTransfer(from, to, amount);Copier
Cette ligne appelle la fonction _beforeTokenTransfer
du ou des contrats dont nous avons hérité et qui la possèdent. Dans ce cas, il s'agit uniquement d'ERC20
, Ownable
n'a pas ce crochet. Bien qu'actuellement ERC20._beforeTokenTransfer
ne fasse rien, nous l'appelons au cas où une fonctionnalité serait ajoutée à l'avenir (et nous décidons alors de redéployer le contrat, car les contrats ne changent pas après le déploiement).
Codage des exigences
Nous voulons ajouter ces exigences à la fonction :
- L'adresse
to
ne peut pas être égale à cetteaddress
, l'adresse du contrat ERC-20 lui-même. - L'adresse
to
ne peut pas être vide, elle doit être soit :- Un compte détenu extérieurement (EOA). Nous ne pouvons pas vérifier directement si une adresse est un EOA, mais nous pouvons vérifier le solde ETH d'une adresse. Les EOA ont presque toujours un solde, même s'ils ne sont plus utilisés - il est difficile de les vider jusqu'au dernier wei.
- Un contrat intelligent. Tester si une adresse est un contrat intelligent est un peu plus difficile. Il existe un opcode qui vérifie la longueur du code externe, appelé
EXTCODESIZE
(opens in a new tab), mais il n'est pas directement disponible en Solidity. Nous devons utiliser Yul(opens in a new tab), qui est l'assembleur EVM, pour cela. Il existe d'autres valeurs que nous pourrions utiliser depuis Solidity (<address>.code
et<address>.codehash
(opens in a new tab)), mais elles coûtent plus cher.
Examinons le nouveau code ligne par ligne :
1 require(to != address(this), "Can't send tokens to the contract address");Copier
C'est la première exigence, vérifier que to
et cette(address)
ne sont pas la même chose.
1 bool isToContract;2 assembly {3 isToContract := gt(extcodesize(to), 0)4 }Copier
C'est ainsi que nous vérifions si une adresse est un contrat. Nous ne pouvons pas recevoir de réponse directement de Yul, nous définissons donc une variable pour contenir le résultat (isToContract
dans ce cas). La façon dont Yul fonctionne est que chaque opcode est considéré comme une fonction. Nous appelons donc d'abord EXTCODESIZE
(opens in a new tab) pour obtenir la taille du contrat, puis utilisons GT
(opens in a new tab) pour vérifier qu'elle n'est pas nulle (nous travaillons avec des entiers non signés, donc bien sûr elle ne peut pas être négative). Nous écrivons ensuite le résultat dans isToContract
.
1 require(to.balance != 0 || isToContract, "Can't send tokens to an empty address");Copier
Et enfin, nous avons la vérification réelle des adresses vides.
Accès administratif
Parfois, il est utile d'avoir un administrateur qui peut annuler des erreurs. Pour réduire le potentiel d'abus, cet administrateur peut être géré par une multisig(opens in a new tab), ce qui signifie que plusieurs personnes doivent accepter une action. Dans cet article, nous aurons deux fonctionnalités administratives :
Le gel et le dégel des comptes. Ceci peut être utile, par exemple, lorsqu'un compte pourrait être compromis.
Nettoyage des actifs.
Parfois, des fraudeurs envoient des jetons frauduleux au vrai contrat pour gagner en légitimité. Par exemple, regardez ici(opens in a new tab). Le contrat ERC-20 légitime est 0x4200....0042(opens in a new tab). L'arnaque qui prétend l'être est 0x234....bbe(opens in a new tab).
Il est également possible que des personnes envoient par erreur des jetons ERC-20 légitimes à notre contrat, ce qui est une autre raison de vouloir trouver un moyen de les retirer.
OpenZeppelin fournit deux mécanismes pour activer l'accès administratif :
- Les contrats
Ownable
(opens in a new tab) ont un seul propriétaire. Les fonctions ayant le modificateur(opens in a new tab)onlyOwner
ne peuvent être appelées que par ce propriétaire. Les propriétaires peuvent transférer la propriété à quelqu'un d'autre ou y renoncer complètement. Les droits de tous les autres comptes sont généralement identiques. - Les contrats
AccessControl
(opens in a new tab) ont un contrôle d'accès basé sur les rôles (RBAC)(opens in a new tab).
Pour simplifier, dans cet article, nous utilisons Ownable
.
Geler et dégeler les contrats
Le gel et le dégel des contrats nécessitent plusieurs modifications :
Un mapping(opens in a new tab) d'adresses à des booléens(opens in a new tab) pour suivre quelles adresses sont gelées. Toutes les valeurs sont initialement à zéro, ce qui, pour les valeurs booléennes, est interprété comme faux. C'est ce que nous voulons, car par défaut, les comptes ne sont pas gelés.
1 mapping(address => bool) public frozenAccounts;CopierDes événements(opens in a new tab) pour informer quiconque intéressé lorsqu'un compte est gelé ou dégelé. Techniquement, ces événements ne sont pas nécessaires pour ces actions, mais cela aide le code hors chaîne à pouvoir écouter ces événements et savoir ce qui se passe. Il est considéré comme une bonne manière pour un contrat intelligent de les émettre lorsque quelque chose qui pourrait être pertinent pour quelqu'un d'autre se produit.
Les événements sont indexés, il sera donc possible de rechercher toutes les fois qu'un compte a été gelé ou dégelé.
1 // When accounts are frozen or unfrozen2 event AccountFrozen(address indexed _addr);3 event AccountThawed(address indexed _addr);CopierFonctions pour geler et dégeler les comptes. Ces deux fonctions sont presque identiques, nous n'expliquerons donc que la fonction de gel.
1 function freezeAccount(address addr)2 public3 onlyOwnerCopierLes fonctions marquées comme
public
(opens in a new tab) peuvent être appelées à partir d'autres contrats intelligents ou directement par une transaction.1 {2 require(!frozenAccounts[addr], "Account already frozen");3 frozenAccounts[addr] = true;4 emit AccountFrozen(addr);5 } // freezeAccountCopierSi le compte est déjà gelé, annulez. Sinon, gelez-le et
émettez
un événement.Modifiez
_beforeTokenTransfer
pour empêcher le transfert d'argent depuis un compte gelé. Notez que l'argent peut toujours être transféré vers le compte gelé.1 require(!frozenAccounts[from], "The account is frozen");Copier
Nettoyage des actifs
Pour libérer les jetons ERC-20 détenus par ce contrat, nous devons appeler une fonction sur le contrat token auquel ils appartiennent, soit transfer
(opens in a new tab) soit approve
(opens in a new tab). Cela ne sert à rien de gaspiller du gaz dans ce cas en quotas, autant transférer directement.
1 function cleanupERC20(2 address erc20,3 address dest4 )5 public6 onlyOwner7 {8 IERC20 token = IERC20(erc20);Copier
C'est la syntaxe pour créer un objet pour un contrat lorsque nous recevons l'adresse. Nous pouvons faire cela parce que nous avons la définition des jetons ERC20 dans le code source (voir ligne 4), et ce fichier inclut la définition pour IERC20(opens in a new tab), l'interface pour un contrat ERC-20 d'OpenZeppelin.
1 uint balance = token.balanceOf(address(this));2 token.transfer(dest, balance);3 }Copier
C'est une fonction de nettoyage, nous ne voulons donc probablement laisser aucun jeton. Au lieu d'obtenir le solde de l'utilisateur manuellement, autant automatiser le processus.
Conclusion
Ce n'est pas une solution parfaite - il n'existe pas de solution parfaite au problème « l'utilisateur a fait une erreur ». Cependant, utiliser ce type de vérifications peut au moins prévenir certaines erreurs. La capacité à geler des comptes, bien que dangereuse, peut être utilisée pour limiter les dégâts de certaines attaques en refusant au pirate les fonds volés.
Dernière modification: @omahs(opens in a new tab), 17 février 2024