Contrats intelligents et Solidity
Comme nous en avons discuté dans [intro_chapter], il existe deux types de comptes différents dans Ethereum : les comptes externes (EOA) et comptes contractuels. Les EOA sont contrôlés par les utilisateurs, souvent via un logiciel tel qu’une application de portefeuille externe à la plate-forme Ethereum. contrats ») qui est exécuté par la machine virtuelle Ethereum. En bref, les EOA sont de simples comptes sans code ni stockage de données associés, tandis que les comptes contractuels ont à la fois un code et un stockage de données associés. Les EOA sont contrôlés par des transactions créées et signées cryptographiquement avec une clé privée dans le "monde réel" externe et indépendant du protocole, alors que les comptes contractuels n’ont pas de clés privées et "se contrôlent" donc de la manière prédéterminée prescrite par leur code de contrat intelligent. Les deux types de comptes sont identifiés par une adresse Ethereum. Dans ce chapitre, nous discuterons des comptes contractuels et du code de programme qui les contrôle.
Qu’est-ce qu’un contrat intelligent ?
Le terme contrat intelligent a été utilisé au fil des ans pour décrire une grande variété de choses différentes. Dans les années 1990, le cryptographe Nick Szabo a inventé le terme et l’a défini comme "un ensemble de promesses, spécifiées sous forme numérique, y compris des protocoles au sein que les parties exécutent sur les autres promesses. Depuis lors, le concept de contrats intelligents a évolué, notamment après l’introduction de plateformes de chaîne de blocs décentralisées avec l’invention de Bitcoin en 2009. Dans le contexte d’Ethereum, le terme est en fait un peu impropre, étant donné que les contrats intelligents Ethereum ne sont ni des contrats intelligents ni légaux, mais le terme est resté. Dans ce livre, nous utilisons le terme «contrats intelligents» pour désigner des programmes informatiques immuables qui s’exécutent de manière déterministe dans le contexte d’une machine virtuelle Ethereum dans le cadre du protocole réseau Ethereum, c’est-à-dire sur l’ordinateur mondial Ethereum décentralisé.
Déballons cette définition :
- Logiciels d’ordinateur
-
Les contrats intelligents sont simplement des programmes informatiques. Le mot « contrat » n’a aucune signification juridique dans ce contexte.
- Immuable
-
Une fois déployé, le code d’un contrat intelligent ne peut pas changer. Contrairement aux logiciels traditionnels, la seule façon de modifier un contrat intelligent est de déployer une nouvelle instance.
- Déterministe
-
Le résultat de l’exécution d’un contrat intelligent est le même pour tous ceux qui l’exécutent, compte tenu du contexte de la transaction qui a initié son exécution et de l’état de la chaîne de blocs Ethereum au moment de l’exécution.
- Contexte EVM
-
Les contrats intelligents fonctionnent avec un contexte d’exécution très limité. Ils peuvent accéder à leur propre état, au contexte de la transaction qui les a appelés et à certaines informations sur les blocs les plus récents.
- Ordinateur mondial décentralisé
-
L’EVM fonctionne comme une instance locale sur chaque nœud Ethereum, mais comme toutes les instances de l’EVM fonctionnent sur le même état initial et produisent le même état final, le système dans son ensemble fonctionne comme un seul "ordinateur mondial".
Cycle de vie d’un contrat intelligent
Les contrats intelligents sont généralement écrits dans un langage de haut niveau, tel que Solidity. Mais pour fonctionner, ils doivent être compilés dans le code intermédiaire (bytecode) de bas niveau qui s’exécute dans l’EVM. Une fois compilés, ils sont déployés sur la plateforme Ethereum à l’aide d’une transaction spéciale de création de contrat, qui est identifiée comme telle en étant envoyée à l’adresse spéciale de création de contrat, à savoir 0x0 (voir [contract_reg]). Chaque contrat est identifié par une adresse Ethereum, qui est dérivée de la transaction de création de contrat en fonction du compte d’origine et du nonce. L’adresse Ethereum d’un contrat peut être utilisée dans une transaction en tant que destinataire, envoyer des fonds au contrat ou appeler l’une des fonctions du contrat. Notez que, contrairement aux EOA, il n’y a pas de clés associées à un compte créé pour un nouveau contrat intelligent. En tant que créateur du contrat, vous n’obtenez aucun privilège spécial au niveau du protocole (bien que vous puissiez les coder explicitement dans le contrat intelligent). Vous ne recevez certainement pas la clé privée du compte de contrat, qui en fait n’existe pas - nous pouvons dire que les comptes de contrat intelligents se possèdent eux-mêmes.
Il est important de noter que les contrats ne s’exécutent que s’ils sont appelés par une transaction. Tous les contrats intelligents dans Ethereum sont exécutés, en fin de compte, en raison d’une transaction initiée à partir d’un EOA. Un contrat peut appeler un autre contrat qui peut appeler un autre contrat, et ainsi de suite, mais le premier contrat d’une telle chaîne d’exécution aura toujours été appelé par une transaction d’un EOA. Les contrats ne fonctionnent jamais "seuls" ou "en arrière-plan". Les contrats restent effectivement inactifs jusqu’à ce qu’une transaction déclenche l’exécution, soit directement, soit indirectement dans le cadre d’une chaîne d’appels de contrats. Il convient également de noter que les contrats intelligents ne sont en aucun cas exécutés "en parallèle" - l’ordinateur mondial Ethereum peut être considéré comme une machine à un seul fil d’exécution (thread).
Les transactions sont atomique, quel que soit le nombre de contrats qu’elles appellent ou ce que ces contrats font lorsqu’ils sont appelés. Les transactions s’exécutent dans leur intégralité, avec tout changement dans l’état global (contrats, comptes, etc.) enregistré uniquement si toutes les exécutions se terminent avec succès. Une fin réussie signifie que le programme s’est exécuté sans erreur et a atteint la fin de l’exécution. Si l’exécution échoue en raison d’une erreur, tous ses effets (changements d’état) sont "annulés" comme si la transaction n’avait jamais été exécutée. Une transaction échouée est toujours enregistrée comme ayant été tentée, et l’ether dépensé en gaz pour l’exécution est déduit du compte d’origine, mais cela n’a par ailleurs aucun autre effet sur le contrat ou l’état du compte.
Comme mentionné précédemment, il est important de se rappeler que le code d’un contrat ne peut pas être modifié. Cependant, un contrat peut être "supprimé", supprimant le code et son état interne (stockage) de son adresse, laissant un compte vide. Toutes les transactions envoyées à cette adresse de compte après la suppression du contrat n’entraînent aucune exécution de code, car il n’y a plus de code à exécuter. Pour supprimer un contrat, vous exécutez un opcode EVM appelé SELFDESTRUCT (précédemment appelé SUICIDE). Cette opération coûte du "gaz négatif", un remboursement de gaz, incitant ainsi à libérer les ressources du client du réseau à partir de la suppression de l’état stocké. La suppression d’un contrat de cette manière ne supprime pas l’historique des transactions (passé) du contrat, car la chaîne de blocs elle-même est immuable. Il est également important de noter que la capacité SELFDESTRUCT ne sera disponible que si l’auteur du contrat a programmé le contrat intelligent pour avoir cette fonctionnalité. Si le code du contrat n’a pas d’opcode SELFDESTRUCT
, ou s’il est inaccessible, le contrat intelligent ne peut pas être supprimé.
Introduction aux langages de haut niveau Ethereum
L’EVM est une machine virtuelle qui exécute une forme spéciale de code appelé code intermédiaire EVM ou EVM bytecode, analogue au processeur de votre ordinateur, qui exécute un code machine tel que x86_64. Nous examinerons le fonctionnement et le langage de l’EVM de manière beaucoup plus détaillée dans [evm_chapter]. Dans cette section, nous verrons comment les contrats intelligents sont écrits pour s’exécuter sur l’EVM.
Bien qu’il soit possible de programmer des contrats intelligents directement en code intermédiaire, le code intermédiaire EVM est plutôt lourd et très difficile à lire et à comprendre pour les programmeurs. Au lieu de cela, la plupart des développeurs Ethereum utilisent un langage de haut niveau pour écrire des programmes et un compilateur pour les convertir en code intermédiaire.
Bien que n’importe quel langage de haut niveau puisse être adapté pour écrire des contrats intelligents, adapter un langage arbitraire pour qu’il soit compilable au code intermédiaire EVM est un exercice assez fastidieux et conduirait en général à une certaine confusion. Les contrats intelligents fonctionnent dans un environnement d’exécution très contraint et minimaliste (l’EVM). De plus, un ensemble spécial de variables système et de fonctions spécifiques à EVM doit être disponible. En tant que tel, il est plus facile de créer un langage de contrat intelligent à partir de zéro que de créer un langage à usage général adapté à l’écriture de contrats intelligents. En conséquence, un certain nombre de langages spéciaux ont émergé pour programmer des contrats intelligents. Ethereum possède plusieurs langages de ce type, ainsi que les compilateurs nécessaires pour produire un code intermédiaire exécutable EVM.
En général, les langages de programmation peuvent être classés en deux grands paradigmes de programmation : déclaratif et impératif, également appelés respectivement fonctionnel et procédural. En programmation déclarative, nous écrivons des fonctions qui expriment la logique d’un programme, mais pas son flux. La programmation déclarative est utilisée pour créer des programmes sans effets secondaires, ce qui signifie qu’il n’y a pas de changement d’état en dehors d’une fonction. Les langages de programmation déclaratifs incluent Haskell et SQL. La programmation impérative, en revanche, est l’endroit où un programmeur écrit un ensemble de procédures qui combinent la logique et le déroulement d’un programme. Les langages de programmation impératifs incluent C++ et Java. Certains langages sont « hybrides », ce qui signifie qu’ils encouragent la programmation déclarative mais peuvent également être utilisés pour exprimer un paradigme de programmation impérative. Ces hybrides incluent Lisp, JavaScript et Python. En général, n’importe quel langage impératif peut être utilisé pour écrire dans un paradigme déclaratif, mais il en résulte souvent un code inélégant. Par comparaison, les langages déclaratifs purs ne peuvent pas être utilisés pour écrire dans un paradigme impératif. Dans les langages purement déclaratifs, il n’y a pas de "variables".
Bien que la programmation impérative soit plus couramment utilisée par les programmeurs, il peut être très difficile d’écrire des programmes qui s’exécutent exactement comme prévu. La capacité de n’importe quelle partie du programme à modifier l’état de n’importe quelle autre rend difficile le raisonnement sur l’exécution d’un programme et introduit de nombreuses opportunités de bogues. La programmation déclarative, par comparaison, permet de comprendre plus facilement le comportement d’un programme : puisqu’elle n’a pas d’effets secondaires, n’importe quelle partie d’un programme peut être comprise isolément.
Dans les contrats intelligents, les bogues coûtent littéralement de l’argent. Par conséquent, il est extrêmement important de rédiger des contrats intelligents sans effets indésirables. Pour ce faire, vous devez être capable de raisonner clairement sur le comportement attendu du programme. Ainsi, les langages déclaratifs jouent un rôle beaucoup plus important dans les contrats intelligents que dans les logiciels à usage général. Néanmoins, comme vous le verrez, le langage le plus utilisé pour les contrats intelligents (Solidity) est impératif. Les programmeurs, comme la plupart des humains, résistent au changement !
Les langages de programmation de haut niveau actuellement pris en charge pour les contrats intelligents incluent (classés par âge approximatif) :
- LLL
-
Un langage de programmation fonctionnel (déclaratif), avec une syntaxe de type Lisp. C’était le premier langage de haut niveau pour les contrats intelligents Ethereum, mais il est rarement utilisé aujourd’hui.
- Serpent
-
Un langage de programmation procédural (impératif) avec une syntaxe similaire à Python. Peut également être utilisé pour écrire du code fonctionnel (déclaratif), bien qu’il ne soit pas entièrement exempt d’effets secondaires.
- Solidity
-
Un langage de programmation procédural (impératif) avec une syntaxe similaire à JavaScript, C++, ou Java. Le langage le plus populaire et le plus fréquemment utilisé pour les contrats intelligents Ethereum.
- Vyper
-
Un langage développé plus récemment, similaire à Serpent et encore une fois avec une syntaxe de type Python. Destiné à se rapprocher d’un langage de type Python purement fonctionnel que Serpent, mais pas à remplacer Serpent.
- Bamboo
-
Un langage nouvellement développé, influencé par Erlang, avec des transitions d’état explicites et sans flux itératifs (boucles). Destiné à réduire les effets secondaires et à augmenter l’auditabilité. Très nouveau et en attente d’être largement adopté.
Comme vous pouvez le voir, il existe de nombreux languages parmi lesquelles choisir. Cependant, de tous, Solidity est de loin le plus populaire, au point d’être le langage de haut niveau de facto d’Ethereum et même d’autres chaînes de blocs de type EVM. Nous passerons la plupart de notre temps à utiliser Solidity, mais nous explorerons également certains des exemples dans d’autres langages de haut niveau pour comprendre leurs différentes philosophies.
Construire un contrat intelligent avec Solidity
Solidity a été créé par Dr. Gavin Wood (co-auteur de ce livre) en tant que langage explicite pour écrire des contrats intelligents avec des fonctionnalités permettant de prendre directement en charge l’exécution dans l’environnement décentralisé de l’ordinateur mondial Ethereum. Les attributs résultants sont assez généraux et ont donc fini par être utilisés pour coder des contrats intelligents sur plusieurs autres plateformes de chaîne de blocs. Il a été développé par Christian Reitiwessner puis également par Alex Beregszaszi, Liana Husikyan, Yoichi Hirai et plusieurs anciens contributeurs principaux d’Ethereum. Solidity est maintenant développé et maintenu en tant que projet indépendant sur GitHub.
Le "produit" principal du projet Solidity est le compilateur Solidity, solc
, qui convertit les programmes écrits dans le langage Solidity en code intermédiaire EVM. Le projet gère également l’importante norme d’interface binaire d’application (ABI) pour les contrats intelligents Ethereum, que nous explorerons en détail dans ce chapitre. Chaque version du compilateur Solidity correspond à et compile une version spécifique de language de Solidity.
Pour commencer, nous allons télécharger un exécutable binaire du compilateur Solidity. Ensuite, nous développerons et compilerons un contrat simple, à la suite de l’exemple avec lequel nous avons commencé dans [intro_chapter].
Sélection d’une version de Solidity
Solidity suit un modèle de versionnage appelé versionnage de sémantique (semantic versioning), qui spécifie les numéros de version structurés comme trois nombres séparés par des points : MAJEUR.MINEUR.CORRECTIF. Le numéro "majeur" est incrémenté pour les modifications majeures et rétro-incompatibles, le numéro "mineur" est incrémenté lorsque des fonctionnalités rétrocompatibles sont ajoutées entre les versions majeures, et le numéro "correctif" est incrémenté pour les corrections de bogues rétrocompatibles.
Au moment de la rédaction, Solidity est à la version 0.4.24. Les règles de la version majeure 0, qui concerne le développement initial d’un projet, sont différentes : tout peut changer à tout moment. En pratique, Solidity traite le numéro "mineur" comme s’il s’agissait de la version majeure et le numéro de "correctif" comme s’il s’agissait de la version mineure. Par conséquent, dans la version 0.4.24, 4 est considérée comme la version majeure et 24 comme la version mineure.
La sortie de la version majeure 0.5 de Solidity est attendue de manière imminente.
Comme vous l’avez vu dans [intro_chapter], vos programmes Solidity peuvent contenir une directive pragma qui spécifie les versions minimale et maximale de Solidity avec lesquelles il est compatible, et peut être utilisée pour compiler votre contrat.
Étant donné que Solidity évolue rapidement, il est souvent préférable d’installer la dernière version.
Télécharger et installer
Il existe un certain nombre de méthodes que vous pouvez utiliser pour télécharger et installer Solidity, soit sous forme de version binaire, soit en compilant à partir du code source. Vous pouvez trouver des instructions détaillées dans http://bit.ly/2RrZmup [la documentation de Solidity].
Voici comment installer la dernière version binaire de Solidity sur un système d’exploitation Ubuntu/Debian, en utilisant le gestionnaire de paquets apt :
$ sudo add-apt-repository ppa:ethereum/ethereum $ sudo apt update $ sudo apt install solc
Une fois que vous avez installé solc
, vérifiez la version en exécutant :
$ solc --version solc, the solidity compiler commandline interface Version: 0.4.24+commit.e67f0147.Linux.g++
Il existe plusieurs autres façons d’installer Solidity, en fonction de votre système d’exploitation et de vos exigences, y compris la compilation directe à partir du code source. Pour plus d’informations, consultez https://github.com/ethereum/solidity.
Environnement de développement
Pour développer dans Solidity, vous pouvez utiliser n’importe quel éditeur de texte et solc sur la ligne de commande. Cependant, vous constaterez peut-être que certains éditeurs de texte conçus pour le développement, tels que Emacs, Vim et Atom, offrent des fonctionnalités supplémentaires telles que la coloration syntaxique et les macros qui facilitent le développement de Solidity.
Il existe également des environnements de développement basés sur le Web, tels que Remix IDE et EthFiddle.
Utilisez les outils qui vous rendent productif. En fin de compte, les programmes Solidity ne sont que des fichiers texte. Bien que des éditeurs et des environnements de développement sophistiqués puissent faciliter les choses, vous n’avez besoin de rien de plus qu’un simple éditeur de texte, tel que nano (Linux/Unix), TextEdit (macOS) ou même NotePad (Windows). Enregistrez simplement le code source de votre programme avec une extension .sol et il sera reconnu par le compilateur Solidity comme un programme Solidity.
Écrire un programme Solidity simple
Dans [intro_chapter], nous avons écrit notre premier programme Solidity. Lorsque nous avons créé le contrat Faucet pour la première fois, nous avons utilisé l’IDE Remix pour compiler et déployer le contrat. Dans cette section, nous allons revisiter, améliorer et embellir Faucet.
Notre première tentative ressemblait à Faucet.sol : Un contrat Solidity mettant en place un robinet.
// Identifiant de licence SPDX : CC-BY-SA-4.0
// Version du compilateur Solidity pour lequel ce programme a été écrit
pragma solidity 0.6.4;
// Notre premier contrat est un faucet !
contract Faucet {
// Accepte tout montant entrant
receive() external payable {}
// Donnez de l'éther à quiconque demande
function withdraw(uint withdraw_amount) public {
// Limiter le montant du retrait
require(withdraw_amount <= 100000000000000000);
// Envoie le montant à l'adresse qui l'a demandé
msg.sender.transfer(withdraw_amount);
}
}
Compiler avec le compilateur Solidity (solc)
Maintenant, nous allons utilisez le compilateur Solidity en ligne de commande pour compiler directement notre contrat. Le compilateur Solidity solc offre une variété d’options, que vous pouvez voir en passant l’argument --help.
Nous utilisons les arguments --bin et --optimize de solc pour produire un binaire optimisé de notre exemple de contrat :
$ solc --optimize --bin Faucet.sol ======= Faucet.sol:Faucet ======= Binary: 6060604052341561000f57600080fd5b60cf8061001d6000396000f300606060405260043610603e5 763ffffffff7c01000000000000000000000000000000000000000000000000000000006000350416 632e1a7d4d81146040575b005b3415604a57600080fd5b603e60043567016345785d8a00008111156 06357600080fd5b73ffffffffffffffffffffffffffffffffffffffff331681156108fc0282604051 600060405180830381858888f19350505050151560a057600080fd5b505600a165627a7a723058203 556d79355f2da19e773a9551e95f1ca7457f2b5fbbf4eacf7748ab59d2532130029
Le résultat que solc produit est un binaire sérialisé hexadécimal qui peut être soumis à la chaîne de blocs Ethereum.
L’ABI du contrat Ethereum
Dans les logiciels informatiques, une interface binaire d’application est une interface entre deux modules de programme ; souvent, entre le système d’exploitation et les programmes utilisateur. Un ABI définit la façon dont les structures de données et les fonctions sont accessibles en code machine ; cela ne doit pas être confondu avec une API, qui définit cet accès dans des formats de haut niveau, souvent lisibles par l’homme, en tant que code source. L’ABI est donc le principal moyen d’encoder et de décoder des données dans et hors du code machine.
Dans Ethereum, l’ABI est utilisé pour coder les appels de contrat pour l’EVM et pour lire les données des transactions. Le but d’un ABI est de définir les fonctions du contrat qui peuvent être invoquées et de décrire comment chaque fonction acceptera les arguments et renverra son résultat.
L’ABI d’un contrat est spécifié sous la forme d’un tableau JSON de descriptions de fonctions (voir Fonctions) et événements (voir Événements). Une description de fonction est un objet JSON avec les champs type
, name
, inputs
, outputs
, constant
et payable
. Un objet de description d’événement a des champs type
, name
, inputs
et anonymous
.
Nous utilisons le compilateur Solidity en ligne de commande solc pour produire l’ABI pour notre exemple de contrat Faucet.sol :
$ solc --abi Faucet.sol ======= Faucet.sol:Faucet ======= Contract JSON ABI [{"constant":false,"inputs":[{"name":"withdraw_amount","type":"uint256"}], \ "name":"withdraw","outputs":[],"payable":false,"stateMutability":"nonpayable", \ "type":"function"},{"payable":true,"stateMutability":"payable", \ "type":"fallback"}]
Comme vous pouvez le voir, le compilateur produit un tableau JSON décrivant les deux fonctions définies par Faucet.sol. Ce JSON peut être utilisé par toute application qui souhaite accéder au contrat Faucet une fois qu’il est déployé. À l’aide de l’ABI, une application telle qu’un portefeuille ou un navigateur DApp peut construire des transactions qui appellent les fonctions dans + Faucet + avec les arguments et types d’arguments corrects. Par exemple, un portefeuille saura que pour appeler la fonction withdraw
, il devra fournir un argument uint256 nommé withdraw_amount
. Le portefeuille pourrait inviter l’utilisateur à fournir cette valeur, puis à créer une transaction qui l’encode et exécute la fonction retirer.
Tout ce qui est nécessaire pour qu’une application interagisse avec un contrat est un ABI et l’adresse où le contrat a été déployé.
Sélection d’un compilateur Solidity et d’une version linguistique
Comme nous l’avons vu dans le code précédent, notre contrat Faucet se compile avec succès avec Solidity version 0.4.21. Et si nous avions utilisé une version différente du compilateur Solidity ? Le langage est toujours en constante évolution et les choses peuvent changer de manière inattendue. Notre contrat est assez simple, mais que se passerait-il si notre programme utilisait une fonctionnalité qui n’a été ajoutée que dans la version 0.4.19 de Solidity et que nous essayions de la compiler avec la version 0.4.18 ?
Pour résoudre ces problèmes, Solidity propose une directive du compilateur connue sous le nom de version pragma qui indique au compilateur que le programme attend un compilateur (et un langage) spécifique à une version. Regardons un exemple :
pragma solidity ^0.4.19;
Le compilateur Solidity lit le pragma de version et génère une erreur si la version du compilateur est incompatible avec le pragma de version. Dans ce cas, notre pragma de version indique que ce programme peut être compilé par un compilateur Solidity avec une version minimale de 0.4.19. Le symbole ^
indique, cependant, que nous autorisons la compilation avec toute révision mineure au-dessus de 0.4.19 ; par exemple, 0.4.20, mais pas 0.5.0 (qui est une révision majeure, pas une révision mineure). Les directives Pragma ne sont pas compilées dans le code intermédiaire EVM. Ils ne sont utilisés par le compilateur que pour vérifier la compatibilité.
Ajoutons une directive pragma à notre contrat Faucet
. Nous nommerons le nouveau fichier Faucet2.sol, pour garder une trace de nos modifications au fur et à mesure que nous avancerons dans ces exemples en commençant par Faucet2.sol : Ajout du pragma de version à Faucet.
// Identifiant de licence SPDX : CC-BY-SA-4.0
// Version du compilateur Solidity pour lequel ce programme a été écrit
pragma solidity ^0.6.4;
// Notre premier contrat est un faucet !
contract Faucet {
// Accepte tout montant entrant
receive() external payable {}
// Donnez de l'éther à quiconque demande
function withdraw(uint withdraw_amount) public {
// Limiter le montant du retrait
require(withdraw_amount <= 100000000000000000);
// Envoie le montant à l'adresse qui l'a demandé
msg.sender.transfer(withdraw_amount);
}
}
L’ajout d’un pragma de version est une bonne pratique, car il évite les problèmes liés aux versions incompatibles du compilateur et du langage. Nous explorerons d’autres bonnes pratiques et continuerons d’améliorer le contrat Faucet tout au long de ce chapitre.
Programmation avec Solidity
Dans cette section, nous examinerons certaines des fonctionnalités du langage Solidity. Comme nous l’avons mentionné dans [intro_chapter], notre premier exemple de contrat était très simple et également défectueux à plusieurs égards. Nous allons l’améliorer progressivement ici, tout en explorant comment utiliser Solidity. Cependant, ce ne sera pas un didacticiel complet sur Solidity, car Solidity est assez complexe et évolue rapidement. Nous couvrirons les bases et vous donnerons suffisamment de bases pour pouvoir explorer le reste par vous-même. La documentation de Solidity se trouve sur le site du projet.
Types de données
D’abord, examinons certains des types de données de base proposés dans Solidity :
- Boolean (bool)
-
valeur booléenne, true ou
false
, avec les opérateurs logiques!
(not), && (and), || (or), == (equal) et != (non égal). - Integer (
int
,uint
) -
Entiers signés (
int
)et non signés (uint), déclarés par incréments de 8 bits de int8 àuint256
. Sans suffixe de taille, des quantités de 256 bits sont utilisées pour correspondre à la taille de mot de l’EVM. - Fixed point (
fixed
,ufixed
) -
Nombres à virgule fixe, déclarés avec (
u
)fixed M x N
où M est la taille en bits (incréments de 8 jusqu’à 256) et N est le nombre de décimales après le point (jusqu’à 18) ; par exemple, ufixed32x2. - Address
-
Une adresse Ethereum de 20 octets. L’objet address a de nombreuses fonctions membres utiles, les principales étant balance (renvoie le solde du compte) et
transfer
(transfère l’ether au compte). - Byte array (fixe)
-
Tableaux d’octets de taille fixe, déclarés avec bytes1 jusqu’à bytes32.
Byte array (dynamique):: tableaux d’octets de taille variable, déclarés avec bytes ou string.
- Enum
-
Type défini par l’utilisateur pour énumérer des valeurs discrètes : enum NAME {LABEL1, LABEL 2, ...}.
- Arrays
-
Un tableau de n’importe quel type, fixe ou dynamique : uint32[][5] est un tableau de taille fixe de cinq tableaux dynamiques d’entiers non signés.
- Struct
-
Conteneurs de données définis par l’utilisateur pour regrouper les variables :
struct NAME {TYPE1 VARIABLE1; TYPE2 VARIABLE2; ...}
. - Mapping
-
Tables de recherche de hachage pour les paires key => value : mapping(KEY_TYPE => VALUE_TYPE) NAME.
En plus de ces types de données, Solidity propose également une variété de valeurs littérales qui peuvent être utilisées pour calculer différentes unités :
- Unités de temps
-
Les unités
seconds
,minutes
,hours
etdays
peuvent être utilisées comme suffixes, convertis en multiples de l’unité de base seconds. - Unités d’ether
-
Les unités
wei
,finney
,szabo
etether
peuvent être utilisées comme suffixes, convertis en multiples de l’unité de base wei.
Dans notre exemple de contrat Faucet
, nous avons utilisé un uint (qui est un alias pour uint256
)pour la variable withdraw_amount
. Nous avons également utilisé indirectement une variable address
, que nous avons définie avec msg.sender
. Nous utiliserons plus de ces types de données dans nos exemples dans le reste de ce chapitre.
Utilisons l’un des multiplicateurs d’unités pour améliorer la lisibilité de notre exemple de contrat. Dans la fonction withdraw nous limitons le retrait maximum, en exprimant la limite en wei, l’unité de base de l’ether :
require(withdraw_amount <= 100000000000000000);
Ce n’est pas très facile à lire. Nous pouvons améliorer notre code en utilisant le multiplicateur d’unité ether
, pour exprimer la valeur en ether au lieu de wei :
require(withdraw_amount <= 0.1 ether);
Variables et fonctions globales prédéfinies
Lorsqu’un contrat est exécuté dans l’EVM, il a accès à un petit ensemble d’objets globaux. Ceux-ci incluent les objets block
, msg et tx
. De plus, Solidity expose un certain nombre d’opcodes EVM en tant que fonctions prédéfinies. Dans cette section, nous examinerons les variables et les fonctions auxquelles vous pouvez accéder à partir d’un contrat intelligent dans Solidity.
Contexte d’appel de transaction/message
L’objet msg est l’appel de transaction (origine EOA) ou l’appel de message (origine contrat) qui lancé l’exécution de ce contrat. Il contient un certain nombre d’attributs utiles :
- msg.sender
-
Nous avons déjà utilisé celui-ci. Il représente l’adresse qui a lancé cet appel de contrat, pas nécessairement l’EOA d’origine qui a envoyé la transaction. Si notre contrat a été appelé directement par une transaction EOA, alors c’est l’adresse qui a signé la transaction, mais sinon ce sera une adresse de contrat.
- msg.value
-
La valeur d’ether envoyée avec cet appel (en wei).
- msg.gas
-
La quantité de gaz restant dans l’approvisionnement en gaz de cet environnement d’exécution. Cela a été déprécié dans Solidity v0.4.21 et remplacé par la fonction gasleft.
- msg.data
-
La charge utile de données de cet appel dans notre contrat.
- msg.sig
-
Les quatre premiers octets de la charge utile de données, qui est le sélecteur de fonction.
Note
|
Chaque fois qu’un contrat appelle un autre contrat, les valeurs de tous les attributs de msg changent pour refléter les informations du nouvel appelant. La seule exception à cela est la fonction |
Contexte transactionnel
L’objet tx fournit un moyen d’accéder aux informations relatives à la transaction :
- tx.gasprice
-
Le prix du gaz dans la transaction d’appel.
- tx.origin
-
L’adresse de l’EOA d’origine pour cette transaction. ATTENTION : dangereux !
Contexte de bloc
L’objet block contient des informations sur le bloc courant :
- block.blockhash(__blockNumber__)
-
Le hachage de bloc du numéro de bloc spécifié, jusqu’à 256 blocs dans le passé. Obsolète et remplacé par la fonction blockhash dans Solidity v0.4.22.
- block.coinbase
-
L’adresse du destinataire des frais du bloc actuel et de la récompense du bloc.
- block.difficulty
-
La difficulté (preuve de travail) du bloc actuel.
- block.gaslimit
-
La quantité maximale de gaz pouvant être dépensée pour toutes les transactions incluses dans le bloc actuel.
- block.number
-
Le numéro de bloc actuel (hauteur de la chaîne de blocs).
- block.timestamp
-
L’horodatage placé dans le bloc actuel par le mineur (nombre de secondes depuis l’époque Unix).
objet d’adresse
Toute adresse, transmise en tant qu’entrée ou extraite d’un objet de contrat, possède un certain nombre d’attributs et de méthodes :
- address.balance
-
Le solde de l’adresse, en wei. Par exemple, le solde actuel du contrat est address(this).balance.
- address.transfer(__amount__)
-
Transfère le montant (en wei) à cette adresse, en levant une exception en cas d’erreur. Nous avons utilisé cette fonction dans notre exemple Faucet comme méthode sur l’adresse
msg.sender
, comme msg.sender.transfer. - address.send(__amount__)
-
similaire à
transfer
, mais au lieu de lancer une exception, il renvoie false en cas d’erreur. ATTENTION : vérifiez toujours la valeur de retour de send. - address.call(__payload__)
-
fonction CALL de bas niveau — peut construire un appel de message arbitraire avec une charge utile de données. Renvoie faux en cas d’erreur. AVERTISSEMENT : dangereux : le destinataire peut (accidentellement ou par malveillance) utiliser tout votre gaz, provoquant l’arrêt de votre contrat avec une exception OOG ; vérifiez toujours la valeur de retour de call.
- address.callcode(__payload__)
-
fonction CALLCODE de bas niveau, comme address(this).call(...) mais avec le code de ce contrat remplacé par celui de
address
. Renvoie faux en cas d’erreur. AVERTISSEMENT : utilisation avancée uniquement ! - address.delegatecall()
-
Fonction DELEGATECALL de bas niveau, comme callcode(...) mais avec le contexte msg complet vu par le contrat actuel. Renvoie faux en cas d’erreur. AVERTISSEMENT : utilisation avancée uniquement !
Fonctions intégrées
Les autres fonctions à noter sont :
- addmod, mulmod
-
Pour l’addition et la multiplication modulo. Par exemple, addmod(x,y,k) calcule (x + y) % k.
- keccak256,
sha256
, sha3, ripemd160 -
Fonctions pour calculer les hachages avec divers algorithmes de hachage standard.
- ecrecover
-
Récupère l’adresse utilisée pour signer un message à partir de la signature.
- selfdestruct(__recipient_address__)
-
Supprime le contrat actuel, en envoyant tout ether restant dans le compte à l’adresse du destinataire.
- this
-
L’adresse du compte de contrat en cours d’exécution.
Définition du contrat
Le type de données principal de Solidity est contract ; notre exemple Faucet définit simplement un objet contract
. Semblable à tout objet dans un langage orienté objet, le contrat est un conteneur qui inclut des données et des méthodes.
Solidity propose deux autres types d’objets qui s’apparentent à un contrat :
- interface
-
Une définition d’interface est structurée exactement comme un contrat, sauf qu’aucune des fonctions n’est définie, elles sont seulement déclarées. Ce type de déclaration est souvent appelé stub ou souche ; il vous indique les arguments des fonctions et les types de retour sans aucune implémentation. Une interface précise la « forme » d’un contrat ; lorsqu’elle est héritée, chacune des fonctions déclarées par l’interface doit être définie par l’enfant.
- library
-
Un contrat de bibliothèque est un contrat destiné à être déployé une seule fois et utilisé par d’autres contrats, en utilisant la méthode delegatecall (voir objet d’adresse).
Fonctions
Au sein d’un contrat, on définit des fonctions qui peuvent être appelées par une transaction EOA ou un autre contrat. Dans notre exemple Faucet
, nous avons deux fonctions : withdraw et la fonction (sans nom) fallback.
La syntaxe que nous utilisons pour déclarer une fonction dans Solidity est la suivante :
function FunctionName([parameters]) {public|private|internal|external} [pure|constant|view|payable] [modifiers] [returns (return types)]
Examinons chacun de ces composants :
- FunctionName
-
Le nom de la fonction, qui est utilisé pour appeler la fonction dans une transaction (à partir d’un EOA), à partir d’un autre contrat, ou même à partir du même contrat. Une fonction dans chaque contrat peut être définie sans nom, auquel cas c’est la fonction fallback, qui est appelée lorsqu’aucune autre fonction n’est nommée. La fonction de secours ne peut pas avoir d’arguments ni rien renvoyer.
- parameters
-
Après le nom, nous spécifions les arguments qui doivent être passés à la fonction, avec leurs noms et leurs types. Dans notre exemple
Faucet
, nous avons défini uint remove_amount comme le seul argument de la fonctionwithdraw
.
Le prochain ensemble de mots clés (public
, private, internal
, external) spécifie la visibilité de la fonction :
- public
-
Public est la valeur par défaut ; ces fonctions peuvent être appelées par d’autres contrats ou transactions EOA, ou depuis le contrat. Dans notre exemple
Faucet
, les deux fonctions sont définies comme publiques. - external
-
Les fonctions externes sont comme des fonctions publiques, sauf qu’elles ne peuvent pas être appelées depuis le contrat à moins d’être explicitement préfixées avec le mot-clé this.
- interne
-
Les fonctions internes ne sont accessibles qu’à partir du contrat - elles ne peuvent pas être appelées par un autre contrat ou une transaction EOA. Ils peuvent être appelés par des contrats dérivés (ceux qui héritent de celui-ci).
- private
-
Les fonctions privées sont comme des fonctions internes mais ne peuvent pas être appelées par les contrats dérivés.
Gardez à l’esprit que les termes internal et private sont quelque peu trompeurs. Toute fonction ou donnée à l’intérieur d’un contrat est toujours visible sur la chaîne de blocs publique, ce qui signifie que n’importe qui peut voir le code ou les données. Les mots clés décrits ici n’affectent que comment et quand une fonction peut être appelée.
Le deuxième ensemble de mots clés (pure
, constant, view
, payable) affecte le comportement de la fonction :
- constant ou view
-
Une fonction marquée comme view promet de ne modifier aucun état. Le terme constant est un alias pour la vue qui sera obsolète dans une future version. Pour le moment, le compilateur n’applique pas le modificateur
view
, ne produisant qu’un avertissement, mais cela devrait devenir un mot-clé appliqué dans la v0.5 de Solidity. - pure
-
Une fonction pure est une fonction qui ne lit ni écrit aucune variable dans le stockage. Il ne peut fonctionner que sur des arguments et renvoyer des données, sans référence à aucune donnée stockée. Les fonctions pures sont destinées à encourager la programmation de style déclaratif sans effets secondaires ni état.
- payable
-
Une fonction payable est une fonction qui peut accepter des paiements entrants. Les fonctions non déclarées comme payable rejetteront les paiements entrants. Il existe deux exceptions, dues à des décisions de conception dans l’EVM : les paiements coinbase et l’héritage SELFDESTRUCT seront payés même si la fonction de secours n’est pas déclarée comme
payable
, mais cela a du sens car l’exécution de code ne fait pas partie de ces paiements de toute façon.
Comme vous pouvez le voir dans notre exemple Faucet
, nous avons une fonction payante (la fonction de secours), qui est la seule fonction qui peut recevoir des paiements entrants.
Constructeur de contrat et autodestruction
Il existe une fonction spéciale qui n’est utilisée qu’une seule fois . Lorsqu’un contrat est créé, il exécute également la fonction constructor s’il en existe une, pour initialiser l’état du contrat. Le constructeur est exécuté dans la même transaction que la création du contrat. La fonction constructeur est facultative ; vous remarquerez que notre exemple Faucet n’en a pas.
Les constructeurs peuvent être spécifiés de deux manières. Jusqu’à et y compris dans Solidity v0.4.21, le constructeur est une fonction dont le nom correspond au nom du contrat, comme vous pouvez le voir ici :
contract MEContract {
function MEContract() {
// C'est le constructeur
}
}
La difficulté avec ce format est que si le nom du contrat est modifié et que le nom de la fonction constructeur n’est pas modifié, ce n’est plus un constructeur. De même, s’il y a une faute de frappe accidentelle dans la dénomination du contrat et/ou du constructeur, la fonction n’est plus un constructeur. Cela peut provoquer des bogues assez désagréables, inattendus et difficiles à trouver. Imaginez par exemple si le constructeur met le propriétaire du contrat à des fins de contrôle. Si la fonction n’est pas réellement le constructeur en raison d’une erreur de nommage, non seulement le propriétaire ne sera pas défini au moment de la création du contrat, mais la fonction peut également être déployée en tant que partie permanente et "appelable" du contrat, comme un fonction normale, permettant à tout tiers de détourner le contrat et d’en devenir le "propriétaire" après la création du contrat.
Pour résoudre les problèmes potentiels avec les fonctions constructeur basées sur le fait d’avoir un nom identique à celui du contrat, Solidity v0.4.22 introduit un mot-clé constructor qui fonctionne comme une fonction constructeur mais n’a pas de nom. Renommer le contrat n’affecte en rien le constructeur. De plus, il est plus facile d’identifier quelle fonction est le constructeur. Il ressemble à ceci :
pragma ^0.4.22
contract MEContract {
constructor () {
// C'est le constructeur
}
}
Pour résumer, le cycle de vie d’un contrat commence par une transaction de création à partir d’un compte EOA ou un contrat. S’il existe un constructeur, il est exécuté dans le cadre de la création du contrat, pour initialiser l’état du contrat lors de sa création, puis est supprimé.
L’autre la fin du cycle de vie du contrat est la destruction du contrat. Les contrats sont détruits par un opcode EVM spécial appelé SELFDESTRUCT
. Il s’appelait autrefois SUICIDE
, mais ce nom a été déprécié en raison des associations négatives du mot. Dans Solidity, cet opcode est exposé sous la forme d’une fonction intégrée de haut niveau appelée selfdestruct
, qui prend un argument : l’adresse pour recevoir tout solde d’ether restant dans le compte du contrat. Il ressemble à ceci :
selfdestruct(address recipient);
Notez que vous devez explicitement ajouter cette commande à votre contrat si vous voulez qu’il soit supprimable - c’est la seule façon de supprimer un contrat, et il n’est pas présent par défaut. De cette manière, les utilisateurs d’un contrat qui pourraient s’attendre à ce qu’un contrat soit là pour toujours peuvent être certains qu’un contrat ne peut pas être supprimé s’il ne contient pas d’opcode SELFDESTRUCT
.
Ajout d’un constructeur et autodestruction à notre exemple de robinet
L’exemple de contrat Faucet que nous avons introduit dans [intro_chapter] n’a pas de constructeur ou de fonctions selfdestruct
. C’est un contrat éternel qui ne peut pas être supprimé. Changeons cela en ajoutant un constructeur et une fonction selfdestruct
. Nous voulons probablement que selfdestruct soit appelable uniquement par l’EOA qui a initialement créé le contrat. Par convention, ceci est généralement stocké dans une variable d’adresse appelée owner
. Notre constructeur définit la variable owner
, et la fonction selfdestruct vérifiera d’abord que le propriétaire l’a appelée directement.
Tout d’abord, notre constructeur :
// Version du compilateur Solidity pour lequel ce programme a été écrit
pragma solidity ^0.4.22;
// Notre premier contrat est un robinet !
contract Faucet {
address owner;
// Initialiser le contrat Faucet : définir le propriétaire
constructor() {
owner = msg.sender;
}
[...]
Nous avons modifié la directive pragma pour spécifier la v0.4.22 comme version minimale pour cet exemple, car nous utilisons le nouveau mot-clé constructor introduit dans la v0.4.22 de Solidity. Notre contrat a maintenant une variable de type address nommée owner
. Le nom "owner" n’est en aucun cas spécial. Nous pourrions appeler cette variable d’adresse "potato" et l’utiliser toujours de la même manière. Le nom owner rend simplement son objectif clair.
Ensuite, notre constructeur, qui s’exécute dans le cadre de la transaction de création de contrat, attribue l’adresse de msg.sender à la variable owner
. Nous avons utilisé msg.sender dans la fonction remove pour identifier l’initiateur de la demande de withdraw
. Dans le constructeur, cependant, le msg.sender est l’EOA ou l’adresse du contrat qui a initié la création du contrat. Nous savons que c’est le cas car il s’agit d’une fonction constructeur : elle ne s’exécute qu’une seule fois, lors de la création du contrat.
Nous pouvons maintenant ajouter une fonction pour détruire le contrat. Nous devons nous assurer que seul le propriétaire peut exécuter cette fonction, nous utiliserons donc une instruction require pour contrôler l’accès. Voici à quoi cela ressemblera :
// Destructeur de contrat
function destroy() public {
require(msg.sender == owner);
selfdestruct(owner);
}
Si quelqu’un appelle cette fonction destroy depuis une adresse autre que owner
, elle échouera. Mais si la même adresse stockée dans owner par le constructeur l’appelle, le contrat s’autodétruira et enverra tout solde restant à l’adresse owner
. Notez que nous n’avons pas utilisé le non sécurisé tx.origin pour déterminer si le propriétaire souhaitait détruire le contrat. L’utilisation de tx.origin permettrait à des contrats malveillants de détruire votre contrat sans votre permission.
Modificateurs de fonction
Solidity offre un type spécial de fonction appelé modificateur de fonction. Vous appliquez des modificateurs aux fonctions en ajoutant le nom du modificateur dans la déclaration de la fonction. Les modificateurs sont le plus souvent utilisés pour créer des conditions qui s’appliquent à de nombreuses fonctions au sein d’un contrat. Nous avons déjà une instruction de contrôle d’accès dans notre fonction destroy
. Créons un modificateur de fonction qui exprime cette condition :
modifier onlyOwner {
require(msg.sender == owner);
_;
}
Ce modificateur de fonction, nommé onlyOwner
, définit une condition sur toute fonction qu’il modifie, exigeant que l’adresse stockée en tant que owner du contrat soit la même que l’adresse du msg.sender de la transaction. Il s’agit du modèle de conception de base pour le contrôle d’accès, permettant uniquement au propriétaire d’un contrat d’exécuter toute fonction qui a le modificateur onlyOwner.
Vous avez peut-être remarqué que notre modificateur de fonction contient un "espace réservé" syntaxique particulier, un trait de soulignement suivi d’un point-virgule (_;). Cet espace réservé est remplacé par le code de la fonction en cours de modification. Essentiellement, le modificateur est "enroulé autour" de la fonction modifiée, plaçant son code à l’emplacement identifié par le caractère de soulignement.
Pour appliquer un modificateur, vous ajoutez son nom à la déclaration de la fonction. Plusieurs modificateurs peuvent être appliqués à une fonction ; ils sont appliqués dans l’ordre dans lequel ils sont déclarés, sous forme de liste séparée par des virgules.
Réécrivons notre fonction destroy pour utiliser le modificateur onlyOwner :
function destroy() public onlyOwner {
selfdestruct(owner);
}
Le nom du modificateur de fonction (onlyOwner
)se trouve après le mot-clé public et nous indique que la fonction destroy est modifiée par le modificateur onlyOwner
. Essentiellement, vous pouvez lire cela comme "Seul le propriétaire peut détruire ce contrat". En pratique, le code résultant équivaut à "envelopper" le code de onlyOwner autour de destroy.
Les modificateurs de fonction sont un outil extrêmement utile car ils nous permettent d’écrire des conditions préalables pour les fonctions et de les appliquer de manière cohérente, ce qui rend le code plus facile à lire et, par conséquent, plus facile à auditer pour la sécurité. Ils sont le plus souvent utilisés pour le contrôle d’accès, mais ils sont assez polyvalents et peuvent être utilisés à diverses autres fins.
A l’intérieur d’un modificateur, vous pouvez accéder à toutes les valeurs (variables et arguments) visibles par la fonction modifiée. Dans ce cas, nous pouvons accéder à la variable owner
, qui est déclarée dans le contrat. Cependant, l’inverse n’est pas vrai : vous ne pouvez accéder à aucune des variables du modificateur à l’intérieur de la fonction modifiée.
Héritage du contrat
L’objet contract de Solidity prend en charge l'héritage, qui est un mécanisme permettant d’étendre un contrat de base avec des fonctionnalités supplémentaires. Pour utiliser l’héritage, spécifiez un contrat parent avec le mot-clé is :
contract Child is Parent {
...
}
Avec cette construction, le contrat Child hérite de toutes les méthodes, fonctionnalités et variables de Parent
. Solidity prend également en charge l’héritage multiple, qui peut être spécifié par des noms de contrat séparés par des virgules après le mot-clé is :
contract Child is Parent1, Parent2 {
...
}
L’héritage de contrat nous permet d’écrire nos contrats de manière à atteindre la modularité, l’extensibilité et la réutilisation. Nous commençons par des contrats simples et implémentons les capacités les plus génériques, puis nous les étendons en héritant de ces capacités dans des contrats plus spécialisés.
Dans notre contrat Faucet
, nous avons introduit le constructeur et le destructeur, ainsi qu’un contrôle d’accès pour un propriétaire, assigné à la construction. Ces capacités sont assez génériques : de nombreux contrats en auront. Nous pouvons les définir comme des contrats génériques, puis utiliser l’héritage pour les étendre au contrat Faucet.
Nous commençons par définir un contrat de base owned
, qui a une variable owner
, en la définissant dans le constructeur du contrat :
contract owned {
address owner;
// Constructeur de contrat : définit le propriétaire
constructor() {
owner = msg.sender;
}
// Modificateur de contrôle d'accès
modifier onlyOwner {
require(msg.sender == owner);
_;
}
}
Ensuite, nous définissons un contrat de base mortal
, qui hérite de owned :
contract mortal is owned {
// Contract destructor
function destroy() public onlyOwner {
selfdestruct(owner);
}
}
Comme vous pouvez le voir, le contrat mortal peut utiliser le modificateur de fonction onlyOwner
, défini dans owned
. Il utilise aussi indirectement la variable d’adresse owner et le constructeur défini dans owned
. L’héritage rend chaque contrat plus simple et axé sur sa fonctionnalité spécifique, nous permettant de gérer les détails de manière modulaire.
Nous pouvons maintenant étendre davantage le contrat owned
, en héritant de ses capacités dans Faucet :
contract Faucet is mortal {
// Donnez de l'ether à quiconque demande
function withdraw(uint withdraw_amount) public {
// Limiter le montant du retrait
require(withdraw_amount <= 0.1 ether);
// Envoie le montant à l'adresse qui l'a demandé
msg.sender.transfer(withdraw_amount);
}
// Accepte tout montant entrant
function () external payable {}
}
En héritant de mortal
, qui à son tour hérite de owned
, le contrat Faucet a maintenant les fonctions constructeur et destroy
, et un propriétaire défini. La fonctionnalité est la même que lorsque ces fonctions étaient dans Faucet
, mais maintenant nous pouvons réutiliser ces fonctions dans d’autres contrats sans les réécrire. La réutilisation et la modularité du code rendent notre code plus propre, plus facile à lire et plus facile à auditer.
Gestion des erreurs (assert, require, revert)
Un appel de contrat peut se terminer et renvoyer une erreur. La gestion des erreurs dans Solidity est gérée par quatre fonctions : assert
, require, revert et throw (désormais obsolète).
Lorsqu’un contrat se termine avec une erreur, tous les changements d’état (changements de variables, de soldes, etc.) sont annulés, tout au long de la chaîne d’appels de contrat si plusieurs contrats ont été appelés. Cela garantit que les transactions sont atomiques, ce qui signifie qu’elles se terminent avec succès ou n’ont aucun effet sur l’état et sont entièrement annulées.
Les fonctions assert et require fonctionnent de la même manière, évaluant une condition et arrêtant l’exécution avec une erreur si la condition c’est fausse. Par convention, assert est utilisé lorsque le résultat est censé être vrai, ce qui signifie que nous utilisons assert pour tester les conditions internes. Par comparaison, require est utilisé lors du test d’entrées (telles que des arguments de fonction ou des champs de transaction), définissant nos attentes pour ces conditions.
Nous avons utilisé require dans notre modificateur de fonction onlyOwner
, pour tester que l’expéditeur du message est le propriétaire du contrat :
require(msg.sender == owner);
La fonction require agit comme une condition d’entrée, empêchant l’exécution du reste de la fonction et produisant une erreur si elle n’est pas satisfaite.
Depuis Solidity v0.4.22, require peut également inclure un message texte utile qui peut être utilisé pour indiquer la raison de l’erreur. Le message d’erreur est enregistré dans le journal des transactions. Ainsi, nous pouvons améliorer notre code en ajoutant un message d’erreur dans notre fonction require :
require(msg.sender == owner, "Seul le propriétaire du contrat peut appeler cette fonction");
Les fonctions revert et throw interrompent l’exécution du contrat et annulent tout changement d’état. La fonction throw est obsolète et sera supprimée dans les futures versions de Solidity ; vous devriez utiliser revert à la place. La fonction revert peut également prendre un message d’erreur comme seul argument, qui est enregistré dans le journal des transactions.
Certaines conditions d’un contrat généreront des erreurs, que nous les vérifiions explicitement ou non. Par exemple, dans notre contrat Faucet
, nous ne vérifions pas s’il y a suffisamment d’ether pour satisfaire une demande de retrait. En effet, la fonction transfer échouera avec une erreur et annulera la transaction si le solde est insuffisant pour effectuer le transfert :
msg.sender.transfer(withdraw_amount);
Cependant, il peut être préférable de vérifier explicitement et de fournir un message d’erreur clair en cas d’échec. Nous pouvons le faire en ajoutant une instruction require avant le transfert :
require(this.balance >= withdraw_amount,
"Solde insuffisant dans le robinet pour la demande de retrait" );
msg.sender.transfer(withdraw_amount);
Un code de vérification d’erreur supplémentaire comme celui-ci augmentera légèrement la consommation de gaz, mais il offre un meilleur rapport d’erreur que s’il est omis. Vous devrez trouver le bon équilibre entre la consommation de gaz et la vérification détaillée des erreurs en fonction de l’utilisation prévue de votre contrat. Dans le cas d’un contrat Faucet destiné à un testnet, nous pouvons probablement abuser du côté des rapports supplémentaires même si cela coûte plus cher en gaz. Peut-être que pour un contrat de réseau principal, nous choisirons plutôt d’être économes avec notre consommation de gaz.
Événements
Lorsque une transaction se termine (avec succès ou non), elle produit une réception de transaction, comme nous le verrons dans [evm_chapter]. Le reçu de transaction contient des entrées log qui fournissent des informations sur les actions qui se sont produites lors de l’exécution de la transaction. Les Événements ou Events sont les objets de haut niveau de Solidity utilisés pour construire ces journaux.
Les événements sont particulièrement utiles pour les clients légers et les services DApp, qui peuvent "surveiller" des événements spécifiques et les signaler à l’interface utilisateur, ou modifier l’état de l’application pour refléter un événement dans un contrat sous-jacent.
Les objets d’événement prennent des arguments qui sont sérialisés et enregistrés dans les journaux de transactions, dans la chaîne de blocs. Vous pouvez fournir le mot-clé indexé avant un argument, pour que la valeur fasse partie d’une table indexée (table de hachage) qui peut être recherchée ou filtrée par une application.
Nous n’avons ajouté aucun événement dans notre exemple Faucet jusqu’à présent, alors faisons cela. Nous ajouterons deux événements, un pour enregistrer les retraits et un pour enregistrer les dépôts. Nous appellerons ces événements Withdrawal et Deposit
, respectivement. Tout d’abord, nous définissons les événements dans le contrat Faucet :
contract Faucet is mortal {
event Withdrawal(address indexed to, uint amount);
event Deposit(address indexed from, uint amount);
[...]
}
Nous avons choisi de rendre les adresses indexed
, pour permettre la recherche et le filtrage dans n’importe quelle interface utilisateur conçue pour accéder à notre Faucet.
Ensuite, nous utilisons le mot-clé emit pour incorporer les données d’événement dans les journaux de transactions :
// Donnez de l'ether à quiconque demande
function withdraw(uint withdraw_amount) public {
[...]
msg.sender.transfer(withdraw_amount);
emit Withdrawal(msg.sender, withdraw_amount);
}
// Accepte tout montant entrant
function () external payable {
emit Deposit(msg.sender, msg.value);
}
Le contrat Faucet.sol résultant ressemble à Faucet8.sol : contrat de robinet révisé, avec événements.
// Identifiant de licence SPDX : CC-BY-SA-4.0
// Version du compilateur Solidity pour lequel ce programme a été écrit
pragma solidity ^0.6.4;
contract Owned {
address payable owner;
// Constructeur de contrat : définit le propriétaire
constructor() public {
owner = msg.sender;
}
// Modificateur de contrôle d'accès
modifier onlyOwner {
require(msg.sender == owner, "Only the contract owner can call this function");
_;
}
}
contract Mortal is Owned {
// Destructeur de contrat
function destroy() public onlyOwner {
selfdestruct(owner);
}
}
contract Faucet is Mortal {
event Withdrawal(address indexed to, uint amount);
event Deposit(address indexed from, uint amount);
// Accepte tout montant entrant
receive() external payable {
emit Deposit(msg.sender, msg.value);
}
// Donnez de l'éther à quiconque demande
function withdraw(uint withdraw_amount) public {
// Limiter le montant du retrait
require(withdraw_amount <= 0.1 ether);
require(
address(this).balance >= withdraw_amount,
"Insufficient balance in faucet for withdrawal request"
);
// Envoie le montant à l'adresse qui l'a demandé
msg.sender.transfer(withdraw_amount);
emit Withdrawal(msg.sender, withdraw_amount);
}
}
Attraper des événements
OK, nous avons donc configuré notre contrat pour émettre des événements. Comment voir les résultats d’une transaction et "attraper" les événements ? La bibliothèque web3.js fournit une structure de données qui contient les journaux d’une transaction. Dans ceux-ci, nous pouvons voir les événements générés par la transaction.
Utilisons truffle pour exécuter une transaction de test sur le contrat Faucet révisé. Suivez les instructions dans [truffle] pour configurer un répertoire de projet et compiler le code Faucet
. Le code source peut être trouvé dans le référentiel GitHub du livre sous code/truffle/FaucetEvents.
$ truffle develop truffle(develop)> compile truffle(develop)> migrate Using network 'develop'. Running migration: 1_initial_migration.js Deploying Migrations... ... 0xb77ceae7c3f5afb7fbe3a6c5974d352aa844f53f955ee7d707ef6f3f8e6b4e61 Migrations: 0x8cdaf0cd259887258bc13a92c0a6da92698644c0 Saving successful migration to network... ... 0xd7bc86d31bee32fa3988f1c1eabce403a1b5d570340a3a9cdba53a472ee8c956 Saving artifacts... Running migration: 2_deploy_contracts.js Deploying Faucet... ... 0xfa850d754314c3fb83f43ca1fa6ee20bc9652d891c00a2f63fd43ab5bfb0d781 Faucet: 0x345ca3e014aaf5dca488057592ee47305d9b3e10 Saving successful migration to network... ... 0xf36163615f41ef7ed8f4a8f192149a0bf633fe1a2398ce001bf44c43dc7bdda0 Saving artifacts... truffle(develop)> Faucet.deployed().then(i => {FaucetDeployed = i}) truffle(develop)> FaucetDeployed.send(web3.utils.toWei(1, "ether")).then(res => \ { console.log(res.logs[0].event, res.logs[0].args) }) Deposit { from: '0x627306090abab3a6e1400e9345bc60c78a8bef57', amount: BigNumber { s: 1, e: 18, c: [ 10000 ] } } truffle(develop)> FaucetDeployed.withdraw(web3.utils.toWei(0.1, "ether")).then(res => \ { console.log(res.logs[0].event, res.logs[0].args) }) Withdrawal { to: '0x627306090abab3a6e1400e9345bc60c78a8bef57', amount: BigNumber { s: 1, e: 17, c: [ 1000 ] } }
Après avoir déployé le contrat à l’aide de la fonction deployed
, nous exécutons deux transactions. La première transaction est un dépôt (en utilisant send), qui émet un événement Deposit dans les journaux de transactions :
Deposit { from: '0x627306090abab3a6e1400e9345bc60c78a8bef57', amount: BigNumber { s: 1, e: 18, c: [ 10000 ] } }
Ensuite, nous utilisons la fonction withdraw pour effectuer un retrait. Cela émet un événement Withdrawal :
Withdrawal { to: '0x627306090abab3a6e1400e9345bc60c78a8bef57', amount: BigNumber { s: 1, e: 17, c: [ 1000 ] } }
Pour obtenir ces événements, nous avons examiné le tableau logs renvoyé comme résultat (res
)des transactions. La première entrée de journal (logs[0]
)contient un nom d’événement dans logs[0].event et les arguments d’événement dans logs[0].args
. En les affichant sur la console, nous pouvons voir le nom de l’événement émis et les arguments de l’événement.
Les événements sont un mécanisme très utile, non seulement pour la communication intra-contrat, mais aussi pour le débogage pendant le développement.
Appel d’autres contrats (send, call, callcode, delegatecall)
Appeler d’autres contrats depuis votre contrat est une opération très utile mais potentiellement dangereuse. Nous examinerons les différentes façons d’y parvenir et évaluerons les risques de chaque méthode. En bref, les risques découlent du fait que vous ne savez peut-être pas grand-chose sur un contrat auquel vous faites appel ou qui fait appel à votre contrat. Lors de la rédaction de contrats intelligents, vous devez garder à l’esprit que même si vous vous attendez principalement à traiter avec des EOA, rien n’empêche des contrats arbitrairement complexes et peut-être malveillants d’appeler et d’être appelés par votre code.
Création d’une nouvelle instance
Le moyen le plus sûr d’appeler un autre contrat est de créer vous-même cet autre contrat. De cette façon, vous êtes certain de ses interfaces et de son comportement. Pour ce faire, vous pouvez simplement l’instancier, en utilisant le mot-clé new
, comme dans d’autres langages orientés objet. Dans Solidity, le mot-clé new créera le contrat sur la chaîne de blocs et renverra un objet que vous pourrez utiliser pour le référencer. Supposons que vous souhaitiez créer et appeler un contrat Faucet à partir d’un autre contrat appelé Token :
contract Token is mortal {
Faucet _faucet;
constructor() {
_faucet = new Faucet();
}
}
Ce mécanisme de construction de contrat garantit que vous connaissez le type exact du contrat et son interface. Le contrat Faucet doit être défini dans la portée de Token
, ce que vous pouvez faire avec une instruction import si la définition se trouve dans un autre fichier :
import "Faucet.sol";
contract Token is mortal {
Faucet _faucet;
constructor() {
_faucet = new Faucet();
}
}
Vous pouvez éventuellement spécifier la value du transfert d’ether lors de la création et passer des arguments au constructeur du nouveau contrat :
import "Faucet.sol";
contract Token is mortal {
Faucet _faucet;
constructor() {
_faucet = (new Faucet).value(0.5 ether)();
}
}
Vous pouvez également appeler ensuite les fonctions Faucet
. Dans cet exemple, nous appelons la fonction destroy de Faucet depuis la fonction destroy de Token :
import "Faucet.sol";
contract Token is mortal {
Faucet _faucet;
constructor() {
_faucet = (new Faucet).value(0.5 ether)();
}
function destroy() ownerOnly {
_faucet.destroy();
}
}
Notez que tant que vous êtes le propriétaire du contrat Token
, le contrat Token lui-même possède le nouveau contrat Faucet
, donc seul le contrat Token peut le détruire.
Adressage d’une instance existante
Une autre façon d’appeler un contrat consiste à convertir l’adresse d’une instance existante du contrat. Avec cette méthode, vous appliquez une interface connue à une instance existante. Il est donc extrêmement important que vous sachiez avec certitude que l’instance à laquelle vous vous adressez est en fait du type que vous supposez. Regardons un exemple :
import "Faucet.sol";
contract Token is mortal {
Faucet _faucet;
constructor(address _f) {
_faucet = Faucet(_f);
_faucet.withdraw(0.1 ether)
}
}
Ici, nous prenons une adresse fournie comme argument au constructeur, _f
, et nous la transformons en un objet Faucet
. C’est beaucoup plus risqué que le mécanisme précédent, car nous ne savons pas avec certitude si cette adresse est réellement un objet Faucet
. Lorsque nous appelons withdraw
, nous supposons qu’il accepte les mêmes arguments et exécute le même code que notre déclaration Faucet
, mais nous ne pouvons pas en être sûrs. Pour autant que nous sachions, la fonction withdraw à cette adresse pourrait exécuter quelque chose de complètement différent de ce que nous attendons, même si elle porte le même nom. Utiliser des adresses passées en entrée et les intégrer dans des objets spécifiques est donc beaucoup plus dangereux que de créer soi-même le contrat.
Appel brut call et delegatecall
Solidity propose des fonctions encore plus "de bas niveau" pour appeler d’autres contrats. Ceux-ci correspondent directement aux opcodes EVM du même nom et nous permettent de construire manuellement un appel de contrat à contrat. En tant que tels, ils représentent les mécanismes les plus flexibles et les plus dangereux pour appeler d’autres contrats.
Voici le même exemple, en utilisant une méthode call :
contract Token is mortal {
constructor(address _faucet) {
_faucet.call("withdraw", 0.1 ether);
}
}
Comme vous pouvez le voir, ce type de call est un appel aveugle dans une fonction, un peu comme la construction d’une transaction brute, uniquement à partir du contexte d’un contrat. Cela peut exposer votre contrat à un certain nombre de risques de sécurité, le plus important étant la réentrance, dont nous discuterons plus en détail dans [reentrancy_security]. La fonction call renverra false s’il y a un problème, vous pouvez donc évaluer la valeur de retour pour la gestion des erreurs :
contract Token is mortal {
constructor(address _faucet) {
if !(_faucet.call("withdraw", 0.1 ether)) {
revert("Withdrawal from faucet failed");
}
}
}
Une autre variante de call est delegatecall
, qui a remplacé le plus dangereux callcode
. La méthode callcode
sera bientôt obsolète, elle ne doit donc pas être utilisée.
Comme mentionné dans objet d’adresse, un delegatecall est différent d’un call en ce que le contexte msg ne change pas. Par exemple, alors qu’un call change la valeur de msg.sender pour être le contrat d’appel, un delegatecall conserve le même msg.sender que dans le contrat d’appel. Essentiellement, delegatecall exécute le code d’un autre contrat dans le contexte de l’exécution du contrat en cours. Il est le plus souvent utilisé pour invoquer du code à partir d’une bibliothèque. Cela vous permet également de dessiner sur le modèle d’utilisation des fonctions de bibliothèque stockées ailleurs, mais de faire fonctionner ce code avec les données de stockage de votre contrat.
L’appel delegatecall doit être utilisé avec beaucoup de prudence. Cela peut avoir des effets inattendus, surtout si le contrat que vous appelez n’a pas été conçu comme une bibliothèque.
Utilisons un exemple de contrat pour démontrer les différentes sémantiques d’appel utilisées par call et delegatecall pour appeler des bibliothèques et des contrats. Dans CallExamples.sol : un exemple de différentes sémantiques d’appel, nous utilisons un événement pour enregistrer les détails de chaque appel et voir comment le contexte d’appel change en fonction du type d’appel.
pragma solidity ^0.4.22;
contract calledContract {
event callEvent(address sender, address origin, address from);
function calledFunction() public {
emit callEvent(msg.sender, tx.origin, this);
}
}
library calledLibrary {
event callEvent(address sender, address origin, address from);
function calledFunction() public {
emit callEvent(msg.sender, tx.origin, this);
}
}
contract caller {
function make_calls(calledContract _calledContract) public {
// Appel calledContract et callLibrary directement
_calledContract.calledFunction();
calledLibrary.calledFunction();
// Appels de bas niveau utilisant l'objet d'adresse pour le contrat appelé
require(address(_calledContract).
call(bytes4(keccak256("calledFunction()"))));
require(address(_calledContract).
delegatecall(bytes4(keccak256("calledFunction()"))));
}
}
Comme vous pouvez le voir dans cet exemple, notre contrat principal est caller
, qui appelle une bibliothèque calledLibrary et un contrat calledContract
. La bibliothèque appelée et le contrat ont des fonctions calledFunction identiques, qui émettent un événement calledEvent
. L’événement calledEvent enregistre trois éléments de données : msg.sender
, tx.origin et this
. Chaque fois que calledFunction est appelé, il peut avoir un contexte d’exécution différent (avec des valeurs différentes pour potentiellement toutes les variables de contexte), selon qu’il est appelé directement ou via delegatecall.
Dans caller
, nous appelons d’abord le contrat et la bibliothèque directement, en invoquant calledFunction dans chacun. Ensuite, nous utilisons explicitement les fonctions de bas niveau call et delegatecall pour appeler calledContract.calledFunction
. De cette façon, nous pouvons voir comment les différents mécanismes d’appel se comportent.
Exécutons ceci dans un environnement de développement Truffle et capturons les événements, pour voir à quoi cela ressemble :
truffle(develop)> migrate Using network 'develop'. [...] Saving artifacts... truffle(develop)> web3.eth.accounts[0] '0x627306090abab3a6e1400e9345bc60c78a8bef57' truffle(develop)> caller.address '0x8f0483125fcb9aaaefa9209d8e9d7b9c8b9fb90f' truffle(develop)> calledContract.address '0x345ca3e014aaf5dca488057592ee47305d9b3e10' truffle(develop)> calledLibrary.address '0xf25186b5081ff5ce73482ad761db0eb0d25abfbf' truffle(develop)> caller.deployed().then( i => { callerDeployed = i }) truffle(develop)> callerDeployed.make_calls(calledContract.address).then(res => \ { res.logs.forEach( log => { console.log(log.args) })}) { sender: '0x8f0483125fcb9aaaefa9209d8e9d7b9c8b9fb90f', origin: '0x627306090abab3a6e1400e9345bc60c78a8bef57', from: '0x345ca3e014aaf5dca488057592ee47305d9b3e10' } { sender: '0x627306090abab3a6e1400e9345bc60c78a8bef57', origin: '0x627306090abab3a6e1400e9345bc60c78a8bef57', from: '0x8f0483125fcb9aaaefa9209d8e9d7b9c8b9fb90f' } { sender: '0x8f0483125fcb9aaaefa9209d8e9d7b9c8b9fb90f', origin: '0x627306090abab3a6e1400e9345bc60c78a8bef57', from: '0x345ca3e014aaf5dca488057592ee47305d9b3e10' } { sender: '0x627306090abab3a6e1400e9345bc60c78a8bef57', origin: '0x627306090abab3a6e1400e9345bc60c78a8bef57', from: '0x8f0483125fcb9aaaefa9209d8e9d7b9c8b9fb90f' }
Voyons ce qui s’est passé ici. Nous avons appelé la fonction make_calls et passé l’adresse de calledContract
, puis avons intercepté les quatre événements émis par chacun des différents appels. Examinons la fonction make_calls et passons en revue chaque étape.
Le premier appel est :
_calledContract.calledFunction();
Ici, nous appelons calledContract.calledFunction directement, en utilisant l’ABI de haut niveau pour calledFunction
. L’événement émis est :
sender: '0x8f0483125fcb9aaaefa9209d8e9d7b9c8b9fb90f', origin: '0x627306090abab3a6e1400e9345bc60c78a8bef57', from: '0x345ca3e014aaf5dca488057592ee47305d9b3e10'
Comme vous pouvez le voir, msg.sender est l’adresse du contrat caller
. Le tx.origin est l’adresse de notre compte, web3.eth.accounts[0]
, qui a envoyé la transaction à caller
. L’événement a été émis par calledContract
, comme nous pouvons le voir sur le dernier argument de l’événement.
Le prochain appel dans make_calls est vers la bibliothèque :
calledLibrary.calledFunction();
Il semble identique à la façon dont nous avons appelé le contrat, mais se comporte très différemment. Regardons le second événement émis :
sender: '0x627306090abab3a6e1400e9345bc60c78a8bef57', origin: '0x627306090abab3a6e1400e9345bc60c78a8bef57', from: '0x8f0483125fcb9aaaefa9209d8e9d7b9c8b9fb90f'
Cette fois, le msg.sender n’est pas l’adresse de caller
. Au lieu de cela, c’est l’adresse de notre compte, et c’est la même que l’origine de la transaction. En effet, lorsque vous appelez une bibliothèque, l’appel est toujours delegatecall et s’exécute dans le contexte de l’appelant. Ainsi, lorsque le code calledLibrary s’exécutait, il héritait du contexte d’exécution de caller
, comme si son code s’exécutait à l’intérieur de caller
. La variable this (affichée comme from dans l’événement émis) est l’adresse de caller
, même si elle est accessible depuis calledLibrary
.
Les deux appels suivants, utilisant les appels call et delegatecall de bas niveau, vérifient nos attentes, émettant des événements qui reflètent ce que nous venons de faire
Considérations sur le gaz
Le gaz, décrit dans plus de détails dans [gas], est une considération extrêmement importante dans la programmation de contrats intelligents. Le gaz est une ressource limitant la quantité maximale de calculs qu’Ethereum permettra à une transaction de consommer. Si la limite de gaz est dépassée pendant le calcul, la série d’événements suivante se produit :
-
Une exception "out of gas" est levée.
-
L’état du contrat avant l’exécution est restauré (inversé).
-
Tout l’ether utilisé pour payer le gaz est considéré comme des frais de transaction ; il n’est pas remboursé.
Étant donné que le gaz est payé par l’utilisateur qui initie la transaction, les utilisateurs sont découragés d’appeler des fonctions qui ont un coût de gaz élevé. Il est donc dans l’intérêt du programmeur de minimiser le coût en gaz des fonctions d’un contrat. À cette fin, certaines pratiques sont recommandées lors de la construction de contrats intelligents, afin de minimiser le coût en gaz d’un appel de fonction.
Évitez les tableaux dimensionnés dynamiquement
Toute boucle à travers un tableau de taille dynamique où une fonction effectue des opérations sur chaque élément ou recherche un élément particulier introduit le risque d’utiliser trop de gaz. En effet, le contrat peut s’essouffler avant d’avoir trouvé le résultat souhaité, ou avant d’agir sur chaque élément, perdant ainsi du temps et de l’ether sans donner le moindre résultat.
Évitez les appels vers d’autres contrats
Appeler d’autres contrats, surtout lorsque le coût en gaz de leurs fonctions n’est pas connu, introduit le risque de manquer de gaz. Évitez d’utiliser des bibliothèques qui ne sont pas bien testées et largement utilisées. Moins une bibliothèque a été examinée par d’autres programmeurs, plus le risque de l’utiliser est grand.
Estimation du coût du gaz
Si vous avez besoin d’estimer le gaz nécessaire pour exécuter une certaine méthode d’un contrat en tenant compte ses arguments, vous pouvez utiliser la procédure suivante :
var contract = web3.eth.contract(abi).at(address);
var gasEstimate = contract.myAweSomeMethod.estimateGas(arg1, arg2,
{from: account});
gasEstimate vous indiquera le nombre d’unités de gaz nécessaires à son exécution. Il s’agit d’une estimation en raison de l’exhaustivité de Turing de l’EVM - il est relativement trivial de créer une fonction qui prendra des quantités de gaz très différentes pour exécuter différents appels. Même le code de production peut modifier les chemins d’exécution de manière subtile, ce qui entraîne des coûts de gaz extrêmement différents d’un appel à l’autre. Cependant, la plupart des fonctions sont sensibles et estimateGas donnera une bonne estimation la plupart du temps.
Pour obtenir le prix du gaz du réseau, vous pouvez utiliser :
var gasPrice = web3.eth.getGasPrice();
Et à partir de là, vous pouvez estimer le coût du gaz :
var gasCostInEther = web3.utils.fromWei((gasEstimate * gasPrice), 'ether');
Appliquons nos fonctions d’estimation de gaz pour estimer le coût du gaz de notre exemple Faucet
, en utilisant le code du référentiel du livre.
Démarrez Truffle en mode développement et exécutez le fichier JavaScript dans gas_estimates.js : Utilisation de la fonction estimateGas, gas_estimates.js.
var FaucetContract = artifacts.require("./Faucet.sol");
FaucetContract.web3.eth.getGasPrice(function(error, result) {
var gasPrice = Number(result);
console.log("Gas Price is " + gasPrice + " wei"); // "10000000000000"
// Récupère l'instance de contrat
FaucetContract.deployed().then(function(FaucetContractInstance) {
// Utilisez le mot clé 'estimateGas' après le nom de la fonction pour obtenir le gaz
// estimation pour cette fonction particulière (aprove)
FaucetContractInstance.send(web3.utils.toWei(1, "ether"));
return FaucetContractInstance.withdraw.estimateGas(web3.utils.toWei(0.1, "ether"));
}).then(function(result) {
var gas = Number(result);
console.log("gas estimation = " + gas + " units");
console.log("gas cost estimation = " + (gas * gasPrice) + " wei");
console.log("gas cost estimation = " +
FaucetContract.web3.utils.fromWei((gas * gasPrice), 'ether') + " ether");
});
});
Voici à quoi cela ressemble dans la console de développement Truffle :
$ truffle develop truffle(develop)> exec gas_estimates.js Using network 'develop'. Gas Price is 20000000000 wei gas estimation = 31397 units gas cost estimation = 627940000000000 wei gas cost estimation = 0.00062794 ether
Il est recommandé d’évaluer le coût en gaz des fonctions dans le cadre de votre workflow de développement, afin d’éviter toute surprise lors du déploiement de contrats sur le réseau principal.
Conclusion
Dans ce chapitre, nous avons commencé à travailler en détail avec les contrats intelligents et exploré le langage de programmation de contrats Solidity. Nous avons pris un exemple de contrat simple, Faucet.sol, et l’avons progressivement amélioré et rendu plus complexe, en l’utilisant pour explorer divers aspects du langage Solidity. Dans [vyper_chap] nous travaillerons avec Vyper, un autre langage de programmation orienté contrat. Nous comparerons Vyper à Solidity, montrant certaines des différences dans la conception de ces deux langages et approfondirons notre compréhension de la programmation de contrats intelligents.