Passer au contenu principal

Examen détaillé du contrat ERC-20

Solidity
erc-20
Débutant
Ori Pomerantz
9 mars 2021
29 minutes de lecture

Introduction

L'une des utilisations les plus courantes d'Ethereum est la création par un groupe d'un jeton échangeable, en un sens leur propre monnaie. Ces jetons suivent généralement un standard, l'ERC-20. Ce standard permet d'écrire des outils, tels que des réserves de liquidité et des portefeuilles, qui fonctionnent avec tous les jetons ERC-20. Dans cet article, nous analyserons l'implémentation ERC20 en Solidity d'OpenZeppelin (opens in a new tab), ainsi que la définition de l'interface (opens in a new tab).

Il s'agit d'un code source annoté. Si vous souhaitez implémenter l'ERC-20, lisez ce tutoriel (opens in a new tab).

L'interface

L'objectif d'un standard comme l'ERC-20 est de permettre de nombreuses implémentations de jetons qui soient interopérables entre les applications, comme les portefeuilles et les échanges décentralisés. Pour y parvenir, nous créons une interface (opens in a new tab). Tout code qui doit utiliser le contrat de jeton peut utiliser les mêmes définitions dans l'interface et être compatible avec tous les contrats de jetons qui l'utilisent, qu'il s'agisse d'un portefeuille tel que MetaMask, d'une application décentralisée (dapp) telle qu'Etherscan.io, ou d'un contrat différent tel qu'une réserve de liquidité.

Illustration of the ERC-20 interface

Si vous êtes un programmeur expérimenté, vous vous souvenez probablement avoir vu des constructions similaires en Java (opens in a new tab) ou même dans des fichiers d'en-tête C (opens in a new tab).

Voici une définition de l'interface ERC-20 (opens in a new tab) d'OpenZeppelin. Il s'agit d'une traduction du standard lisible par l'homme (opens in a new tab) en code Solidity. Bien sûr, l'interface elle-même ne définit pas comment faire quoi que ce soit. Cela est expliqué dans le code source du contrat ci-dessous.

 

// SPDX-License-Identifier: MIT

Les fichiers Solidity sont censés inclure un identifiant de licence. Vous pouvez voir la liste des licences ici (opens in a new tab). Si vous avez besoin d'une licence différente, expliquez-le simplement dans les commentaires.

 

pragma solidity >=0.6.0 <0.8.0;

Le langage Solidity évolue encore rapidement, et les nouvelles versions peuvent ne pas être compatibles avec l'ancien code (voir ici (opens in a new tab)). Par conséquent, il est judicieux de spécifier non seulement une version minimale du langage, mais aussi une version maximale, la dernière avec laquelle vous avez testé le code.

 

/**
 * @dev Interface du standard ERC-20 telle que définie dans l'EIP.
 */

Le @dev dans le commentaire fait partie du format NatSpec (opens in a new tab), utilisé pour produire de la documentation à partir du code source.

 

interface IERC20 {

Par convention, les noms d'interface commencent par I.

 

    /**
     * @dev Retourne la quantité de jetons en existence.
     */
    function totalSupply() external view returns (uint256);

Cette fonction est external, ce qui signifie qu'elle ne peut être appelée que de l'extérieur du contrat (opens in a new tab). Elle renvoie l'offre totale de jetons dans le contrat. Cette valeur est renvoyée en utilisant le type le plus courant dans Ethereum, un entier non signé de 256 bits (256 bits est la taille de mot native de l'EVM). Cette fonction est également une view, ce qui signifie qu'elle ne modifie pas l'état, elle peut donc être exécutée sur un seul nœud au lieu que chaque nœud de la chaîne de blocs l'exécute. Ce type de fonction ne génère pas de transaction et ne coûte pas de gaz.

