Jetons

Bannière Amazon du livre Maîtriser Ethereum

Le mot "token" (jeton) dérive du vieil anglais "tācen", signifiant un signe ou un symbole. Il est couramment utilisé pour désigner des objets de type pièce de monnaie à usage spécial émis par des particuliers et ayant une valeur intrinsèque insignifiante, tels que des token ou jetons de transport, des jetons de blanchisserie et des jetons de jeux d’arcade.

De nos jours, les "tokens" ou "jetons" (nous utiliserons le terme français "jeton" pour le restant du chapitre) administrés sur les chaînes de blocs redéfinissent le mot pour désigner des abstractions basées sur la chaîne de blocs qui peuvent être possédées et qui représentent des actifs, des devises ou des droits d’accès.

L’association entre le mot "jeton" et la valeur insignifiante a beaucoup à voir avec l’utilisation limitée des versions physiques des jetons. Souvent limités à des entreprises, des organisations ou des emplacements spécifiques, les jetons physiques ne sont pas facilement échangeables et n’ont généralement qu’une seule fonction. Avec les jetons de chaîne de blocs, ces restrictions sont levées ou, plus précisément, complètement redéfinissables. De nombreux jetons de chaîne de blocs ont plusieurs objectifs à l’échelle mondiale et peuvent être échangés les uns contre les autres ou contre d’autres devises sur les marchés liquides mondiaux. Avec la disparition des restrictions d’utilisation et de propriété, l’attente d’une "valeur insignifiante" appartient également au passé.

Dans ce chapitre, nous examinons les différentes utilisations des jetons et comment ils sont créés. Nous discutons également des attributs des jetons tels que la fongibilité et l’intrinsèque. Enfin, nous examinons les normes et les technologies sur lesquelles ils sont basés et expérimentons en créant nos propres jetons.

Comment les jetons sont utilisés

L’utilisation la plus évidente des jetons est celle des monnaies privées numériques. Cependant, ce n’est qu’une utilisation possible. Les jetons peuvent être programmés pour remplir de nombreuses fonctions différentes, qui se chevauchent souvent. Par exemple, un jeton peut simultanément transmettre un droit de vote, un droit d’accès et la propriété d’une ressource. Comme le montre la liste suivante, la devise n’est que la première "application" :

Monnaie

Un jeton peut servir de forme de monnaie, avec une valeur déterminée par le commerce privé.

Ressource

Un jeton peut représenter une ressource gagnée ou produite dans une économie de partage ou un environnement de partage de ressources ; par exemple, un jeton de stockage ou de processeur représentant des ressources qui peuvent être partagées sur un réseau.

Actif

un jeton peut représenter la propriété d’un actif intrinsèque ou extrinsèque, tangible ou intangible ; par exemple, de l’or, de l’immobilier, une voiture, du pétrole, de l’énergie, des objets MMOG, etc.

Accès

un jeton peut représenter des droits d’accès et accorder l’accès à une propriété numérique ou physique, telle qu’un forum de discussion, un site Web exclusif, une chambre d’hôtel ou une voiture de location.

Équité

Un jeton peut représenter l’équité des actionnaires dans une organisation numérique (par exemple, un DAO) ou une entité juridique (par exemple, une société).

Vote

Un jeton peut représenter des droits de vote dans un système numérique ou juridique.

Objet de collection

Un jeton peut représenter un objet de collection numérique (par exemple, CryptoPunks) ou un objet de collection physique (par exemple, une peinture).

Identité

Un jeton peut représenter une identité numérique (par exemple, un avatar) ou une identité légale (par exemple, une carte d’identité nationale).

Attestation

Un jeton peut représenter une certification ou une attestation de fait par une autorité ou par un système de réputation décentralisé (par exemple, acte de mariage, certificat de naissance, diplôme universitaire).

Utilitaire

Un jeton peut être utilisé pour accéder ou payer un service.

Souvent, un seul jeton englobe plusieurs de ces fonctions. Parfois, il est difficile de les distinguer, car les équivalents physiques ont toujours été inextricablement liés. Par exemple, dans le monde physique, un permis de conduire (attestation) est aussi un document d’identité (identité) et les deux ne peuvent pas être séparés. Dans le domaine numérique, les fonctions précédemment mélangées peuvent être séparées et développées indépendamment (par exemple, une attestation anonyme).

Jetons et fongibilité

Wikipedia dit : " En économie, la fongibilité est la propriété d’un bien ou d’une marchandise dont les unités individuelles sont essentiellement interchangeables."

Les jetons sont fongibles lorsque nous pouvons remplacer n’importe quelle unité du jeton par une autre sans aucune différence dans sa valeur ou sa fonction.

À proprement parler, si la provenance historique d’un jeton peut être suivie, il n’est pas entièrement fongible. La possibilité de suivre la provenance peut conduire à la mise sur liste noire et à la liste blanche, réduisant ou éliminant la fongibilité.

Les jetons non fongibles sont des jetons qui représentent chacun un élément tangible ou intangible unique et ne sont donc pas interchangeables. Par exemple, un jeton qui représente la propriété d’un tableau spécifique de Van Gogh n’est pas équivalent à un autre jeton qui représente un Picasso, même s’ils peuvent faire partie du même système de "jeton de propriété d’art". De même, un jeton représentant un objet de collection numérique spécifique tel qu’un CryptoKitty spécifique n’est pas interchangeable avec un autre CryptoKitty. Chaque jeton non fongible est associé à un identifiant unique, tel qu’un numéro de série.

Nous verrons des exemples de jetons fongibles et non fongibles plus loin dans ce chapitre.

Note

Notez que "fongible" est souvent utilisé pour signifier "directement échangeable contre de l’argent" (par exemple, un jeton de casino peut être "encaissé", contrairement aux jetons de blanchisserie). Ce n’est pas le sens dans lequel nous utilisons le mot ici.

Risque de contrepartie

Le risque de contrepartie est le risque que l'autre partie à une transaction ne respecte pas ses obligations. Certains types de transactions présentent un risque de contrepartie supplémentaire car il y a plus de deux parties impliquées. Par exemple, si vous détenez un certificat de dépôt pour un métal précieux et que vous le vendez à quelqu’un, il y a au moins trois parties dans cette transaction : le vendeur, l’acheteur et le dépositaire du métal précieux. Quelqu’un détient l’actif physique ; par nécessité, ils deviennent partie à l’exécution de la transaction et ajoutent un risque de contrepartie à toute transaction impliquant cet actif. En général, lorsqu’un actif est négocié indirectement via l’échange d’un jeton de propriété, il existe un risque de contrepartie supplémentaire de la part du dépositaire de l’actif. En ont-ils l’atout ? Reconnaîtront-ils (ou autoriseront-ils) le transfert de propriété basé sur le transfert d’un jeton (tel qu’un certificat, un acte, un titre ou un jeton numérique) ? Dans le monde des jetons numériques représentant des actifs, comme dans le monde non numérique, il est important de comprendre qui détient l’actif représenté par le jeton et quelles règles s’appliquent à cet actif sous-jacent.

Tokens et Intrinsicité

Le mot "intrinsèque" dérive du latin "intra", qui signifie "de l’intérieur".

Certains jetons représentent des éléments numériques intrinsèques à la chaîne de blocs. Ces actifs numériques sont régis par des règles consensuelles, tout comme les jetons eux-mêmes. Cela a une implication importante : les jetons qui représentent des actifs intrinsèques ne comportent pas de risque de contrepartie supplémentaire. Si vous détenez les clés d’un CryptoKitty, aucune autre partie ne détient ce CryptoKitty pour vous - vous le possédez directement. Les règles de consensus de la chaîne de blocs s’appliquent et votre propriété (c’est-à-dire votre contrôle) des clés privées équivaut à la propriété de l’actif, sans aucun intermédiaire.

À l’inverse, de nombreux jetons sont utilisés pour représenter des choses extrinsèques, telles que l’immobilier, les actions avec droit de vote des entreprises, les marques et les lingots d’or. La propriété de ces éléments, qui ne sont pas "dans" la chaîne de blocs, est régie par la loi, la coutume et la politique, distinctes des règles consensuelles qui régissent le jeton. En d’autres termes, les émetteurs et propriétaires de jetons peuvent toujours dépendre de contrats réels non intelligents. En conséquence, ces actifs extrinsèques comportent un risque de contrepartie supplémentaire car ils sont détenus par des dépositaires, enregistrés dans des registres externes ou contrôlés par des lois et des politiques en dehors de l’environnement de la chaîne de blocs.

L’une des ramifications les plus importantes des jetons basés sur la chaîne de blocs est la capacité de convertir des actifs extrinsèques en actifs intrinsèques et ainsi de supprimer le risque de contrepartie. Un bon exemple est le passage de l’équité dans une société (extrinsèque) à une équité ou à un jeton de vote dans un DAO ou une organisation similaire (intrinsèque).

Utilisation de jetons : utilité ou équité

Presque tous les projets d’Ethereum sont lancés aujourd’hui avec une sorte de jeton. Mais est-ce que tous ces projets ont vraiment besoin de jetons ? Y a-t-il des inconvénients à utiliser un jeton, ou verrons-nous le slogan "tokéniser toutes les choses" se concrétiser ? En principe, l’utilisation de jetons peut être considérée comme l’ultime outil de gestion ou d’organisation. Dans la pratique, l’intégration des plateformes de chaîne de blocs, y compris Ethereum, dans les structures existantes de la société signifie que, jusqu’à présent, il existe de nombreuses limites à leur applicabilité.

