Présentation du contrat ERC-20
Introduction
Ethereum est couramment utilisé par des groupes pour créer des jetons échangeables ou, dans un certain sens, leur propre monnaie. Ces jetons suivent généralement une norme, ERC-20. Cette norme permet de créer des outils, tels que des pools de liquidités et des portefeuilles, qui fonctionnent avec tous les jetons ERC-20 . Dans cet article, nous analyserons l'implémentation ERC20 de Solidity par OpenZeppelin (opens in a new tab), ainsi que la définition de l'interface (opens in a new tab).
Ceci est le code source annoté. Si vous souhaitez implémenter l'ERC-20, lisez ce tutoriel (opens in a new tab).
L'interface
L'objectif d'une norme comme l'ERC-20 est de permettre de nombreuses implémentations de jetons interopérables entre les applications, comme les portefeuilles et les échanges décentralisés. Pour ce faire, nous créons une interface (opens in a new tab). Tout code qui a besoin d'utiliser le contrat de jeton peut employer 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 dapp comme etherscan.io, ou d'un contrat différent tel qu'un pool de liquidités.
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).
Ceci est une définition de l'Interface ERC-20 (opens in a new tab) d'OpenZeppelin. C'est une traduction de la norme 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. Ceci est expliqué dans le code source du contrat ci-dessous.
1// SPDX-License-Identifier: MITLes 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 autre licence, il vous suffit de l'expliquer dans les commentaires.
1pragma 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 plus récente avec laquelle vous avez testé le code.
1/**2 * @dev Interface de la norme ERC20 telle que définie dans l'EIP.3 */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.
1interface IERC20 {Par convention, les noms d'interface commencent par I.
1 /**2 * @dev Renvoie la quantité de jetons existants.3 */4 function totalSupply() external view returns (uint256);Cette fonction est external, ce qui signifie qu'elle ne peut être appelée que depuis l'extérieur du contrat (opens in a new tab).
Elle renvoie la quantité totale de jetons dans le contrat. Cette valeur est renvoyée en utilisant le type de données le plus courant sur Ethereum, un entier non signé de 256 bits (256 bits est la taille
de mot native de l'EVM). Cette fonction est également de type view, ce qui signifie qu'elle ne modifie pas l'état. Elle peut donc être exécutée sur un seul nœud, sans que tous les nœuds de la blockchain n'aient à l'exécuter
. Ce type de fonction ne génère pas de transaction et n'a pas de coût en 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 ainsi 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 blockchain. Tout ce qui se passe sur la blockchain peut être vérifié par chaque nœud. Pour ce faire, 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 si vous ne publiez pas 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, consultez ce contrat (opens in a new tab).
1 /**2 * @dev Renvoie la quantité de jetons détenus par `account`.3 */4 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.
1 /**2 * @dev Déplace un `montant` (`amount`) de jetons du compte de l'appelant vers le `destinataire` (`recipient`).3 *4 * Renvoie une valeur booléenne indiquant si l'opération a réussi.5 *6 * Émet un événement {Transfer}.7 */8 function transfer(address recipient, uint256 amount) external returns (bool);La fonction transfer transfère des jetons de l'appelant à une autre adresse. Cela implique un changement d'état, ce n'est donc pas une fonction de type 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
blockchain 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 peut prendre un temps indéfini. L'utilisateur peut voir ce qui s'est passé
en consultant le reçu de transaction (identifié par le hachage de la transaction) ou en recherchant 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, et 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 autre propriétaire. 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 directement des jetons au contrat du vendeur , ce contrat ne saurait pas qu'il a été payé. Au lieu de cela, l'acheteur autorise le contrat du vendeur à dépenser un certain montant, et le vendeur transfère ce montant. Cela se fait par le biais d'une fonction que le contrat du vendeur appelle, de sorte que le contrat du vendeur puisse savoir si l'opération a réussi.
1 /**2 * @dev Renvoie le nombre de jetons restants que `spender` sera3 * autorisé à dépenser au nom de `owner` via {transferFrom}. La valeur par4 * défaut est zéro.5 *6 * Cette valeur change lorsque {approve} ou {transferFrom} sont appelées.7 */8 function allowance(address owner, address spender) external view returns (uint256);La fonction allowance permet à quiconque de demander à voir quelle est l'allocation qu'une
adresse (owner) autorise une autre adresse (spender) à dépenser.
1 /**2 * @dev Définit le `montant` (`amount`) comme l'allocation de `spender` sur les jetons de l'appelant.3 *4 * Renvoie une valeur booléenne indiquant si l'opération a réussi.5 *6 * IMPORTANT : Sachez que la modification d'une allocation avec cette méthode comporte le risque7 * que quelqu'un puisse utiliser à la fois l'ancienne et la nouvelle allocation en raison d'un8 * ordre de transaction malencontreux. Une solution possible pour atténuer cette9 * condition de concurrence consiste à d'abord réduire à 0 l'allocation du dépensier, puis à définir la10 * valeur souhaitée :11 * https://github.com/ethereum/EIPs/issues/20#issuecomment-26352472912 *13 * Émet un événement {Approval}.14 */15 function approve(address spender, uint256 amount) external returns (bool);Afficher toutLa fonction approve crée une allocation. Assurez-vous de lire le message sur
la façon dont elle peut être utilisée de manière abusive. 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 pas votre propre transaction avant de voir que la
transaction de l'autre partie a eu lieu.
1 /**2 * @dev Déplace un `montant` (`amount`) de jetons de `sender` vers `recipient` en utilisant le3 * mécanisme d'allocation. Le `montant` (`amount`) est ensuite déduit de l'allocation4 * de l'appelant.5 *6 * Renvoie une valeur booléenne indiquant si l'opération a réussi.7 *8 * Émet un événement {Transfer}.9 */10 function transferFrom(address sender, address recipient, uint256 amount) external returns (bool);Afficher toutEnfin, transferFrom est utilisé par le dépensier pour dépenser réellement l'allocation.
12 /**3 * @dev Émis lorsque des jetons d'une `valeur` (`value`) sont déplacés d'un compte (`from`) à4 * un autre (`to`).5 *6 * Notez que `value` peut être zéro.7 */8 event Transfer(address indexed from, address indexed to, uint256 value);910 /**11 * @dev Émis lorsque l'allocation d'un `dépensier` (`spender`) pour un `propriétaire` (`owner`) est définie par12 * un appel à {approve}. `value` est la nouvelle allocation.13 */14 event Approval(address indexed owner, address indexed spender, uint256 value);15}Afficher toutCes événements sont émis lorsque l'état du contrat ERC-20 change.
Le contrat réel
Ceci est le contrat réel qui implémente la norme 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.
1// SPDX-License-Identifier: MIT2pragma 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 :
12import "../../GSN/Context.sol";3import "./IERC20.sol";4import "../../math/SafeMath.sol";GSN/Context.solcontient les définitions requises pour utiliser OpenGSN (opens in a new tab), un système qui permet aux utilisateurs sans ether d'utiliser la blockchain. Notez qu'il s'agit d'une ancienne version. Si vous voulez 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/sous-dépassements arithmétiques 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/sous-dépassement, ce qui rend SafeMath inutile. Ce contrat utilise SafeMath pour la compatibilité descendante avec les anciennes versions du compilateur.
Ce commentaire explique l'objectif du contrat.
1/**2 * @dev Implémentation de l'interface {IERC20}.3 *4 * Cette implémentation est agnostique quant à la manière dont les jetons sont créés. Cela signifie5 * qu'un mécanisme d'approvisionnement doit être ajouté dans un contrat dérivé utilisant {_mint}.6 * Pour un mécanisme générique, voir {ERC20PresetMinterPauser}.7 *8 * CONSEIL : pour un exposé détaillé, consultez notre guide9 * https://forum.zeppelin.solutions/t/how-to-implement-erc20-supply-mechanisms/226[Comment10 * implémenter des mécanismes d'approvisionnement].11 *12 * Nous avons suivi les directives générales d'OpenZeppelin : les fonctions s'annulent au lieu de13 * renvoyer « `false` » en cas d'échec. Ce comportement est néanmoins conventionnel14 * et n'entre pas en conflit avec les attentes des applications ERC20.15 *16 * De plus, un événement {Approval} est émis lors des appels à {transferFrom}.17 * Cela permet aux applications de reconstruire l'allocation pour tous les comptes simplement18 * en écoutant lesdits événements. D'autres implémentations de l'EIP peuvent ne pas émettre19 * ces événements, car ce n'est pas requis par la spécification.20 *21 * Enfin, les fonctions non standard {decreaseAllowance} et {increaseAllowance}22 * ont été ajoutées pour atténuer les problèmes bien connus concernant la définition23 * des allocations. Voir {IERC20-approve}.24 */25Afficher toutDéfinition du contrat
1contract ERC20 is Context, IERC20 {Cette ligne spécifie l'héritage, dans ce cas de IERC20 ci-dessus et de Context, pour OpenGSN.
12 using SafeMath for uint256;3Cette 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 comme private, mais
cela signifie seulement que les autres contrats sur la blockchain ne peuvent pas les lire. Il n'y a pas de
secrets sur la blockchain, le logiciel sur chaque nœud a l'état de chaque contrat
à chaque bloc. Par convention, les variables d'état sont nommées _<quelquechose>.
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).
1 mapping (address => uint256) private _balances;Le premier mapping, _balances, contient les adresses et leurs soldes respectifs de ce jeton. Pour accéder
au solde, utilisez cette syntaxe : _balances[<adresse>].
1 mapping (address => mapping (address => uint256)) private _allowances;Cette variable, _allowances, stocke les allocations expliquées précédemment. Le premier index 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 à partir du compte de l'adresse B, utilisez _allowances[B][A].
1 uint256 private _totalSupply;Comme son nom l'indique, cette variable assure le suivi de l'offre totale de jetons.
1 string private _name;2 string private _symbol;3 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 ne dispose pas de variables à virgule flottante ou fractionnaires. D'autre part, les humains aiment pouvoir diviser les jetons. Une des raisons pour lesquelles les gens ont opté pour l'or comme monnaie était qu'il était difficile de rendre la monnaie lorsque quelqu'un voulait acheter l'équivalent d'un canard en vache.
La solution consiste à garder la trace des nombres entiers, mais de compter, au lieu du jeton réel, un jeton fractionnaire qui n'a pratiquement aucune valeur. Dans le cas de l'ether, le jeton fractionnaire est appelé wei, et 10^18 wei est égal à un ETH. Au moment de la rédaction de cet article, 10 000 000 000 000 de wei valent environ un centime de dollar américain ou d'euro.
Les applications doivent savoir comment afficher le solde de jetons. Si un utilisateur a 3 141 000 000 000 000 000 wei, est-ce que cela fait
3,14 ETH ? 31,41 ETH ? 3 141 ETH ? Dans le cas de l'ether, il est défini que 10^18 wei valent 1 ETH, mais pour votre
jeton, vous pouvez sélectionner une valeur différente. Si la division du jeton n'a pas de sens, vous pouvez utiliser une
valeur _decimals de zéro. Si vous voulez utiliser la même norme que l'ETH, utilisez la valeur 18.
Le constructeur
1 /**2 * @dev Définit les valeurs pour {name} et {symbol}, initialise {decimals} avec3 * une valeur par défaut de 18.4 *5 * Pour sélectionner une valeur différente pour {decimals}, utilisez {_setupDecimals}.6 *7 * Ces trois valeurs sont immuables : elles ne peuvent être définies qu'une seule fois pendant8 * la construction.9 */10 constructor (string memory name_, string memory symbol_) public {11 // Dans Solidity ≥0.7.0, 'public' est implicite et peut être omis.1213 _name = name_;14 _symbol = symbol_;15 _decimals = 18;16 }Afficher toutLe constructeur est appelé lors de la création initiale du contrat. Par convention, les paramètres de fonction sont nommés <quelquechose>_.
Fonctions de l'interface utilisateur
1 /**2 * @dev Renvoie le nom du jeton.3 */4 function name() public view returns (string memory) {5 return _name;6 }78 /**9 * @dev Renvoie le symbole du jeton, généralement une version plus courte du10 * nom.11 */12 function symbol() public view returns (string memory) {13 return _symbol;14 }1516 /**17 * @dev Renvoie le nombre de décimales utilisées pour obtenir sa représentation utilisateur.18 * Par exemple, si `decimals` est égal à `2`, un solde de `505` jetons devrait19 * être affiché à un utilisateur comme `5,05` (`505 / 10 ** 2`).20 *21 * Les jetons optent généralement pour une valeur de 18, imitant la relation entre22 * l'ether et le wei. C'est la valeur que {ERC20} utilise, à moins que {_setupDecimals} ne soit23 * appelée.24 *25 * NOTE : Cette information n'est utilisée qu'à des fins d'_affichage_ : elle n'affecte26 * en aucun cas l'arithmétique du contrat, y compris27 * {IERC20-balanceOf} et {IERC20-transfer}.28 */29 function decimals() public view returns (uint8) {30 return _decimals;31 }Afficher toutCes 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 endroits :
| Durée de vie | Accès au contrat | Coût du gaz | |
|---|---|---|---|
| Memory | Appel de fonction | Lecture/écriture | Dizaines ou centaines (plus élevé pour les emplacements plus élevés) |
| Données d'appel | Appel de fonction | Lecture seule | Ne peut pas être utilisé comme type de retour, seulement comme type de paramètre de fonction |
| Stockage | Jusqu'à modification | Lecture/écriture | Élevé (800 pour la lecture, 20k pour l'écriture) |
Dans ce cas, memory est le meilleur choix.
Lire les informations sur le jeton
Ce sont des fonctions qui fournissent des informations sur le jeton, soit l'offre totale, soit le solde d'un compte.
1 /**2 * @dev Voir {IERC20-totalSupply}.3 */4 function totalSupply() public view override returns (uint256) {5 return _totalSupply;6 }La fonction totalSupply renvoie l'offre totale de jetons.
1 /**2 * @dev Voir {IERC20-balanceOf}.3 */4 function balanceOf(address account) public view override returns (uint256) {5 return _balances[account];6 }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 est inutile 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 blockchain.
Transférer des jetons
1 /**2 * @dev Voir {IERC20-transfer}.3 *4 * Exigences :5 *6 * - `recipient` ne peut pas être l'adresse zéro.7 * - l'appelant doit avoir un solde d'au moins `amount`.8 */9 function transfer(address recipient, uint256 amount) public virtual override returns (bool) {Afficher toutLa fonction transfer est appelée pour transférer des 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.
1 _transfer(_msgSender(), recipient, amount);2 return true;3 }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 _<quelquechose>, 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 original 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à de la norme de base pour inclure certaines fonctionnalités qui améliorent la
sécurité : increaseAllowance et decreaseAllowance.
La fonction d'allocation
1 /**2 * @dev Voir {IERC20-allowance}.3 */4 function allowance(address owner, address spender) public view virtual override returns (uint256) {5 return _allowances[owner][spender];6 }La fonction allowance permet à tout le monde de vérifier n'importe quelle allocation.
La fonction d'approbation
1 /**2 * @dev Voir {IERC20-approve}.3 *4 * Exigences :5 *6 * - `spender` ne peut pas être l'adresse zéro.7 */8 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 travail réel. - La fonction renvoie soit
true(en cas de succès), soit annule (sinon).
1 _approve(_msgSender(), spender, amount);2 return true;3 }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.
1 /**2 * @dev Voir {IERC20-transferFrom}.3 *4 * Émet un événement {Approval} indiquant l'allocation mise à jour. Ceci n'est pas5 * requis par l'EIP. Voir la note au début de {ERC20}.6 *7 * Exigences :8 *9 * - `sender` et `recipient` ne peuvent pas être l'adresse zéro.10 * - `sender` doit avoir un solde d'au moins `amount`.11 * - l'appelant doit avoir une allocation pour les jetons de `sender` d'au moins12 * `amount`.13 */14 function transferFrom(address sender, address recipient, uint256 amount) public virtual15 override returns (bool) {16 _transfer(sender, recipient, amount);Afficher tout
L'appel de 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 d'annuler
le _transfer.
1 _approve(sender, _msgSender(), _allowances[sender][_msgSender()].sub(amount,2 "ERC20: transfer amount exceeds allowance"));3 return true;4 }Ajouts de sécurité d'OpenZeppelin
Il est dangereux de définir une allocation non nulle sur une autre valeur non nulle, car vous ne contrôlez que l'ordre de vos propres transactions, pas celles de quelqu'un d'autre. Imaginez que vous ayez deux utilisateurs, Alice qui est naïve et Bill qui est malhonnête. Alice veut un service de la part 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 définit l'allocation de Bill à dix. Dès que 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 de gaz beaucoup plus élevé pour qu'elle soit minée plus rapidement. De cette façon, Bill peut d'abord dépenser cinq jetons, puis, une fois que la nouvelle allocation d'Alice est minée, en dépenser dix de plus pour un prix total de quinze jetons, plus que ce qu'Alice voulait autoriser. Cette technique est appelée le front-running (opens in a new tab)
| Transaction d'Alice | Nonce d'Alice | Transaction de Bill | Nonce de Bill | Allocation de Bill | Revenu total de Bill provenant d'Alice |
|---|---|---|---|---|---|
| approve(Bill, 5) | 10 | 5 | 0 | ||
| transferFrom(Alice, Bill, 5) | 10,123 | 0 | 5 | ||
| approve(Bill, 10) | 11 | 10 | 5 | ||
| transferFrom(Alice, Bill, 10) | 10,124 | 0 | 15 |
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 a 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, les deux se terminant
avec Bill n'obtenant que dix jetons :
A :
| Transaction d'Alice | Nonce d'Alice | Transaction de Bill | Nonce de Bill | Allocation de Bill | Revenu total de Bill provenant d'Alice |
|---|---|---|---|---|---|
| approve(Bill, 5) | 10 | 5 | 0 | ||
| transferFrom(Alice, Bill, 5) | 10,123 | 0 | 5 | ||
| increaseAllowance(Bill, 5) | 11 | 0+5 = 5 | 5 | ||
| transferFrom(Alice, Bill, 5) | 10,124 | 0 | 10 |
B :
| Transaction d'Alice | Nonce d'Alice | Transaction de Bill | Nonce de Bill | Allocation de Bill | Revenu total de Bill provenant d'Alice |
|---|---|---|---|---|---|
| approve(Bill, 5) | 10 | 5 | 0 | ||
| increaseAllowance(Bill, 5) | 11 | 5+5 = 10 | 0 | ||
| transferFrom(Alice, Bill, 10) | 10,124 | 0 | 10 |
1 /**2 * @dev Augmente atomiquement l'allocation accordée à `spender` par l'appelant.3 *4 * Il s'agit d'une alternative à {approve} qui peut être utilisée comme atténuation des5 * problèmes décrits dans {IERC20-approve}.6 *7 * Émet un événement {Approval} indiquant l'allocation mise à jour.8 *9 * Exigences :10 *11 * - `spender` ne peut pas être l'adresse zéro.12 */13 function increaseAllowance(address spender, uint256 addedValue) public virtual returns (bool) {14 _approve(_msgSender(), spender, _allowances[_msgSender()][spender].add(addedValue));15 return true;16 }Afficher toutLa fonction a.add(b) est une addition sûre. Dans le cas peu probable où a+b>=2^256, elle ne boucle pas
comme le fait une addition normale.
12 /**3 * @dev Diminue atomiquement l'allocation accordée à `spender` par l'appelant.4 *5 * Il s'agit d'une alternative à {approve} qui peut être utilisée comme atténuation des6 * problèmes décrits dans {IERC20-approve}.7 *8 * Émet un événement {Approval} indiquant l'allocation mise à jour.9 *10 * Exigences :11 *12 * - `spender` ne peut pas être l'adresse zéro.13 * - `spender` doit avoir une allocation pour l'appelant d'au moins14 * `subtractedValue`.15 */16 function decreaseAllowance(address spender, uint256 subtractedValue) public virtual returns (bool) {17 _approve(_msgSender(), spender, _allowances[_msgSender()][spender].sub(subtractedValue,18 "ERC20: decreased allowance below zero"));19 return true;20 }Afficher toutFonctions qui modifient les informations sur les jetons
Ce sont les quatre fonctions qui font le travail réel : _transfer, _mint, _burn et _approve.
La fonction _transfer
1 /**2 * @dev Déplace un `montant` (`amount`) de jetons de `sender` à `recipient`.3 *4 * Cette fonction interne est équivalente à {transfer}, et peut être utilisée pour5 * par exemple, implémenter des frais de jeton automatiques, des mécanismes de délestage, etc.6 *7 * Émet un événement {Transfer}.8 *9 * Exigences :10 *11 * - `sender` ne peut pas être l'adresse zéro.12 * - `recipient` ne peut pas être l'adresse zéro.13 * - `sender` doit avoir un solde d'au moins `amount`.14 */15 function _transfer(address sender, address recipient, uint256 amount) internal virtual {Afficher toutCette fonction, _transfer, transfère des 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 des allocations
pour transférer depuis le compte de quelqu'un d'autre).
1 require(sender != address(0), "ERC20: transfer from the zero address");2 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.
1 _beforeTokenTransfer(sender, recipient, amount);2Il existe deux façons d'utiliser ce contrat :
- L'utiliser comme modèle pour votre propre code
- 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 que d'auditer 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 hook (opens in a new tab). Vous pouvez remplacer cette fonction, et elle sera appelée à chaque transfert.
1 _balances[sender] = _balances[sender].sub(amount, "ERC20: transfer amount exceeds balance");2 _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 entre les deux, il aurait pu être utilisé pour tromper ce contrat. De cette façon, le transfert est atomique, rien ne peut se produire au milieu de celui-ci.
1 emit Transfer(sender, recipient, amount);2 }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 blockchain
peut écouter les événements et y réagir. Par exemple, un portefeuille peut suivre le 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 des jetons
existants.
NOTE : Chaque jeton ERC-20 a sa propre logique métier qui dicte la gestion des jetons.
Par exemple, un contrat à offre fixe peut 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 certain moment
pour éviter une inflation galopante.
1 /** @dev Crée un `montant` (`amount`) de jetons et les assigne à un `compte` (`account`), augmentant2 * l'offre totale.3 *4 * Émet un événement {Transfer} avec `from` défini sur l'adresse zéro.5 *6 * Exigences :7 *8 * - `to` ne peut pas être l'adresse zéro.9 */10 function _mint(address account, uint256 amount) internal virtual {11 require(account != address(0), "ERC20: mint to the zero address");12 _beforeTokenTransfer(address(0), account, amount);13 _totalSupply = _totalSupply.add(amount);14 _balances[account] = _balances[account].add(amount);15 emit Transfer(address(0), account, amount);16 }Afficher toutAssurez-vous de mettre à jour _totalSupply lorsque le nombre total de jetons change.
1 /**2 * @dev Détruit un `montant` (`amount`) de jetons du `compte` (`account`), réduisant3 * l'offre totale.4 *5 * Émet un événement {Transfer} avec `to` défini sur l'adresse zéro.6 *7 * Exigences :8 *9 * - `account` ne peut pas être l'adresse zéro.10 * - `account` doit avoir au moins un `montant` (`amount`) de jetons.11 */12 function _burn(address account, uint256 amount) internal virtual {13 require(account != address(0), "ERC20: burn from the zero address");1415 _beforeTokenTransfer(account, address(0), amount);1617 _balances[account] = _balances[account].sub(amount, "ERC20: burn amount exceeds balance");18 _totalSupply = _totalSupply.sub(amount);19 emit Transfer(account, address(0), amount);20 }Afficher toutLa 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. Ce n'est pas un problème car le solde est vérifié au moment du transfert, alors qu'il pourrait être différent du solde au moment de la création de l'allocation .
1 /**2 * @dev Définit un `montant` (`amount`) comme allocation de `spender` sur les jetons de `owner`.3 *4 * Cette fonction interne est équivalente à `approve`, et peut être utilisée pour5 * par exemple, définir des allocations automatiques pour certains sous-systèmes, etc.6 *7 * Émet un événement {Approval}.8 *9 * Exigences :10 *11 * - `owner` ne peut pas être l'adresse zéro.12 * - `spender` ne peut pas être l'adresse zéro.13 */14 function _approve(address owner, address spender, uint256 amount) internal virtual {15 require(owner != address(0), "ERC20: approve from the zero address");16 require(spender != address(0), "ERC20: approve to the zero address");1718 _allowances[owner][spender] = amount;Afficher tout
É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.
1 emit Approval(owner, spender, amount);2 }3Modifier la variable des décimales
123 /**4 * @dev Définit {decimals} sur une valeur autre que celle par défaut de 18.5 *6 * AVERTISSEMENT : Cette fonction ne doit être appelée que depuis le constructeur. La plupart des7 * applications qui interagissent avec les contrats de jeton ne s'attendront pas à ce que8 * {decimals} change, et pourraient fonctionner de manière incorrecte si c'est le cas.9 */10 function _setupDecimals(uint8 decimals_) internal {11 _decimals = decimals_;12 }Afficher toutCette fonction modifie la variable _decimals qui est utilisée pour indiquer aux interfaces utilisateur comment interpréter le montant.
Vous devez 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.
Crochets
12 /**3 * @dev Hook appelé avant tout transfert de jetons. Cela inclut4 * la frappe et la brûlure.5 *6 * Conditions d'appel :7 *8 * - lorsque `from` et `to` sont tous deux non nuls, un `montant` de jetons de `from`9 * sera transféré à `to`.10 * - lorsque `from` est zéro, un `montant` de jetons sera frappé pour `to`.11 * - lorsque `to` est zéro, un `montant` de jetons de `from` sera brûlé.12 * - `from` et `to` ne sont jamais tous les deux nuls.13 *14 * Pour en savoir plus sur les hooks, rendez-vous sur xref:ROOT:extending-contracts.adoc#using-hooks[Utilisation des hooks].15 */16 function _beforeTokenTransfer(address from, address to, uint256 amount) internal virtual { }17}Afficher toutIl s'agit de la fonction hook à appeler pendant les 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 secret sur la blockchain. 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 quand les transactions d'autres personnes ont lieu. C'est la raison pour laquelle la modification d'une allocation peut être dangereuse, car elle permet au dépensier de dépenser la somme des deux allocations.
- Les valeurs de type
uint256se bouclent. 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é parapprove,transferFrom,increaseAllowanceetdecreaseAllowance - 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 circulation qu'il ne devrait y en avoir. Cela pourrait être potentiellement exploité 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 OpenZeppelin ERC-20 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).
Dernière mise à jour de la page : 22 octobre 2025