Remarque : En théorie, il pourrait sembler que le créateur d'un contrat puisse tricher en renvoyant une offre totale inférieure à la valeur réelle, faisant paraître chaque jeton plus précieux qu'il ne l'est en réalité. Cependant, cette crainte ignore la véritable nature de la chaîne de blocs. Tout ce qui se passe sur la chaîne de blocs peut être vérifié par chaque nœud. Pour y parvenir, le code en langage machine et le stockage de chaque contrat sont disponibles sur chaque nœud. Bien que vous ne soyez pas obligé de publier le code Solidity de votre contrat, personne ne vous prendrait au sérieux à moins que vous ne publiiez le code source et la version de Solidity avec laquelle il a été compilé, afin qu'il puisse être vérifié par rapport au code en langage machine que vous avez fourni. Par exemple, voir ce contrat (opens in a new tab).

 

    /**
     * @dev Retourne la quantité de jetons possédés par `account`.
     */
    function balanceOf(address account) external view returns (uint256);

Comme son nom l'indique, balanceOf renvoie le solde d'un compte. Les comptes Ethereum sont identifiés dans Solidity à l'aide du type address, qui contient 160 bits. Elle est également external et view.

 

    /**
     * @dev Déplace `amount` jetons du compte de l'appelant vers `recipient`.
     *
     * Retourne une valeur booléenne indiquant si l'opération a réussi.
     *
     * Émet un événement {Transfer}.
     */
    function transfer(address recipient, uint256 amount) external returns (bool);

La fonction transfer effectue un transfert de jetons de l'appelant vers une adresse différente. Cela implique un changement d'état, ce n'est donc pas une view. Lorsqu'un utilisateur appelle cette fonction, cela crée une transaction et coûte du gaz. Elle émet également un événement, Transfer, pour informer tout le monde sur la chaîne de blocs de l'événement.

La fonction a deux types de sortie pour deux types d'appelants différents :

  • Les utilisateurs qui appellent la fonction directement depuis une interface utilisateur. Généralement, l'utilisateur soumet une transaction et n'attend pas de réponse, ce qui pourrait prendre un temps indéfini. L'utilisateur peut voir ce qui s'est passé en cherchant le reçu de la transaction (qui est identifié par le hachage de transaction) ou en cherchant l'événement Transfer.
  • D'autres contrats, qui appellent la fonction dans le cadre d'une transaction globale. Ces contrats obtiennent le résultat immédiatement, car ils s'exécutent dans la même transaction, ils peuvent donc utiliser la valeur de retour de la fonction.

Le même type de sortie est créé par les autres fonctions qui modifient l'état du contrat.

 

Les allocations permettent à un compte de dépenser des jetons qui appartiennent à un propriétaire différent. C'est utile, par exemple, pour les contrats qui agissent en tant que vendeurs. Les contrats ne peuvent pas surveiller les événements, donc si un acheteur devait transférer des jetons directement au contrat du vendeur, ce contrat ne saurait pas qu'il a été payé. Au lieu de cela, l'acheteur permet au contrat du vendeur de dépenser un certain montant, et le vendeur transfère ce montant. Cela se fait via une fonction que le contrat du vendeur appelle, afin que le contrat du vendeur puisse savoir si elle a réussi.

    /**
     * @dev Retourne le nombre restant de jetons que `spender` sera
     * autorisé à dépenser au nom de `owner` via {transferFrom}. Ceci est
     * zéro par défaut.
     *
     * Cette valeur change lorsque {approve} ou {transferFrom} sont appelés.
     */
    function allowance(address owner, address spender) external view returns (uint256);

La fonction allowance permet à quiconque de demander quelle est l'allocation qu'une adresse (owner) permet à une autre adresse (spender) de dépenser.

 

La fonction approve crée une allocation. Assurez-vous de lire le message sur la façon dont elle peut être détournée. Dans Ethereum, vous contrôlez l'ordre de vos propres transactions, mais vous ne pouvez pas contrôler l'ordre dans lequel les transactions des autres personnes seront exécutées, à moins que vous ne soumettiez votre propre transaction qu'après avoir vu que la transaction de l'autre partie a eu lieu.

 