Commençons par clarifier le rôle d’un jeton dans un nouveau projet. La majorité des projets utilisent des jetons de deux manières : soit en tant que « jetons utilitaires », soit en tant que « jetons d’équité ». Très souvent, ces deux rôles sont confondus.

Les jetons utilitaires sont ceux où l’utilisation du jeton est nécessaire pour accéder à un service, une application ou une ressource. Des exemples de jetons utilitaires incluent des jetons qui représentent des ressources telles que le stockage partagé ou l’accès à des services tels que les réseaux de médias sociaux.

Les jetons d’équité sont ceux qui représentent des parts dans le contrôle ou la propriété de quelque chose, comme une startup. Les jetons d’équité peuvent être aussi limités que les actions sans droit de vote pour la distribution des dividendes et des bénéfices, ou aussi expansifs que les actions avec droit de vote dans une organisation autonome décentralisée, où la gestion de la plate-forme se fait par un système de gouvernance complexe basé sur les votes des détenteurs de jetons.

C’est un canard !

De nombreuses startups sont confrontées à un problème difficile : les jetons sont un excellent mécanisme de collecte de fonds, mais offrir des titres (actions) au public est une activité réglementée dans la plupart des juridictions. En déguisant les jetons d’équité en jetons utilitaires, de nombreuses startups espèrent contourner ces restrictions réglementaires et lever des fonds à partir d’une offre publique tout en la présentant comme une prévente de "bons d’accès au service" ou, comme nous les appelons, de jetons utilitaires. Reste à savoir si ces offres d’actions à peine déguisées pourront contourner les régulateurs.

Comme le dit le dicton populaire : "Si ça marche comme un canard et cancane comme un canard, c’est un canard." Les régulateurs ne risquent pas d’être distraits par ces contorsions sémantiques ; bien au contraire, ils sont plus susceptibles de considérer ce sophisme juridique comme une tentative de tromper le public.

Jetons utilitaires : qui en a besoin ?

Le vrai problème est que les jetons utilitaires introduisent des risques importants et des obstacles à l’adoption pour les startups. Peut-être que dans un avenir lointain, "tokéniser toutes les choses" deviendra réalité, mais à l’heure actuelle, l’ensemble des personnes qui comprennent et souhaitent utiliser un jeton est un sous-ensemble du marché déjà petit de la crypto-monnaie.

Pour une startup, chaque innovation représente un risque et un filtre de marché. L’innovation, c’est emprunter le chemin le moins fréquenté, s’éloigner du chemin de la tradition. C’est déjà une promenade solitaire. Si une startup essaie d’innover dans un nouveau domaine technologique, tel que le partage de stockage sur des réseaux P2P, c’est une voie assez solitaire. L’ajout d’un jeton utilitaire à cette innovation et l’obligation pour les utilisateurs d’adopter des jetons afin d’utiliser le service aggravent le risque et augmentent les obstacles à l’adoption. C’est sortir de la piste déjà solitaire de l’innovation de stockage P2P et dans le désert.

Considérez chaque innovation comme un filtre. Cela limite l’adoption au sous-ensemble du marché qui peut devenir les premiers à adopter cette innovation. L’ajout d’un deuxième filtre aggrave cet effet, limitant davantage le marché adressable. Vous demandez à vos premiers utilisateurs d’adopter non pas une mais deux technologies entièrement nouvelles : la nouvelle application/plate-forme/service que vous avez créée et l’économie des jetons.

Pour une startup, chaque innovation introduit des risques qui augmentent les chances d’échec de la startup. Si vous prenez votre idée de démarrage déjà risquée et ajoutez un jeton utilitaire, vous ajoutez tous les risques de la plate-forme sous-jacente (Ethereum), de l’économie plus large (échanges, liquidité), de l’environnement réglementaire (régulateurs des actions/matières premières) et de la technologie (contrats intelligents , normes symboliques). C’est beaucoup de risques pour une startup.

Les partisans de la "tokénisation de toutes les choses" rétorqueront probablement qu’en adoptant des jetons, ils héritent également de l’enthousiasme du marché, des premiers utilisateurs, de la technologie, de l’innovation et de la liquidité de l’ensemble de l’économie des jetons. C’est vrai aussi. La question est de savoir si les avantages et l’enthousiasme l’emportent sur les risques et les incertitudes.

Néanmoins, certaines des idées commerciales les plus innovantes se déroulent effectivement dans le domaine de la cryptographie. Si les régulateurs ne sont pas assez rapides pour adopter des lois et soutenir de nouveaux modèles commerciaux, les entrepreneurs et les talents associés chercheront à opérer dans d’autres juridictions plus favorables à la cryptographie. Cela se produit déjà.

Enfin, au début de ce chapitre, lors de l’introduction des jetons, nous avons discuté de la signification familière du « jeton » comme « quelque chose de valeur insignifiante ». La raison sous-jacente de la valeur insignifiante de la plupart des jetons est qu’ils ne peuvent être utilisés que dans un contexte très étroit : une compagnie d’autobus, une buanderie automatique, une arcade, un hôtel ou un magasin d’entreprise. Une liquidité limitée, une applicabilité limitée et des coûts de conversion élevés réduisent la valeur des jetons jusqu’à ce qu’ils n’aient plus qu’une valeur « symbolique ». Ainsi, lorsque vous ajoutez un jeton utilitaire à votre plateforme, mais que le jeton ne peut être utilisé que sur votre seule plateforme avec un petit marché, vous recréez les conditions qui ont rendu les jetons physiques sans valeur. Cela peut en effet être la bonne façon d’intégrer la tokenisation dans votre projet. Cependant, si pour utiliser votre plate-forme, un utilisateur doit convertir quelque chose en votre jeton utilitaire, l’utiliser, puis reconvertir le reste en quelque chose de plus généralement utile, vous avez créé un certificat d’entreprise. Les coûts de commutation d’un jeton numérique sont des ordres de grandeur inférieurs à ceux d’un jeton physique sans marché, mais ils ne sont pas nuls. Les jetons utilitaires qui fonctionnent dans tout un secteur industriel seront très intéressants et probablement très précieux. Mais si vous configurez votre startup pour qu’elle doive amorcer une norme industrielle entière pour réussir, vous avez peut-être déjà échoué.

Note

L’un des avantages du déploiement de services sur des plates-formes à usage général comme Ethereum est de pouvoir connecter des contrats intelligents (et donc l’utilité des jetons) à travers les projets, augmentant ainsi le potentiel de liquidité et l’utilité des jetons.

Prenez cette décision pour les bonnes raisons. Adoptez un jeton car votre application ne peut pas fonctionner sans jeton. Adoptez-le car le jeton lève une barrière fondamentale du marché ou résout un problème d’accès. N’introduisez pas de jeton utilitaire car c’est le seul moyen de collecter des fonds rapidement et vous devez prétendre qu’il ne s’agit pas d’une offre publique de titres .

Jetons sur Ethereum

Les jetons de chaîne de blocs existaient avant Ethereum. D’une certaine manière, la première monnaie chaîne de blocs, Bitcoin, est un jeton lui-même. De nombreuses plateformes de jetons ont également été développées sur Bitcoin et d’autres crypto-monnaies avant Ethereum. Cependant, l’introduction de la première norme de jeton sur Ethereum a conduit à une explosion de jetons.

Vitalik Buterin a suggéré les jetons comme l’une des applications les plus évidentes et les plus utiles d’une chaîne de blocs programmable généralisée telle qu’Ethereum. En fait, au cours de la première année d’Ethereum, il était courant de voir Vitalik et d’autres porter des T-shirts arborant le logo Ethereum et un échantillon de contrat intelligent au dos. Il y avait plusieurs variantes de ce T-shirt, mais la plus courante montrait une implémentation d’un jeton.

Avant de nous plonger dans les détails de la création de jetons sur Ethereum, il est important d’avoir un aperçu du fonctionnement des jetons sur Ethereum. Les jetons sont différents de l’ether car le protocole Ethereum ne sait rien d’eux. L’envoi d’ether est une action intrinsèque de la plateforme Ethereum, mais l’envoi ou même la possession de jetons ne l’est pas. Le solde d’ether des comptes Ethereum est géré au niveau du protocole, tandis que le solde de jetons des comptes Ethereum est géré au niveau du contrat intelligent. Afin de créer un nouveau jeton sur Ethereum, vous devez créer un nouveau contrat intelligent. Une fois déployé, le contrat intelligent gère tout, y compris la propriété, les transferts et les droits d’accès. Vous pouvez rédiger votre contrat intelligent pour effectuer toutes les actions nécessaires comme vous le souhaitez, mais il est probablement plus sage de suivre une norme existante. Nous examinerons ensuite ces normes. Nous discutons des avantages et des inconvénients des normes suivantes à la fin du chapitre.

La norme de jeton ERC20

La première norme a été introduite en novembre 2015 par Fabian Vogelsteller en tant que demande de commentaires Ethereum (ERC). Il s’est automatiquement vu attribuer le numéro de problème GitHub 20, donnant lieu au nom de "jeton ERC20". La grande majorité des jetons sont actuellement basés sur la norme ERC20. La demande de commentaires ERC20 est finalement devenue la proposition d’amélioration Ethereum 20 (EIP-20), mais elle est encore principalement désignée par le nom d’origine, ERC20.

ERC20 est une norme pour les jetons fongibles, ce qui signifie que différentes unités d’un token ERC20 sont interchangeables et n’ont pas de propriétés uniques.

