ERC-20 avec des garde-fous
Introduction
L'un des grands avantages d'Ethereum est qu'il n'y a pas d'autorité centrale qui peut modifier ou annuler vos transactions. L'un des grands problèmes d'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 utilisions le contrat de jeton ERC-20 d'OpenZeppelin (opens in a new tab), cet article ne l'explique pas en détail. 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 de GitHub (
). - Clonez le dépôt GitHub
https://github.com/qbzzt/20220815-erc20-safety-rails. - Ouvrez contracts > erc20-safety-rails.sol.
Création d'un contrat ERC-20
Avant de pouvoir ajouter la fonctionnalité de garde-fou, 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émint 1000 Fonctionnalités Aucun Contrôle d'accès Ownable Évolutivité Aucun -
Faites défiler vers le haut et cliquez sur Ouvrir dans Remix (pour Remix) ou sur Télécharger pour utiliser un autre environnement. 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>npmpour voir le code importé. -
Compilez, déployez et interagissez avec le contrat pour voir qu'il fonctionne comme un contrat ERC-20. Si vous avez besoin d'apprendre à utiliser Remix, consultez ce tutoriel (opens in a new tab).
Erreurs courantes
Les erreurs
Les utilisateurs envoient parfois des jetons à 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 qui sont faciles à détecter :
-
Envoi des jetons à la propre adresse du contrat. Par exemple, le jeton OP d'Optimism (opens in a new tab) a accumulé plus de 120 000 (opens in a new tab) jetons OP en moins de deux mois. Cela représente une somme d'argent considérable que, vraisemblablement, des personnes ont simplement perdue.
-
Envoi de jetons à une adresse vide, c'est-à-dire une adresse qui ne correspond ni à un compte externe ni à 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 de jetons (opens in a new tab).
Prévention des transferts
Le contrat ERC-20 d'OpenZeppelin inclut un hook, _beforeTokenTransfer (opens in a new tab), qui est appelé avant le transfert d'un jeton. Par défaut, ce hook ne fait rien, mais nous pouvons y ajouter notre propre fonctionnalité, comme des vérifications qui annulent la transaction en cas de problème.
Pour utiliser le hook, 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 }Certaines parties de cette fonction peuvent vous paraître nouvelles si vous n'êtes pas très familier avec Solidity :
1 internal virtualLe mot-clé virtual signifie que, tout comme nous avons hérité de la fonctionnalité de ERC20 et avons substitué cette fonction, d'autres contrats peuvent hériter de nous et substituer également cette fonction.
1 override(ERC20)Nous devons spécifier explicitement que nous substituons (opens in a new tab) la définition de _beforeTokenTransfer du jeton ERC20. 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 de quelle superclasse nous substituons la fonction _beforeTokenTransfer.
1 super._beforeTokenTransfer(from, to, amount);Cette ligne appelle la fonction _beforeTokenTransfer du ou des contrats dont nous avons hérité qui la possèdent. Dans ce cas, il s'agit uniquement de ERC20, car Ownable n'a pas ce hook. Même si ERC20._beforeTokenTransfer ne fait rien actuellement, nous l'appelons au cas où une fonctionnalité serait ajoutée à l'avenir (et que nous décidions alors de redéployer le contrat, car les contrats ne changent pas après leur déploiement).
Coder les exigences
Nous voulons ajouter ces exigences à la fonction :
- L'adresse
tone peut pas être égale àaddress(this), l'adresse du contrat ERC-20 lui-même. - L'adresse
tone peut pas être vide, elle doit être soit :- Un compte externe (EOA). Nous ne pouvons pas vérifier directement si une adresse est un EOA, mais nous pouvons vérifier le solde en 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. Vérifier 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 disponible directement dans Solidity. Pour cela, nous devons utiliser Yul (opens in a new tab), qui est l'assembleur de l'EVM. Il existe d'autres valeurs que nous pourrions utiliser à partir de Solidity (<address>.codeet<address>.codehash(opens in a new tab)), mais elles coûtent plus cher.
Passons en revue le nouveau code, ligne par ligne :
1 require(to != address(this), "Impossible d'envoyer des jetons à l'adresse du contrat");C'est la première exigence : vérifier que to et address(this) ne sont pas la même chose.
1 bool isToContract;2 assembly {3 isToContract := gt(extcodesize(to), 0)4 }Voici comment nous vérifions si une adresse est un contrat. Nous ne pouvons pas recevoir de sortie directement de Yul. À la place, nous définissons donc une variable pour conserver le résultat (isToContract dans ce cas). Yul fonctionne de telle manière que chaque opcode est considéré comme une fonction. Donc, nous appelons d'abord EXTCODESIZE (opens in a new tab) pour obtenir la taille du contrat, puis nous utilisons GT (opens in a new tab) pour vérifier qu'elle n'est pas nulle (nous avons affaire à 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, "Impossible d'envoyer des jetons à une adresse vide");Et enfin, nous avons la vérification effective des adresses vides.
Accès administratif
Il est parfois utile d'avoir un administrateur qui peut annuler des erreurs. Pour réduire le potentiel d'abus, cet administrateur peut être un multisig (opens in a new tab), de sorte que plusieurs personnes doivent approuver une action. Dans cet article, nous verrons 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 contrat du jeton réel pour gagner en légitimité. Par exemple, voir 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 être ce contrat 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
Ownableont un propriétaire unique. Les fonctions qui ont le modificateur (opens in a new tab)onlyOwnerne 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
AccessControlont un contrôle d'accès basé sur les rôles (RBAC) (opens in a new tab).
Par souci de simplicité, nous utilisons Ownable dans cet article.
Gel et dégel des contrats
Le gel et le dégel des contrats nécessitent plusieurs modifications :
-
Un mapping (opens in a new tab) des adresses vers des booléens (opens in a new tab) pour suivre les adresses qui 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; -
Des événements (opens in a new tab) pour informer toute personne intéressée lorsqu'un compte est gelé ou dégelé. Techniquement, les événements ne sont pas requis pour ces actions, mais ils aident le code hors chaîne à écouter ces événements et à savoir ce qui se passe. Il est de bon ton pour un contrat intelligent de les émettre lorsque quelque chose qui pourrait intéresser quelqu'un d'autre se produit.
Les événements sont indexés, il sera donc possible de rechercher toutes les fois où un compte a été gelé ou dégelé.
1 // Lorsque les comptes sont gelés ou dégelés2 event AccountFrozen(address indexed _addr);3 event AccountThawed(address indexed _addr); -
Fonctions pour geler et dégeler les comptes. Ces deux fonctions sont presque identiques, nous n'examinerons donc que la fonction de gel.
1 function freezeAccount(address addr)2 public3 onlyOwnerLes fonctions marquées comme
public(opens in a new tab) peuvent être appelées depuis d'autres contrats intelligents ou directement par une transaction.1 {2 require(!frozenAccounts[addr], "Compte déjà gelé");3 frozenAccounts[addr] = true;4 emit AccountFrozen(addr);5 } // freezeAccountSi le compte est déjà gelé, la transaction est annulée. Sinon, gelez-le et
émettezun événement. -
Modifiez
_beforeTokenTransferpour empêcher que des fonds ne soient déplacés depuis un compte gelé. Notez que des fonds peuvent toujours être transférés vers le compte gelé.1 require(!frozenAccounts[from], "Le compte est gelé");
Nettoyage des actifs
Pour libérer les jetons ERC-20 détenus par ce contrat, nous devons appeler une fonction sur le contrat de jeton auquel ils appartiennent, soit transfer (opens in a new tab) soit approve (opens in a new tab). Il est inutile de gaspiller du gaz sur des allocations dans ce cas, autant transférer directement.
1 function cleanupERC20(2 address erc20,3 address dest4 )5 public6 onlyOwner7 {8 IERC20 token = IERC20(erc20);C'est la syntaxe pour créer un objet pour un contrat lorsque nous recevons l'adresse. Nous pouvons le faire parce que nous avons la définition des jetons ERC20 dans le code source (voir la ligne 4), et ce fichier inclut la définition de IERC20 (opens in a new tab), l'interface pour un contrat ERC-20 OpenZeppelin.
1 uint balance = token.balanceOf(address(this));2 token.transfer(dest, balance);3 }Il s'agit d'une fonction de nettoyage, donc nous ne voulons vraisemblablement laisser aucun jeton. Au lieu de demander manuellement le solde à l'utilisateur, nous pouvons tout aussi bien automatiser le processus.
Conclusion
Ce n'est pas une solution parfaite. Il n'y a pas de solution parfaite au problème « l'utilisateur a fait une erreur ». Cependant, l'utilisation de ce type de vérifications peut au moins éviter certaines erreurs. La possibilité de geler des comptes, bien que dangereuse, peut être utilisée pour limiter les dégâts de certains piratages en refusant au pirate les fonds volés.
Voir ici pour plus de mon travail (opens in a new tab).
Dernière mise à jour de la page : 4 septembre 2025