Enfin, transferFrom est utilisée par le dépensier pour dépenser réellement l'allocation.

 

Ces événements sont émis lorsque l'état du contrat ERC-20 change.

Le contrat réel

Il s'agit du contrat réel qui implémente le standard ERC-20, tiré d'ici (opens in a new tab). Il n'est pas destiné à être utilisé tel quel, mais vous pouvez en hériter (opens in a new tab) pour l'étendre à quelque chose d'utilisable.

// SPDX-License-Identifier: MIT
pragma solidity >=0.6.0 <0.8.0;

 

Instructions d'importation

En plus des définitions d'interface ci-dessus, la définition du contrat importe deux autres fichiers :


import "../../GSN/Context.sol";
import "./IERC20.sol";
import "../../math/SafeMath.sol";
  • GSN/Context.sol contient les définitions requises pour utiliser OpenGSN (opens in a new tab), un système qui permet aux utilisateurs sans ether d'utiliser la chaîne de blocs. Notez qu'il s'agit d'une ancienne version, si vous souhaitez vous intégrer à OpenGSN, utilisez ce tutoriel (opens in a new tab).
  • La bibliothèque SafeMath (opens in a new tab), qui empêche les dépassements de capacité arithmétiques (overflows/underflows) pour les versions de Solidity <0.8.0. Dans Solidity ≥0.8.0, les opérations arithmétiques s'annulent automatiquement en cas de dépassement de capacité, rendant SafeMath inutile. Ce contrat utilise SafeMath pour la rétrocompatibilité avec les anciennes versions du compilateur.

 

Ce commentaire explique l'objectif du contrat.

Définition du contrat

contract ERC20 is Context, IERC20 {

Cette ligne spécifie l'héritage, dans ce cas de IERC20 ci-dessus et de Context, pour OpenGSN.

 


    using SafeMath for uint256;

Cette ligne attache la bibliothèque SafeMath au type uint256. Vous pouvez trouver cette bibliothèque ici (opens in a new tab).

Définitions des variables

Ces définitions spécifient les variables d'état du contrat. Ces variables sont déclarées private, mais cela signifie seulement que les autres contrats sur la chaîne de blocs ne peuvent pas les lire. Il n'y a pas de secrets sur la chaîne de blocs, le logiciel sur chaque nœud possède l'état de chaque contrat à chaque bloc. Par convention, les variables d'état sont nommées _<something>.

Les deux premières variables sont des mappings (opens in a new tab), ce qui signifie qu'elles se comportent à peu près de la même manière que des tableaux associatifs (opens in a new tab), sauf que les clés sont des valeurs numériques. Le stockage n'est alloué que pour les entrées qui ont des valeurs différentes de la valeur par défaut (zéro).

    mapping (address => uint256) private _balances;

Le premier mapping, _balances, correspond aux adresses et à leurs soldes respectifs de ce jeton. Pour accéder au solde, utilisez cette syntaxe : _balances[<address>].

 

    mapping (address => mapping (address => uint256)) private _allowances;

Cette variable, _allowances, stocke les allocations expliquées précédemment. Le premier indice est le propriétaire des jetons, et le second est le contrat avec l'allocation. Pour accéder au montant que l'adresse A peut dépenser depuis le compte de l'adresse B, utilisez _allowances[B][A].

 

    uint256 private _totalSupply;

Comme son nom l'indique, cette variable garde une trace de l'offre totale de jetons.

 

    string private _name;
    string private _symbol;
    uint8 private _decimals;

Ces trois variables sont utilisées pour améliorer la lisibilité. Les deux premières sont explicites, mais _decimals ne l'est pas.

D'une part, Ethereum n'a pas de variables à virgule flottante ou fractionnaires. D'autre part, les humains aiment pouvoir diviser les jetons. L'une des raisons pour lesquelles les gens ont choisi l'or comme monnaie était qu'il était difficile de rendre la monnaie quand quelqu'un voulait acheter l'équivalent d'un canard en vache.

La solution consiste à garder une trace des entiers, mais à compter à la place du jeton réel un jeton fractionnaire qui n'a presque aucune valeur. Dans le cas de l'ether, le jeton fractionnaire s'appelle le Wei, et 10^18 Wei équivalent à un ETH. Au moment de la rédaction, 10 000 000 000 000 Wei valent environ un centime américain ou européen.

Les applications doivent savoir comment afficher le solde du jeton. Si un utilisateur a 3 141 000 000 000 000 000 Wei, est-ce 3,14 ETH ? 31,41 ETH ? 3 141 ETH ? Dans le cas de l'ether, il est défini 10^18 Wei pour un ETH, mais pour votre jeton, vous pouvez sélectionner une valeur différente. Si diviser le jeton n'a pas de sens, vous pouvez utiliser une valeur _decimals de zéro. Si vous souhaitez utiliser le même standard que l'ETH, utilisez la valeur 18.

Le constructeur

Le constructeur est appelé lors de la première création du contrat. Par convention, les paramètres de fonction sont nommés <something>_.

Fonctions de l'interface utilisateur

Ces fonctions, name, symbol et decimals aident les interfaces utilisateur à connaître votre contrat afin qu'elles puissent l'afficher correctement.

Le type de retour est string memory, ce qui signifie renvoyer une chaîne de caractères stockée en mémoire. Les variables, telles que les chaînes de caractères, peuvent être stockées à trois emplacements :

Durée de vieAccès au contratCoût en gaz
MémoireAppel de fonctionLecture/ÉcritureDes dizaines ou des centaines (plus élevé pour les emplacements supérieurs)
Données d'appelAppel de fonctionLecture seuleNe peut pas être utilisé comme type de retour, uniquement comme type de paramètre de fonction
StockageJusqu'à modificationLecture/ÉcritureÉlevé (800 pour la lecture, 20k pour l'écriture)