La norme ERC20 définit une interface commune pour les contrats mettant en œuvre un jeton, de sorte que tout jeton compatible est accessible et utilisable de la même manière. L’interface se compose d’un certain nombre de fonctions qui doivent être présentes dans chaque implémentation de la norme, ainsi que de certaines fonctions et attributs facultatifs qui peuvent être ajoutés par les développeurs.

Fonctions et événements requis par ERC20

Un contrat de jeton conforme à ERC20 doit fournir au moins les fonctions et événements suivants :

totalSupply

Renvoie le nombre total d’unités de ce jeton qui existent actuellement. Les jetons ERC20 peuvent avoir une offre fixe ou variable.

balanceOf

Étant donné une adresse, renvoie le solde du jeton de cette adresse.

transfer

Étant donné une adresse et un montant, transfère ce montant de jetons à cette adresse, à partir du solde de l’adresse qui a exécuté le transfert.

transferFrom

Étant donné un expéditeur, un destinataire et un montant, transfère des jetons d’un compte à un autre. Utilisé en combinaison avec approve.

approve

Étant donné une adresse et un montant de destinataire, autorise cette adresse à exécuter plusieurs virements jusqu’à ce montant, à partir du compte qui a émis l’approbation.

allowance

Étant donné une adresse de propriétaire et une adresse de dépensier, renvoie le montant restant que le dépensier est autorisé à retirer au propriétaire.

Transfer

Evénement déclenché lors d’un transfert réussi (appel à transfer ou transferFrom)(même pour les transferts de valeur nulle).

Approval

Événement enregistré lors d’un appel réussi à approve.

Fonctions optionnelles ERC20

En plus des fonctions requises listées dans la section précédente, les fonctions optionnelles suivantes sont également définies par la norme :

name

Renvoie le nom lisible par l’homme (par exemple, "US Dollars") du jeton.

symbol

Renvoie un symbole lisible par l’homme (par exemple, "USD") pour le jeton.

decimals

Renvoie le nombre de décimales utilisées pour diviser les quantités de jetons. Par exemple, si decimals vaut 2, alors le montant du jeton est divisé par 100 pour obtenir sa représentation d’usage.

L’interface ERC20 définie dans Solidity

Voici à quoi ressemble une spécification d’interface ERC20 dans Solidity :

contract ERC20 {
   function totalSupply() constant returns (uint theTotalSupply);
   function balanceOf(address _owner) constant returns (uint balance);
   function transfer(address _to, uint _value) returns (bool success);
   function transferFrom(address _from, address _to, uint _value) returns
      (bool success);
   function approve(address _spender, uint _value) returns (bool success);
   function allowance(address _owner, address _spender) constant returns
      (uint remaining);
   event Transfer(address indexed _from, address indexed _to, uint _value);
   event Approval(address indexed _owner, address indexed _spender, uint _value);
}
Structures de données ERC20

Si vous examinez une implémentation ERC20, vous verrez qu’elle contient deux structures de données, une pour suivre les soldes et une pour suivre indemnités. Dans Solidity, ils sont implémentés avec un data mapping ou mappage de données.

Le premier mappage de données implémente une table interne des soldes de jetons, par propriétaire. Cela permet au contrat de jeton de garder une trace de qui possède les jetons. Chaque transfert est une déduction d’un solde et un ajout à un autre solde :

mapping(address => uint256) balances;

La deuxième structure de données est un mappage de données d’allocations. Comme nous le verrons dans la section suivante, avec les jetons ERC20, le propriétaire d’un jeton peut déléguer l’autorité à un dépensier, lui permettant de dépenser un montant spécifique (allocation) à partir du solde du propriétaire. Le contrat ERC20 assure le suivi des allocations avec un mappage bidimensionnel, la clé primaire étant l’adresse du propriétaire du jeton, mappée à une adresse de dépense et un montant d’allocation :

mapping (address => mapping (address => uint256)) public allowed;
Flux de travail ERC20 : "transfer" et "approve et transferFrom"

La norme de jeton ERC20 a deux fonctions de transfert. Vous vous demandez peut-être pourquoi.

ERC20 permet deux flux de travail différents. Le premier est un flux de travail simple et à transaction unique utilisant la fonction transfer. Ce flux de travail est celui utilisé par les portefeuilles pour envoyer des jetons à d’autres portefeuilles. La grande majorité des transactions de jetons se produisent avec le flux de travail transfer.

L’exécution du contrat de transfert est très simple. Si Alice veut envoyer 10 jetons à Bob, son portefeuille envoie une transaction à l’adresse du contrat de jeton, en appelant la fonction transfer avec l’adresse de Bob et 10 comme arguments. Le contrat de jeton ajuste le solde d’Alice (–10) et le solde de Bob (+10) et émet un événement Transfer.

Le deuxième flux de travail est un processus à deux transactions qui utilise approve suivi de transferFrom. Ce workflow permet à un propriétaire de jeton de déléguer son contrôle à une autre adresse. Il est le plus souvent utilisé pour déléguer le contrôle à un contrat de distribution de jetons, mais il peut également être utilisé par les échanges.

Par exemple, si une entreprise vend des tokens pour une ICO (Initial Coin Offerings ou "offres initiales de pièces"), elle peut approve (approuver) une adresse de contrat de crowdsale pour distribuer une certaine quantité de jetons. Le contrat de crowdsale peut alors transferFrom le solde du propriétaire du contrat de jeton à chaque acheteur du jeton, comme illustré dans Le flux de travail d’approbation et de transfert en deux étapes des jetons ERC20.

Note