Dans ce cas, memory est le meilleur choix.

Lire les informations du jeton

Ce sont des fonctions qui fournissent des informations sur le jeton, soit l'offre totale, soit le solde d'un compte.

    /**
     * @dev Voir {IERC20-totalSupply}.
     */
    function totalSupply() public view override returns (uint256) {
        return _totalSupply;
    }

La fonction totalSupply renvoie l'offre totale de jetons.

 

    /**
     * @dev Voir {IERC20-balanceOf}.
     */
    function balanceOf(address account) public view override returns (uint256) {
        return _balances[account];
    }

Lire le solde d'un compte. Notez que n'importe qui est autorisé à obtenir le solde du compte de n'importe qui d'autre. Il ne sert à rien d'essayer de cacher cette information, car elle est de toute façon disponible sur chaque nœud. Il n'y a pas de secrets sur la chaîne de blocs.

Transférer des jetons

La fonction transfer est appelée pour effectuer un transfert de jetons du compte de l'expéditeur vers un autre. Notez que même si elle renvoie une valeur booléenne, cette valeur est toujours true. Si le transfert échoue, le contrat annule l'appel.

 

        _transfer(_msgSender(), recipient, amount);
        return true;
    }

La fonction _transfer fait le travail réel. C'est une fonction privée qui ne peut être appelée que par d'autres fonctions du contrat. Par convention, les fonctions privées sont nommées _<something>, tout comme les variables d'état.

Normalement, dans Solidity, nous utilisons msg.sender pour l'expéditeur du message. Cependant, cela casse OpenGSN (opens in a new tab). Si nous voulons autoriser les transactions sans ether avec notre jeton, nous devons utiliser _msgSender(). Elle renvoie msg.sender pour les transactions normales, mais pour celles sans ether, elle renvoie le signataire d'origine et non le contrat qui a relayé le message.

Fonctions d'allocation

Ce sont les fonctions qui implémentent la fonctionnalité d'allocation : allowance, approve, transferFrom et _approve. De plus, l'implémentation d'OpenZeppelin va au-delà du standard de base pour inclure certaines fonctionnalités qui améliorent la sécurité : increaseAllowance et decreaseAllowance.

La fonction allowance

    /**
     * @dev Voir {IERC20-allowance}.
     */
    function allowance(address owner, address spender) public view virtual override returns (uint256) {
        return _allowances[owner][spender];
    }

La fonction allowance permet à tout le monde de vérifier n'importe quelle allocation.

La fonction approve

    /**
     * @dev Voir {IERC20-approve}.
     *
     * Exigences :
     *
     * - `spender` ne peut pas être l'adresse zéro.
     */
    function approve(address spender, uint256 amount) public virtual override returns (bool) {

Cette fonction est appelée pour créer une allocation. Elle est similaire à la fonction transfer ci-dessus :

  • La fonction appelle simplement une fonction interne (dans ce cas, _approve) qui fait le vrai travail.
  • La fonction renvoie soit true (en cas de succès), soit elle annule (sinon).

 

        _approve(_msgSender(), spender, amount);
        return true;
    }

Nous utilisons des fonctions internes pour minimiser le nombre d'endroits où des changements d'état se produisent. Toute fonction qui modifie l'état est un risque de sécurité potentiel qui doit être audité pour la sécurité. De cette façon, nous avons moins de chances de nous tromper.

La fonction transferFrom

C'est la fonction qu'un dépensier appelle pour dépenser une allocation. Cela nécessite deux opérations : transférer le montant dépensé et réduire l'allocation de ce montant.

 

L'appel de la fonction a.sub(b, "message") fait deux choses. Premièrement, il calcule a-b, qui est la nouvelle allocation. Deuxièmement, il vérifie que ce résultat n'est pas négatif. S'il est négatif, l'appel s'annule avec le message fourni. Notez que lorsqu'un appel s'annule, tout traitement effectué précédemment pendant cet appel est ignoré, nous n'avons donc pas besoin de défaire le _transfer.

        _approve(sender, _msgSender(), _allowances[sender][_msgSender()].sub(amount,
             "ERC20: transfer amount exceeds allowance"));
        return true;
    }

Ajouts de sécurité d'OpenZeppelin

Il est dangereux de définir une allocation non nulle à une autre valeur non nulle, car vous ne contrôlez que l'ordre de vos propres transactions, pas celles des autres. Imaginez que vous ayez deux utilisateurs, Alice qui est naïve et Bill qui est malhonnête. Alice veut un service de Bill, qui, selon elle, coûte cinq jetons - elle donne donc à Bill une allocation de cinq jetons.

Puis quelque chose change et le prix de Bill passe à dix jetons. Alice, qui veut toujours le service, envoie une transaction qui fixe l'allocation de Bill à dix. Au moment où Bill voit cette nouvelle transaction dans le pool de transactions, il envoie une transaction qui dépense les cinq jetons d'Alice et a un prix du gaz beaucoup plus élevé afin qu'elle soit minée plus rapidement. De cette façon, Bill peut d'abord dépenser cinq jetons, puis, une fois la nouvelle allocation d'Alice minée, en dépenser dix de plus pour un prix total de quinze jetons, plus que ce qu'Alice avait l'intention d'autoriser. Cette technique s'appelle le front-running (opens in a new tab)

Transaction d'AliceNonce d'AliceTransaction de BillNonce de BillAllocation de BillRevenu total de Bill provenant d'Alice
approve(Bill, 5)1050
transferFrom(Alice, Bill, 5)10,12305
approve(Bill, 10)11105
transferFrom(Alice, Bill, 10)10,124015