("Initial Coin Offerings (ICOs)","défini"Une Initial Coin Offering (ICO) ou Offres initiales de pièces est un mécanisme de financement participatif utilisé par les entreprises et les organisations pour collecter des fonds en vendant des jetons. Le terme est dérivé de l’offre publique initiale (IPO), qui est le processus par lequel une société publique propose des actions à vendre aux investisseurs en bourse. Contrairement aux marchés des introductions en bourse hautement réglementés, les ICO sont ouverts, mondiaux et désordonnés. Les exemples et les explications des ICO dans ce livre ne constituent pas une approbation de ce type de collecte de fonds.

Le flux de travail d’approbation et de transfert en deux étapes des jetons ERC20
Figure 1. Le flux de travail d’approbation et de transfert en deux étapes des jetons ERC20

Pour le flux de travail approve et transferFrom, deux transactions sont nécessaires. Disons qu’Alice veut autoriser le contrat AliceICO à vendre 50 % de tous les jetons AliceCoin à des acheteurs comme Bob et Charlie. Tout d’abord, Alice lance le contrat AliceCoin ERC20, émettant tous les AliceCoin à sa propre adresse. Ensuite, Alice lance le contrat AliceICO qui peut vendre des jetons contre de l’ether. Ensuite, Alice lance le flux de travail approve et transferFrom. Elle envoie une transaction au contrat AliceCoin, en appelant approve avec l’adresse du contrat AliceICO et 50 % du totalSupply comme arguments. Cela déclenchera l’événement Approbation. Désormais, le contrat AliceICO peut vendre AliceCoin.

Lorsque le contrat AliceICO reçoit de l’ether de Bob, il doit envoyer des AliceCoin à Bob en retour. Dans le contrat AliceICO se trouve un taux de change entre AliceCoin et de l’Ether. Le taux de change qu’Alice a fixé lors de la création du contrat AliceICO détermine le nombre de jetons que Bob recevra pour la quantité d’ether envoyé au contrat AliceICO. Lorsque le contrat AliceICO appelle la fonction AliceCoin transferFrom, il définit l’adresse d’Alice comme expéditeur et l’adresse de Bob comme destinataire, et utilise le taux de change pour déterminer le nombre de jetons AliceCoin qui seront transférés à Bob dans le champ value. Le contrat AliceCoin transfère le solde de l’adresse d’Alice à l’adresse de Bob et déclenche un événement Transfer. Le contrat AliceICO peut appeler transferFrom un nombre illimité de fois, tant qu’il ne dépasse pas la limite d’approbation définie par Alice. Le contrat AliceICO peut suivre le nombre de jetons AliceCoin qu’il peut vendre en appelant la fonction allowance.

Implémentations ERC20

Bien qu’il soit possible d’implémenter un jeton compatible ERC20 dans environ 30 lignes de code Solidity, la plupart des implémentations sont plus complexes. Ceci pour tenir compte des vulnérabilités de sécurité potentielles. Deux implémentations sont mentionnées dans la norme EIP-20 :

Consensys EIP20

Une implémentation simple et facile à lire d’un jeton compatible ERC20.

OpenZeppelin StandardToken

Cette implémentation est compatible ERC20, avec des précautions de sécurité supplémentaires. Il constitue la base des bibliothèques OpenZeppelin implémentant des jetons compatibles ERC20 plus complexes avec des plafonds de collecte de fonds, des enchères, des calendriers d’acquisition et d’autres fonctionnalités.

Lancement de notre propre jeton ERC20

Créons et lançons notre propre jeton. Pour cet exemple, nous utiliserons le cadre de développement (framework) Truffle. L’exemple suppose que vous avez déjà installé truffle et que vous l’avez configuré, et que vous êtes familiarisé avec son fonctionnement de base (pour plus de détails, voir [truffle]).

Nous appellerons notre jeton "Mastering Ethereum Token", avec le symbole "MET".

Note

Vous pouvez trouver cet exemple dans le référentiel GitHub du livre.

Commençons par créer et initialiser un répertoire de projet Truffle. Exécutez ces quatre commandes et acceptez les réponses par défaut à toutes les questions :

$ mkdir METoken
$ cd METoken
METoken $ truffle init
METoken $ npm init

Vous devriez maintenant avoir la structure de répertoires suivante :

METoken/
+---- contracts
|   `---- Migrations.sol
+---- migrations
|   `---- 1_initial_migration.js
+---- package.json
+---- test
+---- truffle-config.js
`---- truffle.js

Modifiez le fichier de configuration truffle.js ou truffle-config.js pour configurer votre environnement Truffle, ou copiez ce dernier depuis le référentiel.

Si vous utilisez l’exemple truffle-config.js, pensez à créer un fichier .env dans le dossier METoken contenant vos clés privées de test pour le test et le déploiement sur les réseaux publics de test Ethereum, tels que Ropsten ou Kovan. Vous pouvez exporter votre clé privée de réseau de test à partir de MetaMask.

Après cela, votre répertoire devrait ressembler à :

METoken/
+---- contracts
|   `---- Migrations.sol
+---- migrations
|   `---- 1_initial_migration.js
+---- package.json
+---- test
+---- truffle-config.js
+---- truffle.js
`---- .env *new file*
Warning

N’utilisez que des clés de test ou des mnémoniques de test qui ne sont pas utilisés pour détenir des fonds sur le réseau Ethereum principal. Ne jamais utiliser des clés contenant de l’argent réel pour les tests.

Pour notre exemple, nous allons importer la bibliothèque OpenZeppelin, qui implémente des contrôles de sécurité importants et est facile à étendre :

$ npm install [email protected]

+ [email protected]
added 1 package from 1 contributor and audited 2381 packages in 4.074s

Le module de dévelopement openzeppelin-solidity ajoutera environ 250 fichiers sous le répertoire node_modules. La bibliothèque OpenZeppelin comprend bien plus que le jeton ERC20, mais nous n’en utiliserons qu’une petite partie.

Ensuite, écrivons notre contrat de jeton. Créez un nouveau fichier, METoken.sol, et copiez l’exemple de code depuis GitHub.

Notre contrat, illustré en METoken.sol : Un contrat Solidity implémentant un token ERC20, est très simple, car il hérite toutes ses fonctionnalités de la bibliothèque OpenZeppelin.

Example 1. METoken.sol : Un contrat Solidity implémentant un token ERC20
pragma solidity ^0.4.21;

import 'openzeppelin-solidity/contracts/token/ERC20/StandardToken.sol';

contract METoken is StandardToken {
    string public constant name = 'Mastering Ethereum Token';
    string public constant symbol = 'MET';
    uint8 public constant decimals = 2;
    uint constant _initial_supply = 2100000000;

    function METoken() public {
        totalSupply_ = _initial_supply;
        balances[msg.sender] = _initial_supply;
        emit Transfer(address(0), msg.sender, _initial_supply);
    }
}

Ici, nous définissons les variables optionnelles name, symbol et decimals. Nous définissons également une variable _initial_supply, définie sur 21 millions de jetons ; avec deux décimales de subdivision qui donne 2,1 milliards d’unités au total. Dans la fonction d’initialisation (constructeur) du contrat, nous définissons le totalSupply égal à _initial_supply et allouons tout le _initial_supply au solde du compte (msg.sender)qui crée le contrat METoken.

Nous utilisons maintenant truffle pour compiler le code METoken :

$ truffle compile
Compiling ./contracts/METoken.sol...
Compiling ./contracts/Migrations.sol...
Compiling openzeppelin-solidity/contracts/math/SafeMath.sol...
Compiling openzeppelin-solidity/contracts/token/ERC20/BasicToken.sol...
Compiling openzeppelin-solidity/contracts/token/ERC20/ERC20.sol...
Compiling openzeppelin-solidity/contracts/token/ERC20/ERC20Basic.sol...
Compiling openzeppelin-solidity/contracts/token/ERC20/StandardToken.sol...

Comme vous pouvez le voir, truffle intègre les dépendances nécessaires des bibliothèques OpenZeppelin et compile également ces contrats.

Configurons un script de migration pour déployer le contrat METoken. Créez un nouveau fichier appelé 2_deploy_contracts.js, dans le dossier METoken/migrations. Copiez le contenu de l’exemple dans le référentiel GitHub :

2_deploy_contracts : Migration pour déployer METoken
var METoken = artifacts.require("METoken");

module.exports = function(deployer) {
  // Déployer le contrat METoken comme seule tâche
  deployer.deploy(METoken);
};

Avant de déployer sur l’un des réseaux de test Ethereum, démarrons une chaîne de blocs locale pour tout tester. Démarrez la chaîne de blocs ganache, soit depuis la ligne de commande avec ganache-cli, soit depuis l’interface utilisateur graphique.

Une fois ganache lancé, nous pouvons déployer notre contrat METoken et voir si tout fonctionne comme prévu :

$ truffle migrate --network ganache
Using network 'ganache'.

Running migration: 1_initial_migration.js
  Deploying Migrations...
  ... 0xb2e90a056dc6ad8e654683921fc613c796a03b89df6760ec1db1084ea4a084eb
  Migrations: 0x8cdaf0cd259887258bc13a92c0a6da92698644c0
Saving successful migration to network...
  ... 0xd7bc86d31bee32fa3988f1c1eabce403a1b5d570340a3a9cdba53a472ee8c956
Saving artifacts...
Running migration: 2_deploy_contracts.js
  Deploying METoken...
  ... 0xbe9290d59678b412e60ed6aefedb17364f4ad2977cfb2076b9b8ad415c5dc9f0
  METoken: 0x345ca3e014aaf5dca488057592ee47305d9b3e10
Saving successful migration to network...
  ... 0xf36163615f41ef7ed8f4a8f192149a0bf633fe1a2398ce001bf44c43dc7bdda0
Saving artifacts...

Sur la console ganache, nous devrions voir que notre déploiement a créé quatre nouvelles transactions, comme illustré dans Déploiement METoken sur ganache.

Déploiement de METoken sur Ganache
Figure 2. Déploiement METoken sur ganache
Interagir avec METoken à l’aide de la console Truffle

Nous pouvons interagir avec notre contrat sur la chaîne de blocs ganache en utilisant la console Truffle. Il s’agit d’un environnement JavaScript interactif qui donne accès à l’environnement Truffle et, via web3, à la chaîne de blocs. Dans ce cas, nous allons connecter la console Truffle à la chaîne de blocs ganache :

$ truffle console --network ganache
truffle(ganache)>

L’invite truffle(ganache)> indique que nous sommes connectés à la chaîne de blocs ganache et que nous sommes prêts à taper nos commandes. La console Truffle prend en charge toutes les commandes truffle, nous pourrions donc compiler et migrer depuis la console. Nous avons déjà exécuté ces commandes, allons donc directement au contrat lui-même. Le contrat METoken existe en tant qu’objet JavaScript dans l’environnement Truffle. Tapez METoken** à l’invite et il affichera l’intégralité de la définition du contrat :

truffle(ganache)> METoken
{ [Function: TruffleContract]
  _static_methods:

[...]

currentProvider:
 HttpProvider {
   host: 'http://localhost:7545',
   timeout: 0,
   user: undefined,
   password: undefined,
   headers: undefined,
   send: [Function],
   sendAsync: [Function],
   _alreadyWrapped: true },
network_id: '5777' }

L’objet METoken expose également plusieurs attributs, tels que l’adresse du contrat (telle que déployée par la commande migrate) :

truffle(ganache)> METoken.address
'0x345ca3e014aaf5dca488057592ee47305d9b3e10'

Si nous voulons interagir avec le contrat déployé, nous devons utiliser un appel asynchrone, sous la forme d’une "promesse" JavaScript. Nous utilisons la fonction deployed pour obtenir l’instance de contrat, puis appelons la fonction totalSupply :

truffle(ganache)> METoken.deployed().then(instance => instance.totalSupply())
BigNumber { s: 1, e: 9, c: [ 2100000000 ] }

Ensuite, utilisons les comptes créés par ganache pour vérifier notre solde de METoken et envoyer des METoken à une autre adresse. Commençons par obtenir les adresses de compte :

truffle(ganache)> let accounts
undefined
truffle(ganache)> web3.eth.getAccounts((err,res) => { accounts = res })
undefined
truffle(ganache)> accounts[0]
'0x627306090abab3a6e1400e9345bc60c78a8bef57'

La liste accounts contient maintenant tous les comptes créés par ganache, et account[0] est le compte qui a déployé le contrat METoken. Il devrait avoir un solde de METoken, car notre constructeur METoken donne l’intégralité de l’offre de jetons à l’adresse qui l’a créé. Allons vérifier:

truffle(ganache)> METoken.deployed().then(instance =>
                  { instance.balanceOf(accounts[0]).then(console.log) })
undefined
truffle(ganache)> BigNumber { s: 1, e: 9, c: [ 2100000000 ] }

Enfin, transférons 1000.00 METoken de account[0] vers account[1], en appelant la fonction transfer du contrat :

truffle(ganache)> METoken.deployed().then(instance =>
                  { instance.transfer(accounts[1], 100000) })
undefined
truffle(ganache)> METoken.deployed().then(instance =>
                  { instance.balanceOf(accounts[0]).then(console.log) })
undefined
truffle(ganache)> BigNumber { s: 1, e: 9, c: [ 2099900000 ] }
undefined
truffle(ganache)> METoken.deployed().then(instance =>
                  { instance.balanceOf(accounts[1]).then(console.log) })
undefined
truffle(ganache)> BigNumber { s: 1, e: 5, c: [ 100000 ] }

Tip

METoken a 2 décimales de précision, ce qui signifie que 1 METoken correspond à 100 unités dans le contrat. Lorsque nous transférons 1 000 METoken, nous spécifions la valeur comme 100000 dans l’appel à la fonction transfer.

Comme vous pouvez le voir, dans la console, account[0] a maintenant 20 999 000 MET, et account[1] a 1 000 MET.

Si vous passez à l’interface utilisateur graphique ganache, comme indiqué dans Transfert METoken sur ganache, vous verrez la transaction qui a appelé la fonction transfer.

Transfert METoken sur Ganache
Figure 3. Transfert METoken sur ganache
Envoi de jetons ERC20 aux adresses contractuelles

Jusqu’à présent, nous avons configuré un jeton ERC20 et transféré certains jetons d’un compte à un autre. Tous les comptes que nous avons utilisés pour ces démonstrations sont des comptes externes, ce qui signifie qu’ils sont contrôlés par une clé privée et non par un contrat. Que se passe-t-il si nous envoyons MET à une adresse contractuelle ? Découvrons-le!

Tout d’abord, déployons un autre contrat dans notre environnement de test. Pour cet exemple, nous utiliserons notre premier contrat, Faucet.sol. Ajoutons-le au projet METoken en le copiant dans le répertoire contracts. Notre répertoire devrait ressembler à ceci :

METoken/
+---- contracts
|   +---- Faucet.sol
|   +---- METoken.sol
|   `---- Migrations.sol

Nous ajouterons également une migration, pour déployer Faucet séparément de METoken :

var Faucet = artifacts.require("Faucet");

module.exports = function(deployer) {
  // Déployer le contrat Faucet comme seule tâche
  deployer.deploy(Faucet);
};

Compilons et migrons les contrats depuis la console Truffle :

$ truffle console --network ganache
truffle(ganache)> compile
Compiling ./contracts/Faucet.sol...
Writing artifacts to ./build/contracts

truffle(ganache)> migrate
Using network 'ganache'.

Running migration: 1_initial_migration.js
  Deploying Migrations...
  ... 0x89f6a7bd2a596829c60a483ec99665c7af71e68c77a417fab503c394fcd7a0c9
  Migrations: 0xa1ccce36fb823810e729dce293b75f40fb6ea9c9
Saving artifacts...
Running migration: 2_deploy_contracts.js
  Replacing METoken...
  ... 0x28d0da26f48765f67e133e99dd275fac6a25fdfec6594060fd1a0e09a99b44ba
  METoken: 0x7d6bf9d5914d37bcba9d46df7107e71c59f3791f
Saving artifacts...
Running migration: 3_deploy_faucet.js
  Deploying Faucet...
  ... 0x6fbf283bcc97d7c52d92fd91f6ac02d565f5fded483a6a0f824f66edc6fa90c3
  Faucet: 0xb18a42e9468f7f1342fa3c329ec339f254bc7524
Saving artifacts...

Génial. Envoyons maintenant du MET au contrat Faucet :

truffle(ganache)> METoken.deployed().then(instance =>
                  { instance.transfer(Faucet.address, 100000) })
truffle(ganache)> METoken.deployed().then(instance =>
                  { instance.balanceOf(Faucet.address).then(console.log)})
truffle(ganache)> BigNumber { s: 1, e: 5, c: [ 100000 ] }

D’accord, nous avons transféré 1 000 MET au contrat Faucet. Maintenant, comment retirer ces jetons ?

N’oubliez pas que Faucet.sol est un contrat assez simple. Il n’a qu’une seule fonction, withdraw, qui sert à retirer de l'ether. Il n’a pas de fonction pour retirer le MET, ou tout autre jeton ERC20. Si nous utilisons withdraw, il essaiera d’envoyer de l’ether, mais comme Faucet n’a pas encore de solde d’ether, il échouera.

Le contrat METoken sait que Faucet a un solde, mais le seul moyen de transférer ce solde est de recevoir un appel transfer de l’adresse du contrat. D’une manière ou d’une autre, nous devons faire en sorte que le contrat Faucet appelle la fonction transfer dans METoken.

Si vous vous demandez quoi faire ensuite, ne le faites pas. Il n’y a pas de solution à ce problème. Le MET envoyé à Faucet est bloqué, pour toujours. Seul le contrat Faucet peut le transférer, et le contrat Faucet n’a pas de code pour appeler la fonction transfer d’un contrat de jeton ERC20.

Peut-être avez-vous anticipé ce problème. Très probablement, vous ne l’avez pas fait. En fait, des centaines d’utilisateurs d’Ethereum qui ont accidentellement transféré divers jetons vers des contrats qui n’avaient aucune capacité ERC20 n’en ont pas non plus fait. Selon certaines estimations, des jetons d’une valeur de plus d’environ 2,5 millions de dollars (au moment de la rédaction) sont restés "bloqués" comme ça et sont perdus à jamais.

L’une des façons dont les utilisateurs de jetons ERC20 peuvent perdre par inadvertance leurs jetons lors d’un transfert, c’est lorsqu’ils tentent de transférer vers un échange ou un autre service. Ils copient une adresse Ethereum à partir du site Web d’un échange, pensant qu’ils peuvent simplement lui envoyer des jetons. Cependant, de nombreux échanges publient des adresses de réception qui sont en fait des contrats ! Ces contrats sont uniquement destinés à recevoir de l’ether, pas des jetons ERC20, balayant le plus souvent tous les fonds qui leur sont envoyés vers un « stockage à froid » ou un autre portefeuille centralisé. Malgré les nombreux avertissements indiquant "n’envoyez pas de jetons à cette adresse", de nombreux jetons sont perdus de cette façon.

Démonstration du flux de travail "approuver et transférer de"

Notre contrat Faucet ne pouvait pas gérer les jetons ERC20. L’envoi de jetons à l’aide de la fonction + transfer + a entraîné la perte de ces jetons. Réécrivons maintenant le contrat et faisons en sorte qu’il gère les jetons ERC20. Plus précisément, nous allons le transformer en un robinet qui donne du MET à quiconque le demande.

Pour cet exemple, nous allons faire une copie du répertoire du projet truffle (nous l’appellerons METoken_METFaucet), initialiser truffle et npm, installer les dépendances OpenZeppelin et copier le contrat METoken.sol. Voir notre premier exemple, dans Lancement de notre propre jeton ERC20, pour les instructions détaillées.

Notre nouveau contrat de robinetterie, METFaucet.sol, ressemblera à METFaucet.sol : Un robinet pour METoken.

Example 2. METFaucet.sol : Un robinet pour METoken
// Version du compilateur Solidity pour lequel ce programme a été écrit
pragma solidity ^0.4.19;

import 'openzeppelin-solidity/contracts/token/ERC20/StandardToken.sol';


// Un robinet pour le jeton ERC20 MET
contract METFaucet {

	StandardToken public METoken;
	address public METOwner;

	// Constructeur METFaucet, indique l'adresse du contrat METoken et
	// l'adresse du propriétaire que nous serons autorisés pour transferFrom
	function METFaucet(address _METoken, address _METOwner) public {

		// Initialise le METoken à partir de l'adresse fournie
		METoken = StandardToken(_METoken);
		METOwner = _METOwner;
	}

	function withdraw(uint withdraw_amount) public {

		// Limiter le montant du retrait à 10 MET
    	require(withdraw_amount <= 1000);

		// Utiliser la fonction transferFrom de METoken
		METoken.transferFrom(METOwner, msg.sender, withdraw_amount);
    }

	// REJETER tout éther entrant
	function () external payable { revert(); }

}

Nous avons apporté quelques modifications à l’exemple de base de Faucet. Puisque METFaucet utilisera la fonction transferFrom dans METoken, il aura besoin de deux variables supplémentaires. L’un contiendra l’adresse du contrat METoken déployé. L’autre détiendra l’adresse du propriétaire du MET, qui approuvera les retraits du robinet. Le contrat METFaucet appellera METoken.transferFrom et lui demandera de déplacer le MET du propriétaire vers l’adresse d’où provient la demande de retrait du robinet.

Nous déclarons ici ces deux variables :

StandardToken public METoken;
address public METOwner;

Étant donné que notre robinet doit être initialisé avec les adresses correctes pour METoken et METOwner, nous devons déclarer un constructeur personnalisé :

// Constructeur METFaucet - fournir l'adresse du contrat METoken et
// l'adresse du propriétaire que nous serons autorisés à transférer avec transferFrom
function METFaucet(address _METoken, address _METOwner) public {

	// Initialise le METoken à partir de l'adresse fournie
	METoken = StandardToken(_METoken);
	METOwner = _METOwner;
}

Le prochain changement concerne la fonction withdraw. Au lieu d’appeler transfer, METFaucet utilise la fonction transferFrom dans METoken et demande à METoken de transférer MET au destinataire du robinet :

// Utiliser la fonction transferFrom de METoken
METoken.transferFrom(METOwner, msg.sender, withdraw_amount);

Enfin, puisque notre robinet n’envoie plus d’ether, nous devrions probablement empêcher quiconque d’envoyer de l’ether à METFaucet, car nous ne voudrions pas qu’il reste bloqué. Nous modifions la fonction de paiement de secours pour rejeter l’ether entrant, en utilisant la fonction revert pour annuler tout paiement entrant :

// REJETER tout ether entrant
function () external payable { revert(); }

Maintenant que notre code METFaucet.sol est prêt, nous devons modifier le script de migration pour le déployer. Ce script de migration sera un peu plus complexe, car METFaucet dépend de l’adresse de METoken. Nous utiliserons une promesse JavaScript pour déployer les deux contrats en séquence. Créez 2_deploy_contracts.js comme suit :

var METoken = artifacts.require("METoken");
var METFaucet = artifacts.require("METFaucet");
var owner = web3.eth.accounts[0];

module.exports = function(deployer) {

	// Déployer d'abord le contrat METoken
	deployer.deploy(METoken, {from: owner}).then(function() {
		// Ensuite, déployer METFaucet et passez l'adresse de METoken et
		// l'adresse du propriétaire de tous les MET qui agréera METFaucet
		return deployer.deploy(METFaucet, METoken.address, owner);
  	});
}

Maintenant, nous pouvons tout tester dans la console Truffle. Tout d’abord, nous utilisons migrate pour déployer les contrats. Lorsque METoken est déployé, il alloue tout le MET au compte qui l’a créé, web3.eth.accounts[0]. Ensuite, nous appelons la fonction approve dans METoken pour approuver METFaucet afin d’envoyer jusqu’à 1 000 MET au nom de web3.eth.accounts[0]. Enfin, pour tester notre faucet, nous appelons METFaucet.withdraw depuis web3.eth.accounts[1] et essayons de retirer 10 MET. Voici les commandes de la console :

$ truffle console --network ganache
truffle(ganache)> migrate
Using network 'ganache'.

Running migration: 1_initial_migration.js
  Deploying Migrations...
  ... 0x79352b43e18cc46b023a779e9a0d16b30f127bfa40266c02f9871d63c26542c7
  Migrations: 0xaa588d3737b611bafd7bd713445b314bd453a5c8
Saving artifacts...
Running migration: 2_deploy_contracts.js
  Replacing METoken...
  ... 0xc42a57f22cddf95f6f8c19d794c8af3b2491f568b38b96fef15b13b6e8bfff21
  METoken: 0xf204a4ef082f5c04bb89f7d5e6568b796096735a
  Replacing METFaucet...
  ... 0xd9615cae2fa4f1e8a377de87f86162832cf4d31098779e6e00df1ae7f1b7f864
  METFaucet: 0x75c35c980c0d37ef46df04d31a140b65503c0eed
Saving artifacts...
truffle(ganache)> METoken.deployed().then(instance =>
                  { instance.approve(METFaucet.address, 100000) })
truffle(ganache)> METoken.deployed().then(instance =>
                  { instance.balanceOf(web3.eth.accounts[1]).then(console.log) })
truffle(ganache)> BigNumber { s: 1, e: 0, c: [ 0 ] }
truffle(ganache)> METFaucet.deployed().then(instance =>
                  { instance.withdraw(1000, {from:web3.eth.accounts[1]}) } )
truffle(ganache)> METoken.deployed().then(instance =>
                  { instance.balanceOf(web3.eth.accounts[1]).then(console.log) })
truffle(ganache)> BigNumber { s: 1, e: 3, c: [ 1000 ] }

Comme vous pouvez le voir sur les résultats, nous pouvons utiliser le flux de travail approve et transferFrom pour autoriser un contrat à transférer des jetons définis dans un autre jeton. S’ils sont correctement utilisés, les jetons ERC20 peuvent être utilisés par les EOA et d’autres contrats.

Cependant, la charge de gérer correctement les jetons ERC20 est transmise à l’interface utilisateur. Si un utilisateur tente à tort de transférer des jetons ERC20 vers une adresse de contrat et que ce contrat n’est pas équipé pour recevoir des jetons ERC20, les jetons seront perdus.

Problèmes avec les jetons ERC20

L’adoption de la norme de jeton ERC20 a été vraiment explosive. Des milliers de jetons ont été lancés, à la fois pour expérimenter de nouvelles capacités et pour lever des fonds dans diverses enchères et "financements participatifs" ICO. Cependant, il existe des pièges potentiels, comme nous l’avons vu avec la question du transfert de jetons vers des adresses contractuelles.

L’un des problèmes les moins évidents avec les jetons ERC20 est qu’ils exposent des différences subtiles entre les jetons et l’ether lui-même. Lorsque l’ether est transféré par une transaction qui a une adresse de destinataire comme destination, les transferts de jeton se produisent dans l'état spécifique du contrat de jeton et ont le contrat de jeton comme destination, et non l’adresse du destinataire. Le contrat de jeton suit les soldes et émet des événements. Dans un transfert de jeton, aucune transaction n’est réellement envoyée au destinataire du jeton. Au lieu de cela, l’adresse du destinataire est ajoutée à un tableau dans le contrat de jeton lui-même. Une transaction envoyant de l’ether à une adresse modifie l’état d’une adresse. Une transaction transférant un jeton à une adresse ne modifie que l’état du contrat de jeton, pas l’état de l’adresse du destinataire. Même un portefeuille prenant en charge les jetons ERC20 ne prend pas connaissance d’un solde de jetons à moins que l’utilisateur n’ajoute explicitement un contrat de jeton spécifique à « surveiller ». Certains portefeuilles surveillent les contrats de jetons les plus populaires pour détecter les soldes détenus par les adresses qu’ils contrôlent, mais cela est limité à une petite fraction des contrats ERC20 existants.

En fait, il est peu probable qu’un utilisateur veuille suivre tous les soldes de tous les contrats de jetons ERC20 possibles. De nombreux jetons ERC20 ressemblent plus à du courrier indésirable qu’à des jetons utilisables. Ils créent automatiquement des soldes pour les comptes qui ont une activité ether, afin d’attirer les utilisateurs. Si vous avez une adresse Ethereum avec une longue histoire d’activité, surtout si elle a été créée lors de la prévente, vous la trouverez pleine de jetons "indésirables" qui sont apparus de nulle part. Bien sûr, l’adresse n’est pas vraiment pleine de jetons ; ce sont les contrats symboliques qui contiennent votre adresse. Vous ne voyez ces soldes que si ces contrats de jetons sont surveillés par l’explorateur de blocs ou le portefeuille que vous utilisez pour afficher votre adresse.

Les jetons ne se comportent pas de la même manière que l’ether. L’ether est envoyé avec la fonction send et accepté par toute fonction payante dans un contrat ou toute adresse détenue en externe. Les jetons sont envoyés à l’aide des fonctions transfer ou approve et transferFrom qui n’existent que dans le contrat ERC20, et ne déclenchent (du moins dans ERC20) aucune fonction payante dans un contrat destinataire. Les jetons sont censés fonctionner comme une cryptomonnaie telle que l’ether, mais ils présentent certaines différences qui brisent cette illusion.

Envisagez un autre problème. Pour envoyer de l’ether ou utiliser un contrat Ethereum, vous avez besoin d’ether pour payer le gaz. Pour envoyer des jetons, vous avez également besoin d’ether. Vous ne pouvez pas payer le gaz d’une transaction avec un jeton et le contrat de jeton ne peut pas payer le gaz pour vous. Cela peut changer à un moment donné dans un avenir lointain, mais entre-temps, cela peut entraîner des expériences utilisateur plutôt étranges. Par exemple, supposons que vous utilisiez un échange ou ShapeShift pour convertir du bitcoin en jeton. Vous "recevez" le jeton dans un portefeuille qui suit le contrat de ce jeton et affiche votre solde. Il ressemble à toutes les autres crypto-monnaies que vous avez dans votre portefeuille. Essayez d’envoyer le jeton et votre portefeuille vous informera que vous avez besoin d’ether pour le faire. Vous pourriez être confus - après tout, vous n’aviez pas besoin d’ether pour recevoir le jeton. Peut-être que vous n’avez pas d’ether. Peut-être ne saviez-vous même pas que le jeton était un jeton ERC20 sur Ethereum; peut-être pensiez-vous qu’il s’agissait d’une cryptomonnaie avec sa propre chaîne de blocs. L’illusion vient de se briser.

Certains de ces problèmes sont spécifiques aux jetons ERC20. D’autres sont des problèmes plus généraux liés à l’abstraction et aux limites d’interface au sein d’Ethereum. Certains peuvent être résolus en modifiant l’interface du jeton, tandis que d’autres peuvent nécessiter des modifications des structures fondamentales au sein d’Ethereum (comme la distinction entre EOA et contrats, et entre transactions et messages). Certains peuvent ne pas être exactement "solvable" et peuvent nécessiter une conception d’interface utilisateur pour masquer les nuances et rendre l’expérience utilisateur cohérente, quelles que soient les distinctions sous-jacentes.

Dans les prochaines sections, nous examinerons diverses propositions qui tentent de résoudre certains de ces problèmes.

ERC223 : une norme d’interface de contrat de jeton proposée

La proposition ERC223 tente de résoudre le problème du transfert par inadvertance de jetons vers un contrat (qui peut ou non prendre en charge jetons) en détectant si l’adresse de destination est un contrat ou non. ERC223 exige que les contrats conçus pour accepter des jetons implémentent une fonction nommée tokenFallback. Si la destination d’un transfert est un contrat et que le contrat ne prend pas en charge les jetons (c’est-à-dire qu’il n’implémente pas tokenFallback), le transfert échoue.

Pour détecter si l’adresse de destination est un contrat, l’implémentation de référence ERC223 utilise un petit segment de code intermédiaire en ligne d’une manière plutôt créative :

function isContract(address _addr) private view returns (bool is_contract) {
  uint length;
    assembly {
       // récupère la taille du code sur l'adresse cible ; cela nécessite un assemblage
       length := extcodesize(_addr)
    }
    return (length>0);
}

La spécification de l’interface de contrat ERC223 est :

interface ERC223Token {
  uint public totalSupply;
  function balanceOf(address who) public view returns (uint);

  function name() public view returns (string _name);
  function symbol() public view returns (string _symbol);
  function decimals() public view returns (uint8 _decimals);
  function totalSupply() public view returns (uint256 _supply);

  function transfer(address to, uint value) public returns (bool ok);
  function transfer(address to, uint value, bytes data) public returns (bool ok);
  function transfer(address to, uint value, bytes data, string custom_fallback)
      public returns (bool ok);

  event Transfer(address indexed from, address indexed to, uint value,
                 bytes indexed data);
}

ERC223 n’est pas largement mis en œuvre, et il y a un débat dans le fil de discussion ERC sur la rétrocompatibilité et les compromis entre la mise en œuvre des changements au niveau de l’interface du contrat par rapport à l’interface utilisateur. Le débat continue.

ERC777 : une norme d’interface de contrat de jeton proposée

Une autre proposition pour une norme de contrat de jeton améliorée est ERC777. Cette proposition a plusieurs objectifs, notamment :

  • Offrir une interface compatible ERC20

  • Pour transférer des jetons à l’aide d’une fonction send, similaire aux transferts d’ether

  • Pour être compatible avec ERC820 pour l’enregistrement de contrat de jeton

  • Pour permettre aux contrats et aux adresses de contrôler les jetons qu’ils envoient via une fonction tokensToSend qui est appelée avant l’envoi

  • Pour permettre aux contrats et aux adresses d’être informés de la réception des jetons en appelant une fonction tokensReceived dans le destinataire, et pour réduire la probabilité que les jetons soient verrouillés dans des contrats en exigeant que les contrats fournissent une fonction tokensReceived

  • Pour permettre aux contrats existants d’utiliser des contrats par procuration pour les fonctions tokensToSend et tokensReceived

  • Pour fonctionner de la même manière que vous envoyiez vers un contrat ou un EOA

  • Pour fournir des événements spécifiques pour la frappe et la gravure de jetons

  • Pour permettre aux opérateurs (tiers de confiance, destinés à être des contrats vérifiés) de déplacer des jetons pour le compte d’un détenteur de jetons

  • Pour fournir des métadonnées sur les transactions de transfert de jetons dans les champs userData et operatorData

La discussion en cours sur ERC777 peut être trouvée sur GitHub.

La spécification de l’interface de contrat ERC777 est :

interface ERC777Token {
    function name() public constant returns (string);
    function symbol() public constant returns (string);
    function totalSupply() public constant returns (uint256);
    function granularity() public constant returns (uint256);
    function balanceOf(address owner) public constant returns (uint256);

    function send(address to, uint256 amount, bytes userData) public;

    function authorizeOperator(address operator) public;
    function revokeOperator(address operator) public;
    function isOperatorFor(address operator, address tokenHolder)
        public constant returns (bool);
    function operatorSend(address from, address to, uint256 amount,
                          bytes userData,bytes operatorData) public;

    event Sent(address indexed operator, address indexed from,
               address indexed to, uint256 amount, bytes userData,
               bytes operatorData);
    event Minted(address indexed operator, address indexed to,
                 uint256 amount, bytes operatorData);
    event Burned(address indexed operator, address indexed from,
                 uint256 amount, bytes userData, bytes operatorData);
    event AuthorizedOperator(address indexed operator,
                             address indexed tokenHolder);
    event RevokedOperator(address indexed operator, address indexed tokenHolder);
}
Crochets ERC777

La spécification de crochet d’expéditeur de jetons ERC777 est :

interface ERC777TokensSender {
    function tokensToSend(address operator, address from, address to,
                          uint value, bytes userData, bytes operatorData) public;
}

La mise en place de cette interface est nécessaire pour toute adresse souhaitant être notifiée, gérer ou empêcher le débit de jetons. L’adresse pour laquelle le contrat implémente cette interface doit être enregistrée via ERC820, que le contrat implémente l’interface pour lui-même ou pour une autre adresse.

La spécification du crochet du destinataire des jetons ERC777 est :

interface ERC777TokensRecipient {
  function tokensReceived(
     address operator, address from, address to,
    uint amount, bytes userData, bytes operatorData
  ) public;
}

La mise en place de cette interface est nécessaire pour toute adresse souhaitant être notifiée, traiter ou rejeter la réception de jetons. La même logique et les mêmes exigences s’appliquent au destinataire des jetons qu’à l’interface de l’expéditeur des jetons, avec la contrainte supplémentaire que les contrats destinataires doivent implémenter cette interface pour empêcher le verrouillage des jetons. Si le contrat destinataire n’enregistre pas d’adresse implémentant cette interface, le transfert des jetons échouera.

Un aspect important est qu’un seul expéditeur de jeton et un seul destinataire de jeton peuvent être enregistrés par adresse. Par conséquent, pour chaque transfert de jeton ERC777, les mêmes fonctions de crochet sont appelées lors du débit et de la réception de chaque transfert de jeton ERC777. Un jeton spécifique peut être identifié dans ces fonctions à l’aide de l’expéditeur du message, qui est l’adresse de contrat de jeton spécifique, pour gérer un cas d’utilisation particulier.

D’autre part, les mêmes hooks d’expéditeur et de destinataire de jeton peuvent être enregistrés pour plusieurs adresses et les hooks peuvent distinguer qui sont l’expéditeur et le destinataire prévu à l’aide des paramètres "from" et "to".

Une implémentation de référence d’ERC777 est liée dans la proposition. ERC777 dépend d’une proposition parallèle de contrat de registre, spécifiée dans ERC820. Une partie du débat sur ERC777 porte sur la complexité de l’adoption simultanée de deux grands changements : une nouvelle norme de jeton et une norme de registre. La discussion continue.

ERC721 : norme de jeton non fongible (acte)

Toutes les normes de jeton que nous avons examinées jusqu’à présent concernent les jetons fungible, ce qui signifie que les unités de un jeton sont interchangeables. La norme de jeton ERC20 ne suit que le solde final de chaque compte et ne suit pas (explicitement) la provenance d’un jeton.

La proposition ERC721 concerne une norme pour les jetons non fongibles, également appelés actes.

Extrait du dictionnaire Oxford :

acte : un document juridique qui est signé et remis, en particulier un document concernant la propriété d’un bien ou des droits légaux.

L’utilisation du mot "acte" est destinée à refléter la partie "propriété d’un bien", même si ceux-ci ne sont pas reconnus comme des "documents juridiques" dans aucune juridiction - pour le moment. Il est probable qu’à un moment donné dans le futur, la propriété légale basée sur des signatures numériques sur une plate-forme chaîne de blocs sera légalement reconnue.

Les jetons non fongibles suivent la propriété d’une chose unique. L’objet possédé peut être un objet numérique, tel qu’un objet de jeu ou un objet de collection numérique ; ou la chose peut être un objet physique dont la propriété est suivie par un jeton, comme une maison, une voiture ou une œuvre d’art. Les actes peuvent également représenter des choses de valeur négative, telles que des prêts (dettes), des privilèges, des servitudes, etc. La norme ERC721 n’impose aucune limitation ou attente sur la nature de la chose dont la propriété est suivie par un acte et exige seulement qu’elle puisse être identifié de manière unique, ce qui, dans le cas de cette norme, est obtenu par un identifiant de 256 bits .

Les détails de la norme et de la discussion sont suivis sur deux fils GitHub différents:

Pour saisir la différence fondamentale entre ERC20 et ERC721, il suffit de regarder la structure de données interne utilisée dans ERC721 :

// Mappage de l'ID de l'acte au propriétaire
mapping (uint256 => address) private deedOwner;

Alors que l’ERC20 suit les soldes appartenant à chaque propriétaire, le propriétaire étant la clé primaire du mappage, l’ERC721 suit chaque ID d’acte et son propriétaire, l’ID d’acte étant la clé primaire du mappage. De cette différence fondamentale découlent toutes les propriétés d’un jeton non fongible.

La spécification de l’interface de contrat ERC721 est :

interface ERC721 /* is ERC165 */ {
    event Transfer(address indexed _from, address indexed _to, uint256 _deedId);
    event Approval(address indexed _owner, address indexed _approved,
                   uint256 _deedId);
    event ApprovalForAll(address indexed _owner, address indexed _operator,
                         bool _approved);

    function balanceOf(address _owner) external view returns (uint256 _balance);
    function ownerOf(uint256 _deedId) external view returns (address _owner);
    function transfer(address _to, uint256 _deedId) external payable;
    function transferFrom(address _from, address _to, uint256 _deedId)
        external payable;
    function approve(address _approved, uint256 _deedId) external payable;
    function setApprovalForAll(address _operateor, boolean _approved) payable;
    function supportsInterface(bytes4 interfaceID) external view returns (bool);
}

ERC721 prend également en charge deux interfaces optionnelles, une pour les métadonnées et une pour l’énumération des actes et des propriétaires.

L’interface optionnelle ERC721 pour les métadonnées est :

interface ERC721Metadata /* is ERC721 */ {
    function name() external pure returns (string _name);
    function symbol() external pure returns (string _symbol);
    function deedUri(uint256 _deedId) external view returns (string _deedUri);
}

L’interface optionnelle ERC721 pour l’énumération est :

interface ERC721Enumerable /* is ERC721 */ {
    function totalSupply() external view returns (uint256 _count);
    function deedByIndex(uint256 _index) external view returns (uint256 _deedId);
    function countOfOwners() external view returns (uint256 _count);
    function ownerByIndex(uint256 _index) external view returns (address _owner);
    function deedOfOwnerByIndex(address _owner, uint256 _index) external view
        returns (uint256 _deedId);
}

Utilisation des normes de jeton

Dans la section précédente, nous avons examiné plusieurs normes proposées et quelques normes largement déployées pour les contrats de jetons. A quoi servent exactement ces normes ? Devez-vous utiliser ces normes ? Comment devriez-vous les utiliser ? Devriez-vous ajouter des fonctionnalités au-delà de ces normes ? Quelles normes devez-vous utiliser ? Nous examinerons ensuite certaines de ces questions.

Que sont les normes de jeton ? Quel est leur but ?

Les normes de jetons sont les spécifications minimales pour une implémentation. Cela signifie que pour être conforme, par exemple, à ERC20, vous devez au minimum implémenter les fonctions et le comportement spécifiés par la norme ERC20. Vous êtes également libre d'ajouter à la fonctionnalité en implémentant des fonctions qui ne font pas partie de la norme.

L’objectif principal de ces normes est d’encourager l'interopérabilité entre les contrats. Ainsi, tous les portefeuilles, échanges, interfaces utilisateur et autres composants d’infrastructure peuvent s’interfacer de manière prévisible avec tout contrat qui suit la spécification. En d’autres termes, si vous déployez un contrat qui suit la norme ERC20, tous les utilisateurs de portefeuille existants peuvent commencer à échanger votre jeton de manière transparente sans aucune mise à niveau de portefeuille ni effort de votre part.

Les normes sont censées être descriptives plutôt que prescriptives. La façon dont vous choisissez de mettre en œuvre ces fonctions dépend de vous - le fonctionnement interne du contrat n’est pas pertinent pour la norme. Ils ont des exigences fonctionnelles, qui régissent le comportement dans des circonstances spécifiques, mais ils ne prescrivent pas de mise en œuvre. Un exemple de ceci est le comportement d’une fonction transfer si la valeur est définie sur zéro.

Devriez-vous utiliser ces normes ?

Compte tenu de tous ces standards, chaque développeur est confronté à un dilemme : utiliser les standards existants ou innover au-delà des contraintes qu’ils imposent ?

Ce dilemme n’est pas facile à résoudre. Les normes restreignent nécessairement votre capacité à innover, en créant une "ornière" étroite que vous devez suivre. D’autre part, les normes de base ont émergé de l’expérience de centaines d’applications et s’adaptent souvent bien à la grande majorité des cas d’utilisation.

Dans le cadre de cette considération, il y a un problème encore plus important : la valeur de l’interopérabilité et de l’adoption à grande échelle. Si vous choisissez d’utiliser une norme existante, vous gagnez la valeur de tous les systèmes conçus pour fonctionner avec cette norme. Si vous choisissez de vous écarter de la norme, vous devez prendre en compte le coût de la construction de toute l’infrastructure de support par vous-même ou persuader les autres de prendre en charge votre mise en œuvre en tant que nouvelle norme. La tendance à forger votre propre chemin et à ignorer les normes existantes est connue sous le nom de syndrome "Not Invented Here" (Non inventé ici) et est contraire à la culture open source. D’autre part, le progrès et l’innovation dépendent parfois de l’abandon de la tradition. C’est un choix délicat, alors réfléchissez bien !

Note

Selon Wikipedia, "Not Invented Here" est une position adoptée par les cultures sociales, d’entreprise ou institutionnelles qui évitent d’utiliser ou d’acheter des produits, des recherches, des normes ou des connaissances déjà existants en raison de leurs origines externes et de leurs coûts, tels que les redevances.

Sécurité par maturité

Au-delà du choix de la norme, il y a le choix parallèle de la mise en oeuvre. Lorsque vous décidez d’utiliser une norme telle que ERC20, vous devez ensuite décider comment mettre en œuvre une conception compatible. Il existe un certain nombre d’implémentations "de référence" existantes qui sont largement utilisées dans l’écosystème Ethereum, ou vous pouvez écrire la vôtre à partir de zéro. Encore une fois, ce choix représente un dilemme qui peut avoir de graves implications en matière de sécurité.

Les implémentations existantes sont "testées au combat". Bien qu’il soit impossible de prouver qu’ils sont sécurisés, beaucoup d’entre eux sous-tendent des millions de dollars de jetons. Ils ont été attaqués, à plusieurs reprises et vigoureusement. Jusqu’à présent, aucune vulnérabilité significative n’a été découverte. Rédiger le vôtre n’est pas facile - il existe de nombreuses façons subtiles de compromettre un contrat. Il est beaucoup plus sûr d’utiliser une implémentation largement testée et largement utilisée. Dans nos exemples, nous avons utilisé l’implémentation OpenZeppelin de la norme ERC20, car cette implémentation est entièrement axée sur la sécurité.

Si vous utilisez une implémentation existante, vous pouvez également l’étendre. Encore une fois, cependant, soyez prudent avec cette impulsion. La complexité est l’ennemie de la sécurité. Chaque ligne de code que vous ajoutez étend la surface d’attaque de votre contrat et pourrait représenter une vulnérabilité en attente. Vous ne remarquerez peut-être pas un problème jusqu’à ce que vous accordiez beaucoup de valeur au contrat et que quelqu’un le rompe.

Tip

Les normes et les choix de mise en œuvre sont des éléments importants de la conception globale des contrats intelligents sécurisés, mais ce ne sont pas les seules considérations. Voir [smart_contract_security].

Extensions aux normes d’interface de jeton

Les normes de jetons abordées dans ce chapitre fournissent une interface très minimale, avec des fonctionnalités limitées. De nombreux projets ont créé des implémentations étendues pour prendre en charge les fonctionnalités dont ils ont besoin pour leurs applications. Certaines de ces fonctionnalités incluent :

Contrôle du propriétaire

La possibilité de donner des adresses spécifiques ou des ensembles d’adresses (c’est-à-dire des schémas multisignatures), des capacités spéciales, telles que la liste noire, la liste blanche, la frappe, la récupération, etc.

Brûlure

La capacité de détruire délibérément ("brûler") des jetons en les transférant à une adresse inutilisable ou en effaçant un solde et en réduisant l’approvisionnement.

Frappe

La possibilité d’ajouter à l’offre totale de jetons, à un rythme prévisible ou par "fiat" du créateur du jeton.

Financement participatif

La possibilité de proposer des jetons à la vente, par exemple par le biais d’une vente aux enchères, d’une vente sur le marché, d’une vente aux enchères inversée, etc.

Limites

La possibilité de fixer des limites prédéfinies et immuables sur l’offre totale (à l’opposé de la fonction "frappe").

Portes dérobées de récupération

Fonctions pour récupérer des fonds, annuler des transferts ou démanteler le jeton qui peuvent être activés par une adresse désignée ou un ensemble d’adresses.

Liste blanche

La possibilité de restreindre les actions (telles que les transferts de jetons) à des adresses spécifiques. Le plus souvent utilisé pour offrir des jetons à des "investisseurs accrédités" après vérification par les règles de différentes juridictions. Il existe généralement un mécanisme de mise à jour de la liste blanche.

Liste noire

La possibilité de restreindre les transferts de jetons en interdisant des adresses spécifiques. Il existe généralement une fonction de mise à jour de la liste noire.

Il existe des implémentations de référence pour bon nombre de ces fonctions, par exemple dans la bibliothèque OpenZeppelin. Certains d’entre eux sont spécifiques à un cas d’utilisation et ne sont implémentés que dans quelques jetons. Il n’existe pas, à l’heure actuelle, de normes largement acceptées pour les interfaces avec ces fonctions.

Comme indiqué précédemment, la décision d’étendre une norme de jeton avec des fonctionnalités supplémentaires représente un compromis entre innovation/risque et interopérabilité/sécurité.

Jetons et ICO

Les jetons ont connu un développement explosif dans l’écosystème Ethereum. Il est probable qu’ils deviendront un élément très important de toutes les plateformes de contrats intelligents comme Ethereum.

Néanmoins, l’importance et l’impact futur de ces normes ne doivent pas être confondus avec une approbation des offres de jetons actuelles. Comme dans toute technologie à un stade précoce, la première vague de produits et d’entreprises échouera presque toutes, et certaines échoueront de manière spectaculaire. La plupart des jetons proposés dans Ethereum aujourd’hui sont des escroqueries à peine déguisées, des systèmes pyramidaux et des prises d’argent.

L’astuce consiste à séparer la vision et l’impact à long terme de cette technologie, qui risque d’être énorme, de la bulle à court terme des ICO symboliques, qui sont en proie à la fraude. Les normes de jetons et la plate-forme survivront à la manie actuelle des jetons, puis ils changeront probablement le monde.

Conclusion

Les jetons sont un concept très puissant dans Ethereum et peuvent constituer la base de nombreuses applications décentralisées importantes. Dans ce chapitre, nous avons examiné les différents types de jetons et les normes de jeton, et vous avez construit votre premier jeton et l’application associée. Nous reviendrons à nouveau sur les jetons dans [decentralized_applications_chap], où vous utiliserez un jeton non fongible comme base pour un DApp d’enchères.