Pour éviter ce problème, ces deux fonctions (increaseAllowance et decreaseAllowance) vous permettent de modifier l'allocation d'un montant spécifique. Donc, si Bill avait déjà dépensé cinq jetons, il ne pourra en dépenser que cinq de plus. Selon le moment, il y a deux façons dont cela peut fonctionner, qui se terminent toutes deux par le fait que Bill n'obtient que dix jetons :

A :

Transaction d'AliceNonce d'AliceTransaction de BillNonce de BillAllocation de BillRevenu total de Bill provenant d'Alice
approve(Bill, 5)1050
transferFrom(Alice, Bill, 5)10,12305
increaseAllowance(Bill, 5)110+5 = 55
transferFrom(Alice, Bill, 5)10,124010

B :

Transaction d'AliceNonce d'AliceTransaction de BillNonce de BillAllocation de BillRevenu total de Bill provenant d'Alice
approve(Bill, 5)1050
increaseAllowance(Bill, 5)115+5 = 100
transferFrom(Alice, Bill, 10)10,124010

La fonction a.add(b) est une addition sécurisée. Dans le cas peu probable où a+b>=2^256, elle ne boucle pas (wrap around) comme le fait une addition normale.

Fonctions qui modifient les informations du jeton

Ce sont les quatre fonctions qui font le travail réel : _transfer, _mint, _burn et _approve.

La fonction _transfer

Cette fonction, _transfer, effectue un transfert de jetons d'un compte à un autre. Elle est appelée à la fois par transfer (pour les transferts depuis le propre compte de l'expéditeur) et transferFrom (pour utiliser les allocations afin de transférer depuis le compte de quelqu'un d'autre).

 

        require(sender != address(0), "ERC20: transfer from the zero address");
        require(recipient != address(0), "ERC20: transfer to the zero address");

Personne ne possède réellement l'adresse zéro dans Ethereum (c'est-à-dire que personne ne connaît une clé privée dont la clé publique correspondante est transformée en adresse zéro). Lorsque les gens utilisent cette adresse, il s'agit généralement d'un bogue logiciel - nous échouons donc si l'adresse zéro est utilisée comme expéditeur ou destinataire.

 

        _beforeTokenTransfer(sender, recipient, amount);

Il y a deux façons d'utiliser ce contrat :

  1. L'utiliser comme modèle pour votre propre code
  2. En hériter (opens in a new tab), et ne remplacer que les fonctions que vous devez modifier

La deuxième méthode est bien meilleure car le code ERC-20 d'OpenZeppelin a déjà été audité et s'est avéré sécurisé. Lorsque vous utilisez l'héritage, les fonctions que vous modifiez sont claires, et pour faire confiance à votre contrat, les gens n'ont besoin d'auditer que ces fonctions spécifiques.

Il est souvent utile d'exécuter une fonction chaque fois que des jetons changent de mains. Cependant, _transfer est une fonction très importante et il est possible de l'écrire de manière non sécurisée (voir ci-dessous), il est donc préférable de ne pas la remplacer. La solution est _beforeTokenTransfer, une fonction de hook (opens in a new tab). Vous pouvez remplacer cette fonction, et elle sera appelée à chaque transfert.

 

        _balances[sender] = _balances[sender].sub(amount, "ERC20: transfer amount exceeds balance");
        _balances[recipient] = _balances[recipient].add(amount);

Ce sont les lignes qui effectuent réellement le transfert. Notez qu'il n'y a rien entre elles, et que nous soustrayons le montant transféré de l'expéditeur avant de l'ajouter au destinataire. C'est important car s'il y avait un appel à un contrat différent au milieu, il aurait pu être utilisé pour tromper ce contrat. De cette façon, le transfert est atomique, rien ne peut se passer au milieu.

 

        emit Transfer(sender, recipient, amount);
    }

Enfin, émettre un événement Transfer. Les événements ne sont pas accessibles aux contrats intelligents, mais le code s'exécutant en dehors de la chaîne de blocs peut écouter les événements et y réagir. Par exemple, un portefeuille peut garder une trace du moment où le propriétaire obtient plus de jetons.

Les fonctions _mint et _burn

Ces deux fonctions (_mint et _burn) modifient l'offre totale de jetons. Elles sont internes et aucune fonction ne les appelle dans ce contrat, elles ne sont donc utiles que si vous héritez du contrat et ajoutez votre propre logique pour décider dans quelles conditions frapper de nouveaux jetons ou brûler ceux existants.

REMARQUE : Chaque jeton ERC-20 a sa propre logique métier qui dicte la gestion des jetons. Par exemple, un contrat à offre fixe pourrait n'appeler _mint que dans le constructeur et ne jamais appeler _burn. Un contrat qui vend des jetons appellera _mint lorsqu'il sera payé, et appellera vraisemblablement _burn à un moment donné pour éviter une inflation galopante.

Assurez-vous de mettre à jour _totalSupply lorsque le nombre total de jetons change.

 

La fonction _burn est presque identique à _mint, sauf qu'elle va dans l'autre sens.

La fonction _approve

C'est la fonction qui spécifie réellement les allocations. Notez qu'elle permet à un propriétaire de spécifier une allocation supérieure au solde actuel du propriétaire. C'est acceptable car le solde est vérifié au moment du transfert, lorsqu'il pourrait être différent du solde au moment de la création de l'allocation.

 

Émettre un événement Approval. Selon la façon dont l'application est écrite, le contrat du dépensier peut être informé de l'approbation soit par le propriétaire, soit par un serveur qui écoute ces événements.

        emit Approval(owner, spender, amount);
    }

Modifier la variable Decimals

Cette fonction modifie la variable _decimals qui est utilisée pour indiquer aux interfaces utilisateur comment interpréter le montant. Vous devriez l'appeler depuis le constructeur. Il serait malhonnête de l'appeler à un moment ultérieur, et les applications ne sont pas conçues pour le gérer.

Hooks

C'est la fonction de hook à appeler lors des transferts. Elle est vide ici, mais si vous avez besoin qu'elle fasse quelque chose, il vous suffit de la remplacer.

Conclusion

Pour résumer, voici quelques-unes des idées les plus importantes de ce contrat (à mon avis, le vôtre est susceptible de varier) :

  • Il n'y a pas de secrets sur la chaîne de blocs. Toute information à laquelle un contrat intelligent peut accéder est disponible pour le monde entier.
  • Vous pouvez contrôler l'ordre de vos propres transactions, mais pas le moment où les transactions des autres se produisent. C'est la raison pour laquelle modifier une allocation peut être dangereux, car cela permet au dépensier de dépenser la somme des deux allocations.
  • Les valeurs de type uint256 bouclent (wrap around). En d'autres termes, 0-1=2^256-1. Si ce n'est pas le comportement souhaité, vous devez le vérifier (ou utiliser la bibliothèque SafeMath qui le fait pour vous). Notez que cela a changé dans Solidity 0.8.0 (opens in a new tab).
  • Effectuez tous les changements d'état d'un type spécifique à un endroit spécifique, car cela facilite l'audit. C'est la raison pour laquelle nous avons, par exemple, _approve, qui est appelée par approve, transferFrom, increaseAllowance et decreaseAllowance
  • Les changements d'état doivent être atomiques, sans aucune autre action au milieu (comme vous pouvez le voir dans _transfer). C'est parce que pendant le changement d'état, vous avez un état incohérent. Par exemple, entre le moment où vous déduisez du solde de l'expéditeur et le moment où vous ajoutez au solde du destinataire, il y a moins de jetons en existence qu'il ne devrait y en avoir. Cela pourrait potentiellement être détourné s'il y a des opérations entre eux, en particulier des appels à un contrat différent.

Maintenant que vous avez vu comment le contrat ERC-20 d'OpenZeppelin est écrit, et surtout comment il est rendu plus sécurisé, allez écrire vos propres contrats et applications sécurisés.

Voir ici pour plus de mon travail (opens in a new tab).