Solidity
Solidity est un langage orienté objet et de haut niveau pour la mise en œuvre de contrats intelligents. Les contrats intelligents sont des programmes qui régissent le comportement des comptes dans l’état Ethereum.
Solidity est un langage d’accolades. Il est influencé par le C++, le Python et le JavaScript, et est conçu pour cibler la machine virtuelle Ethereum (EVM). Vous pouvez trouver plus de détails sur les langages dont Solidity s’est inspiré dans la section sur les influences linguistiques.
Solidity est typé statiquement, supporte l’héritage, les bibliothèques et les types complexes définis par l’utilisateur, entre autres caractéristiques.
Avec Solidity, vous pouvez créer des contrats pour des utilisations telles que le vote, le crowdfunding, les enchères à l’aveugle et les portefeuilles à signatures multiples.
Lorsque vous déployez des contrats, vous devez utiliser la dernière version publiée de Solidity. Sauf cas exceptionnel, seule la dernière version reçoit des correctifs de sécurité. En outre, les changements de rupture ainsi que les nouvelles fonctionnalités sont introduites régulièrement. Nous utilisons actuellement un numéro de version 0.y.z pour indiquer ce rythme rapide de changement.
Solidity a récemment publié la version 0.8.x qui a introduit de nombreux changements. Assurez-vous de lire la liste complète.
Les idées pour améliorer Solidity ou cette documentation sont toujours les bienvenues, lisez notre guide des contributeurs pour plus de détails.
Astuce
Vous pouvez télécharger cette documentation au format PDF, HTML ou Epub en cliquant sur le menu déroulant des versions dans le coin inférieur gauche et en sélectionnant le format de téléchargement préféré.
Pour commencer
1. Comprendre les bases des contrats intelligents
Si le concept des contrats intelligents est nouveau pour vous, nous vous recommandons de commencer par vous plonger dans la section « Introduction aux contrats intelligents ». dans la section « Introduction aux contrats intelligents », qui couvre :
Un exemple simple de smart contract écrit sous Solidity.
2. Apprenez à connaître Solidity
Une fois que vous êtes habitué aux bases, nous vous recommandons de lire les sections « Solidity by Example » et « Description du langage » pour comprendre les concepts fondamentaux du langage.
3. Installer le compilateur Solidity
Il existe plusieurs façons d’installer le compilateur Solidity. Il vous suffit de choisir votre option préférée et de suivre les étapes décrites sur la installation page.
Indication
Vous pouvez essayer des exemples de code directement dans votre navigateur grâce à la fonction Remix IDE. Remix est un IDE basé sur un navigateur web qui vous permet d’écrire, de déployer et d’administrer les smart contracts Solidity, sans avoir à sans avoir besoin d’installer Solidity localement.
Avertissement
Comme les humains écrivent des logiciels, ceux-ci peuvent comporter des bugs. Vous devez suivre les meilleures pratiques établies en matière de développement de logiciels lorsque vous écrivez vos contrats intelligents. Cela inclut la révision du code, les tests, les audits et les preuves de correction. Les utilisateurs de contrats intelligents sont parfois plus confiants dans le code que ses auteurs, et les blockchains et les contrats intelligents ont leurs propres problèmes à surveiller. Avant de travailler sur le code de production, assurez-vous de lire la section Security Considerations.
4. En savoir plus
Si vous souhaitez en savoir plus sur la création d’applications décentralisées sur Ethereum, le programme Ethereum Developer Resources peut vous aider à trouver de la documentation générale sur Ethereum, ainsi qu’une large sélection de tutoriels, d’outils et de cadres de développement.
Si vous avez des questions, vous pouvez essayer de chercher des réponses ou de les poser sur Ethereum StackExchange, ou sur notre salon Gitter.
Traductions
Des bénévoles de la communauté aident à traduire cette documentation en plusieurs langues. Leur degré d’exhaustivité et de mise à jour varie. La version anglaise est une référence.
Note
Nous avons récemment mis en place une nouvelle organisation GitHub et un nouveau flux de traduction pour aider à rationaliser les efforts de la communauté. Veuillez vous référer au guide de traduction pour obtenir des informations sur la manière de contribuer aux traductions communautaires en cours.
Contenu
Index des mots-clés, Page de recherche
Introduction aux Smart Contracts
Un Smart Contract simple
Commençons par un exemple de base qui fixe la valeur d’une variable et l’expose pour l’accès par d’autres contrats. C’est très bien si vous ne comprenez pas tout maintenant, nous entrerons plus en détail plus tard.
Exemple: Stockage
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.16 <0.9.0;
contract SimpleStorage {
uint storedData;
function set(uint x) public {
storedData = x;
}
function get() public view returns (uint) {
return storedData;
}
}
The first line tells you that the source code is licensed under the GPL version 3.0. Machine-readable license specifiers are important in a setting where publishing the source code is the default.
La ligne suivante indique simplement que le code source est écrit pour Solidity version 0.4.16 ou tout ce qui est plus récent qui ne casse pas la fonctionnalité (jusqu’à la version 0.9.0, mais non comprise). Il s’agit de s’assurer que le contrat n’est pas compilable avec une nouvelle version du compilateur (de rupture), où il pourrait se comporter différemment. Les pré-cités pragmas sont des instructions courantes pour les compilateurs sur la façon de traiter le code source (par exemple pragma once).
Un contrat au sens de Solidity est un ensemble de code (ses fonctions) et les données
(son état) qui résident à une adresse spécifique sur la blockchain Ethereum. La ligne uint storedData;
déclare une variable d’état appelée storedData`
de type uint
(u*nsigned *int*eger de *256 bits). Vous
pouvez le considérer comme une case mémoire dans une base de données qui peut être interrogée et modifiée en appelant
les fonctions du code qui gèrent la base de données.
Et dans cet exemple, le contrat définit les fonctions set
et get
qui peuvent être utilisées pour modifier
ou récupérer la valeur de la variable.
Pour accéder à une variable d’état, vous n’avez pas besoin du préfixe this.
d’autres langues.
Unlike in some other languages, omitting it is not just a matter of style,
it results in a completely different way to access the member, but more on this later.
Ce contrat ne fait pas encore grand-chose en dehors de (en raison de l’infrastructure construite par Ethereum) permettre à n’importe qui de stocker un numéro unique qui est accessible par n’importe qui dans le monde sans un moyen (faisable) pour vous empêcher de publier ce numéro. Bien sûr, n’importe qui peut simplement appeler set
à nouveau avec une valeur différente.
et écraser votre numéro, mais le numéro sera toujours stocké dans l’historique de la blockchain. Plus tard, nous verrons comment vous pouvez imposer des restrictions d’accès pour que vous seul puissiez modifier le numéro.
Note
Tous les identifiants (noms de contrat, noms de fonctions et noms de variables) sont limités au jeu de caractères ASCII. Il est possible de stocker des données encodées en UTF-8 dans des variables de type string.
Avertissement
Soyez prudent lorsque vous utilisez du texte Unicode, car des caractères d’apparence similaire (ou même identique) peuvent avoir des codages unicode différents et seront donc codés sous la forme d’un tableau d’octets différent.
Exemple de sous-monnaie
Le contrat suivant mettra en œuvre la forme la plus simple d’un contrat de cryptomonnaie. Ce contrat autorise son créateur à générer des pièces à partir de rien (des schéma d’émission différents sont possibles). De plus, n’importe qui peut s’envoyer des pièces sans avoir besoin de s’enregistrer avec un nom d’utilisateur et un mot de passe - tout ce dont vous avez besoin est une paire de clés Ethereum.
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.4;
contract Coin {
// Le mot-clé "public" rend ces variables
// facilement accessible de l'exterieur.
address public minter;
mapping (address => uint) public balances;
// Les Events autorisent les clients légers à réagir
// aux changements efficacement.
event Sent(address from, address to, uint amount);
// C'est le constructor, code qui n'est exécuté
// qu'à la création du contrat.
constructor() {
minter = msg.sender;
}
// Sends an amount of newly created coins to an address
// Can only be called by the contract creator
function mint(address receiver, uint amount) public {
require(msg.sender == minter);
balances[receiver] += amount;
}
// Errors allow you to provide information about
// why an operation failed. They are returned
// to the caller of the function.
error InsufficientBalance(uint requested, uint available);
// Sends an amount of existing coins
// from any caller to an address
function send(address receiver, uint amount) public {
if (amount > balances[msg.sender])
revert InsufficientBalance({
requested: amount,
available: balances[msg.sender]
});
balances[msg.sender] -= amount;
balances[receiver] += amount;
emit Sent(msg.sender, receiver, amount);
}
}
Ce contrat introduit quelques nouveaux concepts, passons-les en revue un à un.
La ligne address public minter;
déclare une variable d’état de type address qui est accessible au public. Le type adress
est une valeur de 160 bits qui ne permet aucune opération arithmétique. Il convient pour le stockage des adresses de contrats ou de paires de clés appartenant à des `comptes externes<accounts>`_ .
Le mot-clé « public » génère automatiquement une fonction qui permet d’accéder à la valeur courante de la variable d’état de l’extérieur du contrat. Sans ce mot-clé, les autres contrats n’ont aucun moyen d’accéder à la variable.
Le code de la fonction générée par le compilateur est à peu près équivalent à ce qui suit (ignorez external'' et ``view
pour l’instant):
function minter() external view returns (address) { return minter; }
Bien sûr, l’ajout d’une fonction exactement comme celle-là ne fonctionnera pas parce que nous aurions une fonction et une variable d’état avec le même nom, mais vous avez l’idée - le compilateur réalisera cela pour vous.
La ligne suivante, mapping (" adress => uint ") public balances;
crée également une variable d’état publique, mais c’est un type de données plus complexe.
Le type mapping fait correspondre les adresses aux entiers non signés.
Les mappings peuvent être vus comme des tables de hachage qui sont
virtuellement initialisées de sorte que toutes les clés possibles existent dès le début et sont mappées à un fichier
dont la représentation octale n’est que de zéros. Cette analogie ne va pas
trop loin, car il n’est pas non plus possible d’obtenir une liste de toutes les clés d’un mapping, ni une liste de toutes les valeurs. Il faut donc garder à l’esprit (ou bien
mieux, gardez une liste ou utilisez un type de données plus avancé) ce que vous avez ajouté à la cartographie ou l’utiliser dans un contexte où cela n’est pas nécessaire.
La fonction getter créé par le mot-clé public
est un peu plus complexe dans ce cas. Ça ressemble grossièrement à ça:
function balances(address _account) external view returns (uint) {
return balances[_account];
}
Comme vous pouvez le voir, vous pouvez utiliser cette fonction pour interroger facilement le solde d’un seul compte.
La ligne event Sent(address from, address to, uint amount);
déclare un bien-nommé « event » qui est émis dans la dernière ligne de la fonction send
. Les interfaces utilisateur (ainsi que les applications serveur bien sûr) peuvent écouter les événements qui sont émis sur la blockchain sans trop de frais. Dès qu’elle est émise, l’auditeur reçoit également le message
des arguments « from », « to » et « amount », ce qui facilite le suivi des transactions.
Pour écouter cet événement, vous devriez utiliser le code JavaScript suivant (qui suppose que ``Coin` est un objet de contrat créé via web3.js ou un module similaire):
Coin.Sent().watch({}, '', function(error, result) {
if (!error) {
console.log("Coin transfer: " + result.args.amount +
" coins were sent from " + result.args.from +
" to " + result.args.to + ".");
console.log("Balances now:\n" +
"Sender: " + Coin.balances.call(result.args.from) +
"Receiver: " + Coin.balances.call(result.args.to));
}
})
Le constructor est une fonction spéciale qui est exécutée pendant la création du contrat et ne peut pas être appelée ultérieurement. Dans cetr exemple, il stocke de façon permanente l’adresse de la personne qui crée le contrat.
La variable msg
(avec tx
et block
) est une special global variable qui contient certaines propriétés qui permettent d’accéder à la blockchain. msg.sender
est toujours l’adresse d’où vient l’appel de la fonction courante (externe).
Les fonctions qui constituent le contrat et qui peuvent être appelées par les utilisateurs et les contrats sont mint
et send
.
== Ma version
Si mint
est appelé par quelqu’un d’autre que le compte qui a créé le contrat, rien ne se passera. Ceci est assuré par la fonction spéciale require qui fait que tous les changements sont annulés si son argument est évalué à faux.
Le deuxième appel à require
permet de s’assurer qu’il n’y aura pas trop de pièces, ce qui pourrait causer des erreurs de débordement de buffer plus tard.
== Reformulation/precision 2022 (remplace mais ne remet pas en cause le propos précéddent)
The mint
function sends an amount of newly created coins to another address. The require function call defines conditions that reverts all changes if not met. In this
example, require(msg.sender == minter);
ensures that only the creator of the contract can call
mint
. In general, the creator can mint as many tokens as they like, but at some point, this will
lead to a phenomenon called « overflow ». Note that because of the default Checked arithmetic, the transaction would revert if the expression balances[receiver] += amount;
overflows, i.e., when balances[receiver] + amount
in arbitrary precision arithmetic is larger
than the maximum value of uint
(2**256 - 1
). This is also true for the statement
balances[receiver] += amount;
in the function send
.
==
Errors allow you to provide more information to the caller about
why a condition or operation failed. Errors are used together with the
revert statement. The revert
statement unconditionally
aborts and reverts all changes similar to the require
function, but it also
allows you to provide the name of an error and additional data which will be supplied to the caller
(and eventually to the front-end application or block explorer) so that
a failure can more easily be debugged or reacted upon.
D’un autre côté, send
peut être utilisé par n’importe qui (qui a déjà certaines de ces pièces) pour envoyer des pièces à n’importe qui d’autre. Si vous n’avez pas assez de pièces à envoyer, l’appel require
échouera et fournira également à l’utilisateur un message d’erreur approprié.
Note
Si vous utilisez ce contrat pour envoyer des pièces à une adresse, vous ne verrez rien lorsque vous regarderez cette adresse sur un explorateur de chaîne de blocs, parce que le fait que vous avez envoyé des pièces et les soldes modifiés sont seulement stockés dans le stockage de données de ce contrat de pièces particulier. Par l’utilisation d’événements, il est relativement facile de créer un « explorateur de chaîne » qui suit les transactions et les soldes de votre nouvelle pièce, mais vous devez inspecter l’adresse du contrat de pièces et non les adresses des propriétaires des pièces.
Blockchain Basics
Les blockchains en tant que concept ne sont pas trop difficiles à comprendre pour les programmeurs. La raison en est que la plupart des complications (mining, hashing, elliptic-curve cryptography, réseaux pair-à-pair, etc.) sont juste là pour fournir un certain nombre de fonctionnalités et de promesses pour la plate-forme. Une fois que vous prenez ces fonctions pour aquises, vous n’avez pas à vous soucier de la technologie sous-jacente - ou devez-vous savoir comment fonctionne le cloud AWS d’Amazon en interne afin de l’utiliser ?
Transactions
Une blockchain est une base de données transactionnelle partagée à l’échelle mondiale. Cela signifie que tout le monde peut lire les entrées de la base de données simplement en participant au réseau. Si vous voulez modifier quelque chose dans la base de données, vous devez créer une transaction qui doit être acceptée par tous les autres. Le mot transaction implique que la modification que vous voulez effectuer (en supposant que vous voulez modifier deux valeurs en même temps) n’est pas effectuée du tout ou est complètement appliquée. De plus, pendant que votre transaction est appliquée à la base de données, aucune autre transaction ne peut la modifier.
Par exemple, imaginez un tableau qui énumère les soldes de tous les comptes dans une devise électronique. Si un transfert d’un compte à un autre est demandé, la nature transactionnelle de la base de données garantit que si le montant est soustrait d’un compte, il est toujours ajouté à l’autre compte. Si, pour quelque raison que ce soit, il n’est pas possible d’ajouter le montant au compte cible, le compte source n’est pas non plus modifié.
De plus, une transaction est toujours signée cryptographiquement par l’expéditeur (créateur). Il est donc facile de garder l’accès à des modifications spécifiques de la base de données. Dans l’exemple de la monnaie électronique, un simple contrôle permet de s’assurer que seule la personne qui détient les clés du compte peut transférer de l’argent à partir de celui-ci.
Blocs
Un obstacle majeur à surmonter est ce que l’on appelle (en termes Bitcoin) une » attaque de double dépense » : Que se passe-t-il si deux transactions existent dans le réseau et que toutes deux veulent vider un compte ? Une seule des transactions peut être valide, généralement celle qui est acceptée en premier. Le problème est que « premier » n’est pas un terme objectif dans un réseau pair-à-pair.
La réponse abstraite à cette question est que vous n’avez pas à vous en soucier. Un ordre des transactions accepté dans le monde entier sera sélectionné pour vous, résolvant ainsi le conflit. Les transactions seront regroupées dans ce que l’on appelle un « bloc », puis elles seront exécutées et réparties entre tous les nœuds participants. Si deux transactions se contredisent, celle qui finit deuxième sera rejetée et ne fera pas partie du bloc.
Ces blocs forment une séquence linéaire dans le temps et c’est de là que vient le mot « blockchain ». Des blocs sont ajoutés à la chaîne à des intervalles assez réguliers - pour Ethereum, c’est à peu près toutes les 17 secondes.
Dans le cadre du mécanisme de sélection d’ordre (qu’on appelle « mining »), il peut arriver que des blocs soient retournés de temps à autre, mais seulement au « sommet » de la chaîne. Plus il y a de blocs ajoutés au-dessus d’un bloc particulier, moins il y a de chances que ce bloc soit retourné. Il se peut donc que vos transactions soient annulées et même supprimées de la blockchain, mais plus vous attendez, moins il est probable qu’elles le soient.
Note
Il n’est pas garanti que les transactions seront incluses dans le bloc suivant ou dans tout bloc futur spécifique, puisque ce n’est pas à l’auteur d’une transaction, mais aux mineurs de déterminer dans quel bloc la transaction est incluse.
Si vous voulez programmer des appels futurs de votre contrat, vous pouvez utiliser le service alarm clock ou un service oracle ou automatisation de contrat similaire.
La Machine Virtuelle Ethereum
Définition
La Machine Virtuelle Ethereum ou EVM est l’environnement d’exécution des contrats intelligents dans Ethereum. Il n’est pas seulement cloisonné, il est aussi complètement isolé, ce qui signifie que le code fonctionnant à l’intérieur de l’EVM n’a pas accès au réseau, au système de fichiers ou à d’autres processus. Les Smart Contracts ont même un accès limité à d’autres Smart Contracts.
Comptes De plus, chaque compte a une balance en Ether (dans « Wei » pour être exact, 1 ether est 10**18 wei) qui peut être modifié en envoyant des transactions qui incluent des Ether.
Transactions
Une transaction est un message envoyé d’un compte à un autre (qui peut être identique ou vide, voir ci-dessous). Il peut inclure des données binaires (ce qu’on appelle charge utile ou « payload ») et de l’éther.
Si le compte cible contient du code, ce code est exécuté et le payload est fourni comme données d’entrée.
Si le compte cible n’est pas défini (la transaction n’a pas de destinataire ou le destinataire est défini sur null
), la transaction crée un nouveau contrat.
Comme nous l’avons déjà mentionné, l’adresse de ce contrat n’est pas l’adresse zéro, mais une adresse dérivée de l’adresse de l’expéditeur et de
son nombre de transactions envoyées (le « nonce »). Le payload d’une telle transaction de création de contrat est considérée comme étant du bytecode EVM et exécuté. Les données de sortie de cette exécution sont stockées en permanence comme code du contrat.
Cela signifie que pour créer un contrat, vous n’envoyez pas le code réel du contrat, mais en fait un code qui retourne ce code lorsqu’il est exécuté.
Note
Pendant la création d’un contrat, son code est toujours vide. Pour cette raison, vous ne devez pas rappeler le contrat en cours de construction tant que son constructeur n’a pas terminé son exécution.
Gas
Lors de la création, chaque transaction est facturée une certaine quantité de gas, dont le but est de limiter la quantité de travail nécessaire à l’exécution de la transaction et de payer pour cette exécution en même temps. Pendant que l’EVM exécute la commande le gaz est progressivement épuisé selon des règles spécifiques.
Le gas price (prix du gas) est une valeur fixée par le créateur de la transaction, qui doit payer gas_price * gas
à l’avance à partir du compte émetteur. S’il reste du gaz après l’exécution, il est remboursé au créateur de la même manière.
Si le gaz est épuisé à n’importe quel moment (c’est-à-dire qu’il serait négatif), une exception « à court de gas » est déclenchée, qui annule toutes les modifications apportées à l’état dans la trame d’appel en cours.
Storage, Memory et la Stack
La machine virtuelle Ethereum dispose de trois zones où elle peut stocker les données, stockage (« storage »), la mémoire (« memory ») et la pile (« stack »), qui sont expliquées dans les paragraphes suivants.
Chaque compte possède une zone de données appelée storage, qui est persistante entre les appels de fonction et les transactions. Storage est un stockage de valeur clé qui mappe les mots de 256 bits en 256 bits. Il n’est pas possible d’énumérer storage à partir d’un contrat et il est comparativement coûteux à lire, et encore plus à modifier le storage. Un contrat ne peut ni lire ni écrire dans un storage autre que le sien.
La deuxième zone de données est appelée memory, dont un contrat obtient une instance fraîchement rapprochée pour chaque appel de message. La mémoire est linéaire et peut être adressée au niveau de l’octet, mais les lectures sont limitées à une largeur de 256 bits, tandis que les écritures peuvent être de 8 bits ou de 256 bits. La mémoire est augmentée d’un mot (256 bits), lors de l’accès (en lecture ou en écriture) à un mot de mémoire qui n’a pas été touché auparavant (c.-à-d. tout décalage dans un mot). Au moment de l’agrandissement, le coût en gaz doit être payé. La mémoire est d’autant plus coûteuse qu’elle s’agrandit (le coût grandit de façon quadratique).
L’EVM n’est pas une machine à registre mais une machine à pile, donc tous les calculs sont effectués sur une zone de données appelée la stack. Elle a une taille maximale de 1024 éléments et contient des mots de 256 bits. L’accès à la stack est limitée à l’extrémité supérieure de la façon suivante : Il est possible de copier l’un des 16 éléments les plus hauts au sommet de la stack ou d’inverser l’élément le plus en haut avec l’un des 16 éléments en dessous. Toutes les autres opérations prennent les deux éléments les plus hauts (ou un, ou plus, selon l’opération) de la stack et poussent le résultat sur la stack. Bien sûr, il est possible de déplacer les éléments de la pile vers le stockage ou la mémoire afin d’obtenir un accès plus profond à la stack, mais il n’est pas possible d’accéder à des éléments arbitraires plus profondément dans la stack sans d’abord en enlever le haut.
Jeu d’Instructions
Le jeu d’instructions de l’EVM est maintenu au minimum afin d’éviter des impl’ementations incorrectes ou incohérentes qui pourraient causer des problèmes de consensus. Toutes les instructions fonctionnent sur le type de données de base, les mots de 256 bits ou sur des tranches de mémoire (ou d’autres tableaux d’octets). Les opérations arithmétiques, binaires, logiques et de comparaison habituelles sont présentes. Des sauts conditionnels et inconditionnels sont possibles. En outre, les contrats peuvent accéder aux propriétés pertinentes du bloc actuel comme son numéro et son horodatage.
Pour une liste complète, veuillez consulter la liste :ref:` liste des opcodes <opcodes>` dans la documentation de l’insertion de langage assembleur.
Les Message Calls
Les contrats peuvent appeler d’autres contrats ou envoyer des Ether sur des comptes non contractuels par le biais d’appels de messages (« message calls »). Les Message Calls sont similaires aux transactions, en ce sens qu’ils ont une source, une cible, une charge utile de données, d’éventuels Ether, le gas et le retour. En fait, chaque transaction consiste en un message call de niveau supérieur qui, à son tour, peut créer d’autres message calls.
Un contrat peut décider de la quantité de gas qu’il doit envoyer avec l’appel de message interne et de la quantité qu’il souhaite conserver. Si une exception fin de gas se produit dans l’appel interne (ou toute autre exception), elle sera signalée par une valeur d’erreur placée sur la stack. Dans ce cas, seul le gas envoyé avec l’appel est épuisé. Dans Solidity, le contrat appelant provoque une exception manuelle par défaut dans de telles situations, de sorte que les exceptions « remontent en surface » de la pile d’appels.
Comme déjà dit, le contrat appelé (qui peut être le même que celui de l’appelant) recevra une instance de mémoire fraîchement effacée et aura accès à la charge utile de l’appel - qui sera fournie dans une zone séparée appelée calldata. Une fois l’exécution terminée, il peut renvoyer des données qui seront stockées à un emplacement de la mémoire de l’appelant pré-alloué par ce dernier. Tous ces appels sont entièrement synchrones.
Les appels sont limités à une profondeur de 1024, ce qui signifie que pour les opérations plus complexes, les boucles doivent être préférées aux appels récursifs. De plus, seul 63/64ème du gaz peut être transféré lors d’un appel de message, ce qui entraîne une limite de profondeur d’un peu moins de 1000 en pratique.
Delegatecall / Callcode et Libraries
Il existe une variante spéciale d’un message call, appelée delegatecall, qui est identique à un appel de message sauf que le code à l’adresse cible est exécuté dans le cadre du contrat d’appel et que msg.sender
et msg.value
ne changent pas leurs valeurs.
Cela signifie qu’un contrat peut charger dynamiquement du code à partir d’une adresse différente lors de l’exécution. Le stockage, l’adresse actuelle et le solde se réfèrent toujours au contrat d’appel, seul le code est repris de l’adresse appelée.
Cela permet d’implémenter la fonctionnalité « bibliothèque » dans Solidity : Code de bibliothèque réutilisable qui peut être appliqué au stockage d’un contrat, par exemple pour implémenter une structure de données complexe.
Logs / Journalisation
Il est possible de stocker les données dans une structure de données spécialement indexée qui s’étend jusqu’au niveau du bloc. Cette fonction appelée logs (journalisation) est utilisé par Solidity pour implémenter les events. Les contrats ne peuvent pas accéder aux données du journal une fois qu’elles ont été créées, mais ils peut être accédé efficacement de l’extérieur de la chaîne de blocs. Puisqu’une partie des données du journal est stockée dans des bloom filters, il est possible de rechercher ces données de manière efficace et cryptographique de manière sécurisée, afin que les pairs du réseau qui ne téléchargent pas la totalité de la blockchain (appelés « clients légers ») peuvent encore trouver ces logs.
Création
Les contrats peuvent même créer d’autres contrats à l’aide d’un opcode spécial (càd qu’ils n’appellent pas simplement l’adresse zéro comme le ferait une transaction). La seule différence entre ces appels de création et des appels de message normaux est que les données de charge utile sont exécutées, le résultat stocké sous forme de code et l’appelant / créateur reçoit l’adresse du nouveau contrat sur la stack.
Désactivation et Auto-Destruction
La seule façon de supprimer du code de la blockchain est lorsqu’un contrat à cette adresse exécute l’opération d’autodestruction selfdestruct
. L’Ether restant stocké à cette adresse est envoyé à une cible désignée, puis le stockage et le code sont retirés de l’état. Supprimer le contrat en théorie semble être une bonne idée, mais c’est potentiellement dangereux, comme en cas d’envoi d’éther à des contrats supprimés, où l’éther est perdu à jamais.
Avertissement
Même si un contrat est supprimé par selfdestruct
, il fait toujours partie de l’historique de la blockchain et probablement conservé par la plupart des nœuds Ethereum. L’utilisation de l’autodestruction n’est donc pas la même chose que la suppression de données d’un disque dur.
Note
Même si le code d’un contrat ne contient pas d’appel à selfdestruct
, il peut toujours effectuer cette opération en utilisant le delegate code
ou le callcode
.
If you want to deactivate your contracts, you should instead disable them by changing some internal state which causes all functions to revert. This makes it impossible to use the contract, as it returns Ether immediately.
Precompiled Contracts
There is a small set of contract addresses that are special:
The address range between 1
and (including) 8
contains
« precompiled contracts » that can be called as any other contract
but their behaviour (and their gas consumption) is not defined
by EVM code stored at that address (they do not contain code)
but instead is implemented in the EVM execution environment itself.
Different EVM-compatible chains might use a different set of
precompiled contracts. It might also be possible that new
precompiled contracts are added to the Ethereum main chain in the future,
but you can reasonably expect them to always be in the range between
1
and 0xffff
(inclusive).
Installer le Compilateur Solidity
Versionnage
Les versions de Solidity suivent un versionnage sémantique. In addition, patch level releases with major release 0 (i.e. 0.x.y) will not contain breaking changes. That means code that compiles with version 0.x.y can be expected to compile with 0.x.z where z > y.
In addition to releases, we provide nightly development builds with the intention of making it easy for developers to try out upcoming features and provide early feedback. Note, however, that while the nightly builds are usually very stable, they contain bleeding-edge code from the development branch and are not guaranteed to be always working. Despite our best efforts, they might contain undocumented and/or broken changes that will not become a part of an actual release. They are not meant for production use.
When deploying contracts, you should use the latest released version of Solidity. This is because breaking changes, as well as new features and bug fixes are introduced regularly. We currently use a 0.x version number to indicate this fast pace of change.
Remix
Nous recommandons Remix pour les petits contrats et pour l’apprentissage rapide de Solidity.
Accédez à Remix en ligne, vous n’avez rien à installer.
Si vous voulez l’utiliser sans connexion à Internet, allez à
https://github.com/ethereum/remix-live/tree/gh-pages et téléchargez le fichier .zip
tel qu’expliqué sur cette page. Remix is also a convenient option for testing nightly builds
without installing multiple Solidity versions.
D’autres options sur cette page détaillent l’installation du compilateur Solidity en ligne de commande sur votre ordinateur. Choisissez un compilateur de ligne de commande si vous travaillez sur un contrat plus important ou si vous avez besoin de plus d’options de compilation.
npm / Node.js
Utilisez npm” pour un moyen pratique et portable d’installer `solcjs”, un compilateur Solidity. Le programme `solcjs a moins de fonctionnalités que le compilateur décrit plus bas sur cette page. La documentation du Using the Commandline Compiler suppose que vous utilisez le compilateur complet, solc. L’utilisation de solcjs est documentée dans son propre dépot.
Note : Le projet solc-js est dérivé du projet C++ solc en utilisant Emscripten, ce qui signifie que les deux utilisent le même code source du compilateur. solc-js peut être utilisé directement dans les projets JavaScript (comme Remix). Veuillez vous référer au dépôt solc-js pour les instructions.
npm install -g solc
Note
L’exécutable en ligne de commande est nommé solcjs.
Les options de la ligne de commande de solcjs ne sont pas compatibles avec solc” et les outils (tels que `geth”) attendant le comportement de `solc ne fonctionneront pas avec solcjs.
Docker
Nous fournissons des images dockers à jour pour le compilateur via l’image solc
distribué par l’organisation ethereum
. Le label stable
contient les versions publiées tandis que le label nightly
contient des changements potentiellement instables dans la branche develop.
Docker images of Solidity builds are available using the solc
image from the ethereum
organisation.
Use the stable
tag for the latest released version, and nightly
for potentially unstable changes in the develop branch.
The Docker image runs the compiler executable, so you can pass all compiler arguments to it.
For example, the command below pulls the stable version of the solc
image (if you do not have it already),
and runs it in a new container, passing the --help
argument.
docker run ethereum/solc:stable --help
You can also specify release build versions in the tag, for example, for the 0.5.4 release.
docker run ethereum/solc:0.5.4 --help
To use the Docker image to compile Solidity files on the host machine mount a local folder for input and output, and specify the contract to compile. For example.
docker run -v /local/path:/sources ethereum/solc:stable -o /sources/output --abi --bin /sources/Contract.sol
You can also use the standard JSON interface (which is recommended when using the compiler with tooling). When using this interface it is not necessary to mount any directories as long as the JSON input is self-contained (i.e. it does not refer to any external files that would have to be loaded by the import callback).
docker run ethereum/solc:stable --standard-json < input.json > output.json
Paquets Linux
Les binaires de Solidity sont disponibles à solidity/releases.
Nous avons également des PPAs for Ubuntu, vous pouvez obtenir la dernière version via la commande:
sudo add-apt-repository ppa:ethereum/ethereum
sudo apt-get update
sudo apt-get install solc
La version nigthly peut s’installer avec la commande:
sudo add-apt-repository ppa:ethereum/ethereum
sudo add-apt-repository ppa:ethereum/ethereum-dev
sudo apt-get update
sudo apt-get install solc
Furthermore, some Linux distributions provide their own packages. These packages are not directly maintained by us, but usually kept up-to-date by the respective package maintainers.
For example, Arch Linux has packages for the latest development version:
pacman -S solidity
There is also a snap package, however, it is currently unmaintained. It is installable in all the supported Linux distros. To install the latest stable version of solc:
sudo snap install solc
Si vous voulez aider aux tests en utilisant la dernière version de développement, avec les changements les plus récents, merci d’utiliser:
sudo snap install solc --edge
Note
The solc
snap uses strict confinement. This is the most secure mode for snap packages
but it comes with limitations, like accessing only the files in your /home
and /media
directories.
For more information, go to Demystifying Snap Confinement.
macOS Packages
Nous distribuons également le compilateur Solidity via homebrew dans une version compilée à partir des sources. Les « bottles » pré-compilées ne sont pas encore supportées pour l’instant.
brew update
brew upgrade
brew tap ethereum/ethereum
brew install solidity
To install the most recent 0.4.x / 0.5.x version of Solidity you can also use brew install solidity@4
and brew install solidity@5
, respectively.
Si vous avec besoin d’une version spécifique, vous pouvez exécuter la formule homebrew correspondante disponible sur GitHub.
Regarder commits de solidity.rb sur Github.
Copy the commit hash of the version you want and check it out on your machine.
git clone https://github.com/ethereum/homebrew-ethereum.git
cd homebrew-ethereum
git checkout <your-hash-goes-here>
Installez-le via brew
:
brew unlink solidity
# eg. Install 0.4.8
brew install solidity.rb
Static Binaries
We maintain a repository containing static builds of past and current compiler versions for all supported platforms at solc-bin. This is also the location where you can find the nightly builds.
The repository is not only a quick and easy way for end users to get binaries ready to be used out-of-the-box but it is also meant to be friendly to third-party tools:
The content is mirrored to https://binaries.soliditylang.org where it can be easily downloaded over HTTPS without any authentication, rate limiting or the need to use git.
Content is served with correct Content-Type headers and lenient CORS configuration so that it can be directly loaded by tools running in the browser.
Binaries do not require installation or unpacking (with the exception of older Windows builds bundled with necessary DLLs).
We strive for a high level of backwards-compatibility. Files, once added, are not removed or moved without providing a symlink/redirect at the old location. They are also never modified in place and should always match the original checksum. The only exception would be broken or unusable files with a potential to cause more harm than good if left as is.
Files are served over both HTTP and HTTPS. As long as you obtain the file list in a secure way (via git, HTTPS, IPFS or just have it cached locally) and verify hashes of the binaries after downloading them, you do not have to use HTTPS for the binaries themselves.
The same binaries are in most cases available on the Solidity release page on Github. The
difference is that we do not generally update old releases on the Github release page. This means
that we do not rename them if the naming convention changes and we do not add builds for platforms
that were not supported at the time of release. This only happens in solc-bin
.
The solc-bin
repository contains several top-level directories, each representing a single platform.
Each one contains a list.json
file listing the available binaries. For example in
emscripten-wasm32/list.json
you will find the following information about version 0.7.4:
{
"path": "solc-emscripten-wasm32-v0.7.4+commit.3f05b770.js",
"version": "0.7.4",
"build": "commit.3f05b770",
"longVersion": "0.7.4+commit.3f05b770",
"keccak256": "0x300330ecd127756b824aa13e843cb1f43c473cb22eaf3750d5fb9c99279af8c3",
"sha256": "0x2b55ed5fec4d9625b6c7b3ab1abd2b7fb7dd2a9c68543bf0323db2c7e2d55af2",
"urls": [
"bzzr://16c5f09109c793db99fe35f037c6092b061bd39260ee7a677c8a97f18c955ab1",
"dweb:/ipfs/QmTLs5MuLEWXQkths41HiACoXDiH8zxyqBHGFDRSzVE5CS"
]
}
This means that:
You can find the binary in the same directory under the name solc-emscripten-wasm32-v0.7.4+commit.3f05b770.js. Note that the file might be a symlink, and you will need to resolve it yourself if you are not using git to download it or your file system does not support symlinks.
The binary is also mirrored at https://binaries.soliditylang.org/emscripten-wasm32/solc-emscripten-wasm32-v0.7.4+commit.3f05b770.js. In this case git is not necessary and symlinks are resolved transparently, either by serving a copy of the file or returning a HTTP redirect.
The file is also available on IPFS at QmTLs5MuLEWXQkths41HiACoXDiH8zxyqBHGFDRSzVE5CS.
The file might in future be available on Swarm at 16c5f09109c793db99fe35f037c6092b061bd39260ee7a677c8a97f18c955ab1.
You can verify the integrity of the binary by comparing its keccak256 hash to
0x300330ecd127756b824aa13e843cb1f43c473cb22eaf3750d5fb9c99279af8c3
. The hash can be computed on the command line usingkeccak256sum
utility provided by sha3sum or keccak256() function from ethereumjs-util in JavaScript.You can also verify the integrity of the binary by comparing its sha256 hash to
0x2b55ed5fec4d9625b6c7b3ab1abd2b7fb7dd2a9c68543bf0323db2c7e2d55af2
.
Avertissement
Due to the strong backwards compatibility requirement the repository contains some legacy elements but you should avoid using them when writing new tools:
Use
emscripten-wasm32/
(with a fallback toemscripten-asmjs/
) instead ofbin/
if you want the best performance. Until version 0.6.1 we only provided asm.js binaries. Starting with 0.6.2 we switched to WebAssembly builds with much better performance. We have rebuilt the older versions for wasm but the original asm.js files remain inbin/
. The new ones had to be placed in a separate directory to avoid name clashes.Use
emscripten-asmjs/
andemscripten-wasm32/
instead ofbin/
andwasm/
directories if you want to be sure whether you are downloading a wasm or an asm.js binary.Use
list.json
instead oflist.js
andlist.txt
. The JSON list format contains all the information from the old ones and more.Use https://binaries.soliditylang.org instead of https://solc-bin.ethereum.org. To keep things simple we moved almost everything related to the compiler under the new
soliditylang.org
domain and this applies tosolc-bin
too. While the new domain is recommended, the old one is still fully supported and guaranteed to point at the same location.
Avertissement
The binaries are also available at https://ethereum.github.io/solc-bin/ but this page stopped being updated just after the release of version 0.7.2, will not receive any new releases or nightly builds for any platform and does not serve the new directory structure, including non-emscripten builds.
If you are using it, please switch to https://binaries.soliditylang.org, which is a drop-in
replacement. This allows us to make changes to the underlying hosting in a transparent way and
minimize disruption. Unlike the ethereum.github.io
domain, which we do not have any control
over, binaries.soliditylang.org
is guaranteed to work and maintain the same URL structure
in the long-term.
Compilation à partir des sources
Prérequis - Linux
Vous aurez besoin des dépendances suivantes pour toutes compilations de Solidity:
Software |
Notes |
---|---|
CMake (version 3.13+) |
Cross-platform build file generator. |
Boost (version 1.77+ on Windows, 1.65+ otherwise) |
C++ libraries. |
Command-line tool for retrieving source code. |
|
z3 (version 4.8+, Optional) |
For use with SMT checker. |
cvc4 (Optional) |
For use with SMT checker. |
Note
Solidity versions prior to 0.5.10 can fail to correctly link against Boost versions 1.70+.
A possible workaround is to temporarily rename <Boost install path>/lib/cmake/Boost-1.70.0
prior to running the cmake command to configure solidity.
Starting from 0.5.10 linking against Boost 1.70+ should work without manual intervention.
Note
The default build configuration requires a specific Z3 version (the latest one at the time the
code was last updated). Changes introduced between Z3 releases often result in slightly different
(but still valid) results being returned. Our SMT tests do not account for these differences and
will likely fail with a different version than the one they were written for. This does not mean
that a build using a different version is faulty. If you pass -DSTRICT_Z3_VERSION=OFF
option
to CMake, you can build with any version that satisfies the requirement given in the table above.
If you do this, however, please remember to pass the --no-smt
option to scripts/tests.sh
to skip the SMT tests.
Minimum Compiler Versions
The following C++ compilers and their minimum versions can build the Solidity codebase:
Prérequis - macOS
Pour macOS, assurez-vous d’avoir installer la dernière version de Xcode. Ceci contient le compilateur C++ Clang, l’IDE Xcode et d’autres outils de développement Apple qui sont nécessaires pour construire des applications C++ sous OS X. Si vous installez Xcode pour la première fois, ou si vous venez d’installer une nouvelle version, vous devrez accepter la licence avant de pouvoir compiler en ligne de commande:
sudo xcodebuild -license accept
Nos versions pour OS X exigent que vous installiez le gestionnaire de paquets Homebrew pour l’installation des dépendances externes. Voici comment désinstaller Homebrew, si vous voulez recommencer à zéro.
Prérequis - Windows
Vous aurez besoin des dépendances suivants pour compiler Solidity sous Windows:
Software |
Notes |
---|---|
C++ compiler |
|
Visual Studio 2019 (Optional) |
C++ compiler and dev environment. |
Boost (version 1.77+) |
C++ libraries. |
Si vous avez déjà eu un IDE et que vous n’avez besoin que du compilateur et des bibliothèques, vous pouvez installer Visual Studio 2019 Build Tools.
Visual Studio 2019 fournit à la fois l’IDE et le compilateur et les bibliothèques nécessaires. Donc si vous n’avez pas d’IDE et que vous préférez développer en Solidity, Visual Studio 2019 peut être un choix pour tout installer facilement.
Voici la liste des composants à installer dans Visual Studio 2019 Build Tools ou Visual Studio 2019 :
Visual Studio C+++ fonctionnalités de base
VC+++ 2019 v141 toolset (x86,x64)
Windows Universal CRT SDK
Windows 8.1 SDK
Support C+++/CLI
We have a helper script which you can use to install all required external dependencies:
scripts\install_deps.ps1
This will install boost
and cmake
to the deps
subdirectory.
Clonez le dépot
Pour cloner le code source, exécutez la commande suivante:
git clone --recursive https://github.com/ethereum/solidity.git
cd solidity
Si vous voulez aider à développer Solidity, vous devriez forker Solidity et ajouter votre fork comme un second dépot distant:
git remote add personal git@github.com:[username]/solidity.git
- . note::
This method will result in a prerelease build leading to e.g. a flag being set in each bytecode produced by such a compiler. If you want to re-build a released Solidity compiler, then please use the source tarball on the github release page:
https://github.com/ethereum/solidity/releases/download/v0.X.Y/solidity_0.X.Y.tar.gz
(not the « Source code » provided by github).
Compilation en ligne de commande
Soyez sûrs d’installer les dépendances externes avant de compiler.
Le projet Solidity utilise CMake pour la configuration de compilation. Vous voulez peut-être installer ccache pour accélérer des compilations successives. CMake l’utilisera automatiquement. Compiler Solidity est similaire sur Linux, macOS et autres systèmes Unix:
mkdir build
cd build
cmake .. && make
ou même sous Linux et macOS, vous pouvez:
#note: les binaires de solc et les tests seront installés dans usr/local/bin
./scripts/build.sh
Avertissement
BSD builds should work, but are untested by the Solidity team.
Et pour Windows:
mkdir build
cd build
cmake -G "Visual Studio 16 2019" ..
In case you want to use the version of boost installed by scripts\install_deps.ps1
, you will
additionally need to pass -DBoost_DIR="deps\boost\lib\cmake\Boost-*"
and -DCMAKE_MSVC_RUNTIME_LIBRARY=MultiThreaded
as arguments to the call to cmake
.
Ceci devrait aboutir à la création de solidity.sln dans ce répertoire de compilation. Double-cliquer sur ce fichier devrait faire démarrer Visual Studio. Nous suggérons de construire la configuration Release, mais toutes les autres fonctionnent.
Alternativement, vous pouvez compiler pour Windows en ligne de commande, comme ça :
cmake --build . --config Release
Options de CMake
La liste des options de Cmake est disponible via la commande: cmake .. -LH
.
Solveurs SMT
Solidity peut être compilé avec les solveurs SMT et le fera par défaut s’ils sont trouvés dans le système. Chaque solveur peut être désactivé par une option cmake.
Remarque : Dans certains cas, cela peut également être une solution de contournement potentielle en cas d’échec de compilation.
Dans le dossier de compilation, vous pouvez les désactiver, car ils sont activés par défaut :
# désactive seulement Z3 SMT Solver.
cmake .. -DUSE_Z3=OFF
# désactive seulement CVC4 SMT Solver.
cmake .. -DUSE_CVC4=OFF
# désactive Z3 et CVC4
cmake .. -DUSE_CVC4=OFF -DUSE_Z3=OFF
La string de version en détail
La string de version de Solidity contient 4 parties:
le numéro de version
la balise de pre-version, généralement définie sur
develop.YYYY.MM.DD
ounightly.YYYY.MM.DD
.commit au format
commit.GITHASH
.plate-forme, qui a un nombre arbitraire d’éléments, contenant des détails sur la plate-forme et le compilateur
S’il y a des modifications locales, le commit sera suffixé avec .mod
.
Ces parties sont combinées comme l’exige Semver, où la balise de pré-version Solidity est identique à la pré-version de Semver. et le commit Solidity et la plate-forme Solidity combinés constituent les métadonnées de la construction Semver.
Un exemple de version : `0.4.8+commit.60cc1668.Emscripten.clang
.
Un exemple de pré-version : 0.4.9-nightly.2017.1.17+commit.6ecb4aaa3.Emscripten.clang
Informations importantes concernant le versionnage
Après la sortie d’une version, la version de correctif est incrémentée, parce que nous supposons que seulement les changements de niveau patch suivent. Lorsque les modifications sont fusionnées, la version doit être supprimée en fonction des éléments suivants et la gravité du changement. Enfin, une version est toujours basée sur la nigthly actuelle, mais sans le spécificateur prerelease
.
Exemple :
la version 0.4.0 est publiée
nightly build a une version de 0.4.1 à partir de maintenant
des modifications incessantes sont introduites - pas de changement de version
un changement de rupture est introduit - la version est augmentée à 0.5.0
la version 0.5.0 est publiée
Ce comportement fonctionne bien avec le version pragma.
Solidity par l’Exemple
Vote
Le contrat suivant est assez complexe, mais il présente de nombreuses caractéristiques de Solidity. Il implémente un contrat de vote. Bien entendu, le principal problème du vote électronique est de savoir comment attribuer les droits de vote aux bonnes personnes et éviter les manipulations. Nous ne résoudrons pas tous les problèmes ici, mais nous montrerons au moins comment le vote délégué peut être effectué de manière à ce que le dépouillement soit à la fois automatique et totalement transparent.
L’idée est de créer un contrat par bulletin de vote, en donnant un nom court à chaque option. Ensuite, le créateur du contrat qui agit à titre de président donnera le droit de vote à chaque adresse individuellement.
Les personnes derrière les adresses peuvent alors choisir de voter elles-mêmes ou de déléguer leur vote à une personne en qui elles ont confiance.
A la fin du temps de vote, la winningProposal()
(proposition gagnante) retournera la proposition avec le plus grand nombre de votes.
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
/// @title Vote par délegation.
contract Ballot {
// Ceci déclare un type complexe, représentant
// un votant, qui sera utilisé
// pour les variables plus tard.
struct Voter {
uint weight; // weight (poids), qui s'accumule avec les délégations
bool voted; // si true, cette personne a déjà voté
address delegate; // Cette personne a délégué son vote à
uint vote; // index la la proposition choisie
}
// Type pour une proposition.
struct Proposal {
bytes32 name; // nom court (jusqu'à 32 octets)
uint voteCount; // nombre de votes cumulés
}
address public chairperson;
// Ceci déclare une variable d'état qui stocke
// un élément de structure 'Voters' pour chaque votant.
mapping(address => Voter) public voters;
// Un tableau dynamique de structs `Proposal`.
Proposal[] public proposals;
- <<<<<<< HEAD
/// Créé un nouveau bulletin pour choisir l’un des proposalNames. constructor(bytes32[] memory proposalNames) {
- >>>>>>> 47d77931747aba8e364452537d989b795df7ca04
chairperson = msg.sender; voters[chairperson].weight = 1;
// Pour chacun des noms proposés, // crée un nouvel objet proposal // à la fin du tableau. for (uint i = 0; i < proposalNames.length; i++) {
// Proposal({…}) créé un objet temporaire // Proposal et proposals.push(…) // l’ajoute à la fin du tableau proposals. proposals.push(Proposal({
name: proposalNames[i], voteCount: 0
}));
}
}
// Donne à un voter un droit de vote pour ce scrutin. // Peut seulement être appelé par chairperson. function giveRightToVote(address voter) external {
// Si le premier argument passé à require s’évalue // à false, l’exécution s’arrete et tous les changements // à l’état et aux soldes sont annulés. // Cette opération consommait tout le gas dans // d’anciennes versions de l’EVM, plus maintenant. // Il est souvent une bonne idée d’appeler require // pour vérifier si les appels de fonctions // s’effectuent correctement. // Comme second argument, vous pouvez fournir une // phrase explicative de ce qui s’est mal passé. require(
msg.sender == chairperson, « Only chairperson can give right to vote. »
); require(
!voters[voter].voted, « The voter already voted. »
); require(voters[voter].weight == 0); voters[voter].weight = 1;
}
/// Delegue son vote au votant to. function delegate(address to) external {
// assigne les références Voter storage sender = voters[msg.sender]; require(!sender.voted, « You already voted. »);
require(to != msg.sender, « Self-delegation is disallowed. »);
// Relaie la délégation tant que to // est également en délégation de vote. // En général, ce type de boucles est très dangereux, // puisque s’il tourne trop longtemps, l’opération // pourrait demander plus de gas qu’il n’est possible // d’en avoir dans un bloc. // Dans ce cas, la délégation ne se ferait pas, // mais dans d’autres circonstances, ces boucles // peuvent complètement paraliser un contrat. while (voters[to].delegate != address(0)) {
to = voters[to].delegate;
// On a trouvé une boucle dans la chaine // de délégations => interdit. require(to != msg.sender, « Found loop in delegation. »);
}
// Comme sender est une référence, ceci // modifie voters[msg.sender].voted Voter storage delegate_ = voters[to];
// Voters cannot delegate to wallets that cannot vote. require(delegate_.weight >= 1); sender.voted = true; sender.delegate = to; if (delegate_.voted) {
// Si le délégué a déjà voté, // on ajoute directement le vote aux autres proposals[delegate_.vote].voteCount += sender.weight;
- } else {
// Sinon, on l’ajoute au poids de son vote. delegate_.weight += sender.weight;
}
}
/// Voter (incluant les procurations par délégation) /// pour la proposition proposals[proposal].name. function vote(uint proposal) external {
Voter storage sender = voters[msg.sender]; require(!sender.voted, « Already voted. »); sender.voted = true; sender.vote = proposal;
// Si proposal n’est pas un index valide, // une erreur sera levée et l’exécution annulée proposals[proposal].voteCount += sender.weight;
}
/// @dev Calcule la proposition gagnante /// en prenant tous les votes précédents en compte. function winningProposal() public view
returns (uint winningProposal_)
- {
uint winningVoteCount = 0; for (uint p = 0; p < proposals.length; p++) {
- if (proposals[p].voteCount > winningVoteCount) {
winningVoteCount = proposals[p].voteCount; winningProposal_ = p;
}
}
}
// Appelle la fonction winningProposal() pour avoir // l’index du gagnant dans le tableau de propositions // et retourne le nom de la proposition gagnante. function winnerName() external view
returns (bytes32 winnerName_)
- {
winnerName_ = proposals[winningProposal()].name;
}
}
Améliorations possibles
À l’heure actuelle, de nombreuses opérations sont nécessaires pour attribuer les droits de vote à tous les participants. Pouvez-vous trouver un meilleur moyen ?
Enchères à l’aveugle
Dans cette section, nous allons montrer à quel point il est facile de créer un contrat d’enchères à l’aveugle sur Ethereum. Nous commencerons par une enchère ouverte où tout le monde pourra voir les offres qui sont faites, puis nous prolongerons ce contrat dans une enchère aveugle où il n’est pas possible de voir l’offre réelle avant la fin de la période de soumission.
Enchère ouverte simple
L’idée générale du contrat d’enchère simple suivant est que chacun peut envoyer ses offres pendant une période d’enchère. Les ordres incluent l’envoi d’argent / éther afin de lier les soumissionnaires à leur offre. Si l’enchère est la plus haute, l’enchérisseur qui avait fait l’offre la plus élevée auparavant récupère son argent. Après la fin de la période de soumission, le contrat doit être appelé manuellement pour que le bénéficiaire reçoive son argent - les contrats ne peuvent pas s’activer eux-mêmes.
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.4;
contract SimpleAuction {
// Paramètres de l'enchère
// temps unix absolus (secondes depuis 01-01-1970)
// ou des durées en secondes.
address payable public beneficiary;
uint public auctionEndTime;
// État actuel de l'enchère.
address public highestBidder;
uint public highestBid;
// Remboursements autorisés d'enchères précédentes
mapping(address => uint) pendingReturns;
// Mis à true à la fin, interdit tout changement.
// Par defaut à `false`, comme un grand.
bool ended;
// Évènements déclenchés aux changements.
event HighestBidIncreased(address bidder, uint amount);
event AuctionEnded(address winner, uint amount);
// Errors that describe failures.
// The triple-slash comments are so-called natspec
// comments. They will be shown when the user
// is asked to confirm a transaction or
// when an error is displayed.
/// The auction has already ended.
error AuctionAlreadyEnded();
/// There is already a higher or equal bid.
error BidNotHighEnough(uint highestBid);
/// The auction has not ended yet.
error AuctionNotYetEnded();
/// The function auctionEnd has already been called.
error AuctionEndAlreadyCalled();
/// Create a simple auction with `biddingTime`
/// seconds bidding time on behalf of the
/// beneficiary address `beneficiaryAddress`.
constructor(
uint biddingTime,
address payable beneficiaryAddress
) {
beneficiary = beneficiaryAddress;
auctionEndTime = block.timestamp + biddingTime;
}
/// Faire une offre avec la valeur envoyée
/// avec cette transaction.
/// La valeur ne sera remboursée que si
// l'enchère est perdue.
function bid() external payable {
// Aucun argument n'est nécessaire, toute
// l'information fait déjà partie
// de la transaction. Le mot-clé payable
// est requis pour autoriser la fonction
// à recevoir de l'Ether.
// Annule l'appel si l'enchère est termminée
if (block.timestamp > auctionEndTime)
revert AuctionAlreadyEnded();
// Rembourse si l'enchère est trop basse
if (msg.value <= highestBid)
revert BidNotHighEnough(highestBid);
if (highestBid != 0) {
// Renvoyer l'argent avec un simple
// highestBidder.send(highestBid) est un risque de sécurité
// car ça pourrait déclencher un appel à un contrat.
// Il est toujours plus sûr de laisser les utilisateurs
// retirer leur argent eux-mêmes.
pendingReturns[highestBidder] += highestBid;
}
highestBidder = msg.sender;
highestBid = msg.value;
emit HighestBidIncreased(msg.sender, msg.value);
}
/// Retirer l'argent d'une enchère dépassée
function withdraw() external returns (bool) {
uint amount = pendingReturns[msg.sender];
if (amount > 0) {
// Il est important de mettre cette valeur à zéro car l'utilisateur
// pourrait rappeler cette fonction avant le retour de `send`.
pendingReturns[msg.sender] = 0;
// msg.sender is not of type `address payable` and must be
// explicitly converted using `payable(msg.sender)` in order
// use the member function `send()`.
if (!payable(msg.sender).send(amount)) {
// No need to call throw here, just reset the amount owing
pendingReturns[msg.sender] = amount;
return false;
}
}
return true;
}
/// Met fin à l'enchère et envoie
/// le montant de l'enchère la plus haute au bénéficiaire.
function auctionEnd() external {
// C'est une bonne pratique de structurer les fonctions qui
// intéragissent avec d'autres contrats (appellent des
// fonctions ou envoient de l'Ether) en trois phases:
// 1. Vérifier les conditions
// 2. éffectuer les actions (potentiellement changeant les conditions)
// 3. interagir avec les autres contrats
// Si ces phases sont mélangées, l'autre contrat pourrait rappeler
// le contrat courant et modifier l'état ou causer des effets
// (paiements en Ether par ex) qui se produiraient plusieurs fois.
// Si des fonctions appelées en interne effectuent des appels
// à des contrats externes, elles doivent aussi êtres considérées
// comme concernées par cette norme.
// 1. Conditions
if (block.timestamp < auctionEndTime)
revert AuctionNotYetEnded();
if (ended)
revert AuctionEndAlreadyCalled();
// 2. Éffets
ended = true;
emit AuctionEnded(highestBidder, highestBid);
// 3. Interaction
beneficiary.transfer(highestBid);
}
}
Enchère aveugle
L’enchère ouverte précédente est étendue en une enchère aveugle dans ce qui suit. L’avantage d’une enchère aveugle est qu’il n’y a pas de pression temporelle vers la fin de la période de soumission. La création d’une enchère aveugle sur une plate-forme informatique transparente peut sembler une contradiction, mais la cryptographie vient à la rescousse.
Pendant la période de soumission, un soumissionnaire n’envoie pas son offre, mais seulement une version hachée de celle-ci. Puisqu’il est actuellement considéré comme pratiquement impossible de trouver deux valeurs (suffisamment longues) dont les valeurs de hachage sont égales, le soumissionnaire s’engage à l’offre par cela. Après la fin de la période de soumission, les soumissionnaires doivent révéler leurs offres : Ils envoient leurs valeurs en clair et le contrat vérifie que la valeur de hachage est la même que celle fournie pendant la période de soumission.
Un autre défi est de savoir comment rendre l’enchère contraignante et aveugle en même temps : La seule façon d’éviter que l’enchérisseur n’envoie pas l’argent après avoir gagné l’enchère est de le lui faire envoyer avec l’enchère. Puisque les transferts de valeur ne peuvent pas être aveuglés dans Ethereum, tout le monde peut voir la valeur.
Le contrat suivant résout ce problème en acceptant toute valeur supérieure à l’offre la plus élevée. Comme cela ne peut bien sûr être vérifié que pendant la phase de révélation, certaines offres peuvent être invalides, et c’est fait exprès (il fournit même un marqueur explicite pour placer des offres invalides avec des transferts de grande valeur) : Les soumissionnaires peuvent brouiller la concurrence en plaçant plusieurs offres invalides hautes ou basses.
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.4;
contract BlindAuction {
struct Bid {
bytes32 blindedBid;
uint deposit;
}
address payable public beneficiary;
uint public biddingEnd;
uint public revealEnd;
bool public ended;
mapping(address => Bid[]) public bids;
address public highestBidder;
uint public highestBid;
// Remboursements autorisés d'enchères précédentes
mapping(address => uint) pendingReturns;
event AuctionEnded(address winner, uint highestBid);
// Errors that describe failures.
/// The function has been called too early.
/// Try again at `time`.
error TooEarly(uint time);
/// The function has been called too late.
/// It cannot be called after `time`.
error TooLate(uint time);
/// The function auctionEnd has already been called.
error AuctionEndAlreadyCalled();
/// Les Modifiers sont une façon pratique de valider des entrées.
/// `onlyBefore` est appliqué à `bid` ci-dessous:
/// Le corps de la fonction sera placé dans le modifier
/// où `_` est placé.
modifier onlyBefore(uint time) {
if (block.timestamp >= time) revert TooLate(time);
_;
}
modifier onlyAfter(uint time) {
if (block.timestamp <= time) revert TooEarly(time);
_;
}
constructor(
uint biddingTime,
uint revealTime,
address payable beneficiaryAddress
) {
beneficiary = beneficiaryAddress;
biddingEnd = block.timestamp + biddingTime;
revealEnd = biddingEnd + revealTime;
}
/// Placer une enchère à l'aveugle avec `_blindedBid` =
/// keccak256(abi.encodePacked(value, fake, secret)).
/// L'éther envoyé n'est remboursé que si l'enchère est correctement
/// révélée dans la phase de révélation. L'offre est valide si
/// l'éther envoyé avec l'offre est d'au moins "valeur" et
/// "fake" n'est pas true. Régler "fake" à true et envoyer
/// envoyer un montant erroné sont des façons de masquer l'enchère
/// mais font toujours le dépot requis. La même addresse peut placer
/// plusieurs ordres
function bid(bytes32 _blindedBid)
external
payable
onlyBefore(biddingEnd)
{
bids[msg.sender].push(Bid({
blindedBid: blindedBid,
deposit: msg.value
}));
}
/// Révèle vos ench1eres aveugles. Vous serez remboursé pour toutes
/// les enchères invalides et toutes les autres exceptée la plus haute
/// le cas échéant.
function reveal(
uint[] calldata values,
bool[] calldata fakes,
bytes32[] calldata secrets
)
external
onlyAfter(biddingEnd)
onlyBefore(revealEnd)
{
uint length = bids[msg.sender].length;
require(values.length == length);
require(fakes.length == length);
require(secrets.length == length);
uint refund;
for (uint i = 0; i < length; i++) {
Bid storage bidToCheck = bids[msg.sender][i];
(uint value, bool fake, bytes32 secret) =
(values[i], fakes[i], secrets[i]);
if (bidToCheck.blindedBid != keccak256(abi.encodePacked(value, fake, secret))) {
// L'enchère n'a pas été révélée.
// Ne pas rembourser.
continue;
}
refund += bidToCheck.deposit;
if (!fake && bidToCheck.deposit >= value) {
if (placeBid(msg.sender, value))
refund -= value;
}
// Rendre impossible un double remboursement
bidToCheck.blindedBid = bytes32(0);
}
payable(msg.sender).transfer(refund);
}
/// Se faire rembourser une enchère battue.
function withdraw() public {
uint amount = pendingReturns[msg.sender];
if (amount > 0) {
// Il est important de mettre cette valeur à zéro car l'utilisateur
// pourrait rappeler cette fonction avant le retour de `send`.
// (voir remarque sur conditions -> effets -> interaction).
pendingReturns[msg.sender] = 0;
payable(msg.sender).transfer(amount);
}
}
/// Met fin à l'enchère et envoie
/// le montant de l'enchère la plus haute au bénéficiaire.
function auctionEnd()
external
onlyAfter(revealEnd)
{
if (ended) revert AuctionEndAlreadyCalled();
emit AuctionEnded(highestBidder, highestBid);
ended = true;
beneficiary.transfer(highestBid);
}
// Cette fonction interne ("internal") ne peut être appelée que
// que depuis l'intérieur du contrat (ou ses contrats dérivés).
function placeBid(address bidder, uint value) internal
returns (bool success)
{
if (value <= highestBid) {
return false;
}
if (highestBidder != address(0)) {
// Rembourse la précédent leader.
pendingReturns[highestBidder] += highestBid;
}
highestBid = value;
highestBidder = bidder;
return true;
}
}
Achat distant sécurisé
Purchasing goods remotely currently requires multiple parties that need to trust each other. The simplest configuration involves a seller and a buyer. The buyer would like to receive an item from the seller and the seller would like to get money (or an equivalent) in return. The problematic part is the shipment here: There is no way to determine for sure that the item arrived at the buyer.
There are multiple ways to solve this problem, but all fall short in one or the other way. In the following example, both parties have to put twice the value of the item into the contract as escrow. As soon as this happened, the money will stay locked inside the contract until the buyer confirms that they received the item. After that, the buyer is returned the value (half of their deposit) and the seller gets three times the value (their deposit plus the value). The idea behind this is that both parties have an incentive to resolve the situation or otherwise their money is locked forever.
This contract of course does not solve the problem, but gives an overview of how you can use state machine-like constructs inside a contract.
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.4;
contract Purchase {
uint public value;
address payable public seller;
address payable public buyer;
enum State { Created, Locked, Release, Inactive }
// The state variable has a default value of the first member, `State.created`
State public state;
modifier condition(bool condition_) {
require(condition_);
_;
}
/// Only the buyer can call this function.
error OnlyBuyer();
/// Only the seller can call this function.
error OnlySeller();
/// The function cannot be called at the current state.
error InvalidState();
/// The provided value has to be even.
error ValueNotEven();
modifier onlyBuyer() {
if (msg.sender != buyer)
revert OnlyBuyer();
_;
}
modifier onlySeller() {
if (msg.sender != seller)
revert OnlySeller();
_;
}
modifier inState(State state_) {
if (state != state_)
revert InvalidState();
_;
}
event Aborted();
event PurchaseConfirmed();
event ItemReceived();
event SellerRefunded();
// Ensure that `msg.value` is an even number.
// Division will truncate if it is an odd number.
// Check via multiplication that it wasn't an odd number.
constructor() payable {
seller = payable(msg.sender);
value = msg.value / 2;
if ((2 * value) != msg.value)
revert ValueNotEven();
}
/// Annule l'achat et rembourse l'ether du dépot.
/// Peut seulement être appelé par le vendeur
/// avant le verrouillage du contrat
function abort()
external
onlySeller
inState(State.Created)
{
emit Aborted();
state = State.Inactive;
// We use transfer here directly. It is
// reentrancy-safe, because it is the
// last call in this function and we
// already changed the state.
seller.transfer(address(this).balance);
}
/// Confirm the purchase as buyer.
/// Transaction has to include `2 * value` ether.
/// The ether will be locked until confirmReceived
/// is called.
function confirmPurchase()
external
inState(State.Created)
condition(msg.value == (2 * value))
payable
{
emit PurchaseConfirmed();
buyer = payable(msg.sender);
state = State.Locked;
}
/// Confirm that you (the buyer) received the item.
/// This will release the locked ether.
function confirmReceived()
external
onlyBuyer
inState(State.Locked)
{
emit ItemReceived();
// It is important to change the state first because
// otherwise, the contracts called using `send` below
// can call in again here.
state = State.Release;
buyer.transfer(value);
}
/// This function refunds the seller, i.e.
/// pays back the locked funds of the seller.
function refundSeller()
external
onlySeller
inState(State.Release)
{
emit SellerRefunded();
// It is important to change the state first because
// otherwise, the contracts called using `send` below
// can call in again here.
state = State.Inactive;
seller.transfer(3 * value);
}
}
Canaux de micro-paiement
Dans cette section, nous allons apprendre comment construire une implémentation simple d’un canal de paiement. Il utilise des signatures cryptographiques pour effectuer des transferts répétés d’Ether entre les mêmes parties en toute sécurité, instantanément et sans frais de transaction. Pour ce faire, nous devons comprendre comment signer et vérifier les signatures, et configurer le canal de paiement.
Création et vérification des signatures
Imaginez qu’Alice veuille envoyer une quantité d’Ether à Bob, c’est-à-dire qu’Alice est l’expéditeur et Bob est le destinataire.
Alice n’a qu’à envoyer des messages cryptographiquement signés hors chaîne (par exemple par e-mail) à Bob et cela sera très similaire à la rédaction de chèques.
Les signatures sont utilisées pour autoriser les transactions et sont un outil généraliste à la disposition des contrats intelligents. Alice construira un simple contrat intelligent qui lui permettra de transmettre des Ether, mais d’une manière inhabituelle, au lieu d’appeler une fonction elle-même pour initier un paiement, elle laissera Bob le faire, et donc payer les frais de transaction.
Le contrat fonctionnera comme suit :
Alice déploie le contrat
ReceiverPays
en y attachant suffisamment d’éther pour couvrir les paiements qui seront effectués.Alice autorise un paiement en signant un message avec sa clé privée.
Alice envoie le message signé cryptographiquement à Bob. Le message n’a pas besoin d’être gardé secret (vous le comprendrez plus tard), et le mécanisme pour l’envoyer n’a pas d’importance.
Bob réclame leur paiement en présentant le message signé au contrat intelligent, il vérifie l’authenticité du message et libère ensuite les fonds.
<<<<<<< HEAD Création de la signature ————————
Alice n’a pas besoin d’interagir avec le réseau Ethereum pour signer la transaction, le processus est complètement hors ligne. Dans ce tutoriel, nous allons signer les messages dans le navigateur en utilisant web3.js et MetaMask. En particulier, nous utiliserons la méthode standard décrite dans EIP-762, car elle offre un certain nombre d’autres avantages en matière de sécurité. =======
Alice deploys the
ReceiverPays
contract, attaching enough Ether to cover the payments that will be made.Alice authorises a payment by signing a message with her private key.
Alice sends the cryptographically signed message to Bob. The message does not need to be kept secret (explained later), and the mechanism for sending it does not matter.
Bob claims his payment by presenting the signed message to the smart contract, it verifies the authenticity of the message and then releases the funds.
Creating the signature
Alice does not need to interact with the Ethereum network to sign the transaction, the process is completely offline. In this tutorial, we will sign messages in the browser using web3.js and MetaMask, using the method described in EIP-712, as it provides a number of other security benefits. >>>>>>> 47d77931747aba8e364452537d989b795df7ca04
/// Hasher d'abord simplifie un peu les choses
var hash = web3.sha3("message to sign");
web3.personal.sign(hash, web3.eth.defaultAccount, function () {...});
- <<<<<<< HEAD
Notez que
web3.personal.sign
préfixe les données signées de la longueur du message. Mais comme nous avons hashé en premier, le message sera toujours exactement 32 octets de long, et donc ce préfixe de longueur est toujours le même, ce qui facilite tout.
Que signer
Dans le cas d’un contrat qui effectue des paiements, le message signé doit inclure :
Adresse du destinataire
le montant à transférer
Protection contre les attaques de rediffusion
Une attaque de rediffusion se produit lorsqu’un message signé est réutilisé pour revendiquer l’autorisation pour une deuxième action. Pour éviter les attaques par rediffusion, nous utiliserons la même méthode que pour les transactions Ethereum elles-mêmes, ce qu’on appelle un nonce, qui est le nombre de transactions envoyées par un compte. Le contrat intelligent vérifiera si un nonce est utilisé plusieurs fois.
Il existe un autre type d’attaques de redifussion, il se produit lorsque
le propriétaire déploie un smart contract ReceiverPays
, effectue certains paiements,
et ensuite détruit le contrat. Plus tard, il décide de déployer
ReceiverPays
encore une fois, mais le nouveau contrat ne peut pas
connaître les nonces utilisés dans le déploiement précédent, donc l’attaquant
peut réutiliser les anciens messages.
Alice peut s’en protéger, notamment en incluant
l’adresse du contrat dans le message, et seulement
les messages contenant l’adresse du contrat lui-même seront acceptés.
Cette fonctionnalité se trouve dans les deux premières lignes de la fonction claimPayment()
du contrat complet
à la fin de ce chapitre.
Encoder les arguments
Maintenant que nous avons déterminé quelles informations inclure dans le message signé,
nous sommes prêts à assembler le message, à le hacher,
et le signer. Par souci de simplicité, nous ne faisons que concaténer les données.
La bibliothèque
ethereumjs-abi fournit
une fonction appelée soliditySHA3
qui imite le comportement
de la fonction keccak256
de Solidity appliquée aux arguments codés
en utilisant abi.encododePacked
.
En résumé, voici une fonction JavaScript qui
crée la signature appropriée pour l’exemple ReceiverPays
:
=======
The
web3.eth.personal.sign
prepends the length of the message to the signed data. Since we hash first, the message will always be exactly 32 bytes long, and thus this length prefix is always the same.
What to Sign
For a contract that fulfils payments, the signed message must include:
The recipient’s address.
The amount to be transferred.
Protection against replay attacks.
A replay attack is when a signed message is reused to claim authorization for a second action. To avoid replay attacks we use the same technique as in Ethereum transactions themselves, a so-called nonce, which is the number of transactions sent by an account. The smart contract checks if a nonce is used multiple times.
Another type of replay attack can occur when the owner
deploys a ReceiverPays
smart contract, makes some
payments, and then destroys the contract. Later, they decide
to deploy the RecipientPays
smart contract again, but the
new contract does not know the nonces used in the previous
deployment, so the attacker can use the old messages again.
Alice can protect against this attack by including the
contract’s address in the message, and only messages containing
the contract’s address itself will be accepted. You can find
an example of this in the first two lines of the claimPayment()
function of the full contract at the end of this section.
Packing arguments
Now that we have identified what information to include in the signed message,
we are ready to put the message together, hash it, and sign it. For simplicity,
we concatenate the data. The ethereumjs-abi
library provides a function called soliditySHA3
that mimics the behaviour of
Solidity’s keccak256
function applied to arguments encoded using abi.encodePacked
.
Here is a JavaScript function that creates the proper signature for the ReceiverPays
example:
>>>>>>> 47d77931747aba8e364452537d989b795df7ca04
// recipient est l'addresse à payer.
// amount, en wei, spécifie combien d'Ether doivent être envoyés.
// nonce peut être n'importe quel nombre unique pour prévenir les attques par redifusion
// contractAddress est utilisé pour éviter les attaque par redifusion de messages inter-contrats
function signPayment(recipient, amount, nonce, contractAddress, callback) {
var hash = "0x" + ethereumjs.ABI.soliditySHA3(
["address", "uint256", "uint256", "address"],
[recipient, amount, nonce, contractAddress]
).toString("hex");
web3.personal.sign(hash, web3.eth.defaultAccount, callback);
}
<<<<<<< HEAD Récupérer le signataire du message en Solidity ———————————————-
En général, les signatures ECDSA se composent de deux paramètres, r
et s
.
Les signatures dans Ethereum incluent un troisième paramètre appelé « v », qui peut être utilisé
pour récupérer la clé privée du compte qui a été utilisée pour signer le message,
l’expéditeur de la transaction. La solidité offre une fonction intégrée
ecrecover
qui accepte un message avec les paramètres r
, s
et v
et
renvoie l’adresse qui a été utilisée pour signer le message.
Extraire les paramètres de signature
Les signatures produites par web3.js sont la concaténation de r
, s
et v
,
donc la première étape est de re-séparer ces paramètres. Cela peut être fait sur le client,
mais le faire à l’intérieur du smart contract signifie qu’un seul paramètre de signature
peut être envoyé au lieu de trois.
Diviser un tableau d’octets en plusieurs parties est un peu compliqué.
Nous utiliserons l”assembleur en ligne pour faire le travail
dans la fonction splitSignature
(la troisième fonction dans le contrat complet
à la fin du présent chapitre).
Calculer le hash du message
In general, ECDSA signatures consist of two parameters,
r
and s
. Signatures in Ethereum include a third
parameter called v
, that you can use to verify which
account’s private key was used to sign the message, and
the transaction’s sender. Solidity provides a built-in
function ecrecover that
accepts a message along with the r
, s
and v
parameters
and returns the address that was used to sign the message.
Extracting the Signature Parameters
Signatures produced by web3.js are the concatenation of r
,
s
and v
, so the first step is to split these parameters
apart. You can do this on the client-side, but doing it inside
the smart contract means you only need to send one signature
parameter rather than three. Splitting apart a byte array into
its constituent parts is a mess, so we use
inline assembly to do the job in the splitSignature
function (the third function in the full contract at the end of this section).
>>>>>>> 47d77931747aba8e364452537d989b795df7ca04
Le smart contract doit savoir exactement quels paramètres ont été signés,
et doit donc recréer le message à partir des paramètres et utiliser cette fonction
pour la vérification des signatures. Les fonctions prefixed
et
recoverSigner
s’occupent de cela et leur utilisation peut se trouver
dans la fonction claimPayment
.
Le contrat complet
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
contract ReceiverPays {
address owner = msg.sender;
mapping(uint256 => bool) usedNonces;
constructor() payable {}
function claimPayment(uint256 amount, uint256 nonce, bytes memory signature) external {
require(!usedNonces[nonce]);
usedNonces[nonce] = true;
// Cette ligne recrée le message signé par le client
bytes32 message = prefixed(keccak256(abi.encodePacked(msg.sender, amount, nonce, this)));
require(recoverSigner(message, signature) == owner);
payable(msg.sender).transfer(amount);
}
- <<<<<<< HEAD
/// détruit le contrat et réclame son solde. function shutdown() public {
- >>>>>>> 47d77931747aba8e364452537d989b795df7ca04
require(msg.sender == owner); selfdestruct(payable(msg.sender));
}
/// methodes de signature. function splitSignature(bytes memory sig)
internal pure returns (uint8 v, bytes32 r, bytes32 s)
- {
require(sig.length == 65);
- assembly {
// premiers 32 octets, après le préfixe r := mload(add(sig, 32)) // 32 octets suivants s := mload(add(sig, 64)) // Octet final (premier du prochain lot de 32) v := byte(0, mload(add(sig, 96)))
}
return (v, r, s);
}
- function recoverSigner(bytes32 message, bytes memory sig)
internal pure returns (address)
- {
(uint8 v, bytes32 r, bytes32 s) = splitSignature(sig);
return ecrecover(message, v, r, s);
}
/// construit un hash préfixé pour mimer le comportement de eth_sign. function prefixed(bytes32 hash) internal pure returns (bytes32) {
return keccak256(abi.encodePacked( »x19Ethereum Signed Message:n32 », hash));
}
}
Écrire un canal de paiement simple
Alice va maintenant construire une implémentation simple mais complète d’un canal de paiement. Les canaux de paiement utilisent des signatures cryptographiques pour effectuer des virements répétés d’Ether en toute sécurité, instantanément et sans frais de transaction.
Qu’est-ce qu’un canal de paiement ?
Les canaux de paiement permettent aux participants d’effectuer des transferts répétés d’Ether sans utiliser de transactions. Cela signifie que les délais et frais associés aux transactions peuvent être évités. Nous allons explorer un canal de paiement unidirectionnel simple entre deux parties (Alice et Bob). Son utilisation implique trois étapes :
- <<<<<<< HEAD
Alice déploie un smart contract avec de l’Ether. Cela « ouvre » (
opens
) le canal de paiement.Alice signe des messages qui précisent combien d’éther est dû au destinataire. Cette étape est répétée pour chaque paiement.
Bob « ferme » (
closes
) le canal de paiement, retirant leur part d’Ether et renvoyant le reste à l’expéditeur.
Note
Non seulement les étapes 1 et 3 exigent des transactions Ethereum, mais l’étape 2 signifie que l’expéditeur transmet un message signé cryptographiquement au destinataire par des moyens hors chaîne (par exemple, par courrier électronique). Cela signifie que seulement deux transactions sont nécessaires pour traiter un nombre quelconque de transferts.
Bob est assuré de recevoir ses fonds parce que le contrat bloque les fonds en Ether et respecte des ordres valides et signés. Le smart contract impose également un délai d’attente, Alice est donc assurée de recouvrer ses fonds même si le bénéficiaire refuse de fermer le canal. C’est aux participants d’un canal de paiement de décider combien de temps il doit rester ouvert. Pour une transaction de courte durée, comme payer un cybercafé pour chaque minute d’accès au réseau, ou dans le cas d’une relation de plus longue durée, comme le versement d’un salaire horaire à un employé, un paiement pourrait durer des mois ou des années.
Ouverture du canal de paiement
Bob « closes » the payment channel, withdrawing his portion of the Ether and sending the remainder back to the sender.
Note
Only steps 1 and 3 require Ethereum transactions, step 2 means that the sender transmits a cryptographically signed message to the recipient via off chain methods (e.g. email). This means only two transactions are required to support any number of transfers.
Bob is guaranteed to receive his funds because the smart contract escrows the Ether and honours a valid signed message. The smart contract also enforces a timeout, so Alice is guaranteed to eventually recover her funds even if the recipient refuses to close the channel. It is up to the participants in a payment channel to decide how long to keep it open. For a short-lived transaction, such as paying an internet café for each minute of network access, the payment channel may be kept open for a limited duration. On the other hand, for a recurring payment, such as paying an employee an hourly wage, the payment channel may be kept open for several months or years.
Opening the Payment Channel
>>>>>>> 47d77931747aba8e364452537d989b795df7ca04
Pour ouvrir le canal de paiement, Alice déploie le contrat,
y attachant de l’Ether en dépot et spécifiant le destinataire prévu,
ainsi qu’une durée de vie maximale du canal. C’est la fonction
SimplePaymentChannel
dans le contrat.
Effectuer des paiements
Alice effectue des paiements en envoyant des messages signés à Bob. Cette étape est entièrement réalisée en dehors du réseau Ethereum. Les messages sont signés cryptographiquement par l’expéditeur puis transmis directement au destinataire.
Chaque message contient les informations suivantes :
L’adresse du contrat, utilisé pour empêcher les attaques de redifussion par contrats croisés.
Le montant total d’Ether qui est dû au bénéficiaire jusqu’alors.
Un canal de paiement est fermé une seule fois, à la fin d’une série de virements. De ce fait, un seul des messages envoyés sera échangé. C’est pourquoi chaque message spécifie un montant total cumulatif d’éther dû, plutôt que le montant total d’un micropaiement individuel. Le destinataire réclamera naturellement le message le plus récent parce que c’est celui dont le total est le plus élevé. Le nonce par message n’est plus nécessaire, car le smart contract ne va honorer qu’un seul message. L’adresse du contrat intelligent est toujours utilisée pour éviter qu’un message destiné à un canal de paiement ne soit utilisé pour un autre canal.
Voici le code javascript modifié pour signer cryptographiquement un message du chapitre précédent :
function constructPaymentMessage(contractAddress, amount) {
return abi.soliditySHA3(
["address", "uint256"],
[contractAddress, amount]
);
}
function signMessage(message, callback) {
web3.eth.personal.sign(
"0x" + message.toString("hex"),
web3.eth.defaultAccount,
callback
);
}
// contractAddress détectera la rediffusion de messages à d'autres contrats.
// amount, en wei, précise combien d'Ether doivent être envoyés.
function signPayment(contractAddress, amount, callback) {
var message = constructPaymentMessage(contractAddress, amount);
signMessage(message, callback);
}
Fermeture du canal de paiement
Lorsque Bob est prêt à recevoir leurs ses, il est temps de
fermer le canal de paiement en appelant une fonction close
sur le smart contract.
La fermeture du canal paie au destinataire l’Ether qui lui est dû et détruit le contrat,
en renvoyant tout Ether restant à Alice.
Pour fermer le canal, Bob doit fournir un message signé par Alice.
Le contrat doit vérifier que le message contient une signature valide de l’expéditeur.
Le processus de vérification est le même que celui utilisé par le destinataire.
Les fonctions Solidity isValidSignature
et recoverSigner
fonctionnent de la même manière que leurs fonctions
JavaScript dans la section précédente. Ce dernier est emprunté au
Le contrat ReceiverPays
du chapitre précédent.
La fonction close
ne peut être appelée que par le destinataire du canal de paiement,
qui enverra naturellement le message de paiement le plus récent car c’est celui qui comporte
le plus haut total dû. Si l’expéditeur était autorisé à appeler cette fonction,
il pourrait fournir un message avec un montant inférieur et escroquer le destinataire de ce qui lui est dû.
La fonction vérifie que le message signé correspond aux paramètres donnés.
Si tout se passe bien, le destinataire reçoit sa part d’Ether,
et l’expéditeur reçoit le reste par selfdestruct
(autodestruction) du contrat.
Vous pouvez voir la fonction close
dans le contrat complet.
Expiration du canal
Bob peut fermer le canal de paiement à tout moment, mais s’il ne le fait pas,
Alice a besoin d’un moyen de récupérer les fonds bloqués. Une durée d”expiration a été définie
au moment du déploiement du contrat. Une fois cette heure atteinte, Alice peut appeler
pour récupérer leurs fonds. Vous pouvez voir la fonction claimTimeout
dans le
contrat déployé.
Après l’appel de cette fonction, Bob ne peut plus recevoir d’Ether. Il est donc important que Bob ferme le canal avant que l’expiration ne soit atteinte.
Le contrat complet
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
contract SimplePaymentChannel {
address payable public sender; // Le compte emvoyant les paiements.
address payable public recipient; // Le compte destinataire des paiements.
uint256 public expiration; // Expitration si le destinataire ne clot pas le canal.
constructor (address payable recipientAddress, uint256 duration)
payable
{
sender = payable(msg.sender);
recipient = recipientAddress;
expiration = block.timestamp + duration;
}
/// Le destinataire peut clore le canal à tout moment en présentant le dernier montant
/// signé par l'expéditeur des fonds. Le destinataire se verra verser ce montant,
/// et le reste sera rendu à l'emetteur des fonds.
function close(uint256 amount, bytes memory signature) external {
require(msg.sender == recipient);
require(isValidSignature(amount, signature));
recipient.transfer(amount);
selfdestruct(sender);
}
/// L'emetteur peut modifier la date d'expiration à tout moment
function extend(uint256 newExpiration) external {
require(msg.sender == sender);
require(newExpiration > expiration);
expiration = newExpiration;
}
/// Si l'expiration est atteinte avant cloture par le destinataire,
/// l'Ether est renvoyé à l'emetteur
function claimTimeout() external {
require(now >= expiration);
selfdestruct(sender);
}
function isValidSignature(uint256 amount, bytes memory signature)
internal
view
returns (bool)
{
bytes32 message = prefixed(keccak256(abi.encodePacked(this, amount)));
// check that the signature is from the payment sender
return recoverSigner(message, signature) == sender;
}
/// Toutes les fonctions ci-dessous sont tirées
/// du chapitre 'créer et vérifier les signatures'.
function splitSignature(bytes memory sig)
internal
pure
returns (uint8 v, bytes32 r, bytes32 s)
{
require(sig.length == 65);
assembly {
// first 32 bytes, after the length prefix
r := mload(add(sig, 32))
// second 32 bytes
s := mload(add(sig, 64))
// final byte (first byte of the next 32 bytes)
v := byte(0, mload(add(sig, 96)))
}
return (v, r, s);
}
function recoverSigner(bytes32 message, bytes memory sig)
internal
pure
returns (address)
{
(uint8 v, bytes32 r, bytes32 s) = splitSignature(sig);
return ecrecover(message, v, r, s);
}
/// construit un hash préfixé pour mimer le comportement de eth_sign.
function prefixed(bytes32 hash) internal pure returns (bytes32) {
return keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", hash));
}
}
Note
La fonction splitSignature
est très simple et n’utilise pas tous les contrôles de sécurité.
Une implémentation réelle devrait utiliser une bibliothèque plus rigoureusement testée de ce code, tel que le fait openzepplin avec version of this code.
Vérification des paiements
Contrairement à notre chapitre précédent, les messages dans un canal de paiement ne sont pas appliqués tout de suite. Le destinataire conserve la trace du dernier message et le fait parvenir au réseau quand il est temps de fermer le canal de paiement. Cela signifie qu’il est essentiel que le destinataire effectue sa propre vérification de chaque message. Sinon, il n’y a aucune garantie que le destinataire sera en mesure d’être payé à la fin.
Le destinataire doit vérifier chaque message à l’aide du processus suivant :
Vérifiez que l’adresse du contact dans le message correspond au canal de paiement.
Vérifiez que le nouveau total est le montant prévu.
Vérifier que le nouveau total ne dépasse pas la quantité d’éther déposée.
Vérifiez que la signature est valide et provient de l’expéditeur du canal de paiement.
Nous utiliserons la librairie ethereumjs-util pour écrire ces vérifications. L’étape finale peut se faire de plusieurs façons, ici en JavaScript, Le code suivant emprunte la fonction constructPaymentMessage du code JavaScript de signature ci-dessous :
// Cette ligne mine le fonctionnement de la méthode JSON-RPC de eth_sign.
function prefixed(hash) {
return ethereumjs.ABI.soliditySHA3(
["string", "bytes32"],
["\x19Ethereum Signed Message:\n32", hash]
);
}
function recoverSigner(message, signature) {
var split = ethereumjs.Util.fromRpcSig(signature);
var publicKey = ethereumjs.Util.ecrecover(message, split.v, split.r, split.s);
var signer = ethereumjs.Util.pubToAddress(publicKey).toString("hex");
return signer;
}
function isValidSignature(contractAddress, amount, signature, expectedSigner) {
var message = prefixed(constructPaymentMessage(contractAddress, amount));
var signer = recoverSigner(message, signature);
return signer.toLowerCase() ==
ethereumjs.Util.stripHexPrefix(expectedSigner).toLowerCase();
}
Les contrats modulaires
Une approche modulaire pour créer vos contrats vous aide pour réduire sa complexité
et améliore la lisibilité qui va aider pour identifier les bugs et les vulnérabilités
durant le développement et la revue du code.
If you specify and control the behaviour or each module in isolation, the
interactions you have to consider are only those between the module specifications
and not every other moving part of the contract.
In the example below, the contract uses the move
method
of the Balances
library to check that balances sent between
addresses match what you expect. In this way, the Balances
library
provides an isolated component that properly tracks balances of accounts.
It is easy to verify that the Balances
library never produces negative balances or overflows
and the sum of all balances is an invariant across the lifetime of the contract.
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.5.0 <0.9.0;
library Balances {
function move(mapping(address => uint256) storage balances, address from, address to, uint amount) internal {
require(balances[from] >= amount);
require(balances[to] + amount >= balances[to]);
balances[from] -= amount;
balances[to] += amount;
}
}
contract Token {
mapping(address => uint256) balances;
using Balances for *;
mapping(address => mapping (address => uint256)) allowed;
event Transfer(address from, address to, uint amount);
event Approval(address owner, address spender, uint amount);
function transfer(address to, uint amount) external returns (bool success) {
balances.move(msg.sender, to, amount);
emit Transfer(msg.sender, to, amount);
return true;
}
function transferFrom(address from, address to, uint amount) external returns (bool success) {
require(allowed[from][msg.sender] >= amount);
allowed[from][msg.sender] -= amount;
balances.move(from, to, amount);
emit Transfer(from, to, amount);
return true;
}
function approve(address spender, uint tokens) external returns (bool success) {
require(allowed[msg.sender][spender] == 0, "");
allowed[msg.sender][spender] = tokens;
emit Approval(msg.sender, spender, tokens);
return true;
}
function balanceOf(address tokenOwner) external view returns (uint balance) {
return balances[tokenOwner];
}
}
Structure d’un fichier source Solidity
Les fichiers sources peuvent contenir un nombre arbitraire de définitions de contrats, directives d”import et directives pragma et définitions de structs et d”enums, fonctions, errors et ddéfinitions de variables constantes.
SPDX License Identifier
Trust in smart contracts can be better established if their source code is available. Since making source code available always touches on legal problems with regards to copyright, the Solidity compiler encourages the use of machine-readable SPDX license identifiers. Every source file should start with a comment indicating its license:
// SPDX-License-Identifier: MIT
The compiler does not validate that the license is part of the list allowed by SPDX, but it does include the supplied string in the bytecode metadata.
If you do not want to specify a license or if the source code is
not open-source, please use the special value UNLICENSED
.
Note that UNLICENSED
(no usage allowed, not present in SPDX license list)
is different from UNLICENSE
(grants all rights to everyone).
Solidity follows the npm recommendation.
Supplying this comment of course does not free you from other obligations related to licensing like having to mention a specific license header in each source file or the original copyright holder.
The comment is recognized by the compiler anywhere in the file at the file level, but it is recommended to put it at the top of the file.
More information about how to use SPDX license identifiers can be found at the SPDX website.
Pragmas
Le mot-clé pragma
peut être utilisé pour activer certaines fonctions ou vérifications du compilateur. Une directive pragma est toujours locale à un fichier source, donc vous devez donc ajouter pragma à tous vos fichiers si vous voulez l’activer dans tout votre projet.
Si vous importez un autre fichier, le pragma de ce fichier ne s’appliquera pas automatiquement au fichier à importer.
Version Pragma
Les fichiers sources peuvent (et devraient) être annotés avec un version pragma
pour refuser d’être compilés avec de futures versions de compilateurs qui pourraient introduire des changements incompatibles. Nous essayons de limiter ces changements au strict minimum, et en particulier
introduire des changements d’une manière telle que les changements de sémantique nécessiteront également des changements de syntaxe, mais ce n’est bien sûr pas toujours possible. Pour cette raison, c’est toujours une bonne idée de lire le fichier des modifications (« changelog ») au moins pour les versions qui contiennent des changements de rupture, ces versions auront toujours des versions de la forme 0.x.0
ou x.0.0
.
Le version pragma
est utilisée comme suit:
pragma solidity ^0.4.0;
Un tel fichier source ne compilera pas avec un compilateur antérieur à la version 0.4.0 et ne fonctionnera pas non plus sur un compilateur à partir de la version 0.5.0 (cette deuxième condition est ajoutée en utilisant ^
). L’idée derrière cela est la supposition qu’il n’y aura pas de changements de rupture jusqu’à la version 0.5.0
, donc nous pouvons toujours être sûrs que notre code compilera la façon dont nous l’avons prévu. Nous ne précisons pas la version exacte de correctif du compilateur, de sorte que les versions corrigées sont toujours possibles.
Il est possible de spécifier des règles beaucoup plus complexes pour la version du compilateur, la syntaxe suit celle utilisée par npm.
Note
L’utilisation de version pragma
ne changera pas la version du compilateur.
Il n’activera ou désactivera pas non plus les fonctions du compilateur. Il demandera simplement au compilateur de vérifier si sa version correspond à celle requise par le pragma. S’il ne correspond pas, le compilateur affichera une erreur.
ABI Coder Pragma
By using pragma abicoder v1
or pragma abicoder v2
you can
select between the two implementations of the ABI encoder and decoder.
The new ABI coder (v2) is able to encode and decode arbitrarily nested
arrays and structs. It might produce less optimal code and has not
received as much testing as the old encoder, but is considered
non-experimental as of Solidity 0.6.0. You still have to explicitly
activate it using pragma abicoder v2;
. Since it will be
activated by default starting from Solidity 0.8.0, there is the option to select
the old coder using pragma abicoder v1;
.
The set of types supported by the new encoder is a strict superset of
the ones supported by the old one. Contracts that use it can interact with ones
that do not without limitations. The reverse is possible only as long as the
non-abicoder v2
contract does not try to make calls that would require
decoding types only supported by the new encoder. The compiler can detect this
and will issue an error. Simply enabling abicoder v2
for your contract is
enough to make the error go away.
Note
This pragma applies to all the code defined in the file where it is activated, regardless of where that code ends up eventually. This means that a contract whose source file is selected to compile with ABI coder v1 can still contain code that uses the new encoder by inheriting it from another contract. This is allowed if the new types are only used internally and not in external function signatures.
Note
Up to Solidity 0.7.4, it was possible to select the ABI coder v2
by using pragma experimental ABIEncoderV2
, but it was not possible
to explicitly select coder v1 because it was the default.
Pragma Expérimental
Le deuxième pragma est le experimental pragma
. Il peut être utilisé pour activer des fonctions du compilateur ou de la langue qui ne sont pas encore activées par défaut.
Les pragmas expérimentaux suivants sont actuellement pris en charge :
ABIEncoderV2
Because the ABI coder v2 is not considered experimental anymore,
it can be selected via pragma abicoder v2
(please see above)
since Solidity 0.7.4.
SMTChecker
Ce composant doit être activé lors de la compilation du compilateur et n’est par conséquent pas forcément présent dans tous les binaires Solidity. Les instructions de compilation expliquent comment activer cette option. Elle est activée pour les versions PPA d’Ubuntu dans la plupart des versions, mais pas pour solc-js, les images Docker, les binaires Windows ni les binaires Linux pré-compilés. It can be activated for solc-js via the smtCallback if you have an SMT solver installed locally and run solc-js via node (not via the browser).
Si vous utilisez pragma experimental SMTChecker;
, vous aurez des avertissements de sécuristé supplémentaires qui sont obtenus en interrogeant un solveur SMT.
Le composant ne prend pas encore en charge toutes les fonctionnalités du langage Solidity et émet probablement de nombreux avertissements. Dans le cas où il signale des caractéristiques non prises en charge, l’analyse peut ne pas être cohérente.
Importation d’autres fichiers sources
Syntaxe et sémantique
Solidity supporte les instructions d’importation qui sont très similaires à celles disponibles en JavaScript (à partir de ES6), bien que Solidity ne connaisse pas le concept de default export.
Au niveau global, vous pouvez utiliser les instructions d’importation sous la forme suivante :
import "filename";
The filename
part is called an import path.
Cette instruction importe tous les symboles globaux de « filename » (et les symboles qui y sont importés) dans le champ d’application global actuel (différent de celui de ES6 mais rétrocompatible pour Solidity).
Cette syntaxe simple n’est pas recommandée car elle pollue l’espace de noms d’une manière imprévisible: Si vous ajoutez de nouveaux éléments de niveau supérieur dans « filename », ils apparaîtront automatiquement dans tous les fichiers qui importent ainsi à partir de « nom de fichier ». Il est préférable d’importer explicitement des symboles spécifiques.
L’exemple suivant crée un nouveau symbole global symbolName
dont les membres sont tous les symboles globaux de "filename"
.
import * as symbolName from "filename";
which results in all global symbols being available in the format symbolName.symbol
.
A variant of this syntax that is not part of ES6, but possibly useful is:
import "filename" as symbolName;
which is equivalent to import * as symbolName from "filename";
.
En cas de collision de noms, vous pouvez également renommer les symboles lors de l’importation.
Ce code crée de nouveaux symboles globaux alias
et symbole2
qui font référence à symbole1
et symbole2
de "nom de fichier"
, respectivement.
import {symbol1 as alias, symbol2} from "filename";
Import Paths
In order to be able to support reproducible builds on all platforms, the Solidity compiler has to abstract away the details of the filesystem where source files are stored. For this reason import paths do not refer directly to files in the host filesystem. Instead the compiler maintains an internal database (virtual filesystem or VFS for short) where each source unit is assigned a unique source unit name which is an opaque and unstructured identifier. The import path specified in an import statement is translated into a source unit name and used to find the corresponding source unit in this database.
Using the Standard JSON API it is possible to directly provide the names and content of all the source files as a part of the compiler input. In this case source unit names are truly arbitrary. If, however, you want the compiler to automatically find and load source code into the VFS, your source unit names need to be structured in a way that makes it possible for an import callback to locate them. When using the command-line compiler the default import callback supports only loading source code from the host filesystem, which means that your source unit names must be paths. Some environments provide custom callbacks that are more versatile. For example the Remix IDE provides one that lets you import files from HTTP, IPFS and Swarm URLs or refer directly to packages in NPM registry.
For a complete description of the virtual filesystem and the path resolution logic used by the compiler see Path Resolution.
Commentaires
Les commentaires sur une seule ligne (//
) et les commentaires sur plusieurs lignes (/*...*/
) sont possibles.
// Ceci est un commentaire sur une ligne.
/*
Ceci est un commentaire
multi-lignes.
*/
Note
Un commentaire d’une seule ligne est terminé par tout terminateur de ligne unicode (LF, VF, FF, CR, NEL, LS ou PS) en codage utf8. Le terminateur fait toujours partie du code source après le commentaire, donc si ce n’est pas un symbole ascii (que sont NEL, LS et PS), il conduira à une erreur de parsing.
De plus, il existe un autre type de commentaire appelé commentaire natspec, détaillé dans style guide. Ils sont écrits avec une triple barre oblique (///
) ou un double bloc d’astérisque (/**... */
) et ils doivent être utilisés directement au-dessus des déclarations ou instructions de fonction.
Structure d’un contrat
Les contrats Solidity sont similaires à des classes dans des langages orientés objet. Chaque contrat peut contenir des déclarations de Variables d’état, Fonctions, Modificateurs de fonction, Évènements, Errors, Types Structure et Types Enum. De plus, les contrats peuvent hériter d’autres contrats.
Il existe également des types de contrats spéciaux appelés libraries et interfaces.
La section sur les contrats contient plus de détails que cette section, qui permet d’avoir une vue d’ensemble rapide.
Variables d’état
Les variables d’état sont des variables dont les valeurs sont stockées en permanence dans le storage du contrat. .. code-block:: solidity
// SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.4.0 <0.9.0;
- contract SimpleStorage {
uint storedData; // State variable // …
}
Voir la section Types pour les types de variables d’état valides et Visibilité et Getters pour les choix possibles de visibilité.
Fonctions
Les fonctions sont les unités exécutables du code d’un contrat. Functions are usually defined inside a contract, but they can also be defined outside of contracts.
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.1 <0.9.0;
contract SimpleAuction {
function bid() public payable { // Function
// ...
}
}
// Helper function defined outside of a contract
function helper(uint x) pure returns (uint) {
return x * 2;
}
Les Appels de fonction peuvent se faire en interne ou en externe et ont différents niveaux de visibilité pour d’autres contrats. Functions accept parameters and return variables to pass parameters and values between them.
Modificateurs de fonction
Les modificateurs de fonction peuvent être utilisés pour modifier la sémantique des fonctions d’une manière déclarative (voir Modificateurs de fonctions dans la section contrats).
Overloading, that is, having the same modifier name with different parameters, is not possible.
Like functions, modifiers can be overridden.
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.22 <0.9.0;
contract Purchase {
address public seller;
modifier onlySeller() { // Modifier
require(
msg.sender == seller,
"Only seller can call this."
);
_;
}
function abort() public view onlySeller { // Modifier usage
// ...
}
}
Évènements
Les évènements (event
) sont une interface d’accès aux fonctionnalités de journalisation (logs) de l’EVM.
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.21 <0.9.0;
contract SimpleAuction {
event HighestBidIncreased(address bidder, uint amount); // Event
function bid() public payable {
// ...
emit HighestBidIncreased(msg.sender, msg.value); // Triggering event
}
}
Voir Événements dans la section contrats pour plus d’informations sur la façon dont les événements sont déclarés et peuvent être utilisés à partir d’une dapp.
Errors
Errors allow you to define descriptive names and data for failure situations. Errors can be used in revert statements. In comparison to string descriptions, errors are much cheaper and allow you to encode additional data. You can use NatSpec to describe the error to the user.
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.4;
/// Not enough funds for transfer. Requested `requested`,
/// but only `available` available.
error NotEnoughFunds(uint requested, uint available);
contract Token {
mapping(address => uint) balances;
function transfer(address to, uint amount) public {
uint balance = balances[msg.sender];
if (balance < amount)
revert NotEnoughFunds(amount, balance);
balances[msg.sender] -= amount;
balances[to] += amount;
// ...
}
}
See Errors and the Revert Statement in the contracts section for more information.
Types Structure
Les structures sont des types personnalisés qui peuvent regrouper plusieurs variables (voir Structs dans la section types).
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.0 <0.9.0;
contract Ballot {
struct Voter { // Struct
uint weight;
bool voted;
address delegate;
uint vote;
}
}
Types Enum
Les Enumérateurs (enum
) peuvent être utilisés pour créer des types personnalisés avec un ensemble fini de “valeurs constantes” (voir Énumérateurs dans la section Types).
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.0 <0.9.0;
contract Purchase {
enum State { Created, Locked, Inactive } // Enum
}
Types
Solidity est un langage à typage statique, ce qui signifie que le type de chaque variable (état et locale) doit être spécifié. Solidity propose plusieurs types élémentaires qui peuvent être combinés pour former des types complexes.
De plus, les types peuvent interagir entre eux dans des expressions contenant des opérateurs. Pour une liste synthétique des différents opérateurs, voir Order of Precedence of Operators.
The concept of « undefined » or « null » values does not exist in Solidity, but newly
declared variables always have a default value dependent
on its type. To handle any unexpected values, you should use the revert function to revert the whole transaction, or return a
tuple with a second bool
value denoting success.
Value Types
The following types are also called value types because variables of these types will always be passed by value, i.e. they are always copied when they are used as function arguments or in assignments.
Booleans
bool
: The possible values are constants true
and false
.
Operators:
!
(logical negation)&&
(logical conjunction, « and »)||
(logical disjunction, « or »)==
(equality)!=
(inequality)
The operators ||
and &&
apply the common short-circuiting rules. This means that in the expression f(x) || g(y)
, if f(x)
evaluates to true
, g(y)
will not be evaluated even if it may have side-effects.
Entiers
int
/ uint
: Entiers signés et non-signés de différentes tailles. Les mots-clé uint8
à uint256
par pas de 8
(entier non signé de 8 à 256 bits) et int8
à int256
. uint
et int
sont des alias de uint256
et int256
, respectivement.
Opérateurs:
Comparaisons:
<=
,<
,==
,!=
,>=
,>
(retournent unbool
)Opérateurs binaires:
&
,|
,^
(ou exclusif binaire),~
(négation binaire)Opérateurs de décalage:
<<
(décalage vers la gauche),>>
(décalage vers la droite)Opérateurs arithmétiques:
+
,-
, l” opérateur unaire-
,*
,/
,%
(modulo),**
(exponentiation)
For an integer type X
, you can use type(X).min
and type(X).max
to
access the minimum and maximum value representable by the type.
Avertissement
Integers in Solidity are restricted to a certain range. For example, with uint32
, this is 0
up to 2**32 - 1
.
There are two modes in which arithmetic is performed on these types: The « wrapping » or « unchecked » mode and the « checked » mode.
By default, arithmetic is always « checked », which mean that if the result of an operation falls outside the value range
of the type, the call is reverted through a failing assertion. You can switch to « unchecked » mode
using unchecked { ... }
. More details can be found in the section about unchecked.
Comparaisons
La valeur d’une comparaison est celle obtenue en comparant la valeur entière.
Opérations binaires
Les opérations binaires sont effectuées sur la représentation du nombre par complément à deux<https://fr.wikipedia.org/wiki/Compl%C3%A9ment_%C3%A0_deux>.
Cela signifie que, par exemple, ~int256(0) == int256(-1)
.
Décalages
The result of a shift operation has the type of the left operand, truncating the result to match the type. The right operand must be of unsigned type, trying to shift by a signed type will produce a compilation error.
Shifts can be « simulated » using multiplication by powers of two in the following way. Note that the truncation to the type of the left operand is always performed at the end, but not mentioned explicitly.
x << y
is equivalent to the mathematical expressionx * 2**y
.x >> y
is equivalent to the mathematical expressionx / 2**y
, rounded towards negative infinity.
Décaler d’un nombre négatif de bits déclenche une exception.
Avertissement
Avant la version 0.5.0.0
, un décalage vers la droite x >> y
pour un x
négatif était équivalent à x / 2**y
, c’est-à-dire que les décalages vers la droite étaient arrondis vers zéro plutôt que vers l’infini négatif.
Note
Overflow checks are never performed for shift operations as they are done for arithmetic operations. Instead, the result is always truncated.
Addition, Soustraction et Multiplication
L’addition, la soustraction et la multiplication ont la sémantique habituelle., with two different modes in regard to over- and underflow:
By default, all arithmetic is checked for under- or overflow, but this can be disabled using the unchecked block, resulting in wrapping arithmetic. More details can be found in that section.
The expression -x
is equivalent to (T(0) - x)
where
T
is the type of x
. It can only be applied to signed types.
The value of -x
can be
positive if x
is negative. There is another caveat also resulting
from two’s complement representation:
L’expression x
équivaut à (T(0) - x)
où T
est le type de x
. Cela signifie que -x
ne sera pas négatif si le type de x
est un type entier non signé. De plus, x
peut être positif si x
est négatif. Il y a une autre mise en garde qui découle également de la représentation en compléments de deux:
int x = -2**255;
assert(-x == x);
Cela signifie que même si un nombre est négatif, vous ne pouvez pas supposer que sa négation sera positive.
If you have int x = type(int).min;
, then -x
does not fit the positive range.
This means that unchecked { assert(-x == x); }
works, and the expression -x
when used in checked mode will result in a failing assertion.
Division
Puisque le type du résultat d’une opération est toujours le type d’un des opérandes, la division sur les entiers donne toujours un entier.
Dans Solidity, la division s’arrondit vers zéro. Cela signifie que int256(-5) / int256(2) == int256(-2)
.
Notez qu’en revanche, la division sur les littéraux donne des valeurs fractionnaires de précision arbitraire.
Note
Division by zero causes a Panic error. This check can not be disabled through unchecked { ... }
.
Note
The expression type(int).min / (-1)
is the only case where division causes an overflow.
In checked arithmetic mode, this will cause a failing assertion, while in wrapping
mode, the value will be type(int).min
.
Modulo
L’opération modulo a % n
donne le reste r
après la division de l’opérande a
par l’opérande n
, où q = int(a / n)
et r = a - (n * q)
. Cela signifie que modulo donne le même signe que son opérande gauche (ou zéro) et a % n == -(abs(a) % n)
est valable pour un a
négatif:
int256(5) % int256(2) == int256(1)
int256(5) % int256(-2) == int256(1)
int256(-5) % int256(2) == int256(-1)
int256(-5) % int256(-2) == int256(-1)
Note
Modulo with zero causes a Panic error. This check can not be disabled through unchecked { ... }
.
Exponentiation
l’exponentiation n’est disponible que pour les types signés et donne un résultat du même type. Veillez à ce que les types que vous utilisez soient suffisamment grands pour conserver le résultat et vous préparer à un éventuel effet d’enroulage (wrapping/int overflow).
Note
In checked mode, exponentiation only uses the comparatively cheap exp
opcode for small bases.
For the cases of x**3
, the expression x*x*x
might be cheaper.
In any case, gas cost tests and the use of the optimizer are advisable.
Note
0**0
est défini par l’EVM comme étant 1
.
Nombre à virgule fixe
Avertissement
Les numéros à point fixe ne sont pas encore entièrement pris en charge par Solidity. Ils peuvent être déclarés, mais ne peuvent pas être affectés à ou de.
fixed
/ ufixed
: Nombre à virgule fixe signés et non-signés de taille variable. Les mots-clés ufixedMxN
et fixedMxN
, où M
représente le nombre de bits pris par le type et N
représente combien de décimales sont disponibles. M
doit être divisible par 8 et peut aller de 8 à 256 bits. N
doit être compris entre 0 et 80, inclusivement.
ufixed
et fixed
sont des alias pour ufixed128x18
et fixed128x18
, respectivement.
Opérateurs:
Comparaisons:
<=
,<
,==
,!=
,>=
,>
(évalue àbool
)Operateurs arithmétiques:
+
,-
, l’opérateur unaire-
,*
,/
,%
(modulo)
Note
La principale différence entre les nombres à virgule flottante (float``et ``double
dans de nombreux langages, plus précisément les nombres IEEE 754) et les nombres à virgule fixe est que le nombre de bits utilisés pour l’entier et la partie fractionnaire (la partie après le point décimal) est flexible dans le premier, alors qu’il est strictement défini dans le second. Généralement, en virgule flottante, presque tout l’espace est utilisé pour représenter le nombre, alors que seul un petit nombre de bits définit où se trouve le point décimal.
Adresses
Le type d’adresse se décline en deux versions, qui sont en grande partie identiques :
address
: Contient une valeur de 20 octets (taille d’une adresse Ethereum).
address payable
: Même chose que « adresse », mais avec les membres additionnelstransfert
etenvoi
.
L’idée derrière cette distinction est que l”address payable
est une adresse à laquelle vous pouvez envoyer de l’éther, alors que vous n’êtes pas censés envoyer d’ethers à une simple address
, for example because it might be a smart contract
that was not built to accept Ether.
Conversions de type :
Les conversions implicites de address payable
à address
sont autorisées, tandis que les conversions de address
à address payable
ne sont pas possibles.
Explicit conversions to and from address
are allowed for uint160
, integer literals,
bytes20
and contract types.
Only expressions of type address
and contract-type can be converted to the type address
payable
via the explicit conversion payable(...)
. For contract-type, this conversion is only
allowed if the contract can receive Ether, i.e., the contract either has a receive or a payable fallback function. Note that payable(0)
is valid and is
an exception to this rule.
Note
If you need a variable of type address
and plan to send Ether to it, then
declare its type as address payable
to make this requirement visible. Also,
try to make this distinction or conversion as early as possible.
Opérateurs :
<=
,<
,==
,!=
,>=
and>
Avertissement
- Si vous convertissez un type qui utilise une taille d’octet plus grande en
address
, par exemplebytes32
, alors l’adresse est tronquée. Pour réduire l’ambiguïté de conversion à partir de la version 0.4.24 du compilateur vous force à rendre la troncature explicite dans la conversion. Prenons par exemple l’adresse
0x1111222222323333434444545555666666777777778888999999AAAABBBBBBCCDDDDEEFEFFFFFFCC
.Vous pouvez utiliser
address(uint160(octets20(b)))
, ce qui donne0x1111212222323333434444545555666677778888889999aAaaa
, ou vous pouvez utiliseraddress(uint160(uint256(b)))
, ce qui donne0x777777888888999999AaAAbBbbCcccddDdeeeEfFFfCcCcCc
.
Note
- La distinction entre
address``et ``address payable
a été introduite avec la version 0.5.0. À partir de cette version également, les contrats ne dérivent pas du type d’adresse, mais peuvent toujours être convertis explicitement en adresse » ou à » adresse payable « , s’ils ont une fonction par défaut payable.
Membres de Address
Pour une liste des membres de address, voir Membres du type address.
balance
ettransfer
.
Il est possible d’interroger le solde d’une adresse en utilisant la propriété balance
et d’envoyer des Ether (en unités de wei) à une adresse payable à l’aide de la fonction transfert
:
address payable x = payable(0x123);
address myAddress = address(this);
if (x.balance < 10 && myAddress.balance >= 10) x.transfer(10);
La fonction transfer
échoue si le solde du contrat en cours n’est pas suffisant ou si le transfert d’Ether est rejeté par le compte destinataire. La fonction transfert
s’inverse en cas d’échec.
Note
Si x
est une adresse de contrat, son code (plus précisément : sa Fonction de repli, si présente) sera exécutée avec l’appel transfer
(c’est une caractéristique de l’EVM et ne peut être empêché). Si cette exécution échoue ou s’il n’y a plus de gas, le transfert d’Ether sera annulé et le contrat en cours s’arrêtera avec une exception.
send
send
est la contrepartie de bas niveau du transfer
. Si l’exécution échoue, le contrat en cours ne s’arrêtera pas avec une exception, mais send
retournera false
.
Avertissement
Il y a certains dangers à utiliser la fonction send
: Le transfert échoue si la profondeur de la stack atteint 1024 (cela peut toujours être forcé par l’appelant) et il échoue également si le destinataire manque de gas. Donc, afin d’effectuer des transferts d’Ether en toute sécurité, vérifiez toujours la valeur de retour de send
, utilisez transfer
ou mieux encore : utilisez un modèle où le destinataire retire l’argent.
call
,delegatecall
etstaticcall
Afin de s’interfacer avec des contrats qui ne respectent pas l’ABI, ou d’obtenir un contrôle plus direct sur l’encodage,
les fonctions call
, delegatecall
et staticcall
sont disponibles.
Elles prennent tous pour argument un seul bytes memory
comme entrée et retournent la condition de succès (en tant que bool
) et les données (bytes memory
).
Les fonctions abi.encoder
, abi.encoderPacked
, abi.encoderWithSelector
et abi.encoderWithSignature
peuvent être utilisées pour coder des données structurées.
Exemple:
bytes memory payload = abi.encodeWithSignature("register(string)", "MyName");
(bool success, bytes memory returnData) = address(nameReg).call(payload);
require(success);
Avertissement
- Toutes ces fonctions sont des fonctions de bas niveau et doivent être utilisées avec précaution.
Plus précisément, tout contrat inconnu peut être malveillant et si vous l’appelez, vous transférez le contrôle à ce contrat qui, à son tour, peut revenir dans votre contrat, donc soyez prêt à modifier les variables de votre état. quand l’appel revient. La façon habituelle d’interagir avec d’autres contrats est d’appeler une fonction sur un objet
contract
(x.f()
)..
- :: note::
Les versions précédentes de Solidity permettaient à ces fonctions de recevoir des arguments arbitraires et de traiter différemment un premier argument de type
bytes4
. Ces cas rares ont été supprimés dans la version 0.5.0.
Il est possible de régler le gas fourni avec le modificateur .gas
:
namReg.call.gas(1000000)(abi.encodeWithSignature("register(string)", "MyName"));
De même, la valeur en Ether fournie peut également être contrôlée:
nameReg.call.value(1 ether)(abi.encodeWithSignature("register(string)", "MyName"));
Enfin, ces modificateurs peuvent être combinés. Leur ordre n’a pas d’importance:
nameReg.call.gas(1000000).value(1 ether)(abi.encodeWithSignature("register(string)", "MyName"));
De la même manière, la fonction delegatecall
peut être utilisée: la différence est que seul le code de l’adresse donnée est utilisé, tous les autres aspects (stockage, balance,…) sont repris du contrat actuel. Le but de delegatecall
est d’utiliser du code de bibliothèque qui est stocké dans un autre contrat. L’utilisateur doit s’assurer que la disposition du stockage dans les deux contrats est adaptée à l’utilisation de delegatecall
.
Note
Avant Homestead, il n’existait qu’une variante limitée appelée callcode
qui ne donnait pas accès aux valeurs originales msg.sender
et msg.value
. Cette fonction a été supprimée dans la version 0.5.0.
Depuis Byzantium, staticcall
peut aussi être utilisé. C’est fondamentalement la même chose que call
, mais reviendra en arrière si la fonction appelée modifie l’état d’une manière ou d’une autre.
Les trois fonctions call
, delegatecall``et ``staticcall
sont des fonctions de très bas niveau et ne devraient être utilisées qu’en dernier recours car elles brisent la sécurité de type de Solidity.
L’option .gas
est disponible sur les trois méthodes, tandis que l’option value
n’est supportée que pour call
.
Note
It is best to avoid relying on hardcoded gas values in your smart contract code, regardless of whether state is read from or written to, as this can have many pitfalls. Also, access to gas might change in the future.
code
andcodehash
You can query the deployed code for any smart contract. Use .code
to get the EVM bytecode as a
bytes memory
, which might be empty. Use .codehash
get the Keccak-256 hash of that code
(as a bytes32
). Note that addr.codehash
is cheaper than using keccak256(addr.code)
.
Note
Tous les contrats pouvant être convertis en type address
, il est possible d’interroger le solde du contrat en cours en utilisant address(this).balance
.
Types Contrat
Chaque contrat définit son propre type.
Vous pouvez implicitement convertir des contrats en contrats dont ils héritent.
Les contrats peuvent être explicitement convertis de et vers tous les autres types de contrats et le type address
.
La conversion explicite vers et depuis le type address payable
n’est possible que si le type de contrat dispose d’une fonction de repli payante.
La conversion est toujours effectuée en utilisant address(x)
et non address payable(x)
. Vous trouverez plus d’informations dans la section sur le type address.
Note
Avant la version 0.5.0, les contrats dérivaient directement du type address et il n’y avait aucune distinction entre address
et address payable
.
Si vous déclarez une variable locale de type contrat (MonContrat c), vous pouvez appeler des fonctions sur ce contrat. Prenez bien soin de l’assigner à un contrat d’un type correspondant.
Vous pouvez également instancier les contrats (ce qui signifie qu’ils sont nouvellement créés). Vous trouverez plus de détails dans la section “contrats de création”.
La représentation des données d’un contrat est identique à celle du type address
et ce type est également utilisé dans l”ABI.
Les contrats ne supportent aucun opérateur.
Les membres du type contrat sont les fonctions externes du contrat, y compris les variables d’état publiques.
For a contract C
you can use type(C)
to access
type information about the contract.
Tableaux d’octets de taille fixe
Les types valeur bytes1
, bytes2
, bytes3
, …, bytes32
contiennent une séquence de 1 à 32 octets.
Opérateurs:
Comparaisons:
<=
,<
,==
,!=
,>=
,>
(retournent unbool
)Opérateurs binaires:
&
,|
,^
(ou exclusif binaire),~
(négation binaire)Opérateurs de décalage:
<<
(décalage vers la gauche),>>
(décalage vers la droite)Accès par indexage: Si
x
estd e typebytesI
, alorsx[k]
pour0 <= k < I
retourne lek
ème byte (lecture seule).
L’opérateur de décalage travaille avec n’importe quel type d’entier comme opérande droite (mais retourne le type de l’opérande gauche), qui indique le nombre de bits à décaler. Le décalage d’un montant négatif entraîne une erreur de compilation.
Membres :
*.length`
donne la longueur fixe du tableau d’octets (lecture seule).
Note
Le type byte[]
est un tableau d’octets, mais en raison des règles de marges, il gaspille 31 octets d’espace pour chaque élément (sauf en storage). Il est préférable d’utiliser le type « bytes » à la place.
Note
Prior to version 0.8.0, byte
used to be an alias for bytes1
.
Tableaux dynamiques d’octets
Adresses Littérales
Les caractères hexadécimaux qui réussissent un test de somme de contrôle d’adresse (« address checksum »), par exemple 0xdCad3a6d3569DF655070DEd06cb7A1b2Ccd1D3AF
sont de type address
.
Les nombres hexadécimaux qui ont entre 39 et 41 chiffres et qui ne passent pas le test de somme de contrôle produisent une erreur. You can prepend (for integer types) or append (for bytesNN types) zeros to remove the error.
Note
Le format de some de contrôle multi-casse est décrit dans EIP-55.
Rationels et entiers littéraux
Les nombres entiers littéraux sont formés à partir d’une séquence de nombres compris entre 0 et 9 interprétés en décimal. Par exemple, 69
signifie soixante-neuf.
Les littéraux octaux n’existent pas dans Solidity et les zéros précédant un nombre sont invalides.
Les fractions décimales sont formées par un .
avec au moins un chiffre sur un côté. Exemples : 1.1
, .1 `` et ``1.3
.
La notation scientifique est également supportée, où la base peut avoir des fractions, alors que l’exposant ne le peut pas.
The literal MeE
is equivalent to M * 10**E
.
Exemples : 2e10
, -2e10
, 2e-10
, 2e-10
, 2.5e1
.
Les soulignements (underscore) peuvent être utilisés pour séparer les chiffres d’un nombre littéral numérique afin d’en faciliter la lecture.
Par exemple, la décimale 123_000
, l’hexadécimale 0x2eff_abde
, la notation décimale scientifique 1_2e345_678
sont toutes valables.
Les tirets de soulignement ne sont autorisés qu’entre deux chiffres et un seul tiret de soulignement consécutif est autorisé.
Il n’y a pas de signification sémantique supplémentaire ajoutée à un nombre contenant des tirets de soulignement, les tirets de soulignement sont ignorés.
Les expressions littérales numériques conservent une précision arbitraire jusqu’à ce qu’elles soient converties en un type non littéral (c’est-à-dire en les utilisant avec une expression non littérale ou par une conversion explicite). Cela signifie que les calculs ne débordent pas (overflow) et que les divisions ne tronquent pas les expressions littérales des nombres.
Par exemple, (2**800 + 1) - 2**800
produit la constante 1
(de type uint8
) bien que les résultats intermédiaires ne rentrent même pas dans la taille d’un mot machine. De plus, .5 * 8
donne l’entier 4
(bien que des nombres non entiers aient été utilisés entre les deux).
N’importe quel opérateur qui peut être appliqué aux nombres entiers peut également être appliqué aux expressions littérales des nombres tant que les opérandes sont des nombres entiers. Si l’un des deux est fractionnaire, les opérations sur bits sont interdites et l’exponentiation est interdite si l’exposant est fractionnaire (parce que cela pourrait résulter en un nombre non rationnel).
Shifts and exponentiation with literal numbers as left (or base) operand and integer types
as the right (exponent) operand are always performed
in the uint256
(for non-negative literals) or int256
(for a negative literals) type,
regardless of the type of the right (exponent) operand.
Avertissement
La dvision d’entiers littéraux tronquait dans les versions de Solidity avant la version 0.4.0, mais elle donne maintenant en un nombre rationnel, c’est-à-dire que 5 / 2
n’est pas égal à 2
, mais à 2.5
.
Note
- Solidity a un type de nombre littéral pour chaque nombre rationnel.
Les nombres entiers littéraux et les nombres rationnels appartiennent à des types de nombres littéraux. De plus, toutes les expressions numériques littérales (c’est-à-dire les expressions qui ne contiennent que des nombres et des opérateurs) appartiennent à des types littéraux de nombres. Ainsi, les expressions littérales
1 + 2
et2 + 1
appartiennent toutes deux au même type littéral de nombre pour le nombre rationnel numéro trois.
Note
Les expressions littérales numériques sont converties en caractères non littéraux dès qu’elles sont utilisées avec des expressions non littérales. Indépendamment des types, la valeur de l’expression assignée à b
ci-dessous est évaluée en entier. Comme a
est de type uint128
, l’expression 2,5 + a
doit cependant avoir un type. Puisqu’il n’y a pas de type commun pour les types 2.5
et uint128
, le compilateur Solidity n’accepte pas ce code.
uint128 a = 1;
uint128 b = 2.5 + a + 0.5;
Chaines de caractères littérales
Les chaînes de caractères littérales sont écrites avec des guillemets simples ou doubles ("foo"
ou 'bar'
). Elles n’impliquent pas de zéro final comme en C ; foo
représente trois octets, pas quatre. Comme pour les entiers littéraux, leur type peut varier, mais ils sont implicitement convertibles en bytes1
, …, bytes32
, ou s’ils conviennent, en bytes
et en string
.
String literals can only contain printable ASCII characters, which means the characters between and including 0x20 .. 0x7E.
Les chaînes de caractères littérales supportent les caractères d’échappement suivants :
\<newline>
(échappe un réel caractère newline)
\\
(barre oblique)
\'
(guillemet simple)
\"
(guillemet double)
\n
(newline)
\r
(carriage return)
\t
(tabulation horizontale)
\xNN
(hex escape, see below)
\uNNNN
(echapement d’unicode, voir ci-dessous)
\xNN
prend une valeur hexadécimale et insère l’octet approprié, tandis que \uNNNNN
prend un codepoint Unicode et insère une séquence UTF-8.
Note
Until version 0.8.0 there were three additional escape sequences: \b
, \f
and \v
.
They are commonly available in other languages but rarely needed in practice.
If you do need them, they can still be inserted via hexadecimal escapes, i.e. \x08
, \x0c
and \x0b
, respectively, just as any other ASCII character.
La chaîne de caractères de l’exemple suivant a une longueur de dix octets.
Elle commence par un octet de newline, suivi d’une guillemet double, d’une guillemet simple, d’un caractère barre oblique inversée et ensuite (sans séparateur) de la séquence de caractères abcdef
.
"\n\"\'\\abc\
def"
Tout terminateur de ligne unicode qui n’est pas une nouvelle ligne (i.e. LF, VF, FF, CR, NEL, LS, PS) est considéré comme terminant la chaîne littérale. Newline ne termine la chaîne littérale que si elle n’est pas précédée d’un \
.
Unicode Literals
While regular string literals can only contain ASCII, Unicode literals – prefixed with the keyword unicode
– can contain any valid UTF-8 sequence.
They also support the very same escape sequences as regular string literals.
string memory a = unicode"Hello 😃";
Hexadécimaux littéraux
Les caractères hexadécimaux sont précédées du mot-clé hex
et sont entourées de guillemets simples ou doubles (hex"001122FF"
). Leur contenu doit être une chaîne hexadécimale et leur valeur sera la représentation binaire de ces valeurs.
Les littéraux hexadécimaux se comportent comme chaînes de caractères littérales et ont les mêmes restrictions de convertibilité.
Énumérateurs
Les enum
sont une façon de créer un type défini par l’utilisateur en Solidity. Ils sont explicitement convertibles de et vers tous les types d’entiers mais la conversion implicite n’est pas autorisée.
La conversion explicite à partir d’un nombre entier vérifie au moment de l’exécution que la valeur se trouve à l’intérieur de la plage de l’enum et provoque une Panic Error autrement.
Un enum a besoin d’au moins un membre, and its default value when declared is the first member.
Enums cannot have more than 256 members.
La représentation des données est la même que pour les énumérations en C : Les options sont représentées par des valeurs entières non signées à partir de 0
.
Using type(NameOfEnum).min
and type(NameOfEnum).max
you can get the
smallest and respectively largest value of the given enum.
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.8;
contract test {
enum ActionChoices { GoLeft, GoRight, GoStraight, SitStill }
ActionChoices choice;
ActionChoices constant defaultChoice = ActionChoices.GoStraight;
function setGoStraight() public {
choice = ActionChoices.GoStraight;
}
// Since enum types are not part of the ABI, the signature of "getChoice"
// will automatically be changed to "getChoice() returns (uint8)"
// for all matters external to Solidity.
function getChoice() public view returns (ActionChoices) {
return choice;
}
function getDefaultChoice() public pure returns (uint) {
return uint(defaultChoice);
}
function getLargestValue() public pure returns (ActionChoices) {
return type(ActionChoices).max;
}
function getSmallestValue() public pure returns (ActionChoices) {
return type(ActionChoices).min;
}
}
Note
Enums can also be declared on the file level, outside of contract or library definitions.
User Defined Value Types
A user defined value type allows creating a zero cost abstraction over an elementary value type. This is similar to an alias, but with stricter type requirements.
A user defined value type is defined using type C is V
, where C
is the name of the newly
introduced type and V
has to be a built-in value type (the « underlying type »). The function
C.wrap
is used to convert from the underlying type to the custom type. Similarly, the
function C.unwrap
is used to convert from the custom type to the underlying type.
The type C
does not have any operators or bound member functions. In particular, even the
operator ==
is not defined. Explicit and implicit conversions to and from other types are
disallowed.
The data-representation of values of such types are inherited from the underlying type and the underlying type is also used in the ABI.
The following example illustrates a custom type UFixed256x18
representing a decimal fixed point
type with 18 decimals and a minimal library to do arithmetic operations on the type.
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.8;
// Represent a 18 decimal, 256 bit wide fixed point type using a user defined value type.
type UFixed256x18 is uint256;
/// A minimal library to do fixed point operations on UFixed256x18.
library FixedMath {
uint constant multiplier = 10**18;
/// Adds two UFixed256x18 numbers. Reverts on overflow, relying on checked
/// arithmetic on uint256.
function add(UFixed256x18 a, UFixed256x18 b) internal pure returns (UFixed256x18) {
return UFixed256x18.wrap(UFixed256x18.unwrap(a) + UFixed256x18.unwrap(b));
}
/// Multiplies UFixed256x18 and uint256. Reverts on overflow, relying on checked
/// arithmetic on uint256.
function mul(UFixed256x18 a, uint256 b) internal pure returns (UFixed256x18) {
return UFixed256x18.wrap(UFixed256x18.unwrap(a) * b);
}
/// Take the floor of a UFixed256x18 number.
/// @return the largest integer that does not exceed `a`.
function floor(UFixed256x18 a) internal pure returns (uint256) {
return UFixed256x18.unwrap(a) / multiplier;
}
/// Turns a uint256 into a UFixed256x18 of the same value.
/// Reverts if the integer is too large.
function toUFixed256x18(uint256 a) internal pure returns (UFixed256x18) {
return UFixed256x18.wrap(a * multiplier);
}
}
Notice how UFixed256x18.wrap
and FixedMath.toUFixed256x18
have the same signature but
perform two very different operations: The UFixed256x18.wrap
function returns a UFixed256x18
that has the same data representation as the input, whereas toUFixed256x18
returns a
UFixed256x18
that has the same numerical value.
Types Fonction
Les types fonction sont les types des fonctions. Les variables du type fonction peuvent être passés et retournés pour transférer les fonctions vers et renvoyer les fonctions des appels de fonction.
Les types de fonctions se déclinent en deux versions : les fonctions internes internal
et les fonctions externes external
:
Les fonctions internes ne peuvent être appelées qu’à l’intérieur du contrat en cours (plus précisément, à l’intérieur de l’unité de code en cours, qui comprend également les fonctions de bibliothèque internes et les fonctions héritées) car elles ne peuvent pas être exécutées en dehors du contexte du contrat actuel. L’appel d’une fonction interne est réalisé en sautant à son label d’entrée, tout comme lors de l’appel interne d’une fonction du contrat en cours.
Les fonctions externes se composent d’une adresse et d’une signature de fonction et peuvent être transférées et renvoyées à partir des appels de fonction externes.
Les types de fonctions sont notés comme suit:
fonction (<types de paramètres>) {internal|external} {pure|view|payable][returns (<types de retour>)]
En contraste avec types de paramètres, les types de retour ne peuvent pas être vides - si le type de fonction ne retourne rien, toute la partie ``returns (<types de retour>)``doit être omise.
Par défaut, les fonctions sont de type internal
, donc le mot-clé internal
peut être omis. Notez que ceci ne s’applique qu’aux types de fonctions. La visibilité doit être spécifiée explicitement car les fonctions définies dans les contrats n’ont pas de valeur par défaut.
Conversions :
A function type A
is implicitly convertible to a function type B
if and only if
their parameter types are identical, their return types are identical,
their internal/external property is identical and the state mutability of A
is more restrictive than the state mutability of B
. In particular:
pure
functions can be converted toview
andnon-payable
functionsview
functions can be converted tonon-payable
functionspayable
functions can be converted tonon-payable
functions
Les fonctions
pure
peuvent être converties en fonctionsview
etnon-payable
.Les fonctions
view
peuvent être converties en fonctionsnon-payable
.les fonctions
payable
peuvent être converties en fonctionsnon-payable
.
Aucune autre conversion entre les types de fonction n’est possible.
If a function type variable is not initialised, calling it results
in a Panic error. The same happens if you call a function after using delete
on it.
Si une variable de type fonction n’est pas initialisée, l’appel de celle-ci entraîne l’échec d’une assertion. Il en va de même si vous appelez une fonction après avoir utilisé delete
dessus.
Si des fonctions de type external
sont appelées d’en dehors du contexte de Solidity, ils sont traités comme le type function
, qui code l’adresse suivie de l’identificateur de fonction ensemble dans un seul type bytes24
.
Notez que les fonctions publiques du contrat actuel peuvent être utilisées à la fois comme une fonction interne et comme une fonction externe. Pour utiliser f
comme fonction interne, utilisez simplement f
, si vous voulez utiliser sa forme externe, utilisez this.f`
.
A function of an internal type can be assigned to a variable of an internal function type regardless
of where it is defined.
This includes private, internal and public functions of both contracts and libraries as well as free
functions.
External function types, on the other hand, are only compatible with public and external contract
functions.
Libraries are excluded because they require a delegatecall
and use a different ABI
convention for their selectors.
Functions declared in interfaces do not have definitions so pointing at them does not make sense either.
Members:
External (or public) functions have the following members:
.address
returns the address of the contract of the function..selector
returns the ABI function selector
Note
External (or public) functions used to have the additional members
.gas(uint)
and .value(uint)
. These were deprecated in Solidity 0.6.2
and removed in Solidity 0.7.0. Instead use {gas: ...}
and {value: ...}
to specify the amount of gas or the amount of wei sent to a function,
respectively. See External Function Calls for
more information.
Example that shows how to use the members:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.6.4 <0.9.0;
contract Example {
function f() public payable returns (bytes4) {
assert(this.f.address == address(this));
return this.f.selector;
}
function g() public {
this.f{gas: 10, value: 800}();
}
}
Exemple d’utilisation des fonctions de type internal
:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.16 <0.9.0;
library ArrayUtils {
// les fonctions internes peuvent être utilisées dams des fonctions
// de librairies internes car elles partagent le même contexte
function map(uint[] memory self, function (uint) pure returns (uint) f)
internal
pure
returns (uint[] memory r)
{
r = new uint[](self.length);
for (uint i = 0; i < self.length; i++) {
r[i] = f(self[i]);
}
}
function reduce(
uint[] memory self,
function (uint, uint) pure returns (uint) f
)
internal
pure
returns (uint r)
{
r = self[0];
for (uint i = 1; i < self.length; i++) {
r = f(r, self[i]);
}
}
function range(uint length) internal pure returns (uint[] memory r) {
r = new uint[](length);
for (uint i = 0; i < r.length; i++) {
r[i] = i;
}
}
}
contract Pyramid {
using ArrayUtils for *;
function pyramid(uint l) public pure returns (uint) {
return ArrayUtils.range(l).map(square).reduce(sum);
}
function square(uint x) internal pure returns (uint) {
return x * x;
}
function sum(uint x, uint y) internal pure returns (uint) {
return x + y;
}
}
Exemple d” usage de fonction external
:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.22 <0.9.0;
contract Oracle {
struct Request {
bytes data;
function(uint) external callback;
}
Request[] private requests;
event NewRequest(uint);
function query(bytes memory data, function(uint) external callback) public {
requests.push(Request(data, callback));
emit NewRequest(requests.length - 1);
}
function reply(uint requestID, uint response) public {
// Here goes the check that the reply comes from a trusted source
requests[requestID].callback(response);
}
}
contract OracleUser {
Oracle constant private ORACLE_CONST = Oracle(address(0x00000000219ab540356cBB839Cbe05303d7705Fa)); // known contract
uint private exchangeRate;
function buySomething() public {
ORACLE_CONST.query("USD", this.oracleResponse);
}
function oracleResponse(uint response) public {
require(
msg.sender == address(ORACLE_CONST),
"Only oracle can call this."
);
exchangeRate = response;
}
}
Note
Les fonctions lambda ou en in-line sont prévues mais pas encore prises en charge.
Types Référence
Les valeurs du type référence peuvent être modifiées par plusieurs noms différents.
Comparez ceci avec les catégories de valeurs où vous obtenez une copie indépendante chaque fois qu’une variable de valeur est utilisée.
Pour cette raison, les types référence doivent être traités avec plus d’attention que les types valeur.
Actuellement, les types référence comprennent les structures, les tableaux et les mappages.
Si vous utilisez un type référence, vous devez toujours indiquer explicitement la zone de données où le type est enregistré :
memory
(dont la durée de vie est limitée à un appel de fonction), storage
(l’emplacement où les variables d’état sont stockées) ou calldata
(emplacement de données spécial qui contient les arguments de fonction, disponible uniquement pour les paramètres d’appel de fonction externe).
Une affectation ou une conversion de type qui modifie l’emplacement des données entraîne toujours une opération de copie automatique, alors que les affectations à l’intérieur du même emplacement de données ne copient que dans certains cas selon le type de stockage.
Emplacement des données
Chaque type référence, c’est-à-dire arrays (tableaux) et structs, comporte une annotation supplémentaire, la localisation des données
, indiquant où elles sont stockées. Il y a trois emplacements de données :
Memory
, Storage
et Calldata
. Calldata est une zone non modifiable, non persistante où les arguments de fonction sont stockés, et se comporte principalement comme memory.
Note
If you can, try to use calldata
as data location because it will avoid copies and
also makes sure that the data cannot be modified. Arrays and structs with calldata
data location can also be returned from functions, but it is not possible to
allocate such types.
Note
Prior to version 0.6.9 data location for reference-type arguments was limited to
calldata
in external functions, memory
in public functions and either
memory
or storage
in internal and private ones.
Now memory
and calldata
are allowed in all functions regardless of their visibility.
Note
Avant la version 0.5.0, l’emplacement des données pouvait être omis, et était par défaut à des emplacements différents selon le type de variable, le type de fonction, etc.
Data location and assignment behaviour
La localisation des données n’est sont pas seulement pertinente pour la persistance des données, mais aussi pour la sémantique des affectations :
Les affectations entre le stockage et la mémoire (ou à partir des données de la calldata) créent toujours une copie indépendante.
Les affectations de mémoire à mémoire ne créent que des références. Cela signifie que les modifications d’une variable mémoire sont également visibles dans toutes les autres variables mémoire qui se réfèrent aux mêmes données.
Les affectations du stockage à une variable de stockage local n’affectent également qu’une référence.
En revanche, toutes les autres affectations au stockage sont toujours copiées. Les affectations à des variables d’état ou à des membres de variables locales de type structure de stockage, même si la variable locale elle-même n’est qu’une référence, constituent des exemples dans ce cas.
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.5.0 <0.9.0;
contract C {
// The data location of x is storage.
// This is the only place where the
// data location can be omitted.
uint[] x;
// The data location of memoryArray is memory.
function f(uint[] memory memoryArray) public {
x = memoryArray; // works, copies the whole array to storage
uint[] storage y = x; // works, assigns a pointer, data location of y is storage
y[7]; // fine, returns the 8th element
y.pop(); // fine, modifies x through y
delete x; // fine, clears the array, also modifies y
// The following does not work; it would need to create a new temporary /
// unnamed array in storage, but storage is "statically" allocated:
// y = memoryArray;
// This does not work either, since it would "reset" the pointer, but there
// is no sensible location it could point to.
// delete y;
g(x); // calls g, handing over a reference to x
h(x); // calls h and creates an independent, temporary copy in memory
}
function g(uint[] storage) internal pure {}
function h(uint[] memory) public pure {}
}
Tableaux
Les tableaux peuvent avoir une taille fixe à la compilation ou peuvent être dynamiques.
The type of an array of fixed size k
and element type T
is written as T[k]
,
and an array of dynamic size as T[]
.
Un tableau de taille fixe k
et de type d’élément T
est écrit T[k]
, un tableau de taille dynamique T[]
.
Par exemple, un tableau de 5 tableaux dynamiques de uint
est uint[][5]
(notez que la notation est inversée par rapport à certains autres langages). Pour accéder au deuxième uint du troisième tableau dynamique, vous utilisez x[2][1]
(les indexs commencent à zéro et
l’accès fonctionne dans le sens inverse de la déclaration, c’est-à-dire que x[2]
supprime un niveau dans le type de déclaration à partir de la droite).
Indices are zero-based, and access is in the opposite direction of the declaration.
Il y a peu de restrictions concernant l’élément contenu, il peut aussi être un autre tableau, un mappage ou une structure. Les restrictions générales
s’appliquent, cependant, en ce sens que les mappages ne peuvent être utilisés que dans le storage
et que les fonctions visibles au public nécessitent des paramètres qui sont des types reconnus par l”ABI types.
Il est possible de marquer les tableaux public
et de demander à Solidity de créer un getter.
L’index numérique deviendra un paramètre obligatoire pour le getter.
For example, if you have a variable uint[][5] memory x
, you access the
seventh uint
in the third dynamic array using x[2][6]
, and to access the
third dynamic array, use x[2]
. Again,
if you have an array T[5] a
for a type T
that can also be an array,
then a[2]
always has type T
.
Accessing an array past its end causes a failing assertion. Methods .push()
and .push(value)
can be used
to append a new element at the end of the array, where .push()
appends a zero-initialized element and returns
a reference to it.
bytes
and string
as Arrays
Les variables de type bytes
et string
sont des tableaux spéciaux. Un byte
est semblable à un byte[]
, mais il est condensé en calldata et en mémoire. string
est égal à bytes
, mais ne permet pas l’accès à la longueur ou à l’index.
Solidity does not have string manipulation functions, but there are
third-party string libraries. You can also compare two strings by their keccak256-hash using
keccak256(abi.encodePacked(s1)) == keccak256(abi.encodePacked(s2))
and
concatenate two strings using string.concat(s1, s2)
.
Il faut donc généralement préférer les bytes
aux bytes[]
car c’est moins cher à l’usage,
since using bytes1[]
in memory
adds 31 padding bytes between the elements. Note that in storage
, the
padding is absent due to tight packing, see bytes and string.
En règle générale, utilisez bytes
pour les données en octets bruts de longueur arbitraire et string
pour les données de chaîne de caractères de longueur arbitraire (UTF-8).
Si vous pouvez limiter la longueur à un certain nombre d’octets, utilisez toujours un des bytes1
à bytes32
, car ils sont beaucoup moins chers également.
Note
Si vous voulez accéder à la représentation en octets d’une chaîne de caractères s
, utilisez bytes(s).length
/ bytes(s)[7] ='x';
. Gardez à l’esprit que vous accédez aux octets de bas niveau de la représentation UTF-8, et non aux caractères individuels !
The functions bytes.concat
and string.concat
You can concatenate an arbitrary number of string
values using string.concat
.
The function returns a single string memory
array that contains the contents of the arguments without padding.
If you want to use parameters of other types that are not implicitly convertible to string
, you need to convert them to string
first.
Analogously, the bytes.concat
function can concatenate an arbitrary number of bytes
or bytes1 ... bytes32
values.
The function returns a single bytes memory
array that contains the contents of the arguments without padding.
If you want to use string parameters or other types that are not implicitly convertible to bytes
, you need to convert them to bytes
or bytes1
/…/bytes32
first.
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.12;
contract C {
string s = "Storage";
function f(bytes calldata bc, string memory sm, bytes16 b) public view {
string memory concat_string = string.concat(s, string(bc), "Literal", sm);
assert((bytes(s).length + bc.length + 7 + bytes(sm).length) == bytes(concat_string).length);
bytes memory concat_bytes = bytes.concat(bytes(s), bc, bc[:2], "Literal", bytes(sm), b);
assert((bytes(s).length + bc.length + 2 + 7 + bytes(sm).length + b.length) == concat_bytes.length);
}
}
If you call string.concat
or bytes.concat
without arguments they return an empty array.
Allouer des tableaux en mémoire
Vous pouvez utiliser le mot-clé new
pour créer des tableaux dont la longueur dépend de la durée d’exécution en mémoire.
Contrairement aux tableaux de stockage, il n’est pas possible de redimensionner les tableaux de mémoire (par exemple en les assignant au membre .length
). Vous devez soit calculer la taille requise à l’avance, soit créer un nouveau tableau de mémoire et copier chaque élément.
As all variables in Solidity, the elements of newly allocated arrays are always initialized with the default value.
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.16 <0.9.0;
contract C {
function f(uint len) public pure {
uint[] memory a = new uint[](7);
bytes memory b = new bytes(len);
assert(a.length == 7);
assert(b.length == len);
a[6] = 8;
}
}
Tableaux littéraux / Inline Arrays
An array literal is a comma-separated list of one or more expressions, enclosed
in square brackets ([...]
). For example [1, a, f(3)]
. The type of the
array literal is determined as follows:
It is always a statically-sized memory array whose length is the number of expressions.
The base type of the array is the type of the first expression on the list such that all other expressions can be implicitly converted to it. It is a type error if this is not possible.
It is not enough that there is a type all the elements can be converted to. One of the elements has to be of that type.
Le type d’un tableau littéral est un tableau mémoire de taille fixe dont le type de base est le type commun des éléments donnés. Le type de [1, 2, 3]
est uint8[3] memory`
, car le type de chacune de ces constantes est uint8
.
Pour cette raison, il est nécessaire de convertir le premier élément de l’exemple ci-dessus en uint
.
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.16 <0.9.0;
contract C {
function f() public pure {
g([uint(1), 2, 3]);
}
function g(uint[3] memory) public pure {
// ...
}
}
The array literal [1, -1]
is invalid because the type of the first expression
is uint8
while the type of the second is int8
and they cannot be implicitly
converted to each other. To make it work, you can use [int8(1), -1]
, for example.
Since fixed-size memory arrays of different type cannot be converted into each other (even if the base types can), you always have to specify a common base type explicitly if you want to use two-dimensional array literals:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.16 <0.9.0;
contract C {
function f() public pure returns (uint24[2][4] memory) {
uint24[2][4] memory x = [[uint24(0x1), 1], [0xffffff, 2], [uint24(0xff), 3], [uint24(0xffff), 4]];
// The following does not work, because some of the inner arrays are not of the right type.
// uint[2][4] memory x = [[0x1, 1], [0xffffff, 2], [0xff, 3], [0xffff, 4]];
return x;
}
}
Fixed size memory arrays cannot be assigned to dynamically-sized memory arrays, i.e. the following is not possible:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.0 <0.9.0;
// Ceci ne compile pas.
contract C {
function f() public {
// La ligne suivant provoque une erreur car uint[3] memory
// ne peut pas être convertit en uint[] memory.
uint[] memory x = [uint(1), 3, 4];
}
}
Il est prévu de supprimer cette restriction à l’avenir, mais crée actuellement certaines complications en raison de la façon dont les tableaux sont transmis dans l’ABI.
If you want to initialize dynamically-sized arrays, you have to assign the individual elements:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.16 <0.9.0;
contract C {
function f() public pure {
uint[] memory x = new uint[](3);
x[0] = 1;
x[1] = 3;
x[2] = 4;
}
}
Array Members
- length:
- Les tableaux ont un membre
length
qui contient leur nombre d’éléments. La longueur des tableaux memory est fixe (mais dynamique, c’est-à-dire qu’elle peut dépendre des paramètres d’exécution) une fois qu’ils sont créés.
- Les tableaux ont un membre
- push():
Les tableaux de stockage dynamique et les
bytes
(et nonstring
) ont une fonction membre appeléepush
que vous pouvez utiliser pour ajouter un élément à la fin du tableau. L’élément sera mis à zéro à l’initialisation. La fonction renvoie la nouvelle longueur.- push(x):
Dynamic storage arrays and
bytes
(notstring
) have a member function calledpush(x)
that you can use to append a given element at the end of the array. The function returns nothing.- pop():
Les tableaux de stockage dynamique et les
bytes
(et nonstring
) ont une fonction membre appeléepop()
que vous pouvez utiliser pour supprimer un élément à la fin du tableau. Ceci appelle aussi implicitement :ref:delete
sur l’élément supprimé.
Note
L’augmentation de la longueur d’un tableau en storage a des coûts en gas constants parce qu’on suppose que le stockage est nul, alors que la diminution de la longueur a au moins un coût linéaire (mais dans la plupart des cas pire que linéaire), parce qu’elle inclut explicitement l’élimination des éléments supprimés comme si on appelait :ref:delete
.
Note
To use arrays of arrays in external (instead of public) functions, you need to activate ABI coder v2.
Note
Dans les versions EVM antérieures à Byzantium, il n’était pas possible d’accéder au retour de tableaux dynamique à partir des appels de fonctions. Si vous appelez des fonctions qui retournent des tableaux dynamiques, assurez-vous d’utiliser un EVM qui est configuré en mode Byzantium.
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.6.0 <0.9.0;
contract ArrayContract {
uint[2**20] m_aLotOfIntegers;
// Notez que ce qui suit n'est pas une paire de tableaux dynamiques
// mais un tableau tableau dynamique de paires (c'est-à-dire de
// tableaux de taille fixe de longueur deux).
// Pour cette raison, T[] est toujours un tableau dynamique
// de T, même si T lui-même est un tableau.
// L'emplacement des données pour toutes les variables d'état
// est storage.
bool[2][] m_pairsOfFlags;
// newPairs est stocké en memory - seule possibilité
// pour les arguments de fonction publique
function setAllFlagPairs(bool[2][] memory newPairs) public {
// l'assignation d' un tableau en storage implique la copie
// de ``newPairs`` et remplace l'array ``m_pairsOfFlags``.
m_pairsOfFlags = newPairs;
}
struct StructType {
uint[] contents;
uint moreInfo;
}
StructType s;
function f(uint[] memory c) public {
// stocke un pointeur sur ``s`` dans ``g``
StructType storage g = s;
// change aussi ``s.moreInfo``.
g.moreInfo = 2;
// assigne une copie car ``g.contents`` n'est
// pas une variable locale mais un membre
// d'une variable locale
g.contents = c;
}
function setFlagPair(uint index, bool flagA, bool flagB) public {
// accès à un index inexistant, déclenche une exception
m_pairsOfFlags[index][0] = flagA;
m_pairsOfFlags[index][1] = flagB;
}
function changeFlagArraySize(uint newSize) public {
// using push and pop is the only way to change the
// length of an array
if (newSize < m_pairsOfFlags.length) {
while (m_pairsOfFlags.length > newSize)
m_pairsOfFlags.pop();
} else if (newSize > m_pairsOfFlags.length) {
while (m_pairsOfFlags.length < newSize)
m_pairsOfFlags.push();
}
}
function clear() public {
// these clear the arrays completely
delete m_pairsOfFlags;
delete m_aLotOfIntegers;
// identical effect here
m_pairsOfFlags = new bool[2][](0);
}
bytes m_byteData;
function byteArrays(bytes memory data) public {
// le tableau de byte ("bytes") sont différents car stockés sans
// padding mais peuvent être traités comme des ``uint8[]``
m_byteData = data;
for (uint i = 0; i < 7; i++)
m_byteData.push();
m_byteData[3] = 0x08;
delete m_byteData[2];
}
function addFlag(bool[2] memory flag) public returns (uint) {
m_pairsOfFlags.push(flag);
return m_pairsOfFlags.length;
}
function createMemoryArray(uint size) public pure returns (bytes memory) {
// Un tableau dynamique est créé via `new`:
uint[2][] memory arrayOfPairs = new uint[2][](size);
// Les tableaux littéraux sont toujours de taille statique
// et en cas d' utilisation de littéraux uniquement, au moins
// un type doit être spécifié.
arrayOfPairs[0] = [uint(1), 2];
// Créée un tableau dynamique de bytes:
bytes memory b = new bytes(200);
for (uint i = 0; i < b.length; i++)
b[i] = bytes1(uint8(i));
return b;
}
}
Array Slices
Array slices are a view on a contiguous portion of an array.
They are written as x[start:end]
, where start
and
end
are expressions resulting in a uint256 type (or
implicitly convertible to it). The first element of the
slice is x[start]
and the last element is x[end - 1]
.
If start
is greater than end
or if end
is greater
than the length of the array, an exception is thrown.
Both start
and end
are optional: start
defaults
to 0
and end
defaults to the length of the array.
Array slices do not have any members. They are implicitly convertible to arrays of their underlying type and support index access. Index access is not absolute in the underlying array, but relative to the start of the slice.
Array slices do not have a type name which means no variable can have an array slices as type, they only exist in intermediate expressions.
Note
As of now, array slices are only implemented for calldata arrays.
Array slices are useful to ABI-decode secondary data passed in function parameters:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.8.5 <0.9.0;
contract Proxy {
/// @dev Address of the client contract managed by proxy i.e., this contract
address client;
constructor(address _client) {
client = _client;
}
/// Forward call to "setOwner(address)" that is implemented by client
/// after doing basic validation on the address argument.
function forward(bytes calldata _payload) external {
bytes4 sig = bytes4(_payload[:4]);
// Due to truncating behaviour, bytes4(_payload) performs identically.
// bytes4 sig = bytes4(_payload);
if (sig == bytes4(keccak256("setOwner(address)"))) {
address owner = abi.decode(_payload[4:], (address));
require(owner != address(0), "Address of owner cannot be zero.");
}
(bool status,) = client.delegatecall(_payload);
require(status, "Forwarded call failed.");
}
}
Structs
Solidity permet de définir de nouveaux types sous forme de structs, comme le montre l’exemple suivant :
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.6.0 <0.9.0;
// Defines a new type with two fields.
// Declaring a struct outside of a contract allows
// it to be shared by multiple contracts.
// Here, this is not really needed.
struct Funder {
address addr;
uint amount;
}
contract CrowdFunding {
// Structs can also be defined inside contracts, which makes them
// visible only there and in derived contracts.
struct Campaign {
address payable beneficiary;
uint fundingGoal;
uint numFunders;
uint amount;
mapping (uint => Funder) funders;
}
uint numCampaigns;
mapping (uint => Campaign) campaigns;
function newCampaign(address payable beneficiary, uint goal) public returns (uint campaignID) {
campaignID = numCampaigns++; // campaignID is return variable
// We cannot use "campaigns[campaignID] = Campaign(beneficiary, goal, 0, 0)"
// because the right hand side creates a memory-struct "Campaign" that contains a mapping.
Campaign storage c = campaigns[campaignID];
c.beneficiary = beneficiary;
c.fundingGoal = goal;
}
function contribute(uint campaignID) public payable {
Campaign storage c = campaigns[campaignID];
// Creates a new temporary memory struct, initialised with the given values
// and copies it over to storage.
// Note that you can also use Funder(msg.sender, msg.value) to initialise.
c.funders[c.numFunders++] = Funder({addr: msg.sender, amount: msg.value});
c.amount += msg.value;
}
function checkGoalReached(uint campaignID) public returns (bool reached) {
Campaign storage c = campaigns[campaignID];
if (c.amount < c.fundingGoal)
return false;
uint amount = c.amount;
c.amount = 0;
c.beneficiary.transfer(amount);
return true;
}
}
Le contrat ne fournit pas toutes les fonctionnalités d’un contrat de crowdfunding, mais il contient les concepts de base nécessaires pour comprendre les struct
.
Les types structs peuvent être utilisés à l’intérieur des mapping
et des array
et peuvent eux-mêmes contenir des mappages et des tableaux.
Il n’est pas possible pour une structure de contenir un membre de son propre type, bien que la structure elle-même puisse être le type de valeur d’un membre de mappage ou peut contenir un tableau de taille dynamique de son type. Cette restriction est nécessaire, car la taille de la structure doit être finie.
Notez que dans toutes les fonctions, un type structure est affecté à une variable locale avec l’emplacement de données storage
.
Ceci ne copie pas la structure mais stocke seulement une référence pour que les affectations aux membres de la variable locale écrivent réellement dans l’état.
Bien sûr, vous pouvez aussi accéder directement aux membres de la structure sans l’affecter à une variable locale, comme dans campaigns[campaignID].amount = 0
.
Note
Until Solidity 0.7.0, memory-structs containing members of storage-only types (e.g. mappings)
were allowed and assignments like campaigns[campaignID] = Campaign(beneficiary, goal, 0, 0)
in the example above would work and just silently skip those members.
Mapping Types
Mapping types use the syntax mapping(_KeyType => _ValueType)
and variables
of mapping type are declared using the syntax mapping(_KeyType => _ValueType) _VariableName
.
The _KeyType
can be any
built-in value type, bytes
, string
, or any contract or enum type. Other user-defined
or complex types, such as mappings, structs or array types are not allowed.
_ValueType
can be any type, including mappings, arrays and structs.
You can think of mappings as hash tables, which are virtually initialised
such that every possible key exists and is mapped to a value whose
byte-representation is all zeros, a type’s default value.
The similarity ends there, the key data is not stored in a
mapping, only its keccak256
hash is used to look up the value.
Because of this, mappings do not have a length or a concept of a key or value being set, and therefore cannot be erased without extra information regarding the assigned keys (see Clearing Mappings).
Mappings can only have a data location of storage
and thus
are allowed for state variables, as storage reference types
in functions, or as parameters for library functions.
They cannot be used as parameters or return parameters
of contract functions that are publicly visible.
These restrictions are also true for arrays and structs that contain mappings.
You can mark state variables of mapping type as public
and Solidity creates a
getter for you. The _KeyType
becomes a parameter for the getter.
If _ValueType
is a value type or a struct, the getter returns _ValueType
.
If _ValueType
is an array or a mapping, the getter has one parameter for
each _KeyType
, recursively.
In the example below, the MappingExample
contract defines a public balances
mapping, with the key type an address
, and a value type a uint
, mapping
an Ethereum address to an unsigned integer value. As uint
is a value type, the getter
returns a value that matches the type, which you can see in the MappingUser
contract that returns the value at the specified address.
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.0 <0.9.0;
contract MappingExample {
mapping(address => uint) public balances;
function update(uint newBalance) public {
balances[msg.sender] = newBalance;
}
}
contract MappingUser {
function f() public returns (uint) {
MappingExample m = new MappingExample();
m.update(100);
return m.balances(address(this));
}
}
The example below is a simplified version of an
ERC20 token.
_allowances
is an example of a mapping type inside another mapping type.
The example below uses _allowances
to record the amount someone else is allowed to withdraw from your account.
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.22 <0.9.0;
contract MappingExample {
mapping (address => uint256) private _balances;
mapping (address => mapping (address => uint256)) private _allowances;
event Transfer(address indexed from, address indexed to, uint256 value);
event Approval(address indexed owner, address indexed spender, uint256 value);
function allowance(address owner, address spender) public view returns (uint256) {
return _allowances[owner][spender];
}
function transferFrom(address sender, address recipient, uint256 amount) public returns (bool) {
require(_allowances[sender][msg.sender] >= amount, "ERC20: Allowance not high enough.");
_allowances[sender][msg.sender] -= amount;
_transfer(sender, recipient, amount);
return true;
}
function approve(address spender, uint256 amount) public returns (bool) {
require(spender != address(0), "ERC20: approve to the zero address");
_allowances[msg.sender][spender] = amount;
emit Approval(msg.sender, spender, amount);
return true;
}
function _transfer(address sender, address recipient, uint256 amount) internal {
require(sender != address(0), "ERC20: transfer from the zero address");
require(recipient != address(0), "ERC20: transfer to the zero address");
require(_balances[sender] >= amount, "ERC20: Not enough funds.");
_balances[sender] -= amount;
_balances[recipient] += amount;
emit Transfer(sender, recipient, amount);
}
}
Iterable Mappings
You cannot iterate over mappings, i.e. you cannot enumerate their keys.
It is possible, though, to implement a data structure on
top of them and iterate over that. For example, the code below implements an
IterableMapping
library that the User
contract then adds data too, and
the sum
function iterates over to sum all the values.
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.6.8 <0.9.0;
struct IndexValue { uint keyIndex; uint value; }
struct KeyFlag { uint key; bool deleted; }
struct itmap {
mapping(uint => IndexValue) data;
KeyFlag[] keys;
uint size;
}
library IterableMapping {
function insert(itmap storage self, uint key, uint value) internal returns (bool replaced) {
uint keyIndex = self.data[key].keyIndex;
self.data[key].value = value;
if (keyIndex > 0)
return true;
else {
keyIndex = self.keys.length;
self.keys.push();
self.data[key].keyIndex = keyIndex + 1;
self.keys[keyIndex].key = key;
self.size++;
return false;
}
}
function remove(itmap storage self, uint key) internal returns (bool success) {
uint keyIndex = self.data[key].keyIndex;
if (keyIndex == 0)
return false;
delete self.data[key];
self.keys[keyIndex - 1].deleted = true;
self.size --;
}
function contains(itmap storage self, uint key) internal view returns (bool) {
return self.data[key].keyIndex > 0;
}
function iterate_start(itmap storage self) internal view returns (uint keyIndex) {
return iterate_next(self, type(uint).max);
}
function iterate_valid(itmap storage self, uint keyIndex) internal view returns (bool) {
return keyIndex < self.keys.length;
}
function iterate_next(itmap storage self, uint keyIndex) internal view returns (uint r_keyIndex) {
keyIndex++;
while (keyIndex < self.keys.length && self.keys[keyIndex].deleted)
keyIndex++;
return keyIndex;
}
function iterate_get(itmap storage self, uint keyIndex) internal view returns (uint key, uint value) {
key = self.keys[keyIndex].key;
value = self.data[key].value;
}
}
// How to use it
contract User {
// Just a struct holding our data.
itmap data;
// Apply library functions to the data type.
using IterableMapping for itmap;
// Insert something
function insert(uint k, uint v) public returns (uint size) {
// This calls IterableMapping.insert(data, k, v)
data.insert(k, v);
// We can still access members of the struct,
// but we should take care not to mess with them.
return data.size;
}
// Computes the sum of all stored data.
function sum() public view returns (uint s) {
for (
uint i = data.iterate_start();
data.iterate_valid(i);
i = data.iterate_next(i)
) {
(, uint value) = data.iterate_get(i);
s += value;
}
}
}
Operators
Arithmetic and bit operators can be applied even if the two operands do not have the same type.
For example, you can compute y = x + z
, where x
is a uint8
and z
has
the type int32
. In these cases, the following mechanism will be used to determine
the type in which the operation is computed (this is important in case of overflow)
and the type of the operator’s result:
If the type of the right operand can be implicitly converted to the type of the left operand, use the type of the left operand,
if the type of the left operand can be implicitly converted to the type of the right operand, use the type of the right operand,
otherwise, the operation is not allowed.
In case one of the operands is a literal number it is first converted to its « mobile type », which is the smallest type that can hold the value (unsigned types of the same bit-width are considered « smaller » than the signed types). If both are literal numbers, the operation is computed with arbitrary precision.
The operator’s result type is the same as the type the operation is performed in,
except for comparison operators where the result is always bool
.
The operators **
(exponentiation), <<
and >>
use the type of the
left operand for the operation and the result.
Compound and Increment/Decrement Operators
If a
is an LValue (i.e. a variable or something that can be assigned to), the
following operators are available as shorthands:
a += e
is equivalent to a = a + e
. The operators -=
, *=
, /=
, %=
,
|=
, &=
, ^=
, <<=
and >>=
are defined accordingly. a++
and a--
are equivalent
to a += 1
/ a -= 1
but the expression itself still has the previous value
of a
. In contrast, --a
and ++a
have the same effect on a
but
return the value after the change.
delete
delete a
affecte la valeur initiale du type à a
. C’est-à-dire que pour les entiers, il est équivalent à a = 0
, mais il peut aussi être utilisé sur les tableaux, où il assigne un tableau dynamique de longueur zéro ou un tableau statique de la même longueur avec tous les éléments initialisés à leur valeur par défaut. delete a[x]
deletes the item at index x
of the array and leaves
all other elements and the length of the array untouched. This especially means that it leaves
a gap in the array. If you plan to remove items, a mapping is probably a better choice.
Pour les structs, il assigne une structure avec tous les membres réinitialisés. En d’autres termes, la valeur de a
après delete a
est la même que si a
était déclaré sans attribution, avec la réserve suivante :
delete
n’a aucun effet sur les mappages (car les clés des mappages peuvent être arbitraires et sont généralement inconnues). Ainsi, si vous supprimez une structure, elle réinitialisera tous les membres qui ne sont pas des mappings
et se propagera récursivement dans les membres à moins qu’ils ne soient des mappings. Toutefois, il est possible de supprimer des clés individuelles et ce à quoi elles correspondent : Si a
est un mappage, alors delete a[x]
supprimera la valeur stockée à x
.
Il est important de noter que delete a
se comporte vraiment comme une affectation à a
, c’est-à-dire qu’il stocke un nouvel objet dans a
.
Cette distinction est visible lorsque a
est une variable par référence : Il ne réinitialisera que a
lui-même, et non la valeur à laquelle il se référait précédemment.
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.0 <0.9.0;
contract DeleteExample {
uint data;
uint[] dataArray;
function f() public {
uint x = data;
delete x; // met x à 0, n' affecte pas data
delete data; // met data à 0, n'affecte pas x
uint[] storage y = dataArray;
delete dataArray; // ceci met dataArray.length à zéro, mais un uint[]
// est un objet complexe, donc y est affecté est un alias
// vers l' objet en storage.
// D' un autre côté: "delete y" est invalid, car l' assignement à
// une variable locale pointant vers un objet en storage n' est
// autorisée que depuis un objet en storage.
assert(y.length == 0);
}
}
Conversions entre les types élémentaires
Conversions implicites
An implicit type conversion is automatically applied by the compiler in some cases during assignments, when passing arguments to functions and when applying operators. En général, une conversion implicite entre les types valeur est possible si elle a un sens sémantique et qu’aucune information n’est perdue.
Par exemple, uint8
est convertible en uint16
et int128
en int256
, mais uint8
n’est pas convertible en uint256
(car uint256
ne peut contenir, par exemple, -1
)
Si un opérateur est appliqué à différents types, le compilateur essaie de convertir implicitement l’un des opérandes au type de l’autre (c’est la même chose pour les assignations). This means that operations are always performed in the type of one of the operands.
For more details about which implicit conversions are possible, please consult the sections about the types themselves.
In the example below, y
and z
, the operands of the addition,
do not have the same type, but uint8
can
be implicitly converted to uint16
and not vice-versa. Because of that,
y
is converted to the type of z
before the addition is performed
in the uint16
type. The resulting type of the expression y + z
is uint16
.
Because it is assigned to a variable of type uint32
another implicit conversion
is performed after the addition.
uint8 y;
uint16 z;
uint32 x = y + z;
Conversions explicites
Si le compilateur ne permet pas la conversion implicite mais que vous savez ce que vous faites, une conversion de type explicite est parfois possible. Notez que cela peut vous donner un comportement inattendu et vous permet de contourner certaines fonctions de sécurité du compilateur, donc assurez-vous de tester que le résultat est ce que vous voulez !
Prenons l’exemple suivant où l’on convertit un int8
négatif en un uint
:
int y = -3;
uint x = uint(y);
A la fin de cet extrait de code, x
aura la valeur 0xfffffff...fd
(64 caractères hexadécimaux), qui est -3 dans la représentation en 256 bits du complément à deux.
Si un entier est explicitement converti en un type plus petit, les bits d’ordre supérieur sont coupés:
uint32 a = 0x12345678;
uint16 b = uint16(a); // b sera désormais 0x5678
Si un entier est explicitement converti en un type plus grand, il est rembourré par la gauche (c’est-à-dire à l’extrémité supérieure de l’ordre). Le résultat de la conversion sera comparé à l’entier original:
uint16 a = 0x1234;
uint32 b = uint32(a); // b will be 0x00001234 now
assert(a == b);
Les types à taille fixe se comportent différemment lors des conversions. Ils peuvent être considérés comme des séquences d’octets individuels et la conversion à un type plus petit coupera la séquence:
bytes2 a = 0x1234;
bytes1 b = bytes1(a); // b sera désormais 0x12
Si un type à taille fixe est explicitement converti en un type plus grand, il est rembourré à droite. L’accès à l’octet par un index fixe donnera la même valeur avant et après la conversion (si l’index est toujours dans la plage):
bytes2 a = 0x1234;
bytes4 b = bytes4(a); // b sera désormais 0x12340000
assert(a[0] == b[0]);
assert(a[1] == b[1]);
Puisque les entiers et les tableaux d’octets de taille fixe se comportent différemment lorsqu’ils sont tronqués ou rembourrés, les conversions explicites entre entiers et tableaux d’octets de taille fixe ne sont autorisées que si les deux ont la même taille. Si vous voulez convertir entre des entiers et des tableaux d’octets de taille fixe de tailles différentes, vous devez utiliser des conversions intermédiaires qui font la troncature et le remplissage désirés. règles explicites:
bytes2 a = 0x1234;
uint32 b = uint16(a); // b sera désormais 0x00001234
uint32 c = uint32(bytes4(a)); // c sera désormais 0x12340000
uint8 d = uint8(uint16(a)); // d sera désormais 0x34
uint8 e = uint8(bytes1(a)); // d sera désormais 0x12
bytes
arrays and bytes
calldata slices can be converted explicitly to fixed bytes types (bytes1
/…/bytes32
).
In case the array is longer than the target fixed bytes type, truncation at the end will happen.
If the array is shorter than the target type, it will be padded with zeros at the end.
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.5;
contract C {
bytes s = "abcdefgh";
function f(bytes calldata c, bytes memory m) public view returns (bytes16, bytes3) {
require(c.length == 16, "");
bytes16 b = bytes16(m); // if length of m is greater than 16, truncation will happen
b = bytes16(s); // padded on the right, so result is "abcdefgh\0\0\0\0\0\0\0\0"
bytes3 b1 = bytes3(s); // truncated, b1 equals to "abc"
b = bytes16(c[:8]); // also padded with zeros
return (b, b1);
}
}
Conversions entre les types littéraux et élémentaires
Types nombres entiers
Les nombres décimaux et hexadécimaux peuvent être implicitement convertis en n’importe quel type entier suffisamment grand pour le représenter sans troncature:
uint8 a = 12; // Bon
uint32 b = 1234; // Bon
uint16 c = 0x123456; // échoue, car devrait tronquer en 0x3456
Note
Prior to version 0.8.0, any decimal or hexadecimal number literals could be explicitly converted to an integer type. From 0.8.0, such explicit conversions are as strict as implicit conversions, i.e., they are only allowed if the literal fits in the resulting range.
Tableaux d’octets de taille fixe
Les nombres décimaux ne peuvent pas être implicitement convertis en tableaux d’octets de taille fixe. Les nombres hexadécimaux peuvent être littéraux, mais seulement si le nombre de chiffres hexadécimaux correspond exactement à la taille du type de bytes
. Par exception, les nombres décimaux et hexadécimaux ayant une valeur de zéro peuvent être convertis en n’importe quel type à taille fixe:
bytes2 a = 54321; // pas autorisé
bytes2 b = 0x12; // pas autorisé
bytes2 c = 0x123; // pas autorisé
bytes2 d = 0x1234; // bon
bytes2 e = 0x0012; // bon
bytes4 f = 0; // bon
bytes4 g = 0x0; // bon
Les littéraux de chaînes de caractères et les littéraux de chaînes hexadécimales peuvent être implicitement convertis en tableaux d’octets de taille fixe, si leur nombre de caractères correspond à la taille du type bytes
:
bytes2 a = hex"1234"; // bon
bytes2 b = "xy"; // bon
bytes2 c = hex"12"; // pas autorisé
bytes2 d = hex"123"; // pas autorisé
bytes2 e = "x"; // pas autorisé
bytes2 f = "xyz"; // débile
Adresses
Comme décrit dans Adresses Littérales, les chaines de caractères hexadécimaux de la bonne taille qui passent le test de somme de contrôle sont de type address
. Aucun autre littéral ne peut être implicitement converti au type address
.
Les conversions explicites de bytes20
ou de tout type entier en address
aboutissent en une address payable`
.
Unités et variables globales
Unités d’Ether
Un nombre littéral peut prendre un suffixe de « wei », gwei
ou « ether » pour spécifier une sous-dénomination d’éther, où les nombres d’éther sans postfix sont supposés être en Wei.
assert(1 wei == 1);
assert(1 gwei == 1e9);
assert(1 ether == 1e18);
Le seul effet du suffixe de sous-dénomination est une multiplication par une puissance de dix..
Note
The denominations finney
and szabo
have been removed in version 0.7.0.
Unités de temps
Des suffixes comme seconds
, minutes
, hours
, days
et weeks
peuvent être utilisés après les nombres littéraux pour spécifier les unités de temps où les secondes sont l’unité de base et les unités sont considérées naïvement de la façon suivante :
1 == 1 seconds
1 minutes == 60 seconds
1 hours == 60 minutes
1 days == 24 hours
1 weeks == 7 days
Faites attention si vous effectuez des calculs calendaires en utilisant ces unités, parce que chaque année n’est pas égale à 365 jours ni même chaque jour n’a 24 heures à cause des secondes bissextiles. Les secondes intercalaires étant imprévisibles, une bibliothèque de calendrier exacte doit être mise à jour par un oracle externe.
Note
Le suffixe years
a été supprimé dans la version 0.5.0 pour les raisons ci-dessus.
Ces suffixes ne peuvent pas être appliqués aux variables. Si vous voulez interpréter un paramètre de fonctions en jours, par exemple, vous pouvez le faire de la manière suivante:
function f(uint start, uint daysAfter) public {
if (block.timestamp >= start + daysAfter * 1 days) {
// ...
}
}
Variables spéciales et fonctions
Il y a des variables et des fonctions spéciales qui existent toujours dans l’espace de nommage global et qui sont principalement utilisées pour fournir des informations sur la chaîne de blocs ou sont des fonctions utilitaires générales.
Propriétés du bloc et des transactions
blockhash(uint blockNumber) returns (bytes32)
: hash du numéro de bloc passé - mnarche seulement pour les 256 plus récents, excluant le bloc courantblock.basefee
(uint
): current block’s base fee (EIP-3198 and EIP-1559)block.chainid
(uint
): current chain idblock.coinbase
(address payable
): addresse du mineur du bloc courantblock.difficulty
(uint
): difficulté du bloc courantblock.gaslimit
(uint
): limite de gas actuelleblock.number
(uint
): numéro du bloc courantblock.timestamp
(uint
): timestamp du bloc en temps unix (secondes)gasleft() returns (uint256)
: gas restantmsg.data
(bytes calldata
): calldata completmsg.sender
(address payable
): expéditeur du message (appel courant)msg.sig
(bytes4
): 4 premiers octets calldata (i.e. identifiant de function)msg.value
(uint
): nombre de wei envoyés avec le messagetx.gasprice
(uint
): prix de la transaction en gastx.origin
(address payable
): expéditeur de la transaction (appel global complet)
Note
Les valeurs de tous les membres de msg
, y compris msg.sender``et ``msg.value
peuvent changer pour chaque appel de fonction external.
Ceci inclut les appels aux fonctions de librairies.
Note
When contracts are evaluated off-chain rather than in context of a transaction included in a
block, you should not assume that block.*
and tx.*
refer to values from any specific
block or transaction. These values are provided by the EVM implementation that executes the
contract and can be arbitrary.
Note
Ne vous basez pas sur block.timestamp
ou blockhash
comme source de hasard, à moins de savoir ce que vous faites.
L’horodatage et le hashage du bloc peuvent tous deux être influencés dans une certaine mesure par les mineurs. Les mauvais acteurs de la communauté minière peuvent par exemple exécuter une fonction de casino sur un hash choisi et simplement réessayer un hash différent s’ils n’ont pas reçu d’argent.
L’horodatage du bloc courant doit être strictement supérieur à celui du dernier bloc, mais la seule garantie est qu’il se situera entre les horodatages de deux blocs consécutifs dans la chaîne canonique.
Note
Les hashs de blocs ne sont pas disponibles pour tous les blocs pour des raisons d’évolutivité/place. Vous ne pouvez accéder qu’aux hachages des 256 blocs les plus récents, toutes les autres valeurs seront nulles.
Note
La fonction blockhash
était auparavant connue sous le nom block.blockhash
. Elle a été dépréciée dans la version 0.4.22 et supprimée dans la version 0.5.0.
Note
La fonction gasleft
était auparavant connue sous le nom de msg.gas
. Elle a été dépréciée dans la version 0.4.21 et supprimée dans la version 0.5.0.
Note
In version 0.7.0, the alias now
(for block.timestamp
) was removed.
Fonctions d’encodage et de décodage de l’ABI
abi.decode(bytes memory encodedData, (...)) returns (...)
: ABI-decodes the given data, while the types are given in parentheses as second argument. Example:(uint a, uint[2] memory b, bytes memory c) = abi.decode(data, (uint, uint[2], bytes))
abi.encode(...) returns (bytes memory)
: ABI-encodes the given argumentsabi.encodePacked(...) returns (bytes memory)
: Performs packed encoding of the given arguments. Note that packed encoding can be ambiguous!abi.encodeWithSelector(bytes4 selector, ...) returns (bytes memory)
: ABI-encodes the given arguments starting from the second and prepends the given four-byte selectorabi.encodeWithSignature(string memory signature, ...) returns (bytes memory)
: Equivalent toabi.encodeWithSelector(bytes4(keccak256(bytes(signature))), ...)
abi.encodeCall(function functionPointer, (...)) returns (bytes memory)
: ABI-encodes a call tofunctionPointer
with the arguments found in the tuple. Performs a full type-check, ensuring the types match the function signature. Result equalsabi.encodeWithSelector(functionPointer.selector, (...))
Note
Ces fonctions d’encodage peuvent être utilisées pour créer des données pour des appels de fonctions externes sans réellement appeler une fonction externe. De plus, keccak256(abi.encododePacked(a, b))
est un moyen de calculer le hash des données structurées (bien qu’il soit possible de créer une collision de hachage
en utilisant différents types d’entrées).
See the documentation about the ABI and the tightly packed encoding for details about the encoding.
Members of bytes
bytes.concat(...) returns (bytes memory)
: Concatenates variable number of bytes and bytes1, …, bytes32 arguments to one byte array
Members of string
string.concat(...) returns (string memory)
: Concatenates variable number of string arguments to one string array
Gestion des erreurs
Voir la section dédiée sur assert and require pour plus de détails sur la gestion des erreurs et quand utiliser quelle fonction.
assert(bool condition)
:entraîne une
Panic error
et donc la réversion du changement d’état si la condition n’est pas remplie - à utiliser pour les erreurs internes.require(bool condition)
:revert
si la condition n’est pas remplie - à utiliser en cas d’erreurs dans les entrées ou les composants externes.require(bool condition, string memory message)
:revert
si la condition n’est pas remplie - à utiliser en cas d’erreurs dans les entrées ou les composants externes. Fournit également un message d’erreur.revert()
:annuler l’exécution et annuler les changements d’état
revert(string memory reason)
:annuler l’exécution et annuler les changements d’état, fournissant une phrase explicative
Fonctions mathématiques et cryptographiques
addmod(uint x, uint y, uint k) returns (uint)
:calcule
(x + y) % k
où l’addition est effectuée avec une précision arbitraire et n’overflow pas à2**256
.assert
quek != 0
à partir de la version 0.5.0.mulmod(uint x, uint y, uint k) returns (uint)
:calcule
(x * y) % k
où la multiplication est effectuée avec une précision arbitraire et n’overflow pas à2**256
.assert
quek != 0
à partir de la version 0.5.0.keccak256(bytes memory) returns (bytes32)
:calcule le hash Keccak-256 du paramètre
Note
Il y avait un alias pour keccak256
appelé sha3
, qui a été supprimé dans la version 0.5.0. pour éviter la confusion
sha256(bytes memory) returns (bytes32)
:calcule le hash SHA-256 du paramètre
ripemd160(bytes memory) returns (bytes20)
:calcule le hash RIPEMD-160 du paramètre
ecrecover(bytes32 hash, uint8 v, bytes32 r, bytes32 s) returns (address)
:récupérer l’adresse associée à la clé publique à partir de la signature de la courbe elliptique ou retourner zéro sur erreur. The function parameters correspond to ECDSA values of the signature:
r
= first 32 bytes of signatures
= second 32 bytes of signaturev
= final 1 byte of signature
La fonction
ecrecover
renvoie uneaddress
, et non uneaddress payable
. Voir adresse payable pour la conversion, au cas où vous auriez besoin de transférer des fonds à l’adresse récupérée.For further details, read example usage.
Avertissement
If you use ecrecover
, be aware that a valid signature can be turned into a different valid signature without
requiring knowledge of the corresponding private key. In the Homestead hard fork, this issue was fixed
for _transaction_ signatures (see EIP-2), but
the ecrecover function remained unchanged.
This is usually not a problem unless you require signatures to be unique or
use them to identify items. OpenZeppelin have a ECDSA helper library that you can use as a wrapper for ecrecover
without this issue.
Note
Il se peut que vous rencontriez out-of-gas
pour sha256
, ripemd160
ou erecover
sur une blockchain privée. La raison en est que ces contrats sont mis en œuvre sous la forme de contrats dits précompilés et que ces contrats n’existent réellement qu’après avoir reçu le premier message (bien que leur code contrat soit codé en dur). Les messages à des contrats inexistants sont plus coûteux et l’exécution se heurte donc à une erreur out-of-gas. Une solution de contournement pour ce problème est d’envoyer d’abord, par exemple, 1 Wei à chacun des contrats avant de les utiliser dans vos contrats réels. Le problème n’existe pas sur la cha^ine publique Ethereum ni sur les différents testnets officiels.
Membres du type address
<address>.balance
(uint256
):balance de l”Adresses en Wei
<address>.code
(bytes memory
)code at the Adresses (can be empty)
<address>.codehash
(bytes32
)the codehash of the Adresses
<address payable>.transfer(uint256 amount)
send given amount of Wei to Adresses, reverts on failure, forwards 2300 gas stipend, not adjustable
<address payable>.send(uint256 amount) returns (bool)
:envoie la quantité donnée de Wei à adress, retourne
false
en cas d’échec, envoie 2300 gas (non réglable)<address>.call(bytes memory) returns (bool, bytes memory)
:émett un appel de bas niveau
CALL
avec la charge utile donnée, renvoie l’état de réussite et les données de retour, achemine tout le gas disponible ou un montant spécifié<address>.delegatecall(bytes memory) returns (bool, bytes memory)
:émet un appel de bas niveau
DELEGATECALL
avec la charge utile donnée, retourne les données de succès et de retour, achemine tout le gas disponible ou un montant spécifié<address>.staticcall(bytes memory) returns (bool, bytes memory)
:émettre un appel de bas niveau
STATICCALL
avec la charge utile donnée, retourne les conditions de succès et les données de retour, achemine tout le gas disponible ou un montant spécifié
Pour plus d’informations, voir la section sur adress.
Avertissement
You should avoid using .call()
whenever possible when executing another contract function as it bypasses type checking,
function existence check, and argument packing.
Avertissement
Il y a certains dangers à utiliser l’option send
: Le transfert échoue si la profondeur de la pile d’appels est à 1024 (cela peut toujours être forcé par l’appelant) et il échoue également si le destinataire manque de gas. Donc, afin d’effectuer des transferts d’éther en toute sécurité, vérifiez toujours la valeur de retour de send
, utilisez transfer
ou mieux encore :
Utilisez un modèle où le bénéficiaire retire l’argent.
Avertissement
Due to the fact that the EVM considers a call to a non-existing contract to always succeed,
Solidity includes an extra check using the extcodesize
opcode when performing external calls.
This ensures that the contract that is about to be called either actually exists (it contains code)
or an exception is raised.
The low-level calls which operate on addresses rather than contract instances (i.e. .call()
,
.delegatecall()
, .staticcall()
, .send()
and .transfer()
) do not include this
check, which makes them cheaper in terms of gas but also less safe.
Note
Avant la version 0.5.0, Solidity permettait aux membres d’adresses d’être accessibles par une instance de contrat, par exemple this.balance
.
Ceci est maintenant interdit et une conversion explicite en adresse doit être faite : address(this).balance
.
Note
- Si l’accès aux variables d’état s’effectue via un appel de délégation de bas niveau, le plan de stockage des deux contrats doit être alignée pour que le contrat appelé puisse accéder correctement aux variables de stockage du contrat appelant par leur nom.
Ce n’est bien sûr pas le cas si les pointeurs de stockage sont passés comme arguments de fonction comme dans le cas des fonctions de librairies (bibliothèques) de haut niveau.
Note
Avant la version 0.5.0, .call
, .delegatecall
et staticcall
ne renvoyaient que la condition de succès et non les données de retour.
Note
Avant la version 0.5.0, il y avait un membre appelé callcode
avec une sémantique similaire mais légèrement différente de celle de delegatecall
.
Type Information
The expression type(X)
can be used to retrieve information about the type
X
. Currently, there is limited support for this feature (X
can be either
a contract or an integer type) but it might be expanded in the future.
The following properties are available for a contract type C
:
type(C).name
The name of the contract.
type(C).creationCode
Memory byte array that contains the creation bytecode of the contract. This can be used in inline assembly to build custom creation routines, especially by using the
create2
opcode. This property can not be accessed in the contract itself or any derived contract. It causes the bytecode to be included in the bytecode of the call site and thus circular references like that are not possible.type(C).runtimeCode
Memory byte array that contains the runtime bytecode of the contract. This is the code that is usually deployed by the constructor of
C
. IfC
has a constructor that uses inline assembly, this might be different from the actually deployed bytecode. Also note that libraries modify their runtime bytecode at time of deployment to guard against regular calls. The same restrictions as with.creationCode
also apply for this property.
In addition to the properties above, the following properties are available
for an interface type I
:
type(I).interfaceId
:A
bytes4
value containing the EIP-165 interface identifier of the given interfaceI
. This identifier is defined as theXOR
of all function selectors defined within the interface itself - excluding all inherited functions.
The following properties are available for an integer type T
:
type(T).min
The smallest value representable by type
T
.type(T).max
The largest value representable by type
T
.
Expressions et structures de contrôle
Structures de controle
La plupart des structures de contrôle connues des langages à accolades sont disponibles dans Solidity :
Nous disposons de: if
, else
, while
, do
, for
, break
, continue
, return
, avec la syntaxe famillière du C ou du JavaScript.
Solidity also supports exception handling in the form of try
/catch
-statements,
but only for external function calls and
contract creation calls. Errors can be created using the revert statement.
Les parenthèses ne peuvent pas être omises pour les conditions, mais les accolades peuvent être omises autour des déclaration en une opération.
Notez qu’il n’y a pas de conversion de types non booléens vers types booléens comme en C et JavaScript, donc if (1) {...}
n’est pas valable en Solidity.
Appels de fonction
Appels de fonction internes
Les fonctions du contrat en cours peuvent être appelées directement (internal
), également de manière récursive, comme le montre cet exemple absurde:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.22 <0.9.0;
// This will report a warning
contract C {
function g(uint a) public pure returns (uint ret) { return a + f(); }
function f() internal pure returns (uint ret) { return g(7) + f(); }
}
Ces appels de fonction sont traduits en simples sauts ( JUMP
) à l’intérieur de l’EVM. Cela a pour effet que la mémoire actuelle n’est pas effacée, c’est-à-dire qu’il est très efficace de passer des références de mémoire aux fonctions appelées en interne. Seules les fonctions du même contrat peuvent être appelées en interne.
Vous devriez toujours éviter une récursivité excessive, car chaque appel de fonction interne utilise au moins un emplacement de pile et il y a au maximum un peu moins de 1024 emplacements disponibles.
Appels de fonction externes
Les fonctions peuvent aussi être appellées avec la syntaxe this.g(8);
et c.g(2);
(où c
est une instance de contrat et g
est une fonction appartenant à c
).
Appeler la fonction g
d’une manière ou d’une autre résulte en un appel « externe », utilisant call
au lieu de sauts dans le code du contrat.
Veuillez noter que les appels de fonction sur this
ne peuvent pas être utilisés dans le constructeur, car le contrat actuel n’a pas encore été créé.
Les fonctions d’autres contrats doivent être appelées en externe. Pour un appel externe, tous les arguments de fonction doivent être copiés en mémoire.
Note
A function call from one contract to another does not create its own transaction, it is a message call as part of the overall transaction.
Lors de l’appel de fonctions d’autres contrats, le montant de Wei envoyé avec l’appel et le gas peut être spécifié avec les options spéciales .value()
et .gas()
: {value: 10, gas: 10000}
.
Note that it is discouraged to specify gas values explicitly, since the gas costs
of opcodes can change in the future. Any Wei you send to the contract is added
to the total balance of that contract:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.6.2 <0.9.0;
contract InfoFeed {
function info() public payable returns (uint ret) { return 42; }
}
contract Consumer {
InfoFeed feed;
function setFeed(InfoFeed addr) public { feed = addr; }
function callFeed() public { feed.info{value: 10, gas: 800}(); }
}
Vous devez utiliser le modificateur payable
avec la fonction info
pour pouvoir appeler .value()
.
Avertissement
Veillez à ce que feed.info.value(10).gas(800)
ne définisse que localement la value
et la quantité de gas
envoyés avec l’appel de fonction, et que les parenthèses à la fin sont bien présentes pour effectuer l’appel. So
feed.info{value: 10, gas: 800}
does not call the function and
the value
and gas
settings are lost, only
feed.info{value: 10, gas: 800}()
performs the function call.
Due to the fact that the EVM considers a call to a non-existing contract to
always succeed, Solidity uses the extcodesize
opcode to check that
the contract that is about to be called actually exists (it contains code)
and causes an exception if it does not. This check is skipped if the return
data will be decoded after the call and thus the ABI decoder will catch the
case of a non-existing contract.
Note that this check is not performed in case of low-level calls which operate on addresses rather than contract instances.
Note
Be careful when using high-level calls to precompiled contracts, since the compiler considers them non-existing according to the above logic even though they execute code and can return data.
Les appels de fonction provoquent des exceptions si le contrat appelé lui-même lève une exception ou manque de gas.
Avertissement
Toute interaction avec un autre contrat présente un danger potentiel, surtout si le code source du contrat n’est pas connu à l’avance. Le contrat actuel cède le contrôle au contrat appelé et cela peut potentiellement faire à peu près n’importe quoi. Même si le contrat appelé hérite d’un contrat parent connu, le contrat d’héritage doit seulement avoir une interface correcte. L’exécution du contrat peut cependant être totalement arbitraire et donc représentent un danger. En outre, soyez prêt au cas où il appelle d’autres fonctions de votre contrat ou même de retour dans le contrat d’appel avant le retour du premier appel. Cela signifie que le contrat appelé peut modifier les variables d’état du contrat appelant via ses fonctions. Écrivez vos fonctions de manière à ce que, par exemple, les appels à les fonctions externes se produisent après tout changement de variables d’état dans votre contrat, de sorte que votre contrat n’est pas vulnérable à un exploit de réentrée.
Note
Before Solidity 0.6.2, the recommended way to specify the value and gas was to
use f.value(x).gas(g)()
. This was deprecated in Solidity 0.6.2 and is no
longer possible since Solidity 0.7.0.
Appels nommés et paramètres de fonction anonymes
Les arguments d’appel de fonction peuvent être donnés par leur nom, dans n’importe quel ordre, s’ils sont inclus dans { }
comme on peut le voir dans l’exemple qui suit. La liste d’arguments doit coïncider par son nom avec la liste des paramètres de la déclaration de fonction, mais peut être dans un ordre arbitraire.
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.0 <0.9.0;
contract C {
mapping(uint => uint) data;
function f() public {
set({value: 2, key: 3});
}
function set(uint key, uint value) public {
data[key] = value;
}
}
Noms des paramètres de fonction omis
Les noms des paramètres inutilisés (en particulier les paramètres de retour) peuvent être omis. Ces paramètres seront toujours présents sur la pile, mais ils sont inaccessibles.
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.22 <0.9.0;
contract C {
// omitted name for parameter
function func(uint k, uint) public pure returns(uint) {
return k;
}
}
Création de contrats via new
Un contrat peut créer d’autres contrats en utilisant le mot-clé new
. Le code complet du contrat en cours de création doit être connu lors de la compilation afin d’éviter les dépendances récursives liées à la création.
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
contract D {
uint public x;
constructor(uint a) payable {
x = a;
}
}
contract C {
D d = new D(4); // sera exécuté dans le constructor de C
function createD(uint arg) public {
D newD = new D(arg);
newD.x();
}
function createAndEndowD(uint arg, uint amount) public payable {
// Send ether along with the creation
D newD = new D{value: amount}(arg);
newD.x();
}
}
Comme dans l’exemple, il est possible d’envoyer des Ether en créant une instance de D
en utilisant l’option .value()
, mais il n’est pas possible de limiter la quantité de gas.
Si la création échoue (à cause d’une rupture de pile, d’un manque de gas ou d’autres problèmes), une exception est levée.
Salted contract creations / create2
When creating a contract, the address of the contract is computed from the address of the creating contract and a counter that is increased with each contract creation.
If you specify the option salt
(a bytes32 value), then contract creation will
use a different mechanism to come up with the address of the new contract:
It will compute the address from the address of the creating contract, the given salt value, the (creation) bytecode of the created contract and the constructor arguments.
In particular, the counter (« nonce ») is not used. This allows for more flexibility in creating contracts: You are able to derive the address of the new contract before it is created. Furthermore, you can rely on this address also in case the creating contracts creates other contracts in the meantime.
The main use-case here is contracts that act as judges for off-chain interactions, which only need to be created if there is a dispute.
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
contract D {
uint public x;
constructor(uint a) {
x = a;
}
}
contract C {
function createDSalted(bytes32 salt, uint arg) public {
// This complicated expression just tells you how the address
// can be pre-computed. It is just there for illustration.
// You actually only need ``new D{salt: salt}(arg)``.
address predictedAddress = address(uint160(uint(keccak256(abi.encodePacked(
bytes1(0xff),
address(this),
salt,
keccak256(abi.encodePacked(
type(D).creationCode,
arg
))
)))));
D d = new D{salt: salt}(arg);
require(address(d) == predictedAddress);
}
}
Avertissement
There are some peculiarities in relation to salted creation. A contract can be re-created at the same address after having been destroyed. Yet, it is possible for that newly created contract to have a different deployed bytecode even though the creation bytecode has been the same (which is a requirement because otherwise the address would change). This is due to the fact that the constructor can query external state that might have changed between the two creations and incorporate that into the deployed bytecode before it is stored.
Ordre d’évaluation des expressions
L’ordre d’évaluation des expressions est non spécifié (plus formellement, l’ordre dans lequel les enfants d’un noeud de l’arbre des expressions sont évalués n’est pas spécifié, mais ils sont bien sûr évalués avant le noeud lui-même). La seule garantie est que les instructions sont exécutées dans l’ordre et que les expressions booléennes sont court-circuitées correctement.
Assignation
Déstructuration d’assignations et retour de valeurs multiples
Solidity permet en interne les tuples, c’est-à-dire une liste d’objets de types potentiellement différents dont le nombre est une constante au moment de la compilation. Ces tuples peuvent être utilisés pour retourner plusieurs valeurs en même temps. Ceux-ci peuvent ensuite être affectés soit à des variables nouvellement déclarées, soit à des variables préexistantes (ou à des LValues en général).
Les tuples ne sont pas des types propres à Solidity, ils ne peuvent être utilisés que pour former des groupes syntaxiques d’expressions.
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.5.0 <0.9.0;
contract C {
uint index;
function f() public pure returns (uint, bool, uint) {
return (7, true, 2);
}
function g() public {
// Variables declared with type and assigned from the returned tuple,
// not all elements have to be specified (but the number must match).
(uint x, , uint y) = f();
// Common trick to swap values -- does not work for non-value storage types.
(x, y) = (y, x);
// Components can be left out (also for variable declarations).
(index, , ) = f(); // Sets the index to 7
}
}
It is not possible to mix variable declarations and non-declaration assignments,
i.e. the following is not valid: (x, uint y) = (1, 2);
Note
Prior to version 0.5.0 it was possible to assign to tuples of smaller size, either filling up on the left or on the right side (which ever was empty). This is now disallowed, so both sides have to have the same number of components.
Avertissement
Be careful when assigning to multiple variables at the same time when reference types are involved, because it could lead to unexpected copying behaviour.
Complications pour les tableaux et les structures
La sémantique des affectations est un peu plus compliquée pour les types autres que valeurs comme les tableaux et les structs, y compris bytes
et string
, voir Emplacement des donnés et comportements à l’assignation pour plus de détails.
In the example below the call to g(x)
has no effect on x
because it creates
an independent copy of the storage value in memory. However, h(x)
successfully modifies x
because only a reference and not a copy is passed.
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.22 <0.9.0;
contract C {
uint[20] x;
function f() public {
g(x);
h(x);
}
function g(uint[20] memory y) internal pure {
y[2] = 3;
}
function h(uint[20] storage y) internal {
y[3] = 4;
}
}
Portée et déclarations
Une variable qui est déclarée aura une valeur par défaut initiale dont la représentation octale est égale à une suite de zéros.
Les « valeurs par défaut » des variables sont les « états zéro » typiques quel que soit le type. Par exemple, la valeur par défaut d’un bool
est false
. La valeur par défaut pour les types uint
ou int
est 0
. Pour les tableaux de taille statique et les bytes1
à bytes32
, chaque élément individuel sera initialisé à la valeur par défaut correspondant à son type. Enfin, pour les tableaux de taille dynamique, les octets et les chaînes de caractères, la valeur par défaut est un tableau ou une chaîne vide.
La portée en Solidity suit les règles de portée très répandues du C99 (et de nombreux autres languages): Les variables sont visibles du point situé juste après leur déclaration jusqu’à la fin du plus petit bloc { }
qui contient la déclaration. Par exception à cette règle, les variables déclarées dans la partie initialisation d’une boucle for
ne sont visibles que jusqu’à la fin de la boucle for.
Les variables et autres éléments déclarés en dehors d’un bloc de code, par exemple les fonctions, les contrats, les types définis par l’utilisateur, etc. sont visibles avant même leur déclaration. Cela signifie que vous pouvez utiliser les variables d’état avant qu’elles ne soient déclarées et appeler les fonctions de manière récursive.
Par conséquent, les exemples suivants seront compilés sans avertissement, puisque les deux variables ont le même nom mais des portées disjointes.
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.5.0 <0.9.0;
contract C {
function minimalScoping() pure public {
{
uint same;
same = 1;
}
{
uint same;
same = 3;
}
}
}
À titre d’exemple particulier des règles de détermination de la portée héritées du C99, notons que, dans ce qui suit, la première affectation à x
affectera en fait la variable externe et non la variable interne. Dans tous les cas, vous obtiendrez un avertissement concernant cette double déclaration.
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.5.0 <0.9.0;
// This will report a warning
contract C {
function f() pure public returns (uint) {
uint x = 1;
{
x = 2; // this will assign to the outer variable
uint x;
}
return x; // x has value 2
}
}
Avertissement
Avant la version 0.5.0, Solidity suivait les mêmes règles de scoping que JavaScript, c’est-à-dire qu’une variable déclarée n’importe où dans une fonction était dans le champ d’application pour l’ensemble de la fonction, peu importe où elle était déclarée. L’exemple suivant montre un extrait de code qui compilait, mais conduit aujourd’hui à une erreur à partir de la version 0.5.0.
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.5.0 <0.9.0;
// This will not compile
contract C {
function f() pure public returns (uint) {
x = 2;
uint x;
return x;
}
}
Checked or Unchecked Arithmetic
An overflow or underflow is the situation where the resulting value of an arithmetic operation, when executed on an unrestricted integer, falls outside the range of the result type.
Prior to Solidity 0.8.0, arithmetic operations would always wrap in case of under- or overflow leading to widespread use of libraries that introduce additional checks.
Since Solidity 0.8.0, all arithmetic operations revert on over- and underflow by default, thus making the use of these libraries unnecessary.
To obtain the previous behaviour, an unchecked
block can be used:
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;
contract C {
function f(uint a, uint b) pure public returns (uint) {
// This subtraction will wrap on underflow.
unchecked { return a - b; }
}
function g(uint a, uint b) pure public returns (uint) {
// This subtraction will revert on underflow.
return a - b;
}
}
The call to f(2, 3)
will return 2**256-1
, while g(2, 3)
will cause
a failing assertion.
The unchecked
block can be used everywhere inside a block, but not as a replacement
for a block. It also cannot be nested.
The setting only affects the statements that are syntactically inside the block.
Functions called from within an unchecked
block do not inherit the property.
Note
To avoid ambiguity, you cannot use _;
inside an unchecked
block.
The following operators will cause a failing assertion on overflow or underflow and will wrap without an error if used inside an unchecked block:
++
, --
, +
, binary -
, unary -
, *
, /
, %
, **
+=
, -=
, *=
, /=
, %=
Avertissement
It is not possible to disable the check for division by zero
or modulo by zero using the unchecked
block.
Note
Bitwise operators do not perform overflow or underflow checks.
This is particularly visible when using bitwise shifts (<<
, >>
, <<=
, >>=
) in
place of integer division and multiplication by a power of 2.
For example type(uint256).max << 3
does not revert even though type(uint256).max * 8
would.
Note
The second statement in int x = type(int).min; -x;
will result in an overflow
because the negative range can hold one more value than the positive range.
Explicit type conversions will always truncate and never cause a failing assertion with the exception of a conversion from an integer to an enum type.
Gestion d’erreurs: Assert, Require, Revert et Exceptions
Solidity utilise des exceptions qui restaurent l’état pour gérer les erreurs. Une telle exception annule toutes les modifications apportées à l’état de l’appel en cours (et de tous ses sous-appels) et signale également une erreur à l’appelant.
Lorsque des exceptions se produisent dans un sous-appel, elles « remontent à la surface » automatiquement (c’est-à-dire que les exceptions sont déclenchées en casacade). Les exceptions à cette règle sont send
et les fonctions de bas niveau call
, delegatecall
et staticcall
, qui retournent false
comme première valeur de retour en cas d’exception au lieu de provoquer une exception qui ne pourra donc pas remonter.
Avertissement
Les fonctions de bas niveau call
, delegatecall
et staticcall
renvoient true
comme première valeur de retour si le compte appelé est inexistant, dû à la conception de l’EVM. L’existence doit être vérifiée avant l’appel si désiré.
Exceptions can contain error data that is passed back to the caller
in the form of error instances.
The built-in errors Error(string)
and Panic(uint256)
are
used by special functions, as explained below. Error
is used for « regular » error conditions
while Panic
is used for errors that should not be present in bug-free code.
Panic via assert
and Error via require
Les fonctions utilitaires assert
et require
peuvent être utilisées pour vérifier les conditions et lancer une exception si la condition n’est pas remplie.
The assert
function creates an error of type Panic(uint256)
.
The same error is created by the compiler in certain situations as listed below.
Assert should only be used to test for internal errors, and to check invariants. Properly functioning code should never create a Panic, not even on invalid external input. If this happens, then there is a bug in your contract which you should fix. Language analysis tools can evaluate your contract to identify the conditions and function calls which will cause a Panic.
A Panic exception is generated in the following situations. The error code supplied with the error data indicates the kind of panic.
0x00: Used for generic compiler inserted panics.
0x01: If you call
assert
with an argument that evaluates to false.0x11: If an arithmetic operation results in underflow or overflow outside of an
unchecked { ... }
block.0x12; If you divide or modulo by zero (e.g.
5 / 0
or23 % 0
).0x21: If you convert a value that is too big or negative into an enum type.
0x22: If you access a storage byte array that is incorrectly encoded.
0x31: If you call
.pop()
on an empty array.0x32: If you access an array,
bytesN
or an array slice at an out-of-bounds or negative index (i.e.x[i]
wherei >= x.length
ori < 0
).0x41: If you allocate too much memory or create an array that is too large.
0x51: If you call a zero-initialized variable of internal function type.
The require
function either creates an error without any data or
an error of type Error(string)
. It
should be used to ensure valid conditions
that cannot be detected until execution time.
This includes conditions on inputs
or return values from calls to external contracts.
Note
It is currently not possible to use custom errors in combination
with require
. Please use if (!condition) revert CustomError();
instead.
An Error(string)
exception (or an exception without data) is generated
by the compiler
in the following situations:
Calling
require(x)
wherex
evaluates tofalse
.If you use
revert()
orrevert("description")
.If you perform an external function call targeting a contract that contains no code.
If your contract receives Ether via a public function without
payable
modifier (including the constructor and the fallback function).If your contract receives Ether via a public getter function.
For the following cases, the error data from the external call (if provided) is forwarded. This means that it can either cause an Error or a Panic (or whatever else was given):
If a
.transfer()
fails.If you call a function via a message call but it does not finish properly (i.e., it runs out of gas, has no matching function, or throws an exception itself), except when a low level operation
call
,send
,delegatecall
,callcode
orstaticcall
is used. The low level operations never throw exceptions but indicate failures by returningfalse
.If you create a contract using the
new
keyword but the contract creation does not finish properly.
Vous pouvez facultativement fournir une chaîne de message pour require
, mais pas pour assert
.
Note
If you do not provide a string argument to require
, it will revert
with empty error data, not even including the error selector.
Dans l’exemple suivant, vous pouvez voir comment require
peut être utilisé pour vérifier facilement les conditions sur les entrées et comment assert
peut être utilisé pour vérifier les erreurs internes.
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.5.0 <0.9.0;
contract Sharer {
function sendHalf(address payable addr) public payable returns (uint balance) {
require(msg.value % 2 == 0, "Even value required.");
uint balanceBeforeTransfer = address(this).balance;
addr.transfer(msg.value / 2);
// Since transfer throws an exception on failure and
// cannot call back here, there should be no way for us to
// still have half of the money.
assert(address(this).balance == balanceBeforeTransfer - msg.value / 2);
return address(this).balance;
}
}
En interne, Solidity exécute une opération de revert
(instruction 0xfd
). Dans les deux cas, cela provoque l’annulation toutes les modifications apportées à l’état de l’EVM dans l’appel courant. La raison du retour en arrière est qu’il n’y a pas de moyen sûr de continuer l’exécution, parce qu’un effet attendu ne s’est pas produit. Parce que nous voulons conserver l’atomicité des transactions, la chose la plus sûre à faire est d’annuler tous les changements et de rendre toute la transaction (ou au moins l’appel) sans effet.
In both cases, the caller can react on such failures using try
/catch
, but
the changes in the callee will always be reverted.
Note
Panic exceptions used to use the invalid
opcode before Solidity 0.8.0,
which consumed all gas available to the call.
Exceptions that use require
used to consume all gas until before the Metropolis release.
revert
A direct revert can be triggered using the revert
statement and the revert
function.
The revert
statement takes a custom error as direct argument without parentheses:
revert CustomError(arg1, arg2);
For backwards-compatibility reasons, there is also the revert()
function, which uses parentheses
and accepts a string:
revert(); revert(« description »);
The error data will be passed back to the caller and can be caught there.
Using revert()
causes a revert without any error data while revert("description")
will create an Error(string)
error.
Using a custom error instance will usually be much cheaper than a string description, because you can use the name of the error to describe it, which is encoded in only four bytes. A longer description can be supplied via NatSpec which does not incur any costs.
The following example shows how to use an error string and a custom error instance
together with revert
and the equivalent require
:
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.4;
contract VendingMachine {
address owner;
error Unauthorized();
function buy(uint amount) public payable {
if (amount > msg.value / 2 ether)
revert("Not enough Ether provided.");
// Alternative way to do it:
require(
amount <= msg.value / 2 ether,
"Not enough Ether provided."
);
// Perform the purchase.
}
function withdraw() public {
if (msg.sender != owner)
revert Unauthorized();
payable(msg.sender).transfer(address(this).balance);
}
}
The two ways if (!condition) revert(...);
and require(condition, ...);
are
equivalent as long as the arguments to revert
and require
do not have side-effects,
for example if they are just strings.
Note
The require
function is evaluated just as any other function.
This means that all arguments are evaluated before the function itself is executed.
In particular, in require(condition, f())
the function f
is executed even if
condition
is true.
La chaîne fournie sera abi-encoded comme si c’était un appel à une fonction Error(string)
.
Dans l’exemple ci-dessus, revert("Not enough Ether provided.");`
fera en sorte que les données hexadécimales suivantes soient définies comme données de retour d’erreur :
0x08c379a0 // Selecteur de fonction pour Error(string)
0x0000000000000000000000000000000000000000000000000000000000000020 // Décalage des données
0x000000000000000000000000000000000000000000000000000000000000001a // Taille de la string
0x4e6f7420656e6f7567682045746865722070726f76696465642e000000000000 // Données de la string
The provided message can be retrieved by the caller using try
/catch
as shown below.
Note
There used to be a keyword called throw
with the same semantics as revert()
which
was deprecated in version 0.4.13 and removed in version 0.5.0.
try
/catch
A failure in an external call can be caught using a try/catch statement, as follows:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.8.1;
interface DataFeed { function getData(address token) external returns (uint value); }
contract FeedConsumer {
DataFeed feed;
uint errorCount;
function rate(address token) public returns (uint value, bool success) {
// Permanently disable the mechanism if there are
// more than 10 errors.
require(errorCount < 10);
try feed.getData(token) returns (uint v) {
return (v, true);
} catch Error(string memory /*reason*/) {
// This is executed in case
// revert was called inside getData
// and a reason string was provided.
errorCount++;
return (0, false);
} catch Panic(uint /*errorCode*/) {
// This is executed in case of a panic,
// i.e. a serious error like division by zero
// or overflow. The error code can be used
// to determine the kind of error.
errorCount++;
return (0, false);
} catch (bytes memory /*lowLevelData*/) {
// This is executed in case revert() was used.
errorCount++;
return (0, false);
}
}
}
The try
keyword has to be followed by an expression representing an external function call
or a contract creation (new ContractName()
).
Errors inside the expression are not caught (for example if it is a complex expression
that also involves internal function calls), only a revert happening inside the external
call itself. The returns
part (which is optional) that follows declares return variables
matching the types returned by the external call. In case there was no error,
these variables are assigned and the contract’s execution continues inside the
first success block. If the end of the success block is reached, execution continues after the catch
blocks.
Solidity supports different kinds of catch blocks depending on the type of error:
catch Error(string memory reason) { ... }
: This catch clause is executed if the error was caused byrevert("reasonString")
orrequire(false, "reasonString")
(or an internal error that causes such an exception).catch Panic(uint errorCode) { ... }
: If the error was caused by a panic, i.e. by a failingassert
, division by zero, invalid array access, arithmetic overflow and others, this catch clause will be run.catch (bytes memory lowLevelData) { ... }
: This clause is executed if the error signature does not match any other clause, if there was an error while decoding the error message, or if no error data was provided with the exception. The declared variable provides access to the low-level error data in that case.catch { ... }
: If you are not interested in the error data, you can just usecatch { ... }
(even as the only catch clause) instead of the previous clause.
It is planned to support other types of error data in the future.
The strings Error
and Panic
are currently parsed as is and are not treated as identifiers.
In order to catch all error cases, you have to have at least the clause
catch { ...}
or the clause catch (bytes memory lowLevelData) { ... }
.
The variables declared in the returns
and the catch
clause are only
in scope in the block that follows.
Note
If an error happens during the decoding of the return data
inside a try/catch-statement, this causes an exception in the currently
executing contract and because of that, it is not caught in the catch clause.
If there is an error during decoding of catch Error(string memory reason)
and there is a low-level catch clause, this error is caught there.
Note
If execution reaches a catch-block, then the state-changing effects of the external call have been reverted. If execution reaches the success block, the effects were not reverted. If the effects have been reverted, then execution either continues in a catch block or the execution of the try/catch statement itself reverts (for example due to decoding failures as noted above or due to not providing a low-level catch clause).
Note
The reason behind a failed call can be manifold. Do not assume that the error message is coming directly from the called contract: The error might have happened deeper down in the call chain and the called contract just forwarded it. Also, it could be due to an out-of-gas situation and not a deliberate error condition: The caller always retains at least 1/64th of the gas in a call and thus even if the called contract goes out of gas, the caller still has some gas left.
Contrats
Les contrats en Solidity sont similaires à des classes dans les langages orientés objets. Ils contiennent des données persistentes dans des variables et des fonctions peuvent les modifier. Appeler la fonction d’un autre contrat (une autre instance) executera un appel de fonction auprès de l’EVM et changera alors le contexte, rendant inaccessibles ces variables. A contract and its functions need to be called for anything to happen. There is no « cron » concept in Ethereum to call a function at a particular event automatically.
Créer des contrats
Les contrats peuvent être créés « en dehors » via des transactions Ethereum ou depuis des contrat en Solidity.
Les EDIs, comme Remix, facilite la tâche via des éléments visuels.
Créer des contrats via du code se fait le plus simplement en utilisant l’API Javascript web3.js. Elle possède une fonction appelée web3.eth.Contract qui facilite cette création
Quand un contrat est créé, son constructeur (une fonction déclarée via le mot-clé constructor
) est executé, de manière unique.
Un constructeur est optionnel. Aussi, un seul constructeur est autorisé, ce qui signifie que l’overloading n’est pas supporté.
Après que le constructeur ai été exécuté, le code final du contrat est déployé sur la Blockchain. Ce code inclut toutes les fonctions publiques et externes, et toutes les fonctions qui sont atteignables par des appels de fonctions. Le code déployé n’inclut pas le constructeur ou les fonctions internes uniquement appelées depuis le constructeur.
En interne, les arguments du constructeur sont passés ABI encodés après le code du contrat lui-même, mais vous n’avez pas à vous en soucier si vous utilisez web3.js
.
Si un contrat veut créer un autre contrat, le code source (et le binaire) du contrat créé doit être connu du créateur. Cela signifie que les dépendances cycliques de création sont impossibles.
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.22 <0.9.0;
contract OwnedToken {
// TokenCreator est un type de contrat défini ci-dessous.
// Il est possible de le référencer tant qu'il n'est pas utilisé
// pour créer un nouveau contrat.
TokenCreator creator;
address owner;
bytes32 name;
// This is the constructor which registers the
// creator and the assigned name.
constructor(bytes32 _name) {
// State variables are accessed via their name
// and not via e.g. `this.owner`. Functions can
// be accessed directly or through `this.f`,
// but the latter provides an external view
// to the function. Especially in the constructor,
// you should not access functions externally,
// because the function does not exist yet.
// See the next section for details.
owner = msg.sender;
// Nous effectuons une conversion de type explicite de `address`.
// vers `TokenCreator` et supposons que le type du
// contrat appelant est TokenCreator,
// Il n'y a pas vraiment moyen de vérifier ça.
creator = TokenCreator(msg.sender);
name = _name;
}
function changeName(bytes32 newName) public {
// Seul le créateur peut modifier le nom --
// la comparaison est possible puisque les contrats
// sont explicitement convertibles en adresses.
if (msg.sender == address(creator))
name = newName;
}
function transfer(address newOwner) public {
// Seul le propriétaire actuel peut transférer le token.
if (msg.sender != owner) return;
// Nous voulons aussi demander au créateur si le transfert
// est valide. Notez que ceci appelle une fonction de la fonction
// contrat défini ci-dessous. Si l'appel échoue (p. ex.
// en raison d'un manque de gas), l'exécution échoue également ici.
if (creator.isTokenTransferOK(owner, newOwner))
owner = newOwner;
}
}
contract TokenCreator {
function createToken(bytes32 name)
public
returns (OwnedToken tokenAddress)
{
// Créer un nouveau contrat Token et renvoyer son adresse.
// Du côté JavaScript, le type de retour est simplement
// `address`, car c'est le type le plus proche disponible dans
// l'ABI.
return new OwnedToken(name);
}
function changeName(OwnedToken tokenAddress, bytes32 name) public {
// Encore une fois, le type externe de `tokenAddress' est
// simplement `adresse`.
tokenAddress.changeName(name);
}
function isTokenTransferOK(address currentOwner, address newOwner)
public
pure
returns (bool ok)
{
// Vérifier une condition arbitraire.
return keccak256(abi.encodePacked(currentOwner, newOwner))[0] == 0x7f;
}
}
Visibilité et Getters
State Variable Visibility
public
Public state variables differ from internal ones only in that the compiler automatically generates getter functions for them, which allows other contracts to read their values. When used within the same contract, the external access (e.g.
this.x
) invokes the getter while internal access (e.g.x
) gets the variable value directly from storage. Setter functions are not generated so other contracts cannot directly modify their values.internal
Internal state variables can only be accessed from within the contract they are defined in and in derived contracts. They cannot be accessed externally. This is the default visibility level for state variables.
private
Private state variables are like internal ones but they are not visible in derived contracts.
Avertissement
Making something private
or internal
only prevents other contracts from reading or modifying the information, but it will still be visible to the whole world outside of the blockchain.
Function Visibility
Puisque Solidity connaît deux types d’appels de fonction (internes qui ne créent pas d’appel EVM réel (également appelés a « message call ») et externes qui le font), il existe quatre types de visibilités pour les fonctions et les variables d’état.
Les fonctions doivent être spécifiées comme étant external
, public
, internal
ou private
.
Pour les variables d’état, external
n’est pas possible.
external
:Les fonctions externes font partie de l’interface du contrat, ce qui signifie qu’elles peuvent être appelées à partir d’autres contrats et via des transactions. Une fonction externe
f
ne peut pas être appelée en interne (c’est-à-diref()``ne fonctionne pas, mais ``this.f()
fonctionne). Les fonctions externes sont parfois plus efficaces lorsqu’elles reçoivent de grandes quantités de données.public
:Les fonctions publiques font partie de l’interface du contrat et peuvent être appelées en interne ou via des messages. Pour les variables d’état publiques, une fonction getter automatique (voir ci-dessous) est générée.
internal
:Ces fonctions et variables d’état ne sont accessibles qu’en interne (c’est-à-dire à partir du contrat en cours ou des contrats qui en découlent), sans utiliser
this
.private
:Les fonctions privées et les variables d’état ne sont visibles que pour le contrat dans lequel elles sont définies et non dans les contrats dérivés.
Note
Tout ce qui se trouve à l’intérieur d’un contrat est visible pour tous les observateurs extérieurs à la blockchain. Passer quelque chose en
private
ne fait qu’empêcher les autres contrats d’accéder à l’information et de la modifier, mais elle sera toujours visible pour le monde entier à l’extérieur de la blockchain.
Le spécificateur de visibilité est donné après le type pour les variables d’état et entre la liste des paramètres et la liste des paramètres de retour pour les fonctions.
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.16 <0.9.0;
contract C {
function f(uint a) private pure returns (uint b) { return a + 1; }
function setData(uint a) internal { data = a; }
uint public data;
}
Dans l’exemple suivant, D
, peut appeler c.getData()
pour retrouver la valeur de data
en mémoire d’état, mais ne peut pas appeler f
. Le contrat E
est dérivé du contrat C
et peut donc appeler compute
.
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.16 <0.9.0;
contract C {
uint private data;
function f(uint a) private pure returns(uint b) { return a + 1; }
function setData(uint a) public { data = a; }
function getData() public view returns(uint) { return data; }
function compute(uint a, uint b) internal pure returns (uint) { return a + b; }
}
// Ceci ne compile pas
contract D {
function readData() public {
C c = new C();
uint local = c.f(7); // Erreur: le membre `f` n'est pas visible
c.setData(3);
local = c.getData();
local = c.compute(3, 5); // Erreur: le membre `compute` n'est pas visible
}
}
contract E is C {
function g() public {
C c = new C();
uint val = compute(3, 5); // accès à un membre interne (du contrat dérivé au contrat parent)
}
}
Fonctions Getter
Le compilateur crée automatiquement des fonctions getter pour toutes les variables d’état public. Pour le contrat donné ci-dessous, le compilateur va générer une fonction appelée data
qui ne prend aucun argument et retourne un uint
, la valeur de la variable d’état data
. Les variables d’état peuvent être initialisées lorsqu’elles sont déclarées.
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.16 <0.9.0;
contract C {
uint public data = 42;
}
contract Caller {
C c = new C();
function f() public view returns (uint) {
return c.data();
}
}
Les fonctions getter ont une visibilité externe. Si le symbole est accédé en interne (c’est-à-dire sans this.
), il est évalué à une variable d’état. S’il est accédé de l’extérieur (c’est-à-dire avec this.
), il évalue à une fonction.
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.16 <0.7.0;
contract C {
uint public data;
function x() public returns (uint) {
data = 3; // accès interne
return this.data(); // accès externe
}
}
Si vous avez une variable d’état public
de type array, alors vous ne pouvez récupérer que des éléments simples de l’array via la fonction getter générée.
Ce mécanisme permet d’éviter des coûts de gas élevés lors du retour d’un tableau complet.
Vous pouvez utiliser des arguments pour spécifier quel élément individuel retourner, par exemple data(0)
. Si vous voulez retourner un tableau entier en un appel, alors vous devez écrire une fonction, par exemple :
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.16 <0.9.0;
contract arrayExample {
// variable d'état publique
uint[] public myArray;
// Fonction getter générée par le compilateur
/*
function myArray(uint i) returns (uint) {
return myArray[i];
}
*/
// fonction retournant une array complète
function getArray() returns (uint[] memory) {
return myArray;
}
}
Maintenant vous pouvez utiliser getArray()
pour récupérer le tableau entier, au lieu de myArray(i)
, qui retourne un seul élément par appel.
L’exemple suivant est plus complexe:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.0 <0.9.0;
contract Complex {
struct Data {
uint a;
bytes3 b;
mapping (uint => uint) map;
uint[3] c;
uint[] d;
bytes e;
}
mapping (uint => mapping(bool => Data[])) public data;
}
It generates a function of the following form. The mapping and arrays (with the exception of byte arrays) in the struct are omitted because there is no good way to select individual struct members or provide a key for the mapping:
function data(uint arg1, bool arg2, uint arg3)
public
returns (uint a, bytes3 b, bytes memory e)
{
a = data[arg1][arg2][arg3].a;
b = data[arg1][arg2][arg3].b;
e = data[arg1][arg2][arg3].e;
}
Modificateurs de fonctions
Les modificateurs peuvent être utilisés pour modifier facilement le comportement des fonctions. Par exemple, ils peuvent vérifier automatiquement une condition avant d’exécuter la fonction.
Les modificateurs sont des propriétés héritables des contrats et peuvent être redéfinis dans les contrats dérivés, mais seulement s’ils sont indiqués virtual
. Pour plus de détails, voir
Modifier Overriding.
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.1 <0.9.0;
contract owned {
constructor() { owner = payable(msg.sender); }
address payable owner;
// Ce contrat ne définit qu'un modificateur mais ne l'utilise pas:
// il sera utilisé dans les contrats dérivés.
// Le corps de la fonction est inséré à l'endroit où le symbole spécial
// `_;` apparaît dans la définition d'un modificateur.
// Cela signifie que si le propriétaire appelle cette fonction, la fonction
// est exécutée et dans le cas contraire, une exception est
// levée.
modifier onlyOwner {
require(
msg.sender == owner,
"Only owner can call this function."
);
_;
}
}
contract mortal is owned {
// Ce contrat hérite du modificateur `onlyOwner` de `owned`
// et l'applique à la fonction `close`, qui
// cause que les appels à `close` n'ont un effet que s'il
// sont passés par le propriétaire enregistré.
function close() public onlyOwner {
selfdestruct(owner);
}
}
contract priced {
// Les modificateurs peuvent prendre des arguments:
modifier costs(uint price) {
if (msg.value >= price) {
_;
}
}
}
contract Register is priced, owned {
mapping (address => bool) registeredAddresses;
uint price;
constructor(uint initialPrice) { price = initialPrice; }
// Il est important de fournir également le
// mot-clé `payable` ici, sinon la fonction
// rejettera automatiquement tous les Ethers qui lui sont envoyés.
function register() public payable costs(price) {
registeredAddresses[msg.sender] = true;
}
function changePrice(uint _price) public onlyOwner {
price = _price;
}
}
contract Mutex {
bool locked;
modifier noReentrancy() {
require(
!locked,
"Reentrant call."
);
locked = true;
_;
locked = false;
}
/// Cette fonction est protégée par un mutex, ce qui signifie que
/// les appels entrants à partir de `msg.sender.call` ne peuvent pas rappeler `f`.
/// L'instruction `return 7` assigne 7 à la valeur de retour, mais en même temps
/// exécute l'instruction `locked = false` dans le modificateur.
function f() public noReentrancy returns (uint) {
(bool success,) = msg.sender.call("");
require(success);
return 7;
}
}
<<<<<<< HEAD
Plusieurs modificateurs sont appliqués à une fonction en les spécifiant dans une liste séparée par des espaces et sont évalués dans l’ordre présenté.
=======
If you want to access a modifier m
defined in a contract C
, you can use C.m
to
reference it without virtual lookup. It is only possible to use modifiers defined in the current
contract or its base contracts. Modifiers can also be defined in libraries but their use is
limited to functions of the same library.
Multiple modifiers are applied to a function by specifying them in a whitespace-separated list and are evaluated in the order presented. >>>>>>> 47d77931747aba8e364452537d989b795df7ca04
Modifiers cannot implicitly access or change the arguments and return values of functions they modify. Their values can only be passed to them explicitly at the point of invocation.
Explicit returns from a modifier or function body only leave the current
modifier or function body. Return variables are assigned and
control flow continues after the _
in the preceding modifier.
Avertissement
Dans une version antérieure de Solidity, les instructions return
des fonctions ayant des modificateurs se comportaient différemment.
<<<<<<< HEAD
Les retours explicites d’un modificateur ou d’un corps de fonction ne laissent que le modificateur ou le corps de fonction courant. Les variables de retour sont affectées et le flow de contrôle continue après le « _ » dans le modificateur précédent.
=======
An explicit return from a modifier with return;
does not affect the values returned by the function.
The modifier can, however, choose not to execute the function body at all and in that case the return
variables are set to their default values just as if the function had an empty
body.
The _
symbol can appear in the modifier multiple times. Each occurrence is replaced with
the function body.
>>>>>>> 47d77931747aba8e364452537d989b795df7ca04
Des expressions arbitraires sont autorisées pour les arguments du modificateur et dans ce contexte, tous les symboles visibles depuis la fonction sont visibles dans le modificateur. Les symboles introduits dans le modificateur ne sont pas visibles dans la fonction (car ils peuvent changer en cas de redéfinition).
Variables d’état constantes et immutables
Les variables d’état peuvent être déclarées comme constantes
ou immutable
. Dans les deux cas, ces variables ne peuvent être modifiées après la construction du contrat.
Dans ce cas, elles doivent être assignées à partir d’une expression constante au moment de la compilation.
Pour les variables constant
, la valeur doit être connue à la compilation.
Pour les variables immutable
, les variables peuvent être assognées jusqu’à la construction.
It is also possible to define constant
variables at the file level.
Le compilateur ne réserve pas d’emplacement de stockage pour ces variables, et chaque occurrence est remplacée par l’expression constante correspondante.
Compared to regular state variables, the gas costs of constant and immutable variables are much lower. For a constant variable, the expression assigned to it is copied to all the places where it is accessed and also re-evaluated each time. This allows for local optimizations. Immutable variables are evaluated once at construction time and their value is copied to all the places in the code where they are accessed. For these values, 32 bytes are reserved, even if they would fit in fewer bytes. Due to this, constant values can sometimes be cheaper than immutable values.
Tous les types de constantes ne sont pas implémentés pour le moment. Les seuls types pris en charge sont chaines de caractères (uniquement pour les constantes) et types valeur.
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.4;
uint constant X = 32**22 + 8;
contract C {
string constant TEXT = "abc";
bytes32 constant MY_HASH = keccak256("abc");
uint immutable decimals;
uint immutable maxBalance;
address immutable owner = msg.sender;
constructor(uint _decimals, address _reference) {
decimals = _decimals;
// L'assignement à des immutables peut même accéder à l'environnement
maxBalance = _reference.balance;
}
function isBalanceTooHigh(address _other) public view returns (bool) {
return _other.balance > maxBalance;
}
}
Constant
Pour les variables constantes
, les valeurs doivent être assignées à partir d’une expression constante au moment de la
compilation et doit être assignée à la déclaration. Toute expression qui accède au stockage, aux données de la blockchain
(par exemple address(this).balance
ou block.number
) ou
les données d’exécution (msg.value
ou gasleft()
) ou les appels vers des contrats externes sont interdits.
Les expressions qui peuvent avoir un effet secondaire sur l’allocation de mémoire sont autorisées,
mais celles qui peuvent avoir un effet secondaire sur d’autres objets mémoire ne le sont pas.
Les fonctions intégrées keccak256
, sha256
, ripemd160
, ecrecover
, addmod
et mulmod
sont autorisées (même si des contrats externes sont appelés).
La raison pour laquelle on autorise les effets secondaires sur l’allocateur de mémoire est qu’il devrait être possible de construire des objets complexes comme par exemple des tables de consultation. Cette fonctionnalité n’est pas encore entièrement utilisable.
Immutable
Variables declared as immutable
are a bit less restricted than those
declared as constant
: Immutable variables can be assigned an arbitrary
value in the constructor of the contract or at the point of their declaration.
They can be assigned only once and can, from that point on, be read even during
construction time.
The contract creation code generated by the compiler will modify the contract’s runtime code before it is returned by replacing all references to immutables by the values assigned to the them. This is important if you are comparing the runtime code generated by the compiler with the one actually stored in the blockchain.
Note
Immutables that are assigned at their declaration are only considered initialized once the constructor of the contract is executing. This means you cannot initialize immutables inline with a value that depends on another immutable. You can do this, however, inside the constructor of the contract.
This is a safeguard against different interpretations about the order of state variable initialization and constructor execution, especially with regards to inheritance.
Fonctions
Functions can be defined inside and outside of contracts.
Functions outside of a contract, also called « free functions », always have implicit internal
visibility. Their code is included in all contracts
that call them, similar to internal library functions.
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.1 <0.9.0;
function sum(uint[] memory _arr) pure returns (uint s) {
for (uint i = 0; i < _arr.length; i++)
s += _arr[i];
}
contract ArrayExample {
bool found;
function f(uint[] memory _arr) public {
// This calls the free function internally.
// The compiler will add its code to the contract.
uint s = sum(_arr);
require(s >= 10);
found = true;
}
}
Note
Functions defined outside a contract are still always executed
in the context of a contract. They still have access to the variable this
,
can call other contracts, send them Ether and destroy the contract that called them,
among other things. The main difference to functions defined inside a contract
is that free functions do not have direct access to storage variables and functions
not in their scope.
Function Parameters and Return Variables
Functions take typed parameters as input and may, unlike in many other languages, also return an arbitrary number of values as output.
Function Parameters
Function parameters are declared the same way as variables, and the name of unused parameters can be omitted.
For example, if you want your contract to accept one kind of external call with two integers, you would use something like the following:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.16 <0.9.0;
contract Simple {
uint sum;
function taker(uint _a, uint _b) public {
sum = _a + _b;
}
}
Function parameters can be used as any other local variable and they can also be assigned to.
Note
An external function cannot accept a
multi-dimensional array as an input
parameter. This functionality is possible if you enable the ABI coder v2
by adding pragma abicoder v2;
to your source file.
An internal function can accept a multi-dimensional array without enabling the feature.
Return Variables
Function return variables are declared with the same syntax after the
returns
keyword.
For example, suppose you want to return two results: the sum and the product of two integers passed as function parameters, then you use something like:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.16 <0.9.0;
contract Simple {
function arithmetic(uint _a, uint _b)
public
pure
returns (uint o_sum, uint o_product)
{
o_sum = _a + _b;
o_product = _a * _b;
}
}
The names of return variables can be omitted. Return variables can be used as any other local variable and they are initialized with their default value and have that value until they are (re-)assigned.
You can either explicitly assign to return variables and
then leave the function as above,
or you can provide return values
(either a single or multiple ones) directly with the return
statement:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.16 <0.9.0;
contract Simple {
function arithmetic(uint _a, uint _b)
public
pure
returns (uint o_sum, uint o_product)
{
return (_a + _b, _a * _b);
}
}
If you use an early return
to leave a function that has return variables,
you must provide return values together with the return statement.
Note
You cannot return some types from non-internal functions, notably
multi-dimensional dynamic arrays and structs. If you enable the
ABI coder v2 by adding pragma abicoder v2;
to your source file then more types are available, but
mapping
types are still limited to inside a single contract and you
cannot transfer them.
Returning Multiple Values
When a function has multiple return types, the statement return (v0, v1, ..., vn)
can be used to return multiple values.
The number of components must be the same as the number of return variables
and their types have to match, potentially after an implicit conversion.
State Mutability
Fonctions View
Les fonctions peuvent être déclarées view
, auquel cas elles promettent de ne pas modifier l’état.
Note
Si la cible EVM du compilateur est Byzantium ou plus récent (par défaut), l’opcode STATICCALL
est utilisé pour les fonctions view
qui imposent à l’état de rester non modifié lors de l’exécution EVM. Pour les librairies, on utilise les fonctions view
et DELEGATECALL
parce qu’il n’y a pas de DELEGATECALL
et STATICCALL
combinés.
Cela signifie que les fonctions view
de librairies n’ont pas de contrôles d’exécution qui empêchent les modifications d’état. Cela ne devrait pas avoir d’impact négatif sur la sécurité car le code de librairies est généralement connu au moment de la compilation et le vérificateur statique effectue les vérifications au moment de la compilation.
Les déclarations suivantes sont considérées comme une modification de l’état :
Ecrire dans les variables d’état.
Utiliser
selfdestruct
.Envoyer des Ethers par des appels.
Appeler une fonction qui n’est pas marquée
view
oupure
.Utilisation d’appels bas niveau.
Utilisation d’assembleur inline qui contient certains opcodes.
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.5.0 <0.9.0;
contract C {
function f(uint a, uint b) public view returns (uint) {
return a * (b + 42) + block.timestamp;
}
}
Note
constant
sur les fonctions était un alias de view
, mais cela a été abandonné dans la version 0.5.0.
Note
Les méthodes Getter sont automatiquement marquées view
.
Note
Avant la version 0.5.0, le compilateur n’utilisait pas l’opcode STATICCALL
.
pour les fonctions view
.
Cela permettait de modifier l’état des fonctions view
grâce à l’utilisation de
conversions de type explicites non valides.
En utilisant STATICCALL
pour les fonctions view
, les modifications de la fonction
sont évités au niveau de l’EVM.
Fonctions Pure
Les fonctions peuvent être déclarées pures
, auquel cas elles promettent de ne pas lire ou modifier l’état.
In particular, it should be possible to evaluate a pure
function at compile-time given
only its inputs and msg.data
, but without any knowledge of the current blockchain state.
This means that reading from immutable
variables can be a non-pure operation.
Note
Si la cible EVM du compilateur est Byzantium ou plus récente (par défaut), on utilise l’opcode STATICCALL
, ce qui ne garantit pas que l’état ne soit pas lu, mais au moins qu’il ne soit pas modifié.
En plus de la liste des modificateurs d’état expliqués ci-dessus, sont considérés comme des lectures de l’état :
Lecture des variables d’état.
Accéder à
address(this).balance
ou<address>.balance
.Accéder à l’un des membres de
block
,tx
,msg
(à l’exception demsg.sig
etmsg.data
).Appeler une fonction qui n’est pas marquée
pure
.Utilisation d’assembleur inline qui contient certains opcodes.
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.5.0 <0.9.0;
contract C {
function f(uint a, uint b) public pure returns (uint) {
return a * (b + 42);
}
}
Note
Avant la version 0.5.0, le compilateur n’utilisait pas l’opcode STATICCALL
pour les fonctions pure
.
Cela permettait de modifier l’état des fonctions pures
en utilisant des conversions de type explicites invalides.
En utilisant STATICCALL
pour des fonctions pures
, les modifications de l’état sont empêchées au niveau de l’EVM.
Avertissement
Il n’est pas possible d’empêcher les fonctions de lire l’état au niveau de l’EVM, il est seulement possible de les empêcher d’écrire dans l’état (c’est-à-dire que seul « view » peut être exécuté au niveau de l’EVM, pure
ne peut pas).
Avertissement
Avant la version 0.4.17, le compilateur n’appliquait pas le fait que pure
ne lisait pas l’état.
Il s’agit d’un contrôle de type à la compilation, qui peut être contourné en effectuant des conversions explicites invalides entre les types de contrats, parce que le compilateur peut vérifier que le type de contrat ne fait pas d’opérations de changement d’état, mais il ne peut pas vérifier que le contrat qui sera appelé à l’exécution est effectivement de ce type.
Special Functions
Receive Ether Function
A contract can have at most one receive
function, declared using
receive() external payable { ... }
(without the function
keyword).
This function cannot have arguments, cannot return anything and must have
external
visibility and payable
state mutability.
It can be virtual, can override and can have modifiers.
The receive function is executed on a
call to the contract with empty calldata. This is the function that is executed
on plain Ether transfers (e.g. via .send()
or .transfer()
). If no such
function exists, but a payable fallback function
exists, the fallback function will be called on a plain Ether transfer. If
neither a receive Ether nor a payable fallback function is present, the
contract cannot receive Ether through regular transactions and throws an
exception.
In the worst case, the receive
function can only rely on 2300 gas being
available (for example when send
or transfer
is used), leaving little
room to perform other operations except basic logging. The following operations
will consume more gas than the 2300 gas stipend:
Writing to storage
Creating a contract
Calling an external function which consumes a large amount of gas
Sending Ether
Avertissement
Contracts that receive Ether directly (without a function call, i.e. using send
or transfer
)
but do not define a receive Ether function or a payable fallback function
throw an exception, sending back the Ether (this was different
before Solidity v0.4.0). So if you want your contract to receive Ether,
you have to implement a receive Ether function (using payable fallback functions for receiving Ether is
not recommended, since it would not fail on interface confusions).
Avertissement
A contract without a receive Ether function can receive Ether as a
recipient of a coinbase transaction (aka miner block reward)
or as a destination of a selfdestruct
.
A contract cannot react to such Ether transfers and thus also cannot reject them. This is a design choice of the EVM and Solidity cannot work around it.
It also means that address(this).balance
can be higher
than the sum of some manual accounting implemented in a
contract (i.e. having a counter updated in the receive Ether function).
Below you can see an example of a Sink contract that uses function receive
.
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.6.0 <0.9.0;
// This contract keeps all Ether sent to it with no way
// to get it back.
contract Sink {
event Received(address, uint);
receive() external payable {
emit Received(msg.sender, msg.value);
}
}
Fonction de repli
Un contrat peut avoir exactement une fonction dwe repli fallback
, declared using either fallback () external [payable]
or fallback (bytes calldata _input) external [payable] returns (bytes memory _output)
(both without the function
keyword).
This function must have external
visibility. A fallback function can be virtual, can override
and can have modifiers.
The fallback function is executed on a call to the contract if none of the other
functions match the given function signature, or if no data was supplied at
all and there is no receive Ether function.
The fallback function always receives data, but in order to also receive Ether
it must be marked payable
.
If the version with parameters is used, _input
will contain the full data sent to the contract
(equal to msg.data
) and can return data in _output
. The returned data will not be
ABI-encoded. Instead it will be returned without modifications (not even padding).
In the worst case, if a payable fallback function is also used in place of a receive function, it can only rely on 2300 gas being available (see receive Ether function for a brief description of the implications of this).
Comme toute fonction, la fonction de fallback peut exécuter des opérations complexes tant que suffisamment de gas lui est transmis.
Note
If you want to decode the input data, you can check the first four bytes
for the function selector and then
you can use abi.decode
together with the array slice syntax to
decode ABI-encoded data:
(c, d) = abi.decode(_input[4:], (uint256, uint256));
Note that this should only be used as a last resort and
proper functions should be used instead.
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.6.2 <0.9.0;
contract Test {
uint x;
// This function is called for all messages sent to
// this contract (there is no other function).
// Sending Ether to this contract will cause an exception,
// because the fallback function does not have the `payable`
// modifier.
fallback() external { x = 1; }
}
contract TestPayable {
uint x;
uint y;
// This function is called for all messages sent to
// this contract, except plain Ether transfers
// (there is no other function except the receive function).
// Any call with non-empty calldata to this contract will execute
// the fallback function (even if Ether is sent along with the call).
fallback() external payable { x = 1; y = msg.value; }
// This function is called for plain Ether transfers, i.e.
// for every call with empty calldata.
receive() external payable { x = 2; y = msg.value; }
}
contract Caller {
function callTest(Test test) public returns (bool) {
(bool success,) = address(test).call(abi.encodeWithSignature("nonExistingFunction()"));
require(success);
// results in test.x becoming == 1.
// address(test) will not allow to call ``send`` directly, since ``test`` has no payable
// fallback function.
// It has to be converted to the ``address payable`` type to even allow calling ``send`` on it.
address payable testPayable = payable(address(test));
// If someone sends Ether to that contract,
// the transfer will fail, i.e. this returns false here.
return testPayable.send(2 ether);
}
function callTestPayable(TestPayable test) public returns (bool) {
(bool success,) = address(test).call(abi.encodeWithSignature("nonExistingFunction()"));
require(success);
// results in test.x becoming == 1 and test.y becoming 0.
(success,) = address(test).call{value: 1}(abi.encodeWithSignature("nonExistingFunction()"));
require(success);
// results in test.x becoming == 1 and test.y becoming 1.
// If someone sends Ether to that contract, the receive function in TestPayable will be called.
// Since that function writes to storage, it takes more gas than is available with a
// simple ``send`` or ``transfer``. Because of that, we have to use a low-level call.
(success,) = address(test).call{value: 2 ether}("");
require(success);
// results in test.x becoming == 2 and test.y becoming 2 ether.
return true;
}
}
Surcharge de fonctions
Un contrat peut avoir plusieurs fonctions du même nom, mais avec des types de paramètres différents.
Ce processus est appelé « surcharge » et s’applique également aux fonctions héritées.
L’exemple suivant montre la surcharge de la fonction f
dans le champ d’application du contrat A
.
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.16 <0.9.0;
contract A {
function f(uint _in) public pure returns (uint out) {
out = _in;
}
function f(uint _in, bool _really) public pure returns (int out) {
if (_really)
out = int(_in);
}
}
Des fonctions surchargées sont également présentes dans l’interface externe. C’est une erreur si deux fonctions visibles de l’extérieur diffèrent par leur type Solidity (ici A et B) mais pas par leur type extérieur (ici address
).
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.16 <0.9.0;
// Ceci ne compile pas
contract A {
function f(B _in) public pure returns (B out) {
out = _in;
}
function f(A _in) public pure returns (B out) {
out = B(address(_in));
}
}
contract B {
}
Les deux fonctions f
surchargées ci-dessus acceptent des addresses du point de vue de l’ABI, mais ces adresses sont considérées comme différents types en Solidity.
Résolution des surcharges et concordance des arguments
Les fonctions surchargées sont sélectionnées en faisant correspondre les déclarations de fonction dans le scope actuel aux arguments fournis dans l’appel de fonction. La fonction évaluée est choisie si tous les arguments peuvent être implicitement convertis en types attendus. S’il y a plusieurs fonctions correspondantes, la résolution échoue.
Note
Le type des valeurs retournées par la fonction n’est pas pris en compte dans la résolution des surcharges.
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.16 <0.9.0;
contract A {
function f(uint8 _in) public pure returns (uint8 out) {
out = _in;
}
function f(uint256 _in) public pure returns (uint256 out) {
out = _in;
}
}
L’appel de f(50)
créerait une erreur de type puisque 50
peut être implicitement converti à la fois en type uint8
et uint256
. D’un autre côté, f(256)
se résoudrait à f(uint256)
car 256
ne peut pas être implicitement converti en uint8
.
Événements
Les événements Solidity autorisent une abstraction en plus de la fonctionnalité de journalisation de l’EVM. Les applications peuvent souscrire à et écouter ces événements via l’interface RPC d’un client Ethereum.
Les événements sont des membres héritables des contrats. Lorsque vous les appelez, ils font en sorte que les arguments soient stockés dans le journal des transactions - une structure de données spéciale dans la blockchain. Ces logs sont associés à l’adresse du contrat, sont incorporés dans la blockchain et y restent tant qu’un bloc est accessible (pour toujours à partir des versions Frontier et Homestead, mais cela peut changer avec Serenity). Le journal et ses données d’événement ne sont pas accessibles depuis les contrats (pas même depuis le contrat qui les a créés).
Il est possible de demander une simple vérification de paiement (SPV) pour les logs, de sorte que si une entité externe fournit un contrat avec une telle vérification, elle peut vérifier que le log existe réellement dans la blockchain. Vous devez fournir des en-têtes (headers) de bloc car le contrat ne peut voir que les 256 derniers hashs de blocs.
Vous pouvez ajouter l’attribut indexed
à un maximum de trois paramètres qui les ajoute à une structure de données spéciale appelée « topics » au lieu de la partie data du log.
Si vous utilisez des types « référence » (tableaux, y compris les string
et bytes
)
comme arguments indexés, leurs hashs Keccak-256 sont stockés comme topic à la place, car un topic ne peut contenir qu’un seul mot (32 octets).
Tous les paramètres sans l’attribut indexed
sont ABI-encoded dans la partie données du log.
Les topics vous permettent de rechercher des événements, par exemple lors du filtrage d’une séquence de blocs pour certains événements. Vous pouvez également filtrer les événements par l’adresse du contrat qui les a émis.
Par exemple, le code ci-dessous utilise web3.js subscribe("logs")
method pour filtrer les logs qui correspondent à un sujet avec une certaine valeur d’adresse :
var options = {
fromBlock: 0,
address: web3.eth.defaultAccount,
topics: ["0x0000000000000000000000000000000000000000000000000000000000000000", null, null]
};
web3.eth.subscribe('logs', options, function (error, result) {
if (!error)
console.log(result);
})
.on("data", function (log) {
console.log(log);
})
.on("changed", function (log) {
});
Le hash de la signature de l’event est l’un des topics, sauf si vous avez déclaré l’événement avec le spécificateur anonymous
.
Cela signifie qu’il n’est pas possible de filtrer des événements anonymes spécifiques par leur nom, you can
only filter by the contract address. The advantage of anonymous events
is that they are cheaper to deploy and call. It also allows you to declare
four indexed arguments rather than three.
Note
Since the transaction log only stores the event data and not the type, you have to know the type of the event, including which parameter is indexed and if the event is anonymous in order to correctly interpret the data. In particular, it is possible to « fake » the signature of another event using an anonymous event.
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.21 <0.9.0;
contract ClientReceipt {
event Deposit(
address indexed _from,
bytes32 indexed _id,
uint _value
);
function deposit(bytes32 _id) public payable {
// Les événements sont émis à l'aide de `emit`, suivi du
// nom de l'événement et des arguments
// (le cas échéant) entre parenthèses. Une telle invocation
// (même profondément imbriquée) peut être détectée à partir de
// l'API JavaScript en filtrant `Deposit`.
emit Deposit(msg.sender, _id, msg.value);
}
}
L’utilisation dans l’API JavaScript est la suivante :
var abi = /* abi telle que génerée par le compilateur */;
var ClientReceipt = web3.eth.contract(abi);
var clientReceipt = ClientReceipt.at("0x1234...ab67" /* adresse */);
var depositEvent = clientReceipt.Deposit();
// inspecter les eventuels changements
depositEvent.watch(function(error, result){
// le résultat contient des arguments et topics non indexés
// passées à l'appel de `Deposit`.
if (!error)
console.log(result);
});
// Ou passez une fonction pour ecouter dès maintenant
var depositEvent = clientReceipt.Deposit(function(error, result) {
if (!error)
console.log(result);
});
La sortie du code ci-dessus ressemble à (trimmée):
{
"returnValues": {
"_from": "0x1111…FFFFCCCC",
"_id": "0x50…sd5adb20",
"_value": "0x420042"
},
"raw": {
"data": "0x7f…91385",
"topics": ["0xfd4…b4ead7", "0x7f…1a91385"]
}
}
Additional Resources for Understanding Events
Errors and the Revert Statement
Errors in Solidity provide a convenient and gas-efficient way to explain to the user why an operation failed. They can be defined inside and outside of contracts (including interfaces and libraries).
They have to be used together with the revert statement which causes all changes in the current call to be reverted and passes the error data back to the caller.
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.4;
/// Insufficient balance for transfer. Needed `required` but only
/// `available` available.
/// @param available balance available.
/// @param required requested amount to transfer.
error InsufficientBalance(uint256 available, uint256 required);
contract TestToken {
mapping(address => uint) balance;
function transfer(address to, uint256 amount) public {
if (amount > balance[msg.sender])
revert InsufficientBalance({
available: balance[msg.sender],
required: amount
});
balance[msg.sender] -= amount;
balance[to] += amount;
}
// ...
}
Errors cannot be overloaded or overridden but are inherited.
The same error can be defined in multiple places as long as the scopes are distinct.
Instances of errors can only be created using revert
statements.
The error creates data that is then passed to the caller with the revert operation to either return to the off-chain component or catch it in a try/catch statement. Note that an error can only be caught when coming from an external call, reverts happening in internal calls or inside the same function cannot be caught.
If you do not provide any parameters, the error only needs four bytes of data and you can use NatSpec as above to further explain the reasons behind the error, which is not stored on chain. This makes this a very cheap and convenient error-reporting feature at the same time.
More specifically, an error instance is ABI-encoded in the same way as
a function call to a function of the same name and types would be
and then used as the return data in the revert
opcode.
This means that the data consists of a 4-byte selector followed by ABI-encoded data.
The selector consists of the first four bytes of the keccak256-hash of the signature of the error type.
Note
It is possible for a contract to revert with different errors of the same name or even with errors defined in different places that are indistinguishable by the caller. For the outside, i.e. the ABI, only the name of the error is relevant, not the contract or file where it is defined.
The statement require(condition, "description");
would be equivalent to
if (!condition) revert Error("description")
if you could define
error Error(string)
.
Note, however, that Error
is a built-in type and cannot be defined in user-supplied code.
Similarly, a failing assert
or similar conditions will revert with an error
of the built-in type Panic(uint256)
.
Note
Error data should only be used to give an indication of failure, but not as a means for control-flow. The reason is that the revert data of inner calls is propagated back through the chain of external calls by default. This means that an inner call can « forge » revert data that looks like it could have come from the contract that called it.
Héritage
Solidity supporte l’héritage multiple en copiant du code, incluant le polymorphisme.
Polymorphism means that a function call (internal and external)
always executes the function of the same name (and parameter types)
in the most derived contract in the inheritance hierarchy.
This has to be explicitly enabled on each function in the
hierarchy using the virtual
and override
keywords.
See Function Overriding for more details.
It is possible to call functions further up in the inheritance
hierarchy internally by explicitly specifying the contract
using ContractName.functionName()
or using super.functionName()
if you want to call the function one level higher up in
the flattened inheritance hierarchy (see below).
Lorsqu’un contrat hérite d’autres contrats, un seul contrat
est créé dans la blockchain et le code de tous les contrats de base
est copié dans le contrat créé. This means that all internal calls
to functions of base contracts also just use internal function calls
(super.f(..)
will use JUMP and not a message call).
State variable shadowing is considered as an error. A derived contract can
only declare a state variable x
, if there is no visible state variable
with the same name in any of its bases.
Le système général d’héritage est très similaire à celui de Python, surtout en ce qui concerne l’héritage multiple, mais il y a aussi quelques differences.
Les détails sont donnés dans l’exemple suivant.
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
contract Owned {
constructor() { owner = payable(msg.sender); }
address payable owner;
}
// Utilisez `is` pour dériver d'un autre contrat. Les contrats dérivés peuvent accéder à tous les membres non privés, y compris les fonctions internes et les variables d'état. Il n'est cependant pas possible d'y accéder de l'extérieur via `this`.
contract Destructible is Owned {
// The keyword `virtual` means that the function can change
// its behaviour in derived classes ("overriding").
function destroy() virtual public {
if (msg.sender == owner) selfdestruct(owner);
}
}
// Ces contrats abstraits ne sont fournis que pour faire connaître l'interface au compilateur. Notez la fonction sans corps. Si un contrat n'implémente pas toutes les fonctions, il ne peut être utilisé que comme interface.
abstract contract Config {
function lookup(uint id) public virtual returns (address adr);
}
abstract contract NameReg {
function register(bytes32 name) public virtual;
function unregister() public virtual;
}
// L'héritage multiple est possible. Notez que `owned` est aussi une classe de base de `mortal`, pourtant il n'y a qu'une seule instance de `owned` (comme pour l'héritage virtuel en C++).
contract Named is Owned, Destructible {
constructor(bytes32 name) {
Config config = Config(0xD5f9D8D94886E70b06E474c3fB14Fd43E2f23970);
NameReg(config.lookup(1)).register(name);
}
// Les fonctions peuvent être remplacées par une autre fonction ayant le même nom et le même nombre/type d'entrées. Si la fonction de surcharge a différents types de paramètres de sortie, cela provoque une erreur.
// Les appels de fonction locaux et les appels de fonction basés sur la messagerie tiennent compte de ces dérogations.
// If you want the function to override, you need to use the
// `override` keyword. You need to specify the `virtual` keyword again
// if you want this function to be overridden again.
function destroy() public virtual override {
if (msg.sender == owner) {
Config config = Config(0xD5f9D8D94886E70b06E474c3fB14Fd43E2f23970);
NameReg(config.lookup(1)).unregister();
// It is still possible to call a specific
// overridden function.
Destructible.destroy();
}
}
}
// Si un constructeur prend un argument, il doit être fourni dans l'en-tête (ou dans le constructeur du contrat dérivé (voir ci-dessous)).
contract PriceFeed is Owned, Destructible, Named("GoldFeed") {
function updateInfo(uint newInfo) public {
if (msg.sender == owner) info = newInfo;
}
// Here, we only specify `override` and not `virtual`.
// This means that contracts deriving from `PriceFeed`
// cannot change the behaviour of `destroy` anymore.
function destroy() public override(Destructible, Named) { Named.destroy(); }
function get() public view returns(uint r) { return info; }
uint info;
}
Notez que ci-dessus, nous appelons Destructible.destroy()
pour « transmettre » la demande de destruction. La façon dont cela est fait est problématique, comme vu dans l’exemple suivant:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
contract owned {
constructor() { owner = payable(msg.sender); }
address payable owner;
}
contract Destructible is owned {
function destroy() public virtual {
if (msg.sender == owner) selfdestruct(owner);
}
}
contract Base1 is Destructible {
function destroy() public virtual override { /* do cleanup 1 */ Destructible.destroy(); }
}
contract Base2 is Destructible {
function destroy() public virtual override { /* do cleanup 2 */ Destructible.destroy(); }
}
contract Final is Base1, Base2 {
function destroy() public override(Base1, Base2) { Base2.destroy(); }
}
Un appel à Final.destroy()
appellera Base2.destroy
puisque nous le demandons explicitement dans l’override, mais cet appel évitera
Base1.destroy
. La solution à ce problème est d’utiliser super
:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
contract owned {
constructor() { owner = payable(msg.sender); }
address payable owner;
}
contract Destructible is owned {
function destroy() virtual public {
if (msg.sender == owner) selfdestruct(owner);
}
}
contract Base1 is Destructible {
function destroy() public virtual override { /* do cleanup 1 */ super.destroy(); }
}
contract Base2 is Destructible {
function destroy() public virtual override { /* do cleanup 2 */ super.destroy(); }
}
contract Final is Base1, Base2 {
function destroy() public override(Base1, Base2) { super.destroy(); }
}
Si Base2
appelle une fonction de super
, elle n’appelle pas simplement cette fonction sur un de ses contrats de base. Elle appelle plutôt cette fonction sur le prochain contrat de base dans le graph d’héritage final, donc elle appellera Base1.destroy()
(notez que la séquence d’héritage finale est – en commençant par le contrat le plus dérivé : Final, Base2, Base1, Destructible, owned).
La fonction réelle qui est appelée lors de l’utilisation de super n’est pas connue dans le contexte de la classe où elle est utilisée, bien que son type soit connu. Il en va de même pour la recherche de méthodes virtuelles ordinaires.
Function Overriding
Base functions can be overridden by inheriting contracts to change their
behavior if they are marked as virtual
. The overriding function must then
use the override
keyword in the function header.
The overriding function may only change the visibility of the overridden function from external
to public
.
The mutability may be changed to a more strict one following the order:
nonpayable
can be overridden by view
and pure
. view
can be overridden by pure
.
payable
is an exception and cannot be changed to any other mutability.
The following example demonstrates changing mutability and visibility:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
contract Base
{
function foo() virtual external view {}
}
contract Middle is Base {}
contract Inherited is Middle
{
function foo() override public pure {}
}
For multiple inheritance, the most derived base contracts that define the same
function must be specified explicitly after the override
keyword.
In other words, you have to specify all base contracts that define the same function
and have not yet been overridden by another base contract (on some path through the inheritance graph).
Additionally, if a contract inherits the same function from multiple (unrelated)
bases, it has to explicitly override it:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.6.0 <0.9.0;
contract Base1
{
function foo() virtual public {}
}
contract Base2
{
function foo() virtual public {}
}
contract Inherited is Base1, Base2
{
// Derives from multiple bases defining foo(), so we must explicitly
// override it
function foo() public override(Base1, Base2) {}
}
An explicit override specifier is not required if the function is defined in a common base contract or if there is a unique function in a common base contract that already overrides all other functions.
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.6.0 <0.9.0;
contract A { function f() public pure{} }
contract B is A {}
contract C is A {}
// No explicit override required
contract D is B, C {}
More formally, it is not required to override a function (directly or indirectly) inherited from multiple bases if there is a base contract that is part of all override paths for the signature, and (1) that base implements the function and no paths from the current contract to the base mentions a function with that signature or (2) that base does not implement the function and there is at most one mention of the function in all paths from the current contract to that base.
In this sense, an override path for a signature is a path through the inheritance graph that starts at the contract under consideration and ends at a contract mentioning a function with that signature that does not override.
If you do not mark a function that overrides as virtual
, derived
contracts can no longer change the behaviour of that function.
Note
Functions with the private
visibility cannot be virtual
.
Note
Functions without implementation have to be marked virtual
outside of interfaces. In interfaces, all functions are
automatically considered virtual
.
Note
Starting from Solidity 0.8.8, the override
keyword is not
required when overriding an interface function, except for the
case where the function is defined in multiple bases.
Public state variables can override external functions if the parameter and return types of the function matches the getter function of the variable:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.6.0 <0.9.0;
contract A
{
function f() external view virtual returns(uint) { return 5; }
}
contract B is A
{
uint public override f;
}
Note
While public state variables can override external functions, they themselves cannot be overridden.
Modifier Overriding
Function modifiers can override each other. This works in the same way as
function overriding (except that there is no overloading for modifiers). The
virtual
keyword must be used on the overridden modifier
and the override
keyword must be used in the overriding modifier:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.6.0 <0.9.0;
contract Base
{
modifier foo() virtual {_;}
}
contract Inherited is Base
{
modifier foo() override {_;}
}
In case of multiple inheritance, all direct base contracts must be specified explicitly:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.6.0 <0.9.0;
contract Base1
{
modifier foo() virtual {_;}
}
contract Base2
{
modifier foo() virtual {_;}
}
contract Inherited is Base1, Base2
{
modifier foo() override(Base1, Base2) {_;}
}
Constructeurs
Un constructeur est une fonction optionnelle déclarée avec le mot-clé constructeur
qui est exécuté lors de la création du contrat, et où vous pouvez exécuter le code d’initialisation du contrat.
Avant l’exécution du code constructeur, les variables d’état sont initialisées à leur valeur spécifiée si vous les initialisez en ligne, ou leur default value si vous ne le faites pas.
Après l’exécution du constructeur, le code final du contrat est déployé dans la chaîne de blocs. Le déploiement du code coûte du gas supplémentaire linéairement à la longueur du code. Ce code inclut toutes les fonctions qui font partie de l’interface publique et toutes les fonctions qui sont accessibles à partir de là par des appels de fonctions. Il n’inclut pas le code constructeur ni les fonctions internes qui ne sont appelées que par le constructeur.
S’il n’y a pas de constructeur, le contrat assumera le constructeur par défaut, ce qui est équivalent à constructor() {}
. Par exemple :
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
abstract contract A {
uint public a;
constructor(uint _a) {
a = _a;
}
}
contract B is A(1) {
constructor() {}
}
You can use internal parameters in a constructor (for example storage pointers). In this case, the contract has to be marked abstract, because these parameters cannot be assigned valid values from outside but only through the constructors of derived contracts.
Attention
Avant 0.4.22, ont été définis comme des fonctions portant le même nom que le contrat. Cette syntaxe a été dépréciée et n’est plus autorisée dans la version 0.5.0.
Avertissement
Prior to version 0.7.0, you had to specify the visibility of constructors as either
internal
or public
.
Arguments des Constructeurs de Base
Les constructeurs de tous les contrats de base seront appelés selon les règles de linéarisation expliquées ci-dessous. Si les constructeurs de base ont des arguments, les contrats dérivés doivent les spécifier tous. Cela peut se faire de deux façons:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
contract Base {
uint x;
constructor(uint _x) { x = _x; }
}
// Either directly specify in the inheritance list...
contract Derived1 is Base(7) {
constructor() {}
}
// or through a "modifier" of the derived constructor.
contract Derived2 is Base {
constructor(uint _y) Base(_y * _y) {}
}
One way is directly in the inheritance list (is Base(7)
). The other is in
the way a modifier is invoked as part of
the derived constructor (Base(_y * _y)
). The first way to
do it is more convenient if the constructor argument is a
constant and defines the behaviour of the contract or
describes it. The second way has to be used if the
constructor arguments of the base depend on those of the
derived contract. Arguments have to be given either in the
inheritance list or in modifier-style in the derived constructor.
Specifying arguments in both places is an error.
If a derived contract does not specify the arguments to all of its base contracts” constructors, it will be abstract.
Multiple Inheritance and Linearization
Languages that allow multiple inheritance have to deal with
several problems. One is the Diamond Problem.
Solidity is similar to Python in that it uses « C3 Linearization »
to force a specific order in the directed acyclic graph (DAG) of base classes. This
results in the desirable property of monotonicity but
disallows some inheritance graphs. Especially, the order in
which the base classes are given in the is
directive is
important: You have to list the direct base contracts
in the order from « most base-like » to « most derived ».
Note that this order is the reverse of the one used in Python.
Another simplifying way to explain this is that when a function is called that is defined multiple times in different contracts, the given bases are searched from right to left (left to right in Python) in a depth-first manner, stopping at the first match. If a base contract has already been searched, it is skipped.
In the following code, Solidity will give the error « Linearization of inheritance graph impossible ».
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.0 <0.9.0;
contract X {}
contract A is X {}
// This will not compile
contract C is A, X {}
The reason for this is that C
requests X
to override A
(by specifying A, X
in this order), but A
itself
requests to override X
, which is a contradiction that
cannot be resolved.
Due to the fact that you have to explicitly override a function that is inherited from multiple bases without a unique override, C3 linearization is not too important in practice.
One area where inheritance linearization is especially important and perhaps not as clear is when there are multiple constructors in the inheritance hierarchy. The constructors will always be executed in the linearized order, regardless of the order in which their arguments are provided in the inheriting contract’s constructor. For example:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
contract Base1 {
constructor() {}
}
contract Base2 {
constructor() {}
}
// Constructors are executed in the following order:
// 1 - Base1
// 2 - Base2
// 3 - Derived1
contract Derived1 is Base1, Base2 {
constructor() Base1() Base2() {}
}
// Constructors are executed in the following order:
// 1 - Base2
// 2 - Base1
// 3 - Derived2
contract Derived2 is Base2, Base1 {
constructor() Base2() Base1() {}
}
// Constructors are still executed in the following order:
// 1 - Base2
// 2 - Base1
// 3 - Derived3
contract Derived3 is Base2, Base1 {
constructor() Base1() Base2() {}
}
Inheriting Different Kinds of Members of the Same Name
- It is an error when any of the following pairs in a contract have the same name due to inheritance:
a function and a modifier
a function and an event
an event and a modifier
As an exception, a state variable getter can override an external function.
Abstract Contracts
Contracts need to be marked as abstract when at least one of their functions is not implemented. Contracts may be marked as abstract even though all functions are implemented.
This can be done by using the abstract
keyword as shown in the following example. Note that this contract needs to be
defined as abstract, because the function utterance()
was defined, but no implementation was
provided (no implementation body { }
was given).
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.6.0 <0.9.0;
abstract contract Feline {
function utterance() public virtual returns (bytes32);
}
Such abstract contracts can not be instantiated directly. This is also true, if an abstract contract itself does implement all defined functions. The usage of an abstract contract as a base class is shown in the following example:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.6.0 <0.9.0;
abstract contract Feline {
function utterance() public pure virtual returns (bytes32);
}
contract Cat is Feline {
function utterance() public pure override returns (bytes32) { return "miaow"; }
}
If a contract inherits from an abstract contract and does not implement all non-implemented functions by overriding, it needs to be marked as abstract as well.
Note that a function without implementation is different from a Function Type even though their syntax looks very similar.
Example of function without implementation (a function declaration):
function foo(address) external returns (address);
Example of a declaration of a variable whose type is a function type:
function(address) external returns (address) foo;
Abstract contracts decouple the definition of a contract from its implementation providing better extensibility and self-documentation and facilitating patterns like the Template method and removing code duplication. Abstract contracts are useful in the same way that defining methods in an interface is useful. It is a way for the designer of the abstract contract to say « any child of mine must implement this method ».
Note
Abstract contracts cannot override an implemented virtual function with an unimplemented one.
Interfaces
Interfaces are similar to abstract contracts, but they cannot have any functions implemented. There are further restrictions:
They cannot inherit from other contracts, but they can inherit from other interfaces.
All declared functions must be external.
They cannot declare a constructor.
They cannot declare state variables.
They cannot declare modifiers.
Some of these restrictions might be lifted in the future.
Interfaces are basically limited to what the Contract ABI can represent, and the conversion between the ABI and an interface should be possible without any information loss.
Interfaces are denoted by their own keyword:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.6.2 <0.9.0;
interface Token {
enum TokenType { Fungible, NonFungible }
struct Coin { string obverse; string reverse; }
function transfer(address recipient, uint amount) external;
}
Contracts can inherit interfaces as they would inherit other contracts.
All functions declared in interfaces are implicitly virtual
and any
functions that override them do not need the override
keyword.
This does not automatically mean that an overriding function can be overridden again -
this is only possible if the overriding function is marked virtual
.
Interfaces can inherit from other interfaces. This has the same rules as normal inheritance.
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.6.2 <0.9.0;
interface ParentA {
function test() external returns (uint256);
}
interface ParentB {
function test() external returns (uint256);
}
interface SubInterface is ParentA, ParentB {
// Must redefine test in order to assert that the parent
// meanings are compatible.
function test() external override(ParentA, ParentB) returns (uint256);
}
Types defined inside interfaces and other contract-like structures
can be accessed from other contracts: Token.TokenType
or Token.Coin
.
Libraries
Libraries are similar to contracts, but their purpose is that they are deployed
only once at a specific address and their code is reused using the DELEGATECALL
(CALLCODE
until Homestead)
feature of the EVM. This means that if library functions are called, their code
is executed in the context of the calling contract, i.e. this
points to the
calling contract, and especially the storage from the calling contract can be
accessed. As a library is an isolated piece of source code, it can only access
state variables of the calling contract if they are explicitly supplied (it
would have no way to name them, otherwise). Library functions can only be
called directly (i.e. without the use of DELEGATECALL
) if they do not modify
the state (i.e. if they are view
or pure
functions),
because libraries are assumed to be stateless. In particular, it is
not possible to destroy a library.
Note
Until version 0.4.20, it was possible to destroy libraries by
circumventing Solidity’s type system. Starting from that version,
libraries contain a mechanism that
disallows state-modifying functions
to be called directly (i.e. without DELEGATECALL
).
Libraries can be seen as implicit base contracts of the contracts that use them.
They will not be explicitly visible in the inheritance hierarchy, but calls
to library functions look just like calls to functions of explicit base
contracts (using qualified access like L.f()
).
Of course, calls to internal functions
use the internal calling convention, which means that all internal types
can be passed and types stored in memory will be passed by reference and not copied.
To realize this in the EVM, the code of internal library functions
that are called from a contract
and all functions called from therein will at compile time be included in the calling
contract, and a regular JUMP
call will be used instead of a DELEGATECALL
.
Note
The inheritance analogy breaks down when it comes to public functions.
Calling a public library function with L.f()
results in an external call (DELEGATECALL
to be precise).
In contrast, A.f()
is an internal call when A
is a base contract of the current contract.
The following example illustrates how to use libraries (but using a manual method, be sure to check out using for for a more advanced example to implement a set).
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.6.0 <0.9.0;
// We define a new struct datatype that will be used to
// hold its data in the calling contract.
struct Data {
mapping(uint => bool) flags;
}
library Set {
// Note that the first parameter is of type "storage
// reference" and thus only its storage address and not
// its contents is passed as part of the call. This is a
// special feature of library functions. It is idiomatic
// to call the first parameter `self`, if the function can
// be seen as a method of that object.
function insert(Data storage self, uint value)
public
returns (bool)
{
if (self.flags[value])
return false; // already there
self.flags[value] = true;
return true;
}
function remove(Data storage self, uint value)
public
returns (bool)
{
if (!self.flags[value])
return false; // not there
self.flags[value] = false;
return true;
}
function contains(Data storage self, uint value)
public
view
returns (bool)
{
return self.flags[value];
}
}
contract C {
Data knownValues;
function register(uint value) public {
// The library functions can be called without a
// specific instance of the library, since the
// "instance" will be the current contract.
require(Set.insert(knownValues, value));
}
// In this contract, we can also directly access knownValues.flags, if we want.
}
Of course, you do not have to follow this way to use libraries: they can also be used without defining struct data types. Functions also work without any storage reference parameters, and they can have multiple storage reference parameters and in any position.
The calls to Set.contains
, Set.insert
and Set.remove
are all compiled as calls (DELEGATECALL
) to an external
contract/library. If you use libraries, be aware that an
actual external function call is performed.
msg.sender
, msg.value
and this
will retain their values
in this call, though (prior to Homestead, because of the use of CALLCODE
, msg.sender
and
msg.value
changed, though).
The following example shows how to use types stored in memory and internal functions in libraries in order to implement custom types without the overhead of external function calls:
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;
struct bigint {
uint[] limbs;
}
library BigInt {
function fromUint(uint x) internal pure returns (bigint memory r) {
r.limbs = new uint[](1);
r.limbs[0] = x;
}
function add(bigint memory _a, bigint memory _b) internal pure returns (bigint memory r) {
r.limbs = new uint[](max(_a.limbs.length, _b.limbs.length));
uint carry = 0;
for (uint i = 0; i < r.limbs.length; ++i) {
uint a = limb(_a, i);
uint b = limb(_b, i);
unchecked {
r.limbs[i] = a + b + carry;
if (a + b < a || (a + b == type(uint).max && carry > 0))
carry = 1;
else
carry = 0;
}
}
if (carry > 0) {
// too bad, we have to add a limb
uint[] memory newLimbs = new uint[](r.limbs.length + 1);
uint i;
for (i = 0; i < r.limbs.length; ++i)
newLimbs[i] = r.limbs[i];
newLimbs[i] = carry;
r.limbs = newLimbs;
}
}
function limb(bigint memory _a, uint _limb) internal pure returns (uint) {
return _limb < _a.limbs.length ? _a.limbs[_limb] : 0;
}
function max(uint a, uint b) private pure returns (uint) {
return a > b ? a : b;
}
}
contract C {
using BigInt for bigint;
function f() public pure {
bigint memory x = BigInt.fromUint(7);
bigint memory y = BigInt.fromUint(type(uint).max);
bigint memory z = x.add(y);
assert(z.limb(1) > 0);
}
}
It is possible to obtain the address of a library by converting
the library type to the address
type, i.e. using address(LibraryName)
.
As the compiler does not know the address where the library will be deployed, the compiled hex code
will contain placeholders of the form __$30bbc0abd4d6364515865950d3e0d10953$__
. The placeholder
is a 34 character prefix of the hex encoding of the keccak256 hash of the fully qualified library
name, which would be for example libraries/bigint.sol:BigInt
if the library was stored in a file
called bigint.sol
in a libraries/
directory. Such bytecode is incomplete and should not be
deployed. Placeholders need to be replaced with actual addresses. You can do that by either passing
them to the compiler when the library is being compiled or by using the linker to update an already
compiled binary. See Library Linking for information on how to use the commandline compiler
for linking.
In comparison to contracts, libraries are restricted in the following ways:
they cannot have state variables
they cannot inherit nor be inherited
they cannot receive Ether
they cannot be destroyed
(These might be lifted at a later point.)
Function Signatures and Selectors in Libraries
While external calls to public or external library functions are possible, the calling convention for such calls is considered to be internal to Solidity and not the same as specified for the regular contract ABI. External library functions support more argument types than external contract functions, for example recursive structs and storage pointers. For that reason, the function signatures used to compute the 4-byte selector are computed following an internal naming schema and arguments of types not supported in the contract ABI use an internal encoding.
The following identifiers are used for the types in the signatures:
Value types, non-storage
string
and non-storagebytes
use the same identifiers as in the contract ABI.Non-storage array types follow the same convention as in the contract ABI, i.e.
<type>[]
for dynamic arrays and<type>[M]
for fixed-size arrays ofM
elements.Non-storage structs are referred to by their fully qualified name, i.e.
C.S
forcontract C { struct S { ... } }
.Storage pointer mappings use
mapping(<keyType> => <valueType>) storage
where<keyType>
and<valueType>
are the identifiers for the key and value types of the mapping, respectively.Other storage pointer types use the type identifier of their corresponding non-storage type, but append a single space followed by
storage
to it.
The argument encoding is the same as for the regular contract ABI, except for storage pointers, which are encoded as a
uint256
value referring to the storage slot to which they point.
Similarly to the contract ABI, the selector consists of the first four bytes of the Keccak256-hash of the signature.
Its value can be obtained from Solidity using the .selector
member as follows:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.5.14 <0.9.0;
library L {
function f(uint256) external {}
}
contract C {
function g() public pure returns (bytes4) {
return L.f.selector;
}
}
Call Protection For Libraries
As mentioned in the introduction, if a library’s code is executed
using a CALL
instead of a DELEGATECALL
or CALLCODE
,
it will revert unless a view
or pure
function is called.
The EVM does not provide a direct way for a contract to detect
whether it was called using CALL
or not, but a contract
can use the ADDRESS
opcode to find out « where » it is
currently running. The generated code compares this address
to the address used at construction time to determine the mode
of calling.
More specifically, the runtime code of a library always starts with a push instruction, which is a zero of 20 bytes at compilation time. When the deploy code runs, this constant is replaced in memory by the current address and this modified code is stored in the contract. At runtime, this causes the deploy time address to be the first constant to be pushed onto the stack and the dispatcher code compares the current address against this constant for any non-view and non-pure function.
This means that the actual code stored on chain for a library
is different from the code reported by the compiler as
deployedBytecode
.
Using For
The directive using A for B;
can be used to attach library
functions (from the library A
) to any type (B
)
in the context of a contract.
These functions will receive the object they are called on
as their first parameter (like the self
variable in Python).
The effect of using A for *;
is that the functions from
the library A
are attached to any type.
In both situations, all functions in the library are attached, even those where the type of the first parameter does not match the type of the object. The type is checked at the point the function is called and function overload resolution is performed.
The using A for B;
directive is active only within the current
contract, including within all of its functions, and has no effect
outside of the contract in which it is used. The directive
may only be used inside a contract, not inside any of its functions.
Let us rewrite the set example from the Libraries in this way:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.6.0 <0.9.0;
// This is the same code as before, just without comments
struct Data { mapping(uint => bool) flags; }
library Set {
function insert(Data storage self, uint value)
public
returns (bool)
{
if (self.flags[value])
return false; // already there
self.flags[value] = true;
return true;
}
function remove(Data storage self, uint value)
public
returns (bool)
{
if (!self.flags[value])
return false; // not there
self.flags[value] = false;
return true;
}
function contains(Data storage self, uint value)
public
view
returns (bool)
{
return self.flags[value];
}
}
contract C {
using Set for Data; // this is the crucial change
Data knownValues;
function register(uint value) public {
// Here, all variables of type Data have
// corresponding member functions.
// The following function call is identical to
// `Set.insert(knownValues, value)`
require(knownValues.insert(value));
}
}
It is also possible to extend elementary types in that way:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.6.8 <0.9.0;
library Search {
function indexOf(uint[] storage self, uint value)
public
view
returns (uint)
{
for (uint i = 0; i < self.length; i++)
if (self[i] == value) return i;
return type(uint).max;
}
}
contract C {
using Search for uint[];
uint[] data;
function append(uint value) public {
data.push(value);
}
function replace(uint _old, uint _new) public {
// This performs the library function call
uint index = data.indexOf(_old);
if (index == type(uint).max)
data.push(_new);
else
data[index] = _new;
}
}
Note that all external library calls are actual EVM function calls. This means that
if you pass memory or value types, a copy will be performed, even of the
self
variable. The only situation where no copy will be performed
is when storage reference variables are used or when internal library
functions are called.
Assembleur en ligne
Vous pouvez entrelacer les instructions en Solidity avec de l’assembleur en ligne, dans un langage proche de celui de la machine virtuelle. Cela vous donne un contrôle plus fin, en particulier lorsque vous améliorez le langage en écrivant des bibliothèques.
Le langage utilisé pour l’assembleur en ligne en Solidity s’appelle Yul et est documenté dans sa propre section. Cette section montre comment le code assembleur en ligne peut s’interfacer au code Solidity l’entourant.
Avertissement
L’assembleur en ligne est un moyen d’accéder à la machine virtuelle Ethereum en bas niveau. Ceci permet de contourner plusieurs normes de sécurité importantes et contrôles de Solidity. Vous ne devriez l’utiliser que pour les tâches qui en ont besoin, et seulement si vous êtes sûr de pourquoi/comment l’utiliser.
Le bloc de code d’assembleur en ligne est indiqué par assembly { ... }
, où le code entre les accolades est écrit en langage Yul.
Le bloc de code assembleur en ligne peut accéder aux variables locales de Solidity comme expliqué ci-dessous.
Différents bloc de coe assembleur ne partagent pas le même espace de noms, c’est à dire qu’il n’est pas possible d’appeler une fonction Yul où d’accéder à une variable Yul variable definie dans un autre bloc.
Exemple
L’exemple suivant fournit le code de librairie pour accéder au code d’un autre contrat et le charger dans une variable bytes
. C’est possible de base avec Solidity en utilisant <address>.code
mais l’idée est que les librairies assembleur peuvent améliorer le langage Solidity sans changer le compilateur.
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.16 <0.9.0;
library GetCode {
function at(address _addr) public view returns (bytes memory o_code) {
assembly {
// récupère la taille du code, a besoin d'assembleur
let size := extcodesize(_addr)
// allouer le tableau de bytes de sortie - ceci serait fait en Solidity via o_code = new bytes(size)
o_code := mload(0x40)
// nouvelle "fin de mémoire" en incluant le padding
mstore(0x40, add(o_code, and(add(add(size, 0x20), 0x1f), not(0x1f))))
// stocke la taille en mémoire
mstore(o_code, size)
// récupère le code lui-même, nécessite de l'assembleur
extcodecopy(_addr, add(o_code, 0x20), 0, size)
}
}
}
L’assembleur en ligne est également utile dans les cas où l’optimiseur ne parvient pas à produire un code efficace, par exemple :
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.16 <0.9.0;
library VectorSum {
// Cette fonction est moins efficace car l'optimiseur ne parvient
// pas à supprimer les contrôles de limites dans l'accès aux tableaux.
function sumSolidity(uint[] memory _data) public pure returns (uint o_sum) {
for (uint i = 0; i < _data.length; ++i)
o_sum += _data[i];
}
// Nous savons que nous n'accédons au tableau que dans ses
// limites, ce qui nous permet d'éviter la vérification. 0x20
// doit être ajouté à un tableau car le premier emplacement
// contient la longueur du tableau.
function sumAsm(uint[] memory _data) public pure returns (uint o_sum) {
for (uint i = 0; i < _data.length; ++i) {
assembly {
o_sum := add(o_sum, mload(add(add(_data, 0x20), mul(i, 0x20))))
}
}
}
// Même chose que ci-dessus, mais exécute le code entier en assembleur en ligne.
function sumPureAsm(uint[] memory _data) public pure returns (uint o_sum) {
assembly {
// Charge la taille (premiers 32 bytes)
let len := mload(_data)
// Saute le champ de taille.
//
// Garde une variable temporaire pour pouvoir l'incrémenter.
//
// NOTE: incrémenter _data resulterait en une
// variable _data inutilisable après ce bloc d'assembleur
let data := add(_data, 0x20)
// Itère jusqu'à la limite.
for
{ let end := add(data, mul(len, 0x20)) }
lt(data, end)
{ data := add(data, 0x20) }
{
o_sum := add(o_sum, mload(data))
}
}
}
}
Access to External Variables, Functions and Libraries
You can access Solidity variables and other identifiers by using their name.
Local variables of value type are directly usable in inline assembly. They can both be read and assigned to.
Local variables that refer to memory evaluate to the address of the variable in memory not the value itself. Such variables can also be assigned to, but note that an assignment will only change the pointer and not the data and that it is your responsibility to respect Solidity’s memory management. See Conventions in Solidity.
Similarly, local variables that refer to statically-sized calldata arrays or calldata structs
evaluate to the address of the variable in calldata, not the value itself.
The variable can also be assigned a new offset, but note that no validation to ensure that
the variable will not point beyond calldatasize()
is performed.
For external function pointers the address and the function selector can be
accessed using x.address
and x.selector
.
The selector consists of four right-aligned bytes.
Both values are can be assigned to. For example:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.8.10 <0.9.0;
contract C {
// Assigns a new selector and address to the return variable @fun
function combineToFunctionPointer(address newAddress, uint newSelector) public pure returns (function() external fun) {
assembly {
fun.selector := newSelector
fun.address := newAddress
}
}
}
For dynamic calldata arrays, you can access
their calldata offset (in bytes) and length (number of elements) using x.offset
and x.length
.
Both expressions can also be assigned to, but as for the static case, no validation will be performed
to ensure that the resulting data area is within the bounds of calldatasize()
.
For local storage variables or state variables, a single Yul identifier
is not sufficient, since they do not necessarily occupy a single full storage slot.
Therefore, their « address » is composed of a slot and a byte-offset
inside that slot. To retrieve the slot pointed to by the variable x
, you
use x.slot
, and to retrieve the byte-offset you use x.offset
.
Using x
itself will result in an error.
You can also assign to the .slot
part of a local storage variable pointer.
For these (structs, arrays or mappings), the .offset
part is always zero.
It is not possible to assign to the .slot
or .offset
part of a state variable,
though.
Local Solidity variables are available for assignments, for example:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
contract C {
uint b;
function f(uint x) public view returns (uint r) {
assembly {
// We ignore the storage slot offset, we know it is zero
// in this special case.
r := mul(x, sload(b.slot))
}
}
}
Avertissement
If you access variables of a type that spans less than 256 bits
(for example uint64
, address
, or bytes16
),
you cannot make any assumptions about bits not part of the
encoding of the type. Especially, do not assume them to be zero.
To be safe, always clear the data properly before you use it
in a context where this is important:
uint32 x = f(); assembly { x := and(x, 0xffffffff) /* now use x */ }
To clean signed types, you can use the signextend
opcode:
assembly { signextend(<num_bytes_of_x_minus_one>, x) }
Since Solidity 0.6.0 the name of a inline assembly variable may not shadow any declaration visible in the scope of the inline assembly block (including variable, contract and function declarations).
Since Solidity 0.7.0, variables and functions declared inside the
inline assembly block may not contain .
, but using .
is
valid to access Solidity variables from outside the inline assembly block.
Things to Avoid
Inline assembly might have a quite high-level look, but it actually is extremely low-level. Function calls, loops, ifs and switches are converted by simple rewriting rules and after that, the only thing the assembler does for you is re-arranging functional-style opcodes, counting stack height for variable access and removing stack slots for assembly-local variables when the end of their block is reached.
Conventions in Solidity
In contrast to EVM assembly, Solidity has types which are narrower than 256 bits,
e.g. uint24
. For efficiency, most arithmetic operations ignore the fact that
types can be shorter than 256
bits, and the higher-order bits are cleaned when necessary,
i.e., shortly before they are written to memory or before comparisons are performed.
This means that if you access such a variable
from within inline assembly, you might have to manually clean the higher-order bits
first.
Solidity manages memory in the following way. There is a « free memory pointer »
at position 0x40
in memory. If you want to allocate memory, use the memory
starting from where this pointer points at and update it.
There is no guarantee that the memory has not been used before and thus
you cannot assume that its contents are zero bytes.
There is no built-in mechanism to release or free allocated memory.
Here is an assembly snippet you can use for allocating memory that follows the process outlined above
function allocate(length) -> pos {
pos := mload(0x40)
mstore(0x40, add(pos, length))
}
The first 64 bytes of memory can be used as « scratch space » for short-term
allocation. The 32 bytes after the free memory pointer (i.e., starting at 0x60
)
are meant to be zero permanently and is used as the initial value for
empty dynamic memory arrays.
This means that the allocatable memory starts at 0x80
, which is the initial value
of the free memory pointer.
Elements in memory arrays in Solidity always occupy multiples of 32 bytes (this is
even true for bytes1[]
, but not for bytes
and string
). Multi-dimensional memory
arrays are pointers to memory arrays. The length of a dynamic array is stored at the
first slot of the array and followed by the array elements.
Avertissement
Statically-sized memory arrays do not have a length field, but it might be added later to allow better convertibility between statically- and dynamically-sized arrays, so do not rely on this.
Cheatsheet
Order of Precedence of Operators
The following is the order of precedence for operators, listed in order of evaluation.
Precedence |
Description |
Operator |
---|---|---|
1 |
Postfix increment and decrement |
|
New expression |
|
|
Array subscripting |
|
|
Member access |
|
|
Function-like call |
|
|
Parentheses |
|
|
2 |
Prefix increment and decrement |
|
Unary minus |
|
|
Unary operations |
|
|
Logical NOT |
|
|
Bitwise NOT |
|
|
3 |
Exponentiation |
|
4 |
Multiplication, division and modulo |
|
5 |
Addition and subtraction |
|
6 |
Bitwise shift operators |
|
7 |
Bitwise AND |
|
8 |
Bitwise XOR |
|
9 |
Bitwise OR |
|
10 |
Inequality operators |
|
11 |
Equality operators |
|
12 |
Logical AND |
|
13 |
Logical OR |
|
14 |
Ternary operator |
|
Assignment operators |
|
|
15 |
Comma operator |
|
Global Variables
abi.decode(bytes memory encodedData, (...)) returns (...)
: ABI-decodes the provided data. The types are given in parentheses as second argument. Example:(uint a, uint[2] memory b, bytes memory c) = abi.decode(data, (uint, uint[2], bytes))
abi.encode(...) returns (bytes memory)
: ABI-encodes the given argumentsabi.encodePacked(...) returns (bytes memory)
: Performs packed encoding of the given arguments. Note that this encoding can be ambiguous!abi.encodeWithSelector(bytes4 selector, ...) returns (bytes memory)
: ABI-encodes the given arguments starting from the second and prepends the given four-byte selectorabi.encodeCall(function functionPointer, (...)) returns (bytes memory)
: ABI-encodes a call tofunctionPointer
with the arguments found in the tuple. Performs a full type-check, ensuring the types match the function signature. Result equalsabi.encodeWithSelector(functionPointer.selector, (...))
abi.encodeWithSignature(string memory signature, ...) returns (bytes memory)
: Equivalent toabi.encodeWithSelector(bytes4(keccak256(bytes(signature)), ...)
bytes.concat(...) returns (bytes memory)
: Concatenates variable number of arguments to one byte arraystring.concat(...) returns (string memory)
: Concatenates variable number of arguments to one string arrayblock.basefee
(uint
): current block’s base fee (EIP-3198 and EIP-1559)block.chainid
(uint
): current chain idblock.coinbase
(address payable
): current block miner’s addressblock.difficulty
(uint
): current block difficultyblock.gaslimit
(uint
): current block gaslimitblock.number
(uint
): current block numberblock.timestamp
(uint
): current block timestamp in seconds since Unix epochgasleft() returns (uint256)
: remaining gasmsg.data
(bytes
): complete calldatamsg.sender
(address
): sender of the message (current call)msg.sig
(bytes4
): first four bytes of the calldata (i.e. function identifier)msg.value
(uint
): number of wei sent with the messagetx.gasprice
(uint
): gas price of the transactiontx.origin
(address
): sender of the transaction (full call chain)assert(bool condition)
: abort execution and revert state changes if condition isfalse
(use for internal error)require(bool condition)
: abort execution and revert state changes if condition isfalse
(use for malformed input or error in external component)require(bool condition, string memory message)
: abort execution and revert state changes if condition isfalse
(use for malformed input or error in external component). Also provide error message.revert()
: abort execution and revert state changesrevert(string memory message)
: abort execution and revert state changes providing an explanatory stringblockhash(uint blockNumber) returns (bytes32)
: hash of the given block - only works for 256 most recent blockskeccak256(bytes memory) returns (bytes32)
: compute the Keccak-256 hash of the inputsha256(bytes memory) returns (bytes32)
: compute the SHA-256 hash of the inputripemd160(bytes memory) returns (bytes20)
: compute the RIPEMD-160 hash of the inputecrecover(bytes32 hash, uint8 v, bytes32 r, bytes32 s) returns (address)
: recover address associated with the public key from elliptic curve signature, return zero on erroraddmod(uint x, uint y, uint k) returns (uint)
: compute(x + y) % k
where the addition is performed with arbitrary precision and does not wrap around at2**256
. Assert thatk != 0
starting from version 0.5.0.mulmod(uint x, uint y, uint k) returns (uint)
: compute(x * y) % k
where the multiplication is performed with arbitrary precision and does not wrap around at2**256
. Assert thatk != 0
starting from version 0.5.0.this
(current contract’s type): the current contract, explicitly convertible toaddress
oraddress payable
super
: the contract one level higher in the inheritance hierarchyselfdestruct(address payable recipient)
: destroy the current contract, sending its funds to the given address<address>.balance
(uint256
): balance of the Adresses in Wei<address>.code
(bytes memory
): code at the Adresses (can be empty)<address>.codehash
(bytes32
): the codehash of the Adresses<address payable>.send(uint256 amount) returns (bool)
: send given amount of Wei to Adresses, returnsfalse
on failure<address payable>.transfer(uint256 amount)
: send given amount of Wei to Adresses, throws on failuretype(C).name
(string
): the name of the contracttype(C).creationCode
(bytes memory
): creation bytecode of the given contract, see Type Information.type(C).runtimeCode
(bytes memory
): runtime bytecode of the given contract, see Type Information.type(I).interfaceId
(bytes4
): value containing the EIP-165 interface identifier of the given interface, see Type Information.type(T).min
(T
): the minimum value representable by the integer typeT
, see Type Information.type(T).max
(T
): the maximum value representable by the integer typeT
, see Type Information.
Note
When contracts are evaluated off-chain rather than in context of a transaction included in a
block, you should not assume that block.*
and tx.*
refer to values from any specific
block or transaction. These values are provided by the EVM implementation that executes the
contract and can be arbitrary.
Note
Do not rely on block.timestamp
or blockhash
as a source of randomness,
unless you know what you are doing.
Both the timestamp and the block hash can be influenced by miners to some degree. Bad actors in the mining community can for example run a casino payout function on a chosen hash and just retry a different hash if they did not receive any money.
The current block timestamp must be strictly larger than the timestamp of the last block, but the only guarantee is that it will be somewhere between the timestamps of two consecutive blocks in the canonical chain.
Note
The block hashes are not available for all blocks for scalability reasons. You can only access the hashes of the most recent 256 blocks, all other values will be zero.
Note
In version 0.5.0, the following aliases were removed: suicide
as alias for selfdestruct
,
msg.gas
as alias for gasleft
, block.blockhash
as alias for blockhash
and
sha3
as alias for keccak256
.
Note
In version 0.7.0, the alias now
(for block.timestamp
) was removed.
Function Visibility Specifiers
function myFunction() <visibility specifier> returns (bool) {
return true;
}
public
: visible externally and internally (creates a getter function for storage/state variables)private
: only visible in the current contractexternal
: only visible externally (only for functions) - i.e. can only be message-called (viathis.func
)internal
: only visible internally
Modifiers
pure
for functions: Disallows modification or access of state.view
for functions: Disallows modification of state.payable
for functions: Allows them to receive Ether together with a call.constant
for state variables: Disallows assignment (except initialisation), does not occupy storage slot.immutable
for state variables: Allows exactly one assignment at construction time and is constant afterwards. Is stored in code.anonymous
for events: Does not store event signature as topic.indexed
for event parameters: Stores the parameter as topic.virtual
for functions and modifiers: Allows the function’s or modifier’s behaviour to be changed in derived contracts.override
: States that this function, modifier or public state variable changes the behaviour of a function or modifier in a base contract.
Reserved Keywords
These keywords are reserved in Solidity. They might become part of the syntax in the future:
after
, alias
, apply
, auto
, byte
, case
, copyof
, default
,
define
, final
, implements
, in
, inline
, let
, macro
, match
,
mutable
, null
, of
, partial
, promise
, reference
, relocatable
,
sealed
, sizeof
, static
, supports
, switch
, typedef
, typeof
,
var
.
Using the Compiler
Using the Commandline Compiler
Note
This section does not apply to solcjs, not even if it is used in commandline mode.
Basic Usage
One of the build targets of the Solidity repository is solc
, the solidity commandline compiler.
Using solc --help
provides you with an explanation of all options. The compiler can produce various outputs, ranging from simple binaries and assembly over an abstract syntax tree (parse tree) to estimations of gas usage.
If you only want to compile a single file, you run it as solc --bin sourceFile.sol
and it will print the binary. If you want to get some of the more advanced output variants of solc
, it is probably better to tell it to output everything to separate files using solc -o outputDirectory --bin --ast-compact-json --asm sourceFile.sol
.
Optimizer Options
Before you deploy your contract, activate the optimizer when compiling using solc --optimize --bin sourceFile.sol
.
By default, the optimizer will optimize the contract assuming it is called 200 times across its lifetime
(more specifically, it assumes each opcode is executed around 200 times).
If you want the initial contract deployment to be cheaper and the later function executions to be more expensive,
set it to --optimize-runs=1
. If you expect many transactions and do not care for higher deployment cost and
output size, set --optimize-runs
to a high number.
This parameter has effects on the following (this might change in the future):
the size of the binary search in the function dispatch routine
the way constants like large numbers or strings are stored
Base Path and Import Remapping
The commandline compiler will automatically read imported files from the filesystem, but
it is also possible to provide path redirects using prefix=path
in the following way:
solc github.com/ethereum/dapp-bin/=/usr/local/lib/dapp-bin/ file.sol
This essentially instructs the compiler to search for anything starting with
github.com/ethereum/dapp-bin/
under /usr/local/lib/dapp-bin
.
When accessing the filesystem to search for imports, paths that do not start with ./
or ../ are treated as relative to the directories specified using
--base-path
and --include-path
options (or the current working directory if base path is not specified).
Furthermore, the part of the path added via these options will not appear in the contract metadata.
For security reasons the compiler has restrictions on what directories it can access.
Directories of source files specified on the command line and target paths of
remappings are automatically allowed to be accessed by the file reader, but everything
else is rejected by default.
Additional paths (and their subdirectories) can be allowed via the
--allow-paths /sample/path,/another/sample/path
switch.
Everything inside the path specified via --base-path
is always allowed.
The above is only a simplification of how the compiler handles import paths. For a detailed explanation with examples and discussion of corner cases please refer to the section on path resolution.
Library Linking
If your contracts use libraries, you will notice that the bytecode contains substrings of the form __$53aea86b7d70b31448b230b20ae141a537$__
. These are placeholders for the actual library addresses.
The placeholder is a 34 character prefix of the hex encoding of the keccak256 hash of the fully qualified library name.
The bytecode file will also contain lines of the form // <placeholder> -> <fq library name>
at the end to help
identify which libraries the placeholders represent. Note that the fully qualified library name
is the path of its source file and the library name separated by :
.
You can use solc
as a linker meaning that it will insert the library addresses for you at those points:
Either add --libraries "file.sol:Math=0x1234567890123456789012345678901234567890 file.sol:Heap=0xabCD567890123456789012345678901234567890"
to your command to provide an address for each library (use commas or spaces as separators) or store the string in a file (one library per line) and run solc
using --libraries fileName
.
Note
Starting Solidity 0.8.1 accepts =
as separator between library and address, and :
as a separator is deprecated. It will be removed in the future. Currently --libraries "file.sol:Math:0x1234567890123456789012345678901234567890 file.sol:Heap:0xabCD567890123456789012345678901234567890"
will work too.
If solc
is called with the option --standard-json
, it will expect a JSON input (as explained below) on the standard input, and return a JSON output on the standard output. This is the recommended interface for more complex and especially automated uses. The process will always terminate in a « success » state and report any errors via the JSON output.
The option --base-path
is also processed in standard-json mode.
If solc
is called with the option --link
, all input files are interpreted to be unlinked binaries (hex-encoded) in the __$53aea86b7d70b31448b230b20ae141a537$__
-format given above and are linked in-place (if the input is read from stdin, it is written to stdout). All options except --libraries
are ignored (including -o
) in this case.
Avertissement
Manually linking libraries on the generated bytecode is discouraged because it does not update contract metadata. Since metadata contains a list of libraries specified at the time of compilation and bytecode contains a metadata hash, you will get different binaries, depending on when linking is performed.
You should ask the compiler to link the libraries at the time a contract is compiled by either
using the --libraries
option of solc
or the libraries
key if you use the
standard-JSON interface to the compiler.
Note
The library placeholder used to be the fully qualified name of the library itself
instead of the hash of it. This format is still supported by solc --link
but
the compiler will no longer output it. This change was made to reduce
the likelihood of a collision between libraries, since only the first 36 characters
of the fully qualified library name could be used.
Setting the EVM Version to Target
When you compile your contract code you can specify the Ethereum virtual machine version to compile for to avoid particular features or behaviours.
Avertissement
Compiling for the wrong EVM version can result in wrong, strange and failing behaviour. Please ensure, especially if running a private chain, that you use matching EVM versions.
On the command line, you can select the EVM version as follows:
solc --evm-version <VERSION> contract.sol
In the standard JSON interface, use the "evmVersion"
key in the "settings"
field:
{
"sources": {/* ... */},
"settings": {
"optimizer": {/* ... */},
"evmVersion": "<VERSION>"
}
}
Target Options
Below is a list of target EVM versions and the compiler-relevant changes introduced at each version. Backward compatibility is not guaranteed between each version.
homestead
(oldest version)
tangerineWhistle
Gas cost for access to other accounts increased, relevant for gas estimation and the optimizer.
All gas sent by default for external calls, previously a certain amount had to be retained.
spuriousDragon
Gas cost for the
exp
opcode increased, relevant for gas estimation and the optimizer.
byzantium
Opcodes
returndatacopy
,returndatasize
andstaticcall
are available in assembly.The
staticcall
opcode is used when calling non-library view or pure functions, which prevents the functions from modifying state at the EVM level, i.e., even applies when you use invalid type conversions.It is possible to access dynamic data returned from function calls.
revert
opcode introduced, which means thatrevert()
will not waste gas.
constantinople
Opcodes
create2`, ``extcodehash
,shl
,shr
andsar
are available in assembly.Shifting operators use shifting opcodes and thus need less gas.
petersburg
The compiler behaves the same way as with constantinople.
istanbul
Opcodes
chainid
andselfbalance
are available in assembly.
berlin
Gas costs for
SLOAD
,*CALL
,BALANCE
,EXT*
andSELFDESTRUCT
increased. The compiler assumes cold gas costs for such operations. This is relevant for gas estimation and the optimizer.
Compiler Input and Output JSON Description
The recommended way to interface with the Solidity compiler especially for more complex and automated setups is the so-called JSON-input-output interface. The same interface is provided by all distributions of the compiler.
The fields are generally subject to change, some are optional (as noted), but we try to only make backwards compatible changes.
The compiler API expects a JSON formatted input and outputs the compilation result in a JSON formatted output. The standard error output is not used and the process will always terminate in a « success » state, even if there were errors. Errors are always reported as part of the JSON output.
The following subsections describe the format through an example. Comments are of course not permitted and used here only for explanatory purposes.
Input Description
{
// Required: Source code language. Currently supported are "Solidity" and "Yul".
"language": "Solidity",
// Required
"sources":
{
// The keys here are the "global" names of the source files,
// imports can use other files via remappings (see below).
"myFile.sol":
{
// Optional: keccak256 hash of the source file
// It is used to verify the retrieved content if imported via URLs.
"keccak256": "0x123...",
// Required (unless "content" is used, see below): URL(s) to the source file.
// URL(s) should be imported in this order and the result checked against the
// keccak256 hash (if available). If the hash doesn't match or none of the
// URL(s) result in success, an error should be raised.
// Using the commandline interface only filesystem paths are supported.
// With the JavaScript interface the URL will be passed to the user-supplied
// read callback, so any URL supported by the callback can be used.
"urls":
[
"bzzr://56ab...",
"ipfs://Qma...",
"/tmp/path/to/file.sol"
// If files are used, their directories should be added to the command line via
// `--allow-paths <path>`.
]
},
"destructible":
{
// Optional: keccak256 hash of the source file
"keccak256": "0x234...",
// Required (unless "urls" is used): literal contents of the source file
"content": "contract destructible is owned { function shutdown() { if (msg.sender == owner) selfdestruct(owner); } }"
}
},
// Optional
"settings":
{
// Optional: Stop compilation after the given stage. Currently only "parsing" is valid here
"stopAfter": "parsing",
// Optional: Sorted list of remappings
"remappings": [ ":g=/dir" ],
// Optional: Optimizer settings
"optimizer": {
// Disabled by default.
// NOTE: enabled=false still leaves some optimizations on. See comments below.
// WARNING: Before version 0.8.6 omitting the 'enabled' key was not equivalent to setting
// it to false and would actually disable all the optimizations.
"enabled": true,
// Optimize for how many times you intend to run the code.
// Lower values will optimize more for initial deployment cost, higher
// values will optimize more for high-frequency usage.
"runs": 200,
// Switch optimizer components on or off in detail.
// The "enabled" switch above provides two defaults which can be
// tweaked here. If "details" is given, "enabled" can be omitted.
"details": {
// The peephole optimizer is always on if no details are given,
// use details to switch it off.
"peephole": true,
// The inliner is always on if no details are given,
// use details to switch it off.
"inliner": true,
// The unused jumpdest remover is always on if no details are given,
// use details to switch it off.
"jumpdestRemover": true,
// Sometimes re-orders literals in commutative operations.
"orderLiterals": false,
// Removes duplicate code blocks
"deduplicate": false,
// Common subexpression elimination, this is the most complicated step but
// can also provide the largest gain.
"cse": false,
// Optimize representation of literal numbers and strings in code.
"constantOptimizer": false,
// The new Yul optimizer. Mostly operates on the code of ABI coder v2
// and inline assembly.
// It is activated together with the global optimizer setting
// and can be deactivated here.
// Before Solidity 0.6.0 it had to be activated through this switch.
"yul": false,
// Tuning options for the Yul optimizer.
"yulDetails": {
// Improve allocation of stack slots for variables, can free up stack slots early.
// Activated by default if the Yul optimizer is activated.
"stackAllocation": true,
// Select optimization steps to be applied.
// Optional, the optimizer will use the default sequence if omitted.
"optimizerSteps": "dhfoDgvulfnTUtnIf..."
}
}
},
// Version of the EVM to compile for.
// Affects type checking and code generation. Can be homestead,
// tangerineWhistle, spuriousDragon, byzantium, constantinople, petersburg, istanbul or berlin
"evmVersion": "byzantium",
// Optional: Change compilation pipeline to go through the Yul intermediate representation.
// This is a highly EXPERIMENTAL feature, not to be used for production. This is false by default.
"viaIR": true,
// Optional: Debugging settings
"debug": {
// How to treat revert (and require) reason strings. Settings are
// "default", "strip", "debug" and "verboseDebug".
// "default" does not inject compiler-generated revert strings and keeps user-supplied ones.
// "strip" removes all revert strings (if possible, i.e. if literals are used) keeping side-effects
// "debug" injects strings for compiler-generated internal reverts, implemented for ABI encoders V1 and V2 for now.
// "verboseDebug" even appends further information to user-supplied revert strings (not yet implemented)
"revertStrings": "default",
// Optional: How much extra debug information to include in comments in the produced EVM
// assembly and Yul code. Available components are:
// - `location`: Annotations of the form `@src <index>:<start>:<end>` indicating the
// location of the corresponding element in the original Solidity file, where:
// - `<index>` is the file index matching the `@use-src` annotation,
// - `<start>` is the index of the first byte at that location,
// - `<end>` is the index of the first byte after that location.
// - `snippet`: A single-line code snippet from the location indicated by `@src`.
// The snippet is quoted and follows the corresponding `@src` annotation.
// - `*`: Wildcard value that can be used to request everything.
"debugInfo": ["location", "snippet"]
},
// Metadata settings (optional)
"metadata": {
// Use only literal content and not URLs (false by default)
"useLiteralContent": true,
// Use the given hash method for the metadata hash that is appended to the bytecode.
// The metadata hash can be removed from the bytecode via option "none".
// The other options are "ipfs" and "bzzr1".
// If the option is omitted, "ipfs" is used by default.
"bytecodeHash": "ipfs"
},
// Addresses of the libraries. If not all libraries are given here,
// it can result in unlinked objects whose output data is different.
"libraries": {
// The top level key is the the name of the source file where the library is used.
// If remappings are used, this source file should match the global path
// after remappings were applied.
// If this key is an empty string, that refers to a global level.
"myFile.sol": {
"MyLib": "0x123123..."
}
},
// The following can be used to select desired outputs based
// on file and contract names.
// If this field is omitted, then the compiler loads and does type checking,
// but will not generate any outputs apart from errors.
// The first level key is the file name and the second level key is the contract name.
// An empty contract name is used for outputs that are not tied to a contract
// but to the whole source file like the AST.
// A star as contract name refers to all contracts in the file.
// Similarly, a star as a file name matches all files.
// To select all outputs the compiler can possibly generate, use
// "outputSelection: { "*": { "*": [ "*" ], "": [ "*" ] } }"
// but note that this might slow down the compilation process needlessly.
//
// The available output types are as follows:
//
// File level (needs empty string as contract name):
// ast - AST of all source files
//
// Contract level (needs the contract name or "*"):
// abi - ABI
// devdoc - Developer documentation (natspec)
// userdoc - User documentation (natspec)
// metadata - Metadata
// ir - Yul intermediate representation of the code before optimization
// irOptimized - Intermediate representation after optimization
// storageLayout - Slots, offsets and types of the contract's state variables.
// evm.assembly - New assembly format
// evm.legacyAssembly - Old-style assembly format in JSON
// evm.bytecode.functionDebugData - Debugging information at function level
// evm.bytecode.object - Bytecode object
// evm.bytecode.opcodes - Opcodes list
// evm.bytecode.sourceMap - Source mapping (useful for debugging)
// evm.bytecode.linkReferences - Link references (if unlinked object)
// evm.bytecode.generatedSources - Sources generated by the compiler
// evm.deployedBytecode* - Deployed bytecode (has all the options that evm.bytecode has)
// evm.deployedBytecode.immutableReferences - Map from AST ids to bytecode ranges that reference immutables
// evm.methodIdentifiers - The list of function hashes
// evm.gasEstimates - Function gas estimates
// ewasm.wast - Ewasm in WebAssembly S-expressions format
// ewasm.wasm - Ewasm in WebAssembly binary format
//
// Note that using a using `evm`, `evm.bytecode`, `ewasm`, etc. will select every
// target part of that output. Additionally, `*` can be used as a wildcard to request everything.
//
"outputSelection": {
"*": {
"*": [
"metadata", "evm.bytecode" // Enable the metadata and bytecode outputs of every single contract.
, "evm.bytecode.sourceMap" // Enable the source map output of every single contract.
],
"": [
"ast" // Enable the AST output of every single file.
]
},
// Enable the abi and opcodes output of MyContract defined in file def.
"def": {
"MyContract": [ "abi", "evm.bytecode.opcodes" ]
}
},
// The modelChecker object is experimental and subject to changes.
"modelChecker":
{
// Chose which contracts should be analyzed as the deployed one.
"contracts":
{
"source1.sol": ["contract1"],
"source2.sol": ["contract2", "contract3"]
},
// Choose whether division and modulo operations should be replaced by
// multiplication with slack variables. Default is `true`.
// Using `false` here is recommended if you are using the CHC engine
// and not using Spacer as the Horn solver (using Eldarica, for example).
// See the Formal Verification section for a more detailed explanation of this option.
"divModWithSlacks": true,
// Choose which model checker engine to use: all (default), bmc, chc, none.
"engine": "chc",
// Choose which types of invariants should be reported to the user: contract, reentrancy.
"invariants": ["contract", "reentrancy"],
// Choose whether to output all unproved targets. The default is `false`.
"showUnproved": true,
// Choose which solvers should be used, if available.
// See the Formal Verification section for the solvers description.
"solvers": ["cvc4", "smtlib2", "z3"],
// Choose which targets should be checked: constantCondition,
// underflow, overflow, divByZero, balance, assert, popEmptyArray, outOfBounds.
// If the option is not given all targets are checked by default,
// except underflow/overflow for Solidity >=0.8.7.
// See the Formal Verification section for the targets description.
"targets": ["underflow", "overflow", "assert"],
// Timeout for each SMT query in milliseconds.
// If this option is not given, the SMTChecker will use a deterministic
// resource limit by default.
// A given timeout of 0 means no resource/time restrictions for any query.
"timeout": 20000
}
}
}
Output Description
{
// Optional: not present if no errors/warnings/infos were encountered
"errors": [
{
// Optional: Location within the source file.
"sourceLocation": {
"file": "sourceFile.sol",
"start": 0,
"end": 100
},
// Optional: Further locations (e.g. places of conflicting declarations)
"secondarySourceLocations": [
{
"file": "sourceFile.sol",
"start": 64,
"end": 92,
"message": "Other declaration is here:"
}
],
// Mandatory: Error type, such as "TypeError", "InternalCompilerError", "Exception", etc.
// See below for complete list of types.
"type": "TypeError",
// Mandatory: Component where the error originated, such as "general", "ewasm", etc.
"component": "general",
// Mandatory ("error", "warning" or "info", but please note that this may be extended in the future)
"severity": "error",
// Optional: unique code for the cause of the error
"errorCode": "3141",
// Mandatory
"message": "Invalid keyword",
// Optional: the message formatted with source location
"formattedMessage": "sourceFile.sol:100: Invalid keyword"
}
],
// This contains the file-level outputs.
// It can be limited/filtered by the outputSelection settings.
"sources": {
"sourceFile.sol": {
// Identifier of the source (used in source maps)
"id": 1,
// The AST object
"ast": {}
}
},
// This contains the contract-level outputs.
// It can be limited/filtered by the outputSelection settings.
"contracts": {
"sourceFile.sol": {
// If the language used has no contract names, this field should equal to an empty string.
"ContractName": {
// The Ethereum Contract ABI. If empty, it is represented as an empty array.
// See https://docs.soliditylang.org/en/develop/abi-spec.html
"abi": [],
// See the Metadata Output documentation (serialised JSON string)
"metadata": "{/* ... */}",
// User documentation (natspec)
"userdoc": {},
// Developer documentation (natspec)
"devdoc": {},
// Intermediate representation (string)
"ir": "",
// See the Storage Layout documentation.
"storageLayout": {"storage": [/* ... */], "types": {/* ... */} },
// EVM-related outputs
"evm": {
// Assembly (string)
"assembly": "",
// Old-style assembly (object)
"legacyAssembly": {},
// Bytecode and related details.
"bytecode": {
// Debugging data at the level of functions.
"functionDebugData": {
// Now follows a set of functions including compiler-internal and
// user-defined function. The set does not have to be complete.
"@mint_13": { // Internal name of the function
"entryPoint": 128, // Byte offset into the bytecode where the function starts (optional)
"id": 13, // AST ID of the function definition or null for compiler-internal functions (optional)
"parameterSlots": 2, // Number of EVM stack slots for the function parameters (optional)
"returnSlots": 1 // Number of EVM stack slots for the return values (optional)
}
},
// The bytecode as a hex string.
"object": "00fe",
// Opcodes list (string)
"opcodes": "",
// The source mapping as a string. See the source mapping definition.
"sourceMap": "",
// Array of sources generated by the compiler. Currently only
// contains a single Yul file.
"generatedSources": [{
// Yul AST
"ast": {/* ... */},
// Source file in its text form (may contain comments)
"contents":"{ function abi_decode(start, end) -> data { data := calldataload(start) } }",
// Source file ID, used for source references, same "namespace" as the Solidity source files
"id": 2,
"language": "Yul",
"name": "#utility.yul"
}],
// If given, this is an unlinked object.
"linkReferences": {
"libraryFile.sol": {
// Byte offsets into the bytecode.
// Linking replaces the 20 bytes located there.
"Library1": [
{ "start": 0, "length": 20 },
{ "start": 200, "length": 20 }
]
}
}
},
"deployedBytecode": {
/* ..., */ // The same layout as above.
"immutableReferences": {
// There are two references to the immutable with AST ID 3, both 32 bytes long. One is
// at bytecode offset 42, the other at bytecode offset 80.
"3": [{ "start": 42, "length": 32 }, { "start": 80, "length": 32 }]
}
},
// The list of function hashes
"methodIdentifiers": {
"delegate(address)": "5c19a95c"
},
// Function gas estimates
"gasEstimates": {
"creation": {
"codeDepositCost": "420000",
"executionCost": "infinite",
"totalCost": "infinite"
},
"external": {
"delegate(address)": "25000"
},
"internal": {
"heavyLifting()": "infinite"
}
}
},
// Ewasm related outputs
"ewasm": {
// S-expressions format
"wast": "",
// Binary format (hex string)
"wasm": ""
}
}
}
}
}
Error Types
JSONError
: JSON input doesn’t conform to the required format, e.g. input is not a JSON object, the language is not supported, etc.IOError
: IO and import processing errors, such as unresolvable URL or hash mismatch in supplied sources.ParserError
: Source code doesn’t conform to the language rules.DocstringParsingError
: The NatSpec tags in the comment block cannot be parsed.SyntaxError
: Syntactical error, such ascontinue
is used outside of afor
loop.DeclarationError
: Invalid, unresolvable or clashing identifier names. e.g.Identifier not found
TypeError
: Error within the type system, such as invalid type conversions, invalid assignments, etc.UnimplementedFeatureError
: Feature is not supported by the compiler, but is expected to be supported in future versions.InternalCompilerError
: Internal bug triggered in the compiler - this should be reported as an issue.Exception
: Unknown failure during compilation - this should be reported as an issue.CompilerError
: Invalid use of the compiler stack - this should be reported as an issue.FatalError
: Fatal error not processed correctly - this should be reported as an issue.Warning
: A warning, which didn’t stop the compilation, but should be addressed if possible.Info
: Information that the compiler thinks the user might find useful, but is not dangerous and does not necessarily need to be addressed.
Compiler Tools
solidity-upgrade
solidity-upgrade
can help you to semi-automatically upgrade your contracts
to breaking language changes. While it does not and cannot implement all
required changes for every breaking release, it still supports the ones, that
would need plenty of repetitive manual adjustments otherwise.
Note
solidity-upgrade
carries out a large part of the work, but your
contracts will most likely need further manual adjustments. We recommend
using a version control system for your files. This helps reviewing and
eventually rolling back the changes made.
Avertissement
solidity-upgrade
is not considered to be complete or free from bugs, so
please use with care.
How it Works
You can pass (a) Solidity source file(s) to solidity-upgrade [files]
. If
these make use of import
statement which refer to files outside the
current source file’s directory, you need to specify directories that
are allowed to read and import files from, by passing
--allow-paths [directory]
. You can ignore missing files by passing
--ignore-missing
.
solidity-upgrade
is based on libsolidity
and can parse, compile and
analyse your source files, and might find applicable source upgrades in them.
Source upgrades are considered to be small textual changes to your source code.
They are applied to an in-memory representation of the source files
given. The corresponding source file is updated by default, but you can pass
--dry-run
to simulate to whole upgrade process without writing to any file.
The upgrade process itself has two phases. In the first phase source files are
parsed, and since it is not possible to upgrade source code on that level,
errors are collected and can be logged by passing --verbose
. No source
upgrades available at this point.
In the second phase, all sources are compiled and all activated upgrade analysis modules are run alongside compilation. By default, all available modules are activated. Please read the documentation on available modules for further details.
This can result in compilation errors that may
be fixed by source upgrades. If no errors occur, no source upgrades are being
reported and you’re done.
If errors occur and some upgrade module reported a source upgrade, the first
reported one gets applied and compilation is triggered again for all given
source files. The previous step is repeated as long as source upgrades are
reported. If errors still occur, you can log them by passing --verbose
.
If no errors occur, your contracts are up to date and can be compiled with
the latest version of the compiler.
Available Upgrade Modules
Module |
Version |
Description |
---|---|---|
|
0.5.0 |
Constructors must now be defined using the
|
|
0.5.0 |
Explicit function visibility is now mandatory,
defaults to |
|
0.6.0 |
The keyword |
|
0.6.0 |
Functions without implementation outside an
interface have to be marked |
|
0.6.0 |
When overriding a function or modifier, the new
keyword |
|
0.7.0 |
The following syntax is deprecated:
|
|
0.7.0 |
The |
|
0.7.0 |
Removes visibility of constructors. |
Please read 0.5.0 release notes, 0.6.0 release notes, 0.7.0 release notes and 0.8.0 release notes for further details.
Synopsis
Usage: solidity-upgrade [options] contract.sol
Allowed options:
--help Show help message and exit.
--version Show version and exit.
--allow-paths path(s)
Allow a given path for imports. A list of paths can be
supplied by separating them with a comma.
--ignore-missing Ignore missing files.
--modules module(s) Only activate a specific upgrade module. A list of
modules can be supplied by separating them with a comma.
--dry-run Apply changes in-memory only and don't write to input
file.
--verbose Print logs, errors and changes. Shortens output of
upgrade patches.
--unsafe Accept *unsafe* changes.
Bug Reports / Feature Requests
If you found a bug or if you have a feature request, please file an issue on Github.
Example
Assume that you have the following contract in Source.sol
:
pragma solidity >=0.6.0 <0.6.4;
// This will not compile after 0.7.0
// SPDX-License-Identifier: GPL-3.0
contract C {
// FIXME: remove constructor visibility and make the contract abstract
constructor() internal {}
}
contract D {
uint time;
function f() public payable {
// FIXME: change now to block.timestamp
time = now;
}
}
contract E {
D d;
// FIXME: remove constructor visibility
constructor() public {}
function g() public {
// FIXME: change .value(5) => {value: 5}
d.f.value(5)();
}
}
Required Changes
The above contract will not compile starting from 0.7.0. To bring the contract up to date with the
current Solidity version, the following upgrade modules have to be executed:
constructor-visibility
, now
and dotsyntax
. Please read the documentation on
available modules for further details.
Running the Upgrade
It is recommended to explicitly specify the upgrade modules by using --modules
argument.
solidity-upgrade --modules constructor-visibility,now,dotsyntax Source.sol
The command above applies all changes as shown below. Please review them carefully (the pragmas will have to be updated manually.)
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
abstract contract C {
// FIXME: remove constructor visibility and make the contract abstract
constructor() {}
}
contract D {
uint time;
function f() public payable {
// FIXME: change now to block.timestamp
time = block.timestamp;
}
}
contract E {
D d;
// FIXME: remove constructor visibility
constructor() {}
function g() public {
// FIXME: change .value(5) => {value: 5}
d.f{value: 5}();
}
}
Analysing the Compiler Output
It is often useful to look at the assembly code generated by the compiler. The generated binary,
i.e., the output of solc --bin contract.sol
, is generally difficult to read. It is recommended
to use the flag --asm
to analyse the assembly output. Even for large contracts, looking at a
visual diff of the assembly before and after a change is often very enlightening.
Consider the following contract (named, say contract.sol
):
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.5.0 <0.9.0;
contract C {
function one() public pure returns (uint) {
return 1;
}
}
The following would be the output of solc --asm contract.sol
======= contract.sol:C =======
EVM assembly:
/* "contract.sol":0:86 contract C {... */
mstore(0x40, 0x80)
callvalue
dup1
iszero
tag_1
jumpi
0x00
dup1
revert
tag_1:
pop
dataSize(sub_0)
dup1
dataOffset(sub_0)
0x00
codecopy
0x00
return
stop
sub_0: assembly {
/* "contract.sol":0:86 contract C {... */
mstore(0x40, 0x80)
callvalue
dup1
iszero
tag_1
jumpi
0x00
dup1
revert
tag_1:
pop
jumpi(tag_2, lt(calldatasize, 0x04))
shr(0xe0, calldataload(0x00))
dup1
0x901717d1
eq
tag_3
jumpi
tag_2:
0x00
dup1
revert
/* "contract.sol":17:84 function one() public pure returns (uint) {... */
tag_3:
tag_4
tag_5
jump // in
tag_4:
mload(0x40)
tag_6
swap2
swap1
tag_7
jump // in
tag_6:
mload(0x40)
dup1
swap2
sub
swap1
return
tag_5:
/* "contract.sol":53:57 uint */
0x00
/* "contract.sol":76:77 1 */
0x01
/* "contract.sol":69:77 return 1 */
swap1
pop
/* "contract.sol":17:84 function one() public pure returns (uint) {... */
swap1
jump // out
/* "#utility.yul":7:125 */
tag_10:
/* "#utility.yul":94:118 */
tag_12
/* "#utility.yul":112:117 */
dup2
/* "#utility.yul":94:118 */
tag_13
jump // in
tag_12:
/* "#utility.yul":89:92 */
dup3
/* "#utility.yul":82:119 */
mstore
/* "#utility.yul":72:125 */
pop
pop
jump // out
/* "#utility.yul":131:353 */
tag_7:
0x00
/* "#utility.yul":262:264 */
0x20
/* "#utility.yul":251:260 */
dup3
/* "#utility.yul":247:265 */
add
/* "#utility.yul":239:265 */
swap1
pop
/* "#utility.yul":275:346 */
tag_15
/* "#utility.yul":343:344 */
0x00
/* "#utility.yul":332:341 */
dup4
/* "#utility.yul":328:345 */
add
/* "#utility.yul":319:325 */
dup5
/* "#utility.yul":275:346 */
tag_10
jump // in
tag_15:
/* "#utility.yul":229:353 */
swap3
swap2
pop
pop
jump // out
/* "#utility.yul":359:436 */
tag_13:
0x00
/* "#utility.yul":425:430 */
dup2
/* "#utility.yul":414:430 */
swap1
pop
/* "#utility.yul":404:436 */
swap2
swap1
pop
jump // out
auxdata: 0xa2646970667358221220a5874f19737ddd4c5d77ace1619e5160c67b3d4bedac75fce908fed32d98899864736f6c637827302e382e342d646576656c6f702e323032312e332e33302b636f6d6d69742e65613065363933380058
}
Alternatively, the above output can also be obtained from Remix, under the option « Compilation Details » after compiling a contract.
Notice that the asm
output starts with the creation / constructor code. The deploy code is
provided as part of the sub object (in the above example, it is part of the sub-object sub_0
).
The auxdata
field corresponds to the contract metadata. The comments in the assembly output point to the
source location. Note that #utility.yul
is an internally generated file of utility functions
that can be obtained using the flags --combined-json
generated-sources,generated-sources-runtime
.
Similarly, the optimized assembly can be obtained with the command: solc --optimize --asm
contract.sol
. Often times, it is interesting to see if two different sources in Solidity result in
the same optimized code. For example, to see if the expressions (a * b) / c
, a * b / c
generates the same bytecode. This can be easily done by taking a diff
of the corresponding
assembly output, after potentially stripping comments that reference the source locations.
Note
The --asm
output is not designed to be machine readable. Therefore, there may be breaking
changes on the output between minor versions of solc.
Solidity IR-based Codegen Changes
Solidity can generate EVM bytecode in two different ways: Either directly from Solidity to EVM opcodes (« old codegen ») or through an intermediate representation (« IR ») in Yul (« new codegen » or « IR-based codegen »).
The IR-based code generator was introduced with an aim to not only allow code generation to be more transparent and auditable but also to enable more powerful optimization passes that span across functions.
Currently, the IR-based code generator is still marked experimental, but it supports all language features and has received a lot of testing, so we consider it almost ready for production use.
You can enable it on the command line using --experimental-via-ir
or with the option {"viaIR": true}
in standard-json and we
encourage everyone to try it out!
For several reasons, there are tiny semantic differences between the old and the IR-based code generator, mostly in areas where we would not expect people to rely on this behaviour anyway. This section highlights the main differences between the old and the IR-based codegen.
Semantic Only Changes
This section lists the changes that are semantic-only, thus potentially hiding new and different behavior in existing code.
When storage structs are deleted, every storage slot that contains a member of the struct is set to zero entirely. Formerly, padding space was left untouched. Consequently, if the padding space within a struct is used to store data (e.g. in the context of a contract upgrade), you have to be aware that
delete
will now also clear the added member (while it wouldn’t have been cleared in the past).// SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.7.1; contract C { struct S { uint64 y; uint64 z; } S s; function f() public { // ... delete s; // s occupies only first 16 bytes of the 32 bytes slot // delete will write zero to the full slot } }
We have the same behavior for implicit delete, for example when array of structs is shortened.
Function modifiers are implemented in a slightly different way regarding function parameters and return variables. This especially has an effect if the placeholder
_;
is evaluated multiple times in a modifier. In the old code generator, each function parameter and return variable has a fixed slot on the stack. If the function is run multiple times because_;
is used multiple times or used in a loop, then a change to the function parameter’s or return variable’s value is visible in the next execution of the function. The new code generator implements modifiers using actual functions and passes function parameters on. This means that multiple evaluations of a function’s body will get the same values for the parameters, and the effect on return variables is that they are reset to their default (zero) value for each execution.// SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.7.0; contract C { function f(uint _a) public pure mod() returns (uint _r) { _r = _a++; } modifier mod() { _; _; } }
If you execute
f(0)
in the old code generator, it will return2
, while it will return1
when using the new code generator.// SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.7.1 <0.9.0; contract C { bool active = true; modifier mod() { _; active = false; _; } function foo() external mod() returns (uint ret) { if (active) ret = 1; // Same as ``return 1`` } }
The function
C.foo()
returns the following values:Old code generator:
1
as the return variable is initialized to0
only once before the first_;
evaluation and then overwritten by thereturn 1;
. It is not initialized again for the second_;
evaluation andfoo()
does not explicitly assign it either (due toactive == false
), thus it keeps its first value.New code generator:
0
as all parameters, including return parameters, will be re-initialized before each_;
evaluation.
The order of contract initialization has changed in case of inheritance.
The order used to be:
All state variables are zero-initialized at the beginning.
Evaluate base constructor arguments from most derived to most base contract.
Initialize all state variables in the whole inheritance hierarchy from most base to most derived.
Run the constructor, if present, for all contracts in the linearized hierarchy from most base to most derived.
New order:
All state variables are zero-initialized at the beginning.
Evaluate base constructor arguments from most derived to most base contract.
For every contract in order from most base to most derived in the linearized hierarchy execute:
If present at declaration, initial values are assigned to state variables.
Constructor, if present.
This causes differences in some contracts, for example:
// SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.7.1; contract A { uint x; constructor() { x = 42; } function f() public view returns(uint256) { return x; } } contract B is A { uint public y = f(); }Previously,
y
would be set to 0. This is due to the fact that we would first initialize state variables: First,x
is set to 0, and when initializingy
,f()
would return 0 causingy
to be 0 as well. With the new rules,y
will be set to 42. We first initializex
to 0, then call A’s constructor which setsx
to 42. Finally, when initializingy
,f()
returns 42 causingy
to be 42.
Copying
bytes
arrays from memory to storage is implemented in a different way. The old code generator always copies full words, while the new one cuts the byte array after its end. The old behaviour can lead to dirty data being copied after the end of the array (but still in the same storage slot). This causes differences in some contracts, for example:// SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.8.1; contract C { bytes x; function f() public returns (uint _r) { bytes memory m = "tmp"; assembly { mstore(m, 8) mstore(add(m, 32), "deadbeef15dead") } x = m; assembly { _r := sload(x.slot) } } }
Previously
f()
would return0x6465616462656566313564656164000000000000000000000000000000000010
(it has correct length, and correct first 8 elements, but then it contains dirty data which was set via assembly). Now it is returning0x6465616462656566000000000000000000000000000000000000000000000010
(it has correct length, and correct elements, but does not contain superfluous data).For the old code generator, the evaluation order of expressions is unspecified. For the new code generator, we try to evaluate in source order (left to right), but do not guarantee it. This can lead to semantic differences.
For example:
// SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.8.1; contract C { function preincr_u8(uint8 _a) public pure returns (uint8) { return ++_a + _a; } }
The function
preincr_u8(1)
returns the following values:Old code generator: 3 (
1 + 2
) but the return value is unspecified in generalNew code generator: 4 (
2 + 2
) but the return value is not guaranteed
On the other hand, function argument expressions are evaluated in the same order by both code generators with the exception of the global functions
addmod
andmulmod
. For example:// SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.8.1; contract C { function add(uint8 _a, uint8 _b) public pure returns (uint8) { return _a + _b; } function g(uint8 _a, uint8 _b) public pure returns (uint8) { return add(++_a + ++_b, _a + _b); } }
The function
g(1, 2)
returns the following values:Old code generator:
10
(add(2 + 3, 2 + 3)
) but the return value is unspecified in generalNew code generator:
10
but the return value is not guaranteed
The arguments to the global functions
addmod
andmulmod
are evaluated right-to-left by the old code generator and left-to-right by the new code generator. For example:// SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.8.1; contract C { function f() public pure returns (uint256 aMod, uint256 mMod) { uint256 x = 3; // Old code gen: add/mulmod(5, 4, 3) // New code gen: add/mulmod(4, 5, 5) aMod = addmod(++x, ++x, x); mMod = mulmod(++x, ++x, x); } }
The function
f()
returns the following values:Old code generator:
aMod = 0
andmMod = 2
New code generator:
aMod = 4
andmMod = 0
The new code generator imposes a hard limit of
type(uint64).max
(0xffffffffffffffff
) for the free memory pointer. Allocations that would increase its value beyond this limit revert. The old code generator does not have this limit.For example:
// SPDX-License-Identifier: GPL-3.0 pragma solidity >0.8.0; contract C { function f() public { uint[] memory arr; // allocation size: 576460752303423481 // assumes freeMemPtr points to 0x80 initially uint solYulMaxAllocationBeforeMemPtrOverflow = (type(uint64).max - 0x80 - 31) / 32; // freeMemPtr overflows UINT64_MAX arr = new uint[](solYulMaxAllocationBeforeMemPtrOverflow); } }
The function f() behaves as follows:
Old code generator: runs out of gas while zeroing the array contents after the large memory allocation
New code generator: reverts due to free memory pointer overflow (does not run out of gas)
Internals
Internal function pointers
The old code generator uses code offsets or tags for values of internal function pointers. This is especially complicated since these offsets are different at construction time and after deployment and the values can cross this border via storage. Because of that, both offsets are encoded at construction time into the same value (into different bytes).
In the new code generator, function pointers use internal IDs that are allocated in sequence. Since calls via jumps are not possible,
calls through function pointers always have to use an internal dispatch function that uses the switch
statement to select
the right function.
The ID 0
is reserved for uninitialized function pointers which then cause a panic in the dispatch function when called.
In the old code generator, internal function pointers are initialized with a special function that always causes a panic. This causes a storage write at construction time for internal function pointers in storage.
Cleanup
The old code generator only performs cleanup before an operation whose result could be affected by the values of the dirty bits. The new code generator performs cleanup after any operation that can result in dirty bits. The hope is that the optimizer will be powerful enough to eliminate redundant cleanup operations.
For example:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.8.1;
contract C {
function f(uint8 _a) public pure returns (uint _r1, uint _r2)
{
_a = ~_a;
assembly {
_r1 := _a
}
_r2 = _a;
}
}
The function f(1)
returns the following values:
Old code generator: (
fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe
,00000000000000000000000000000000000000000000000000000000000000fe
)New code generator: (
00000000000000000000000000000000000000000000000000000000000000fe
,00000000000000000000000000000000000000000000000000000000000000fe
)
Note that, unlike the new code generator, the old code generator does not perform a cleanup after the bit-not assignment (_a = ~_a
).
This results in different values being assigned (within the inline assembly block) to return value _r1
between the old and new code generators.
However, both code generators perform a cleanup before the new value of _a
is assigned to _r2
.
Layout of State Variables in Storage
State variables of contracts are stored in storage in a compact way such
that multiple values sometimes use the same storage slot.
Except for dynamically-sized arrays and mappings (see below), data is stored
contiguously item after item starting with the first state variable,
which is stored in slot 0
. For each variable,
a size in bytes is determined according to its type.
Multiple, contiguous items that need less than 32 bytes are packed into a single
storage slot if possible, according to the following rules:
The first item in a storage slot is stored lower-order aligned.
Value types use only as many bytes as are necessary to store them.
If a value type does not fit the remaining part of a storage slot, it is stored in the next storage slot.
Structs and array data always start a new slot and their items are packed tightly according to these rules.
Items following struct or array data always start a new storage slot.
For contracts that use inheritance, the ordering of state variables is determined by the C3-linearized order of contracts starting with the most base-ward contract. If allowed by the above rules, state variables from different contracts do share the same storage slot.
The elements of structs and arrays are stored after each other, just as if they were given as individual values.
Avertissement
When using elements that are smaller than 32 bytes, your contract’s gas usage may be higher. This is because the EVM operates on 32 bytes at a time. Therefore, if the element is smaller than that, the EVM must use more operations in order to reduce the size of the element from 32 bytes to the desired size.
It might be beneficial to use reduced-size types if you are dealing with storage values because the compiler will pack multiple elements into one storage slot, and thus, combine multiple reads or writes into a single operation. If you are not reading or writing all the values in a slot at the same time, this can have the opposite effect, though: When one value is written to a multi-value storage slot, the storage slot has to be read first and then combined with the new value such that other data in the same slot is not destroyed.
When dealing with function arguments or memory values, there is no inherent benefit because the compiler does not pack these values.
Finally, in order to allow the EVM to optimize for this, ensure that you try to order your
storage variables and struct
members such that they can be packed tightly. For example,
declaring your storage variables in the order of uint128, uint128, uint256
instead of
uint128, uint256, uint128
, as the former will only take up two slots of storage whereas the
latter will take up three.
Note
The layout of state variables in storage is considered to be part of the external interface of Solidity due to the fact that storage pointers can be passed to libraries. This means that any change to the rules outlined in this section is considered a breaking change of the language and due to its critical nature should be considered very carefully before being executed. In the event of such a breaking change, we would want to release a compatibility mode in which the compiler would generate bytecode supporting the old layout.
Mappings and Dynamic Arrays
Due to their unpredictable size, mappings and dynamically-sized array types cannot be stored « in between » the state variables preceding and following them. Instead, they are considered to occupy only 32 bytes with regards to the rules above and the elements they contain are stored starting at a different storage slot that is computed using a Keccak-256 hash.
Assume the storage location of the mapping or array ends up being a slot p
after applying the storage layout rules.
For dynamic arrays,
this slot stores the number of elements in the array (byte arrays and
strings are an exception, see below).
For mappings, the slot stays empty, but it is still needed to ensure that even if there are
two mappings next to each other, their content ends up at different storage locations.
Array data is located starting at keccak256(p)
and it is laid out in the same way as
statically-sized array data would: One element after the other, potentially sharing
storage slots if the elements are not longer than 16 bytes. Dynamic arrays of dynamic arrays apply this
rule recursively. The location of element x[i][j]
, where the type of x
is uint24[][]
, is
computed as follows (again, assuming x
itself is stored at slot p
):
The slot is keccak256(keccak256(p) + i) + floor(j / floor(256 / 24))
and
the element can be obtained from the slot data v
using (v >> ((j % floor(256 / 24)) * 24)) & type(uint24).max
.
The value corresponding to a mapping key k
is located at keccak256(h(k) . p)
where .
is concatenation and h
is a function that is applied to the key depending on its type:
for value types,
h
pads the value to 32 bytes in the same way as when storing the value in memory.for strings and byte arrays,
h(k)
is just the unpadded data.
If the mapping value is a non-value type, the computed slot marks the start of the data. If the value is of struct type, for example, you have to add an offset corresponding to the struct member to reach the member.
As an example, consider the following contract:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.0 <0.9.0;
contract C {
struct S { uint16 a; uint16 b; uint256 c; }
uint x;
mapping(uint => mapping(uint => S)) data;
}
Let us compute the storage location of data[4][9].c
.
The position of the mapping itself is 1
(the variable x
with 32 bytes precedes it).
This means data[4]
is stored at keccak256(uint256(4) . uint256(1))
. The type of data[4]
is
again a mapping and the data for data[4][9]
starts at slot
keccak256(uint256(9) . keccak256(uint256(4) . uint256(1)))
.
The slot offset of the member c
inside the struct S
is 1
because a
and b
are packed
in a single slot. This means the slot for
data[4][9].c
is keccak256(uint256(9) . keccak256(uint256(4) . uint256(1))) + 1
.
The type of the value is uint256
, so it uses a single slot.
bytes
and string
bytes
and string
are encoded identically.
In general, the encoding is similar to bytes1[]
, in the sense that there is a slot for the array itself and
a data area that is computed using a keccak256
hash of that slot’s position.
However, for short values (shorter than 32 bytes) the array elements are stored together with the length in the same slot.
In particular: if the data is at most 31
bytes long, the elements are stored
in the higher-order bytes (left aligned) and the lowest-order byte stores the value length * 2
.
For byte arrays that store data which is 32
or more bytes long, the main slot p
stores length * 2 + 1
and the data is
stored as usual in keccak256(p)
. This means that you can distinguish a short array from a long array
by checking if the lowest bit is set: short (not set) and long (set).
Note
Handling invalidly encoded slots is currently not supported but may be added in the future.
If you are compiling via the experimental IR-based compiler pipeline, reading an invalidly encoded
slot results in a Panic(0x22)
error.
JSON Output
The storage layout of a contract can be requested via
the standard JSON interface. The output is a JSON object containing two keys,
storage
and types
. The storage
object is an array where each
element has the following form:
{
"astId": 2,
"contract": "fileA:A",
"label": "x",
"offset": 0,
"slot": "0",
"type": "t_uint256"
}
The example above is the storage layout of contract A { uint x; }
from source unit fileA
and
astId
is the id of the AST node of the state variable’s declarationcontract
is the name of the contract including its path as prefixlabel
is the name of the state variableoffset
is the offset in bytes within the storage slot according to the encodingslot
is the storage slot where the state variable resides or starts. This number may be very large and therefore its JSON value is represented as a string.type
is an identifier used as key to the variable’s type information (described in the following)
The given type
, in this case t_uint256
represents an element in
types
, which has the form:
{
"encoding": "inplace",
"label": "uint256",
"numberOfBytes": "32",
}
where
encoding
how the data is encoded in storage, where the possible values are:label
is the canonical type name.numberOfBytes
is the number of used bytes (as a decimal string). Note that ifnumberOfBytes > 32
this means that more than one slot is used.
Some types have extra information besides the four above. Mappings contain
its key
and value
types (again referencing an entry in this mapping
of types), arrays have its base
type, and structs list their members
in
the same format as the top-level storage
(see above).
Note
The JSON output format of a contract’s storage layout is still considered experimental and is subject to change in non-breaking releases of Solidity.
The following example shows a contract and its storage layout, containing value and reference types, types that are encoded packed, and nested types.
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.0 <0.9.0;
contract A {
struct S {
uint128 a;
uint128 b;
uint[2] staticArray;
uint[] dynArray;
}
uint x;
uint y;
S s;
address addr;
mapping (uint => mapping (address => bool)) map;
uint[] array;
string s1;
bytes b1;
}
{
"storage": [
{
"astId": 15,
"contract": "fileA:A",
"label": "x",
"offset": 0,
"slot": "0",
"type": "t_uint256"
},
{
"astId": 17,
"contract": "fileA:A",
"label": "y",
"offset": 0,
"slot": "1",
"type": "t_uint256"
},
{
"astId": 20,
"contract": "fileA:A",
"label": "s",
"offset": 0,
"slot": "2",
"type": "t_struct(S)13_storage"
},
{
"astId": 22,
"contract": "fileA:A",
"label": "addr",
"offset": 0,
"slot": "6",
"type": "t_address"
},
{
"astId": 28,
"contract": "fileA:A",
"label": "map",
"offset": 0,
"slot": "7",
"type": "t_mapping(t_uint256,t_mapping(t_address,t_bool))"
},
{
"astId": 31,
"contract": "fileA:A",
"label": "array",
"offset": 0,
"slot": "8",
"type": "t_array(t_uint256)dyn_storage"
},
{
"astId": 33,
"contract": "fileA:A",
"label": "s1",
"offset": 0,
"slot": "9",
"type": "t_string_storage"
},
{
"astId": 35,
"contract": "fileA:A",
"label": "b1",
"offset": 0,
"slot": "10",
"type": "t_bytes_storage"
}
],
"types": {
"t_address": {
"encoding": "inplace",
"label": "address",
"numberOfBytes": "20"
},
"t_array(t_uint256)2_storage": {
"base": "t_uint256",
"encoding": "inplace",
"label": "uint256[2]",
"numberOfBytes": "64"
},
"t_array(t_uint256)dyn_storage": {
"base": "t_uint256",
"encoding": "dynamic_array",
"label": "uint256[]",
"numberOfBytes": "32"
},
"t_bool": {
"encoding": "inplace",
"label": "bool",
"numberOfBytes": "1"
},
"t_bytes_storage": {
"encoding": "bytes",
"label": "bytes",
"numberOfBytes": "32"
},
"t_mapping(t_address,t_bool)": {
"encoding": "mapping",
"key": "t_address",
"label": "mapping(address => bool)",
"numberOfBytes": "32",
"value": "t_bool"
},
"t_mapping(t_uint256,t_mapping(t_address,t_bool))": {
"encoding": "mapping",
"key": "t_uint256",
"label": "mapping(uint256 => mapping(address => bool))",
"numberOfBytes": "32",
"value": "t_mapping(t_address,t_bool)"
},
"t_string_storage": {
"encoding": "bytes",
"label": "string",
"numberOfBytes": "32"
},
"t_struct(S)13_storage": {
"encoding": "inplace",
"label": "struct A.S",
"members": [
{
"astId": 3,
"contract": "fileA:A",
"label": "a",
"offset": 0,
"slot": "0",
"type": "t_uint128"
},
{
"astId": 5,
"contract": "fileA:A",
"label": "b",
"offset": 16,
"slot": "0",
"type": "t_uint128"
},
{
"astId": 9,
"contract": "fileA:A",
"label": "staticArray",
"offset": 0,
"slot": "1",
"type": "t_array(t_uint256)2_storage"
},
{
"astId": 12,
"contract": "fileA:A",
"label": "dynArray",
"offset": 0,
"slot": "3",
"type": "t_array(t_uint256)dyn_storage"
}
],
"numberOfBytes": "128"
},
"t_uint128": {
"encoding": "inplace",
"label": "uint128",
"numberOfBytes": "16"
},
"t_uint256": {
"encoding": "inplace",
"label": "uint256",
"numberOfBytes": "32"
}
}
}
Layout in Memory
Solidity reserves four 32-byte slots, with specific byte ranges (inclusive of endpoints) being used as follows:
0x00
-0x3f
(64 bytes): scratch space for hashing methods0x40
-0x5f
(32 bytes): currently allocated memory size (aka. free memory pointer)0x60
-0x7f
(32 bytes): zero slot
Scratch space can be used between statements (i.e. within inline assembly). The zero slot
is used as initial value for dynamic memory arrays and should never be written to
(the free memory pointer points to 0x80
initially).
Solidity always places new objects at the free memory pointer and memory is never freed (this might change in the future).
Elements in memory arrays in Solidity always occupy multiples of 32 bytes (this
is even true for bytes1[]
, but not for bytes
and string
).
Multi-dimensional memory arrays are pointers to memory arrays. The length of a
dynamic array is stored at the first slot of the array and followed by the array
elements.
Avertissement
There are some operations in Solidity that need a temporary memory area larger than 64 bytes and therefore will not fit into the scratch space. They will be placed where the free memory points to, but given their short lifetime, the pointer is not updated. The memory may or may not be zeroed out. Because of this, one should not expect the free memory to point to zeroed out memory.
While it may seem like a good idea to use msize
to arrive at a
definitely zeroed out memory area, using such a pointer non-temporarily
without updating the free memory pointer can have unexpected results.
Differences to Layout in Storage
As described above the layout in memory is different from the layout in storage. Below there are some examples.
Example for Difference in Arrays
The following array occupies 32 bytes (1 slot) in storage, but 128 bytes (4 items with 32 bytes each) in memory.
uint8[4] a;
Example for Difference in Struct Layout
The following struct occupies 96 bytes (3 slots of 32 bytes) in storage, but 128 bytes (4 items with 32 bytes each) in memory.
struct S {
uint a;
uint b;
uint8 c;
uint8 d;
}
Layout of Call Data
The input data for a function call is assumed to be in the format defined by the ABI specification. Among others, the ABI specification requires arguments to be padded to multiples of 32 bytes. The internal function calls use a different convention.
Arguments for the constructor of a contract are directly appended at the end of the
contract’s code, also in ABI encoding. The constructor will access them through a hard-coded offset, and
not by using the codesize
opcode, since this of course changes when appending
data to the code.
Cleaning Up Variables
When a value is shorter than 256 bit, in some cases the remaining bits must be cleaned. The Solidity compiler is designed to clean such remaining bits before any operations that might be adversely affected by the potential garbage in the remaining bits. For example, before writing a value to memory, the remaining bits need to be cleared because the memory contents can be used for computing hashes or sent as the data of a message call. Similarly, before storing a value in the storage, the remaining bits need to be cleaned because otherwise the garbled value can be observed.
Note that access via inline assembly is not considered such an operation: If you use inline assembly to access Solidity variables shorter than 256 bits, the compiler does not guarantee that the value is properly cleaned up.
Moreover, we do not clean the bits if the immediately
following operation is not affected. For instance, since any non-zero
value is considered true
by JUMPI
instruction, we do not clean
the boolean values before they are used as the condition for
JUMPI
.
In addition to the design principle above, the Solidity compiler cleans input data when it is loaded onto the stack.
Different types have different rules for cleaning up invalid values:
Type |
Valid Values |
Invalid Values Mean |
---|---|---|
enum of n members |
0 until n - 1 |
exception |
bool |
0 or 1 |
1 |
signed integers |
sign-extended word |
currently silently wraps; in the future exceptions will be thrown |
unsigned integers |
higher bits zeroed |
currently silently wraps; in the future exceptions will be thrown |
Source Mappings
As part of the AST output, the compiler provides the range of the source code that is represented by the respective node in the AST. This can be used for various purposes ranging from static analysis tools that report errors based on the AST and debugging tools that highlight local variables and their uses.
Furthermore, the compiler can also generate a mapping from the bytecode to the range in the source code that generated the instruction. This is again important for static analysis tools that operate on bytecode level and for displaying the current position in the source code inside a debugger or for breakpoint handling. This mapping also contains other information, like the jump type and the modifier depth (see below).
Both kinds of source mappings use integer identifiers to refer to source files.
The identifier of a source file is stored in
output['sources'][sourceName]['id']
where output
is the output of the
standard-json compiler interface parsed as JSON.
For some utility routines, the compiler generates « internal » source files
that are not part of the original input but are referenced from the source
mappings. These source files together with their identifiers can be
obtained via output['contracts'][sourceName][contractName]['evm']['bytecode']['generatedSources']
.
Note
In the case of instructions that are not associated with any particular source file,
the source mapping assigns an integer identifier of -1
. This may happen for
bytecode sections stemming from compiler-generated inline assembly statements.
The source mappings inside the AST use the following notation:
s:l:f
Where s
is the byte-offset to the start of the range in the source file,
l
is the length of the source range in bytes and f
is the source
index mentioned above.
The encoding in the source mapping for the bytecode is more complicated:
It is a list of s:l:f:j:m
separated by ;
. Each of these
elements corresponds to an instruction, i.e. you cannot use the byte offset
but have to use the instruction offset (push instructions are longer than a single byte).
The fields s
, l
and f
are as above. j
can be either
i
, o
or -
signifying whether a jump instruction goes into a
function, returns from a function or is a regular jump as part of e.g. a loop.
The last field, m
, is an integer that denotes the « modifier depth ». This depth
is increased whenever the placeholder statement (_
) is entered in a modifier
and decreased when it is left again. This allows debuggers to track tricky cases
like the same modifier being used twice or multiple placeholder statements being
used in a single modifier.
In order to compress these source mappings especially for bytecode, the following rules are used:
If a field is empty, the value of the preceding element is used.
If a
:
is missing, all following fields are considered empty.
This means the following source mappings represent the same information:
1:2:1;1:9:1;2:1:2;2:1:2;2:1:2
1:2:1;:9;2:1:2;;
Important to note is that when the verbatim builtin is used, the source mappings will be invalid: The builtin is considered a single instruction instead of potentially multiple.
The Optimizer
The Solidity compiler uses two different optimizer modules: The « old » optimizer that operates at the opcode level and the « new » optimizer that operates on Yul IR code.
The opcode-based optimizer applies a set of simplification rules to opcodes. It also combines equal code sets and removes unused code.
The Yul-based optimizer is much more powerful, because it can work across function calls. For example, arbitrary jumps are not possible in Yul, so it is possible to compute the side-effects of each function. Consider two function calls, where the first does not modify storage and the second does modify storage. If their arguments and return values do not depend on each other, we can reorder the function calls. Similarly, if a function is side-effect free and its result is multiplied by zero, you can remove the function call completely.
Currently, the parameter --optimize
activates the opcode-based optimizer for the
generated bytecode and the Yul optimizer for the Yul code generated internally, for example for ABI coder v2.
One can use solc --ir-optimized --optimize
to produce an
optimized experimental Yul IR for a Solidity source. Similarly, one can use solc --strict-assembly --optimize
for a stand-alone Yul mode.
You can find more details on both optimizer modules and their optimization steps below.
Benefits of Optimizing Solidity Code
Overall, the optimizer tries to simplify complicated expressions, which reduces both code size and execution cost, i.e., it can reduce gas needed for contract deployment as well as for external calls made to the contract. It also specializes or inlines functions. Especially function inlining is an operation that can cause much bigger code, but it is often done because it results in opportunities for more simplifications.
Differences between Optimized and Non-Optimized Code
Generally, the most visible difference is that constant expressions are evaluated at compile time.
When it comes to the ASM output, one can also notice a reduction of equivalent or duplicate
code blocks (compare the output of the flags --asm
and --asm --optimize
). However,
when it comes to the Yul/intermediate-representation, there can be significant
differences, for example, functions may be inlined, combined, or rewritten to eliminate
redundancies, etc. (compare the output between the flags --ir
and
--optimize --ir-optimized
).
Optimizer Parameter Runs
The number of runs (--optimize-runs
) specifies roughly how often each opcode of the
deployed code will be executed across the life-time of the contract. This means it is a
trade-off parameter between code size (deploy cost) and code execution cost (cost after deployment).
A « runs » parameter of « 1 » will produce short but expensive code. In contrast, a larger « runs »
parameter will produce longer but more gas efficient code. The maximum value of the parameter
is 2**32-1
.
Note
A common misconception is that this parameter specifies the number of iterations of the optimizer. This is not true: The optimizer will always run as many times as it can still improve the code.
Opcode-Based Optimizer Module
The opcode-based optimizer module operates on assembly code. It splits the
sequence of instructions into basic blocks at JUMPs
and JUMPDESTs
.
Inside these blocks, the optimizer analyzes the instructions and records every modification to the stack,
memory, or storage as an expression which consists of an instruction and
a list of arguments which are pointers to other expressions.
Additionally, the opcode-based optimizer
uses a component called « CommonSubexpressionEliminator » that, amongst other
tasks, finds expressions that are always equal (on every input) and combines
them into an expression class. It first tries to find each new
expression in a list of already known expressions. If no such matches are found,
it simplifies the expression according to rules like
constant + constant = sum_of_constants
or X * 1 = X
. Since this is
a recursive process, we can also apply the latter rule if the second factor
is a more complex expression which we know always evaluates to one.
Certain optimizer steps symbolically track the storage and memory locations. For example, this information is used to compute Keccak-256 hashes that can be evaluated during compile time. Consider the sequence:
PUSH 32
PUSH 0
CALLDATALOAD
PUSH 100
DUP2
MSTORE
KECCAK256
or the equivalent Yul
let x := calldataload(0)
mstore(x, 100)
let value := keccak256(x, 32)
In this case, the optimizer tracks the value at a memory location calldataload(0)
and then
realizes that the Keccak-256 hash can be evaluated at compile time. This only works if there is no
other instruction that modifies memory between the mstore
and keccak256
. So if there is an
instruction that writes to memory (or storage), then we need to erase the knowledge of the current
memory (or storage). There is, however, an exception to this erasing, when we can easily see that
the instruction doesn’t write to a certain location.
For example,
let x := calldataload(0)
mstore(x, 100)
// Current knowledge memory location x -> 100
let y := add(x, 32)
// Does not clear the knowledge that x -> 100, since y does not write to [x, x + 32)
mstore(y, 200)
// This Keccak-256 can now be evaluated
let value := keccak256(x, 32)
Therefore, modifications to storage and memory locations, of say location l
, must erase
knowledge about storage or memory locations which may be equal to l
. More specifically, for
storage, the optimizer has to erase all knowledge of symbolic locations, that may be equal to l
and for memory, the optimizer has to erase all knowledge of symbolic locations that may not be at
least 32 bytes away. If m
denotes an arbitrary location, then this decision on erasure is done
by computing the value sub(l, m)
. For storage, if this value evaluates to a literal that is
non-zero, then the knowledge about m
will be kept. For memory, if the value evaluates to a
literal that is between 32
and 2**256 - 32
, then the knowledge about m
will be kept. In
all other cases, the knowledge about m
will be erased.
After this process, we know which expressions have to be on the stack at the end, and have a list of modifications to memory and storage. This information is stored together with the basic blocks and is used to link them. Furthermore, knowledge about the stack, storage and memory configuration is forwarded to the next block(s).
If we know the targets of all JUMP
and JUMPI
instructions,
we can build a complete control flow graph of the program. If there is only
one target we do not know (this can happen as in principle, jump targets can
be computed from inputs), we have to erase all knowledge about the input state
of a block as it can be the target of the unknown JUMP
. If the opcode-based
optimizer module finds a JUMPI
whose condition evaluates to a constant, it transforms it
to an unconditional jump.
As the last step, the code in each block is re-generated. The optimizer creates a dependency graph from the expressions on the stack at the end of the block, and it drops every operation that is not part of this graph. It generates code that applies the modifications to memory and storage in the order they were made in the original code (dropping modifications which were found not to be needed). Finally, it generates all values that are required to be on the stack in the correct place.
These steps are applied to each basic block and the newly generated code
is used as replacement if it is smaller. If a basic block is split at a
JUMPI
and during the analysis, the condition evaluates to a constant,
the JUMPI
is replaced based on the value of the constant. Thus code like
uint x = 7;
data[7] = 9;
if (data[x] != x + 2) // this condition is never true
return 2;
else
return 1;
simplifies to this:
data[7] = 9;
return 1;
Simple Inlining
Since Solidity version 0.8.2, there is another optimizer step that replaces certain
jumps to blocks containing « simple » instructions ending with a « jump » by a copy of these instructions.
This corresponds to inlining of simple, small Solidity or Yul functions. In particular, the sequence
PUSHTAG(tag) JUMP
may be replaced, whenever the JUMP
is marked as jump « into » a
function and behind tag
there is a basic block (as described above for the
« CommonSubexpressionEliminator ») that ends in another JUMP
which is marked as a jump
« out of » a function.
In particular, consider the following prototypical example of assembly generated for a call to an internal Solidity function:
tag_return
tag_f
jump // in
tag_return:
...opcodes after call to f...
tag_f:
...body of function f...
jump // out
As long as the body of the function is a continuous basic block, the « Inliner » can replace tag_f jump
by
the block at tag_f
resulting in:
tag_return
...body of function f...
jump
tag_return:
...opcodes after call to f...
tag_f:
...body of function f...
jump // out
Now ideally, the other optimizer steps described above will result in the return tag push being moved towards the remaining jump resulting in:
...body of function f...
tag_return
jump
tag_return:
...opcodes after call to f...
tag_f:
...body of function f...
jump // out
In this situation the « PeepholeOptimizer » will remove the return jump. Ideally, all of this can be done
for all references to tag_f
leaving it unused, s.t. it can be removed, yielding:
...body of function f...
...opcodes after call to f...
So the call to function f
is inlined and the original definition of f
can be removed.
Inlining like this is attempted, whenever a heuristics suggests that inlining is cheaper over the lifetime of a contract than not inlining. This heuristics depends on the size of the function body, the number of other references to its tag (approximating the number of calls to the function) and the expected number of executions of the contract (the global optimizer parameter « runs »).
Yul-Based Optimizer Module
The Yul-based optimizer consists of several stages and components that all transform the AST in a semantically equivalent way. The goal is to end up either with code that is shorter or at least only marginally longer but will allow further optimization steps.
Avertissement
Since the optimizer is under heavy development, the information here might be outdated. If you rely on a certain functionality, please reach out to the team directly.
The optimizer currently follows a purely greedy strategy and does not do any backtracking.
All components of the Yul-based optimizer module are explained below. The following transformation steps are the main components:
SSA Transform
Common Subexpression Eliminator
Expression Simplifier
Redundant Assign Eliminator
Full Inliner
Optimizer Steps
This is a list of all steps the Yul-based optimizer sorted alphabetically. You can find more information on the individual steps and their sequence below.
Selecting Optimizations
By default the optimizer applies its predefined sequence of optimization steps to
the generated assembly. You can override this sequence and supply your own using
the --yul-optimizations
option:
solc --optimize --ir-optimized --yul-optimizations 'dhfoD[xarrscLMcCTU]uljmul'
The sequence inside [...]
will be applied multiple times in a loop until the Yul code
remains unchanged or until the maximum number of rounds (currently 12) has been reached.
Available abbreviations are listed in the Yul optimizer docs.
Preprocessing
The preprocessing components perform transformations to get the program into a certain normal form that is easier to work with. This normal form is kept during the rest of the optimization process.
Disambiguator
The disambiguator takes an AST and returns a fresh copy where all identifiers have unique names in the input AST. This is a prerequisite for all other optimizer stages. One of the benefits is that identifier lookup does not need to take scopes into account which simplifies the analysis needed for other steps.
All subsequent stages have the property that all names stay unique. This means if a new identifier needs to be introduced, a new unique name is generated.
FunctionHoister
The function hoister moves all function definitions to the end of the topmost block. This is a semantically equivalent transformation as long as it is performed after the disambiguation stage. The reason is that moving a definition to a higher-level block cannot decrease its visibility and it is impossible to reference variables defined in a different function.
The benefit of this stage is that function definitions can be looked up more easily and functions can be optimized in isolation without having to traverse the AST completely.
FunctionGrouper
The function grouper has to be applied after the disambiguator and the function hoister. Its effect is that all topmost elements that are not function definitions are moved into a single block which is the first statement of the root block.
After this step, a program has the following normal form:
{ I F... }
Where I
is a (potentially empty) block that does not contain any function definitions (not even recursively)
and F
is a list of function definitions such that no function contains a function definition.
The benefit of this stage is that we always know where the list of function begins.
ForLoopConditionIntoBody
This transformation moves the loop-iteration condition of a for-loop into loop body.
We need this transformation because ExpressionSplitter will not
apply to iteration condition expressions (the C
in the following example).
for { Init... } C { Post... } {
Body...
}
is transformed to
for { Init... } 1 { Post... } {
if iszero(C) { break }
Body...
}
This transformation can also be useful when paired with LoopInvariantCodeMotion
, since
invariants in the loop-invariant conditions can then be taken outside the loop.
ForLoopInitRewriter
This transformation moves the initialization part of a for-loop to before the loop:
for { Init... } C { Post... } {
Body...
}
is transformed to
Init...
for {} C { Post... } {
Body...
}
This eases the rest of the optimization process because we can ignore the complicated scoping rules of the for loop initialisation block.
VarDeclInitializer
This step rewrites variable declarations so that all of them are initialized.
Declarations like let x, y
are split into multiple declaration statements.
Only supports initializing with the zero literal for now.
Pseudo-SSA Transformation
The purpose of this components is to get the program into a longer form, so that other components can more easily work with it. The final representation will be similar to a static-single-assignment (SSA) form, with the difference that it does not make use of explicit « phi » functions which combines the values from different branches of control flow because such a feature does not exist in the Yul language. Instead, when control flow merges, if a variable is re-assigned in one of the branches, a new SSA variable is declared to hold its current value, so that the following expressions still only need to reference SSA variables.
An example transformation is the following:
{
let a := calldataload(0)
let b := calldataload(0x20)
if gt(a, 0) {
b := mul(b, 0x20)
}
a := add(a, 1)
sstore(a, add(b, 0x20))
}
When all the following transformation steps are applied, the program will look as follows:
{
let _1 := 0
let a_9 := calldataload(_1)
let a := a_9
let _2 := 0x20
let b_10 := calldataload(_2)
let b := b_10
let _3 := 0
let _4 := gt(a_9, _3)
if _4
{
let _5 := 0x20
let b_11 := mul(b_10, _5)
b := b_11
}
let b_12 := b
let _6 := 1
let a_13 := add(a_9, _6)
let _7 := 0x20
let _8 := add(b_12, _7)
sstore(a_13, _8)
}
Note that the only variable that is re-assigned in this snippet is b
.
This re-assignment cannot be avoided because b
has different values
depending on the control flow. All other variables never change their
value once they are defined. The advantage of this property is that
variables can be freely moved around and references to them
can be exchanged by their initial value (and vice-versa),
as long as these values are still valid in the new context.
Of course, the code here is far from being optimized. To the contrary, it is much longer. The hope is that this code will be easier to work with and furthermore, there are optimizer steps that undo these changes and make the code more compact again at the end.
ExpressionSplitter
The expression splitter turns expressions like add(mload(0x123), mul(mload(0x456), 0x20))
into a sequence of declarations of unique variables that are assigned sub-expressions
of that expression so that each function call has only variables
as arguments.
The above would be transformed into
{
let _1 := 0x20
let _2 := 0x456
let _3 := mload(_2)
let _4 := mul(_3, _1)
let _5 := 0x123
let _6 := mload(_5)
let z := add(_6, _4)
}
Note that this transformation does not change the order of opcodes or function calls.
It is not applied to loop iteration-condition, because the loop control flow does not allow this « outlining » of the inner expressions in all cases. We can sidestep this limitation by applying ForLoopConditionIntoBody to move the iteration condition into loop body.
The final program should be in a form such that (with the exception of loop conditions) function calls cannot appear nested inside expressions and all function call arguments have to be variables.
The benefits of this form are that it is much easier to re-order the sequence of opcodes and it is also easier to perform function call inlining. Furthermore, it is simpler to replace individual parts of expressions or re-organize the « expression tree ». The drawback is that such code is much harder to read for humans.
SSATransform
This stage tries to replace repeated assignments to existing variables by declarations of new variables as much as possible. The reassignments are still there, but all references to the reassigned variables are replaced by the newly declared variables.
Example:
{
let a := 1
mstore(a, 2)
a := 3
}
is transformed to
{
let a_1 := 1
let a := a_1
mstore(a_1, 2)
let a_3 := 3
a := a_3
}
Exact semantics:
For any variable a
that is assigned to somewhere in the code
(variables that are declared with value and never re-assigned
are not modified) perform the following transforms:
replace
let a := v
bylet a_i := v let a := a_i
replace
a := v
bylet a_i := v a := a_i
wherei
is a number such thata_i
is yet unused.
Furthermore, always record the current value of i
used for a
and replace each
reference to a
by a_i
.
The current value mapping is cleared for a variable a
at the end of each block
in which it was assigned to and at the end of the for loop init block if it is assigned
inside the for loop body or post block.
If a variable’s value is cleared according to the rule above and the variable is declared outside
the block, a new SSA variable will be created at the location where control flow joins,
this includes the beginning of loop post/body block and the location right after
If/Switch/ForLoop/Block statement.
After this stage, the Redundant Assign Eliminator is recommended to remove the unnecessary intermediate assignments.
This stage provides best results if the Expression Splitter and the Common Subexpression Eliminator are run right before it, because then it does not generate excessive amounts of variables. On the other hand, the Common Subexpression Eliminator could be more efficient if run after the SSA transform.
RedundantAssignEliminator
The SSA transform always generates an assignment of the form a := a_i
, even though
these might be unnecessary in many cases, like the following example:
{
let a := 1
a := mload(a)
a := sload(a)
sstore(a, 1)
}
The SSA transform converts this snippet to the following:
{
let a_1 := 1
let a := a_1
let a_2 := mload(a_1)
a := a_2
let a_3 := sload(a_2)
a := a_3
sstore(a_3, 1)
}
The Redundant Assign Eliminator removes all the three assignments to a
, because
the value of a
is not used and thus turn this
snippet into strict SSA form:
{
let a_1 := 1
let a_2 := mload(a_1)
let a_3 := sload(a_2)
sstore(a_3, 1)
}
Of course the intricate parts of determining whether an assignment is redundant or not are connected to joining control flow.
The component works as follows in detail:
The AST is traversed twice: in an information gathering step and in the actual removal step. During information gathering, we maintain a mapping from assignment statements to the three states « unused », « undecided » and « used » which signifies whether the assigned value will be used later by a reference to the variable.
When an assignment is visited, it is added to the mapping in the « undecided » state (see remark about for loops below) and every other assignment to the same variable that is still in the « undecided » state is changed to « unused ». When a variable is referenced, the state of any assignment to that variable still in the « undecided » state is changed to « used ».
At points where control flow splits, a copy of the mapping is handed over to each branch. At points where control flow joins, the two mappings coming from the two branches are combined in the following way: Statements that are only in one mapping or have the same state are used unchanged. Conflicting values are resolved in the following way:
« unused », « undecided » -> « undecided »
« unused », « used » -> « used »
« undecided, « used » -> « used »
For for-loops, the condition, body and post-part are visited twice, taking the joining control-flow at the condition into account. In other words, we create three control flow paths: Zero runs of the loop, one run and two runs and then combine them at the end.
Simulating a third run or even more is unnecessary, which can be seen as follows:
A state of an assignment at the beginning of the iteration will deterministically
result in a state of that assignment at the end of the iteration. Let this
state mapping function be called f
. The combination of the three different
states unused
, undecided
and used
as explained above is the max
operation where unused = 0
, undecided = 1
and used = 2
.
The proper way would be to compute
max(s, f(s), f(f(s)), f(f(f(s))), ...)
as state after the loop. Since f
just has a range of three different values,
iterating it has to reach a cycle after at most three iterations,
and thus f(f(f(s)))
has to equal one of s
, f(s)
, or f(f(s))
and thus
max(s, f(s), f(f(s))) = max(s, f(s), f(f(s)), f(f(f(s))), ...).
In summary, running the loop at most twice is enough because there are only three different states.
For switch statements that have a « default »-case, there is no control-flow part that skips the switch.
When a variable goes out of scope, all statements still in the « undecided » state are changed to « unused », unless the variable is the return parameter of a function - there, the state changes to « used ».
In the second traversal, all assignments that are in the « unused » state are removed.
This step is usually run right after the SSA transform to complete the generation of the pseudo-SSA.
Tools
Movability
Movability is a property of an expression. It roughly means that the expression is side-effect free and its evaluation only depends on the values of variables and the call-constant state of the environment. Most expressions are movable. The following parts make an expression non-movable:
function calls (might be relaxed in the future if all statements in the function are movable)
opcodes that (can) have side-effects (like
call
orselfdestruct
)opcodes that read or write memory, storage or external state information
opcodes that depend on the current PC, memory size or returndata size
DataflowAnalyzer
The Dataflow Analyzer is not an optimizer step itself but is used as a tool
by other components. While traversing the AST, it tracks the current value of
each variable, as long as that value is a movable expression.
It records the variables that are part of the expression
that is currently assigned to each other variable. Upon each assignment to
a variable a
, the current stored value of a
is updated and
all stored values of all variables b
are cleared whenever a
is part
of the currently stored expression for b
.
At control-flow joins, knowledge about variables is cleared if they have or would be assigned in any of the control-flow paths. For instance, upon entering a for loop, all variables are cleared that will be assigned during the body or the post block.
Expression-Scale Simplifications
These simplification passes change expressions and replace them by equivalent and hopefully simpler expressions.
CommonSubexpressionEliminator
This step uses the Dataflow Analyzer and replaces subexpressions that syntactically match the current value of a variable by a reference to that variable. This is an equivalence transform because such subexpressions have to be movable.
All subexpressions that are identifiers themselves are replaced by their current value if the value is an identifier.
The combination of the two rules above allow to compute a local value numbering, which means that if two variables have the same value, one of them will always be unused. The Unused Pruner or the Redundant Assign Eliminator will then be able to fully eliminate such variables.
This step is especially efficient if the expression splitter is run before. If the code is in pseudo-SSA form, the values of variables are available for a longer time and thus we have a higher chance of expressions to be replaceable.
The expression simplifier will be able to perform better replacements if the common subexpression eliminator was run right before it.
Expression Simplifier
The Expression Simplifier uses the Dataflow Analyzer and makes use
of a list of equivalence transforms on expressions like X + 0 -> X
to simplify the code.
It tries to match patterns like X + 0
on each subexpression.
During the matching procedure, it resolves variables to their currently
assigned expressions to be able to match more deeply nested patterns
even when the code is in pseudo-SSA form.
Some of the patterns like X - X -> 0
can only be applied as long
as the expression X
is movable, because otherwise it would remove its potential side-effects.
Since variable references are always movable, even if their current
value might not be, the Expression Simplifier is again more powerful
in split or pseudo-SSA form.
LiteralRematerialiser
To be documented.
LoadResolver
Optimisation stage that replaces expressions of type sload(x)
and mload(x)
by the value
currently stored in storage resp. memory, if known.
Works best if the code is in SSA form.
Prerequisite: Disambiguator, ForLoopInitRewriter.
ReasoningBasedSimplifier
This optimizer uses SMT solvers to check whether if
conditions are constant.
If
constraints AND condition
is UNSAT, the condition is never true and the whole body can be removed.If
constraints AND NOT condition
is UNSAT, the condition is always true and can be replaced by1
.
The simplifications above can only be applied if the condition is movable.
It is only effective on the EVM dialect, but safe to use on other dialects.
Prerequisite: Disambiguator, SSATransform.
Statement-Scale Simplifications
CircularReferencesPruner
This stage removes functions that call each other but are neither externally referenced nor referenced from the outermost context.
ConditionalSimplifier
The Conditional Simplifier inserts assignments to condition variables if the value can be determined from the control-flow.
Destroys SSA form.
Currently, this tool is very limited, mostly because we do not yet have support for boolean types. Since conditions only check for expressions being nonzero, we cannot assign a specific value.
Current features:
switch cases: insert « <condition> := <caseLabel> »
after if statement with terminating control-flow, insert « <condition> := 0 »
Future features:
allow replacements by « 1 »
take termination of user-defined functions into account
Works best with SSA form and if dead code removal has run before.
Prerequisite: Disambiguator.
ConditionalUnsimplifier
Reverse of Conditional Simplifier.
ControlFlowSimplifier
Simplifies several control-flow structures:
replace if with empty body with pop(condition)
remove empty default switch case
remove empty switch case if no default case exists
replace switch with no cases with pop(expression)
turn switch with single case into if
replace switch with only default case with pop(expression) and body
replace switch with const expr with matching case body
replace
for
with terminating control flow and without other break/continue byif
remove
leave
at the end of a function.
None of these operations depend on the data flow. The StructuralSimplifier performs similar tasks that do depend on data flow.
The ControlFlowSimplifier does record the presence or absence of break
and continue
statements during its traversal.
Prerequisite: Disambiguator, FunctionHoister, ForLoopInitRewriter. Important: Introduces EVM opcodes and thus can only be used on EVM code for now.
DeadCodeEliminator
This optimization stage removes unreachable code.
Unreachable code is any code within a block which is preceded by a leave, return, invalid, break, continue, selfdestruct or revert.
Function definitions are retained as they might be called by earlier code and thus are considered reachable.
Because variables declared in a for loop’s init block have their scope extended to the loop body, we require ForLoopInitRewriter to run before this step.
Prerequisite: ForLoopInitRewriter, Function Hoister, Function Grouper
EqualStoreEliminator
This steps removes mstore(k, v)
and sstore(k, v)
calls if
there was a previous call to mstore(k, v)
/ sstore(k, v)
,
no other store in between and the values of k
and v
did not change.
This simple step is effective if run after the SSA transform and the Common Subexpression Eliminator, because SSA will make sure that the variables will not change and the Common Subexpression Eliminator re-uses exactly the same variable if the value is known to be the same.
Prerequisites: Disambiguator, ForLoopInitRewriter
UnusedPruner
This step removes the definitions of all functions that are never referenced.
It also removes the declaration of variables that are never referenced. If the declaration assigns a value that is not movable, the expression is retained, but its value is discarded.
All movable expression statements (expressions that are not assigned) are removed.
StructuralSimplifier
This is a general step that performs various kinds of simplifications on a structural level:
replace if statement with empty body by
pop(condition)
replace if statement with true condition by its body
remove if statement with false condition
turn switch with single case into if
replace switch with only default case by
pop(expression)
and bodyreplace switch with literal expression by matching case body
replace for loop with false condition by its initialization part
This component uses the Dataflow Analyzer.
BlockFlattener
This stage eliminates nested blocks by inserting the statement in the inner block at the appropriate place in the outer block. It depends on the FunctionGrouper and does not flatten the outermost block to keep the form produced by the FunctionGrouper.
{
{
let x := 2
{
let y := 3
mstore(x, y)
}
}
}
is transformed to
{
{
let x := 2
let y := 3
mstore(x, y)
}
}
As long as the code is disambiguated, this does not cause a problem because the scopes of variables can only grow.
LoopInvariantCodeMotion
This optimization moves movable SSA variable declarations outside the loop.
Only statements at the top level in a loop’s body or post block are considered, i.e variable declarations inside conditional branches will not be moved out of the loop.
Requirements:
The Disambiguator, ForLoopInitRewriter and FunctionHoister must be run upfront.
Expression splitter and SSA transform should be run upfront to obtain better result.
Function-Level Optimizations
FunctionSpecializer
This step specializes the function with its literal arguments.
If a function, say, function f(a, b) { sstore (a, b) }
, is called with literal arguments, for
example, f(x, 5)
, where x
is an identifier, it could be specialized by creating a new
function f_1
that takes only one argument, i.e.,
function f_1(a_1) {
let b_1 := 5
sstore(a_1, b_1)
}
Other optimization steps will be able to make more simplifications to the function. The optimization step is mainly useful for functions that would not be inlined.
Prerequisites: Disambiguator, FunctionHoister
LiteralRematerialiser is recommended as a prerequisite, even though it’s not required for correctness.
UnusedFunctionParameterPruner
This step removes unused parameters in a function.
If a parameter is unused, like c
and y
in, function f(a,b,c) -> x, y { x := div(a,b) }
, we
remove the parameter and create a new « linking » function as follows:
function f(a,b) -> x { x := div(a,b) }
function f2(a,b,c) -> x, y { x := f(a,b) }
and replace all references to f
by f2
.
The inliner should be run afterwards to make sure that all references to f2
are replaced by
f
.
Prerequisites: Disambiguator, FunctionHoister, LiteralRematerialiser.
The step LiteralRematerialiser is not required for correctness. It helps deal with cases such as:
function f(x) -> y { revert(y, y} }
where the literal y
will be replaced by its value 0
,
allowing us to rewrite the function.
EquivalentFunctionCombiner
If two functions are syntactically equivalent, while allowing variable renaming but not any re-ordering, then any reference to one of the functions is replaced by the other.
The actual removal of the function is performed by the Unused Pruner.
Function Inlining
ExpressionInliner
This component of the optimizer performs restricted function inlining by inlining functions that can be inlined inside functional expressions, i.e. functions that:
return a single value.
have a body like
r := <functional expression>
.neither reference themselves nor
r
in the right hand side.
Furthermore, for all parameters, all of the following need to be true:
The argument is movable.
The parameter is either referenced less than twice in the function body, or the argument is rather cheap (« cost » of at most 1, like a constant up to 0xff).
Example: The function to be inlined has the form of function f(...) -> r { r := E }
where
E
is an expression that does not reference r
and all arguments in the function call are movable expressions.
The result of this inlining is always a single expression.
This component can only be used on sources with unique names.
FullInliner
The Full Inliner replaces certain calls of certain functions by the function’s body. This is not very helpful in most cases, because it just increases the code size but does not have a benefit. Furthermore, code is usually very expensive and we would often rather have shorter code than more efficient code. In same cases, though, inlining a function can have positive effects on subsequent optimizer steps. This is the case if one of the function arguments is a constant, for example.
During inlining, a heuristic is used to tell if the function call should be inlined or not. The current heuristic does not inline into « large » functions unless the called function is tiny. Functions that are only used once are inlined, as well as medium-sized functions, while function calls with constant arguments allow slightly larger functions.
In the future, we may include a backtracking component that, instead of inlining a function right away, only specializes it, which means that a copy of the function is generated where a certain parameter is always replaced by a constant. After that, we can run the optimizer on this specialized function. If it results in heavy gains, the specialized function is kept, otherwise the original function is used instead.
Cleanup
The cleanup is performed at the end of the optimizer run. It tries to combine split expressions into deeply nested ones again and also improves the « compilability » for stack machines by eliminating variables as much as possible.
ExpressionJoiner
This is the opposite operation of the expression splitter. It turns a sequence of variable declarations that have exactly one reference into a complex expression. This stage fully preserves the order of function calls and opcode executions. It does not make use of any information concerning the commutativity of the opcodes; if moving the value of a variable to its place of use would change the order of any function call or opcode execution, the transformation is not performed.
Note that the component will not move the assigned value of a variable assignment or a variable that is referenced more than once.
The snippet let x := add(0, 2) let y := mul(x, mload(2))
is not transformed,
because it would cause the order of the call to the opcodes add
and
mload
to be swapped - even though this would not make a difference
because add
is movable.
When reordering opcodes like that, variable references and literals are ignored.
Because of that, the snippet let x := add(0, 2) let y := mul(x, 3)
is
transformed to let y := mul(add(0, 2), 3)
, even though the add
opcode
would be executed after the evaluation of the literal 3
.
SSAReverser
This is a tiny step that helps in reversing the effects of the SSA transform if it is combined with the Common Subexpression Eliminator and the Unused Pruner.
The SSA form we generate is detrimental to code generation on the EVM and WebAssembly alike because it generates many local variables. It would be better to just re-use existing variables with assignments instead of fresh variable declarations.
The SSA transform rewrites
let a := calldataload(0)
mstore(a, 1)
to
let a_1 := calldataload(0)
let a := a_1
mstore(a_1, 1)
let a_2 := calldataload(0x20)
a := a_2
The problem is that instead of a
, the variable a_1
is used
whenever a
was referenced. The SSA transform changes statements
of this form by just swapping out the declaration and the assignment. The above
snippet is turned into
let a := calldataload(0)
let a_1 := a
mstore(a_1, 1)
a := calldataload(0x20)
let a_2 := a
This is a very simple equivalence transform, but when we now run the
Common Subexpression Eliminator, it will replace all occurrences of a_1
by a
(until a
is re-assigned). The Unused Pruner will then
eliminate the variable a_1
altogether and thus fully reverse the
SSA transform.
StackCompressor
One problem that makes code generation for the Ethereum Virtual Machine hard is the fact that there is a hard limit of 16 slots for reaching down the expression stack. This more or less translates to a limit of 16 local variables. The stack compressor takes Yul code and compiles it to EVM bytecode. Whenever the stack difference is too large, it records the function this happened in.
For each function that caused such a problem, the Rematerialiser is called with a special request to aggressively eliminate specific variables sorted by the cost of their values.
On failure, this procedure is repeated multiple times.
Rematerialiser
The rematerialisation stage tries to replace variable references by the expression that was last assigned to the variable. This is of course only beneficial if this expression is comparatively cheap to evaluate. Furthermore, it is only semantically equivalent if the value of the expression did not change between the point of assignment and the point of use. The main benefit of this stage is that it can save stack slots if it leads to a variable being eliminated completely (see below), but it can also save a DUP opcode on the EVM if the expression is very cheap.
The Rematerialiser uses the Dataflow Analyzer to track the current values of variables, which are always movable. If the value is very cheap or the variable was explicitly requested to be eliminated, the variable reference is replaced by its current value.
ForLoopConditionOutOfBody
Reverses the transformation of ForLoopConditionIntoBody.
For any movable c
, it turns
for { ... } 1 { ... } {
if iszero(c) { break }
...
}
into
for { ... } c { ... } {
...
}
and it turns
for { ... } 1 { ... } {
if c { break }
...
}
into
for { ... } iszero(c) { ... } {
...
}
The LiteralRematerialiser should be run before this step.
WebAssembly specific
MainFunction
Changes the topmost block to be a function with a specific name (« main ») which has no inputs nor outputs.
Depends on the Function Grouper.
Contract Metadata
The Solidity compiler automatically generates a JSON file, the contract metadata, that contains information about the compiled contract. You can use this file to query the compiler version, the sources used, the ABI and NatSpec documentation to more safely interact with the contract and verify its source code.
The compiler appends by default the IPFS hash of the metadata file to the end of the bytecode (for details, see below) of each contract, so that you can retrieve the file in an authenticated way without having to resort to a centralized data provider. The other available options are the Swarm hash and not appending the metadata hash to the bytecode. These can be configured via the Standard JSON Interface.
You have to publish the metadata file to IPFS, Swarm, or another service so
that others can access it. You create the file by using the solc --metadata
command that generates a file called ContractName_meta.json
. It contains
IPFS and Swarm references to the source code, so you have to upload all source
files and the metadata file.
The metadata file has the following format. The example below is presented in a human-readable way. Properly formatted metadata should use quotes correctly, reduce whitespace to a minimum and sort the keys of all objects to arrive at a unique formatting. Comments are not permitted and used here only for explanatory purposes.
{
// Required: The version of the metadata format
"version": "1",
// Required: Source code language, basically selects a "sub-version"
// of the specification
"language": "Solidity",
// Required: Details about the compiler, contents are specific
// to the language.
"compiler": {
// Required for Solidity: Version of the compiler
"version": "0.4.6+commit.2dabbdf0.Emscripten.clang",
// Optional: Hash of the compiler binary which produced this output
"keccak256": "0x123..."
},
// Required: Compilation source files/source units, keys are file names
"sources":
{
"myFile.sol": {
// Required: keccak256 hash of the source file
"keccak256": "0x123...",
// Required (unless "content" is used, see below): Sorted URL(s)
// to the source file, protocol is more or less arbitrary, but a
// Swarm URL is recommended
"urls": [ "bzzr://56ab..." ],
// Optional: SPDX license identifier as given in the source file
"license": "MIT"
},
"destructible": {
// Required: keccak256 hash of the source file
"keccak256": "0x234...",
// Required (unless "url" is used): literal contents of the source file
"content": "contract destructible is owned { function destroy() { if (msg.sender == owner) selfdestruct(owner); } }"
}
},
// Required: Compiler settings
"settings":
{
// Required for Solidity: Sorted list of remappings
"remappings": [ ":g=/dir" ],
// Optional: Optimizer settings. The fields "enabled" and "runs" are deprecated
// and are only given for backwards-compatibility.
"optimizer": {
"enabled": true,
"runs": 500,
"details": {
// peephole defaults to "true"
"peephole": true,
// inliner defaults to "true"
"inliner": true,
// jumpdestRemover defaults to "true"
"jumpdestRemover": true,
"orderLiterals": false,
"deduplicate": false,
"cse": false,
"constantOptimizer": false,
"yul": true,
// Optional: Only present if "yul" is "true"
"yulDetails": {
"stackAllocation": false,
"optimizerSteps": "dhfoDgvulfnTUtnIf..."
}
}
},
"metadata": {
// Reflects the setting used in the input json, defaults to false
"useLiteralContent": true,
// Reflects the setting used in the input json, defaults to "ipfs"
"bytecodeHash": "ipfs"
},
// Required for Solidity: File and name of the contract or library this
// metadata is created for.
"compilationTarget": {
"myFile.sol": "MyContract"
},
// Required for Solidity: Addresses for libraries used
"libraries": {
"MyLib": "0x123123..."
}
},
// Required: Generated information about the contract.
"output":
{
// Required: ABI definition of the contract
"abi": [/* ... */],
// Required: NatSpec user documentation of the contract
"userdoc": [/* ... */],
// Required: NatSpec developer documentation of the contract
"devdoc": [/* ... */]
}
}
Avertissement
Since the bytecode of the resulting contract contains the metadata hash by default, any change to the metadata might result in a change of the bytecode. This includes changes to a filename or path, and since the metadata includes a hash of all the sources used, a single whitespace change results in different metadata, and different bytecode.
Note
The ABI definition above has no fixed order. It can change with compiler versions. Starting from Solidity version 0.5.12, though, the array maintains a certain order.
Encoding of the Metadata Hash in the Bytecode
Because we might support other ways to retrieve the metadata file in the future,
the mapping {"ipfs": <IPFS hash>, "solc": <compiler version>}
is stored
CBOR-encoded. Since the mapping might
contain more keys (see below) and the beginning of that
encoding is not easy to find, its length is added in a two-byte big-endian
encoding. The current version of the Solidity compiler usually adds the following
to the end of the deployed bytecode
0xa2
0x64 'i' 'p' 'f' 's' 0x58 0x22 <34 bytes IPFS hash>
0x64 's' 'o' 'l' 'c' 0x43 <3 byte version encoding>
0x00 0x33
So in order to retrieve the data, the end of the deployed bytecode can be checked to match that pattern and use the IPFS hash to retrieve the file.
Whereas release builds of solc use a 3 byte encoding of the version as shown above (one byte each for major, minor and patch version number), prerelease builds will instead use a complete version string including commit hash and build date.
Note
The CBOR mapping can also contain other keys, so it is better to fully
decode the data instead of relying on it starting with 0xa264
.
For example, if any experimental features that affect code generation
are used, the mapping will also contain "experimental": true
.
Note
The compiler currently uses the IPFS hash of the metadata by default, but
it may also use the bzzr1 hash or some other hash in the future, so do
not rely on this sequence to start with 0xa2 0x64 'i' 'p' 'f' 's'
. We
might also add additional data to this CBOR structure, so the best option
is to use a proper CBOR parser.
Usage for Automatic Interface Generation and NatSpec
The metadata is used in the following way: A component that wants to interact with a contract (e.g. Mist or any wallet) retrieves the code of the contract, from that the IPFS/Swarm hash of a file which is then retrieved. That file is JSON-decoded into a structure like above.
The component can then use the ABI to automatically generate a rudimentary user interface for the contract.
Furthermore, the wallet can use the NatSpec user documentation to display a confirmation message to the user whenever they interact with the contract, together with requesting authorization for the transaction signature.
For additional information, read Ethereum Natural Language Specification (NatSpec) format.
Usage for Source Code Verification
In order to verify the compilation, sources can be retrieved from IPFS/Swarm
via the link in the metadata file.
The compiler of the correct version (which is checked to be part of the « official » compilers)
is invoked on that input with the specified settings. The resulting
bytecode is compared to the data of the creation transaction or CREATE
opcode data.
This automatically verifies the metadata since its hash is part of the bytecode.
Excess data corresponds to the constructor input data, which should be decoded
according to the interface and presented to the user.
In the repository sourcify (npm package) you can see example code that shows how to use this feature.
Contract ABI Specification
Basic Design
The Contract Application Binary Interface (ABI) is the standard way to interact with contracts in the Ethereum ecosystem, both from outside the blockchain and for contract-to-contract interaction. Data is encoded according to its type, as described in this specification. The encoding is not self describing and thus requires a schema in order to decode.
We assume the interface functions of a contract are strongly typed, known at compilation time and static. We assume that all contracts will have the interface definitions of any contracts they call available at compile-time.
This specification does not address contracts whose interface is dynamic or otherwise known only at run-time.
Function Selector
The first four bytes of the call data for a function call specifies the function to be called. It is the first (left, high-order in big-endian) four bytes of the Keccak-256 hash of the signature of the function. The signature is defined as the canonical expression of the basic prototype without data location specifier, i.e. the function name with the parenthesised list of parameter types. Parameter types are split by a single comma - no spaces are used.
Note
The return type of a function is not part of this signature. In Solidity’s function overloading return types are not considered. The reason is to keep function call resolution context-independent. The JSON description of the ABI however contains both inputs and outputs.
Argument Encoding
Starting from the fifth byte, the encoded arguments follow. This encoding is also used in other places, e.g. the return values and also event arguments are encoded in the same way, without the four bytes specifying the function.
Types
The following elementary types exist:
uint<M>
: unsigned integer type ofM
bits,0 < M <= 256
,M % 8 == 0
. e.g.uint32
,uint8
,uint256
.int<M>
: two’s complement signed integer type ofM
bits,0 < M <= 256
,M % 8 == 0
.address
: equivalent touint160
, except for the assumed interpretation and language typing. For computing the function selector,address
is used.uint
,int
: synonyms foruint256
,int256
respectively. For computing the function selector,uint256
andint256
have to be used.bool
: equivalent touint8
restricted to the values 0 and 1. For computing the function selector,bool
is used.fixed<M>x<N>
: signed fixed-point decimal number ofM
bits,8 <= M <= 256
,M % 8 == 0
, and0 < N <= 80
, which denotes the valuev
asv / (10 ** N)
.ufixed<M>x<N>
: unsigned variant offixed<M>x<N>
.fixed
,ufixed
: synonyms forfixed128x18
,ufixed128x18
respectively. For computing the function selector,fixed128x18
andufixed128x18
have to be used.bytes<M>
: binary type ofM
bytes,0 < M <= 32
.function
: an address (20 bytes) followed by a function selector (4 bytes). Encoded identical tobytes24
.
The following (fixed-size) array type exists:
<type>[M]
: a fixed-length array ofM
elements,M >= 0
, of the given type.Note
While this ABI specification can express fixed-length arrays with zero elements, they’re not supported by the compiler.
The following non-fixed-size types exist:
bytes
: dynamic sized byte sequence.string
: dynamic sized unicode string assumed to be UTF-8 encoded.<type>[]
: a variable-length array of elements of the given type.
Types can be combined to a tuple by enclosing them inside parentheses, separated by commas:
(T1,T2,...,Tn)
: tuple consisting of the typesT1
, …,Tn
,n >= 0
It is possible to form tuples of tuples, arrays of tuples and so on. It is also possible to form zero-tuples (where n == 0
).
Mapping Solidity to ABI types
Solidity supports all the types presented above with the same names with the exception of tuples. On the other hand, some Solidity types are not supported by the ABI. The following table shows on the left column Solidity types that are not part of the ABI, and on the right column the ABI types that represent them.
Solidity |
ABI |
---|---|
|
|
|
|
|
|
its underlying value type |
|
|
Avertissement
Before version 0.8.0
enums could have more than 256 members and were represented by the
smallest integer type just big enough to hold the value of any member.
Design Criteria for the Encoding
The encoding is designed to have the following properties, which are especially useful if some arguments are nested arrays:
The number of reads necessary to access a value is at most the depth of the value inside the argument array structure, i.e. four reads are needed to retrieve
a_i[k][l][r]
. In a previous version of the ABI, the number of reads scaled linearly with the total number of dynamic parameters in the worst case.The data of a variable or array element is not interleaved with other data and it is relocatable, i.e. it only uses relative « addresses ».
Formal Specification of the Encoding
We distinguish static and dynamic types. Static types are encoded in-place and dynamic types are encoded at a separately allocated location after the current block.
Definition: The following types are called « dynamic »:
bytes
string
T[]
for anyT
T[k]
for any dynamicT
and anyk >= 0
(T1,...,Tk)
ifTi
is dynamic for some1 <= i <= k
All other types are called « static ».
Definition: len(a)
is the number of bytes in a binary string a
.
The type of len(a)
is assumed to be uint256
.
We define enc
, the actual encoding, as a mapping of values of the ABI types to binary strings such
that len(enc(X))
depends on the value of X
if and only if the type of X
is dynamic.
Definition: For any ABI value X
, we recursively define enc(X)
, depending
on the type of X
being
(T1,...,Tk)
fork >= 0
and any typesT1
, …,Tk
enc(X) = head(X(1)) ... head(X(k)) tail(X(1)) ... tail(X(k))
where
X = (X(1), ..., X(k))
andhead
andtail
are defined forTi
as follows:if
Ti
is static:head(X(i)) = enc(X(i))
andtail(X(i)) = ""
(the empty string)otherwise, i.e. if
Ti
is dynamic:head(X(i)) = enc(len( head(X(1)) ... head(X(k)) tail(X(1)) ... tail(X(i-1)) ))
tail(X(i)) = enc(X(i))
Note that in the dynamic case,
head(X(i))
is well-defined since the lengths of the head parts only depend on the types and not the values. The value ofhead(X(i))
is the offset of the beginning oftail(X(i))
relative to the start ofenc(X)
.T[k]
for anyT
andk
:enc(X) = enc((X[0], ..., X[k-1]))
i.e. it is encoded as if it were a tuple with
k
elements of the same type.T[]
whereX
hask
elements (k
is assumed to be of typeuint256
):enc(X) = enc(k) enc([X[0], ..., X[k-1]])
i.e. it is encoded as if it were an array of static size
k
, prefixed with the number of elements.bytes
, of lengthk
(which is assumed to be of typeuint256
):enc(X) = enc(k) pad_right(X)
, i.e. the number of bytes is encoded as auint256
followed by the actual value ofX
as a byte sequence, followed by the minimum number of zero-bytes such thatlen(enc(X))
is a multiple of 32.string
:enc(X) = enc(enc_utf8(X))
, i.e.X
is UTF-8 encoded and this value is interpreted as ofbytes
type and encoded further. Note that the length used in this subsequent encoding is the number of bytes of the UTF-8 encoded string, not its number of characters.uint<M>
:enc(X)
is the big-endian encoding ofX
, padded on the higher-order (left) side with zero-bytes such that the length is 32 bytes.address
: as in theuint160
caseint<M>
:enc(X)
is the big-endian two’s complement encoding ofX
, padded on the higher-order (left) side with0xff
bytes for negativeX
and with zero-bytes for non-negativeX
such that the length is 32 bytes.bool
: as in theuint8
case, where1
is used fortrue
and0
forfalse
fixed<M>x<N>
:enc(X)
isenc(X * 10**N)
whereX * 10**N
is interpreted as aint256
.fixed
: as in thefixed128x18
caseufixed<M>x<N>
:enc(X)
isenc(X * 10**N)
whereX * 10**N
is interpreted as auint256
.ufixed
: as in theufixed128x18
casebytes<M>
:enc(X)
is the sequence of bytes inX
padded with trailing zero-bytes to a length of 32 bytes.
Note that for any X
, len(enc(X))
is a multiple of 32.
Function Selector and Argument Encoding
All in all, a call to the function f
with parameters a_1, ..., a_n
is encoded as
function_selector(f) enc((a_1, ..., a_n))
and the return values v_1, ..., v_k
of f
are encoded as
enc((v_1, ..., v_k))
i.e. the values are combined into a tuple and encoded.
Examples
Given the contract:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.16 <0.9.0;
contract Foo {
function bar(bytes3[2] memory) public pure {}
function baz(uint32 x, bool y) public pure returns (bool r) { r = x > 32 || y; }
function sam(bytes memory, bool, uint[] memory) public pure {}
}
Thus for our Foo
example if we wanted to call baz
with the parameters 69
and
true
, we would pass 68 bytes total, which can be broken down into:
0xcdcd77c0
: the Method ID. This is derived as the first 4 bytes of the Keccak hash of the ASCII form of the signaturebaz(uint32,bool)
.0x0000000000000000000000000000000000000000000000000000000000000045
: the first parameter, a uint32 value69
padded to 32 bytes0x0000000000000000000000000000000000000000000000000000000000000001
: the second parameter - booleantrue
, padded to 32 bytes
In total:
0xcdcd77c000000000000000000000000000000000000000000000000000000000000000450000000000000000000000000000000000000000000000000000000000000001
It returns a single bool
. If, for example, it were to return false
, its output would be
the single byte array 0x0000000000000000000000000000000000000000000000000000000000000000
, a single bool.
If we wanted to call bar
with the argument ["abc", "def"]
, we would pass 68 bytes total, broken down into:
0xfce353f6
: the Method ID. This is derived from the signaturebar(bytes3[2])
.0x6162630000000000000000000000000000000000000000000000000000000000
: the first part of the first parameter, abytes3
value"abc"
(left-aligned).0x6465660000000000000000000000000000000000000000000000000000000000
: the second part of the first parameter, abytes3
value"def"
(left-aligned).
In total:
0xfce353f661626300000000000000000000000000000000000000000000000000000000006465660000000000000000000000000000000000000000000000000000000000
If we wanted to call sam
with the arguments "dave"
, true
and [1,2,3]
, we would
pass 292 bytes total, broken down into:
0xa5643bf2
: the Method ID. This is derived from the signaturesam(bytes,bool,uint256[])
. Note thatuint
is replaced with its canonical representationuint256
.0x0000000000000000000000000000000000000000000000000000000000000060
: the location of the data part of the first parameter (dynamic type), measured in bytes from the start of the arguments block. In this case,0x60
.0x0000000000000000000000000000000000000000000000000000000000000001
: the second parameter: boolean true.0x00000000000000000000000000000000000000000000000000000000000000a0
: the location of the data part of the third parameter (dynamic type), measured in bytes. In this case,0xa0
.0x0000000000000000000000000000000000000000000000000000000000000004
: the data part of the first argument, it starts with the length of the byte array in elements, in this case, 4.0x6461766500000000000000000000000000000000000000000000000000000000
: the contents of the first argument: the UTF-8 (equal to ASCII in this case) encoding of"dave"
, padded on the right to 32 bytes.0x0000000000000000000000000000000000000000000000000000000000000003
: the data part of the third argument, it starts with the length of the array in elements, in this case, 3.0x0000000000000000000000000000000000000000000000000000000000000001
: the first entry of the third parameter.0x0000000000000000000000000000000000000000000000000000000000000002
: the second entry of the third parameter.0x0000000000000000000000000000000000000000000000000000000000000003
: the third entry of the third parameter.
In total:
0xa5643bf20000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000000464617665000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000003
Use of Dynamic Types
A call to a function with the signature f(uint,uint32[],bytes10,bytes)
with values
(0x123, [0x456, 0x789], "1234567890", "Hello, world!")
is encoded in the following way:
We take the first four bytes of sha3("f(uint256,uint32[],bytes10,bytes)")
, i.e. 0x8be65246
.
Then we encode the head parts of all four arguments. For the static types uint256
and bytes10
,
these are directly the values we want to pass, whereas for the dynamic types uint32[]
and bytes
,
we use the offset in bytes to the start of their data area, measured from the start of the value
encoding (i.e. not counting the first four bytes containing the hash of the function signature). These are:
0x0000000000000000000000000000000000000000000000000000000000000123
(0x123
padded to 32 bytes)0x0000000000000000000000000000000000000000000000000000000000000080
(offset to start of data part of second parameter, 4*32 bytes, exactly the size of the head part)0x3132333435363738393000000000000000000000000000000000000000000000
("1234567890"
padded to 32 bytes on the right)0x00000000000000000000000000000000000000000000000000000000000000e0
(offset to start of data part of fourth parameter = offset to start of data part of first dynamic parameter + size of data part of first dynamic parameter = 4*32 + 3*32 (see below))
After this, the data part of the first dynamic argument, [0x456, 0x789]
follows:
0x0000000000000000000000000000000000000000000000000000000000000002
(number of elements of the array, 2)0x0000000000000000000000000000000000000000000000000000000000000456
(first element)0x0000000000000000000000000000000000000000000000000000000000000789
(second element)
Finally, we encode the data part of the second dynamic argument, "Hello, world!"
:
0x000000000000000000000000000000000000000000000000000000000000000d
(number of elements (bytes in this case): 13)0x48656c6c6f2c20776f726c642100000000000000000000000000000000000000
("Hello, world!"
padded to 32 bytes on the right)
All together, the encoding is (newline after function selector and each 32-bytes for clarity):
0x8be65246
0000000000000000000000000000000000000000000000000000000000000123
0000000000000000000000000000000000000000000000000000000000000080
3132333435363738393000000000000000000000000000000000000000000000
00000000000000000000000000000000000000000000000000000000000000e0
0000000000000000000000000000000000000000000000000000000000000002
0000000000000000000000000000000000000000000000000000000000000456
0000000000000000000000000000000000000000000000000000000000000789
000000000000000000000000000000000000000000000000000000000000000d
48656c6c6f2c20776f726c642100000000000000000000000000000000000000
Let us apply the same principle to encode the data for a function with a signature g(uint[][],string[])
with values ([[1, 2], [3]], ["one", "two", "three"])
but start from the most atomic parts of the encoding:
First we encode the length and data of the first embedded dynamic array [1, 2]
of the first root array [[1, 2], [3]]
:
0x0000000000000000000000000000000000000000000000000000000000000002
(number of elements in the first array, 2; the elements themselves are1
and2
)0x0000000000000000000000000000000000000000000000000000000000000001
(first element)0x0000000000000000000000000000000000000000000000000000000000000002
(second element)
Then we encode the length and data of the second embedded dynamic array [3]
of the first root array [[1, 2], [3]]
:
0x0000000000000000000000000000000000000000000000000000000000000001
(number of elements in the second array, 1; the element is3
)0x0000000000000000000000000000000000000000000000000000000000000003
(first element)
Then we need to find the offsets a
and b
for their respective dynamic arrays [1, 2]
and [3]
.
To calculate the offsets we can take a look at the encoded data of the first root array [[1, 2], [3]]
enumerating each line in the encoding:
0 - a - offset of [1, 2]
1 - b - offset of [3]
2 - 0000000000000000000000000000000000000000000000000000000000000002 - count for [1, 2]
3 - 0000000000000000000000000000000000000000000000000000000000000001 - encoding of 1
4 - 0000000000000000000000000000000000000000000000000000000000000002 - encoding of 2
5 - 0000000000000000000000000000000000000000000000000000000000000001 - count for [3]
6 - 0000000000000000000000000000000000000000000000000000000000000003 - encoding of 3
Offset a
points to the start of the content of the array [1, 2]
which is line
2 (64 bytes); thus a = 0x0000000000000000000000000000000000000000000000000000000000000040
.
Offset b
points to the start of the content of the array [3]
which is line 5 (160 bytes);
thus b = 0x00000000000000000000000000000000000000000000000000000000000000a0
.
Then we encode the embedded strings of the second root array:
0x0000000000000000000000000000000000000000000000000000000000000003
(number of characters in word"one"
)0x6f6e650000000000000000000000000000000000000000000000000000000000
(utf8 representation of word"one"
)0x0000000000000000000000000000000000000000000000000000000000000003
(number of characters in word"two"
)0x74776f0000000000000000000000000000000000000000000000000000000000
(utf8 representation of word"two"
)0x0000000000000000000000000000000000000000000000000000000000000005
(number of characters in word"three"
)0x7468726565000000000000000000000000000000000000000000000000000000
(utf8 representation of word"three"
)
In parallel to the first root array, since strings are dynamic elements we need to find their offsets c
, d
and e
:
0 - c - offset for "one"
1 - d - offset for "two"
2 - e - offset for "three"
3 - 0000000000000000000000000000000000000000000000000000000000000003 - count for "one"
4 - 6f6e650000000000000000000000000000000000000000000000000000000000 - encoding of "one"
5 - 0000000000000000000000000000000000000000000000000000000000000003 - count for "two"
6 - 74776f0000000000000000000000000000000000000000000000000000000000 - encoding of "two"
7 - 0000000000000000000000000000000000000000000000000000000000000005 - count for "three"
8 - 7468726565000000000000000000000000000000000000000000000000000000 - encoding of "three"
Offset c
points to the start of the content of the string "one"
which is line 3 (96 bytes);
thus c = 0x0000000000000000000000000000000000000000000000000000000000000060
.
Offset d
points to the start of the content of the string "two"
which is line 5 (160 bytes);
thus d = 0x00000000000000000000000000000000000000000000000000000000000000a0
.
Offset e
points to the start of the content of the string "three"
which is line 7 (224 bytes);
thus e = 0x00000000000000000000000000000000000000000000000000000000000000e0
.
Note that the encodings of the embedded elements of the root arrays are not dependent on each other
and have the same encodings for a function with a signature g(string[],uint[][])
.
Then we encode the length of the first root array:
0x0000000000000000000000000000000000000000000000000000000000000002
(number of elements in the first root array, 2; the elements themselves are[1, 2]
and[3]
)
Then we encode the length of the second root array:
0x0000000000000000000000000000000000000000000000000000000000000003
(number of strings in the second root array, 3; the strings themselves are"one"
,"two"
and"three"
)
Finally we find the offsets f
and g
for their respective root dynamic arrays [[1, 2], [3]]
and
["one", "two", "three"]
, and assemble parts in the correct order:
0x2289b18c - function signature
0 - f - offset of [[1, 2], [3]]
1 - g - offset of ["one", "two", "three"]
2 - 0000000000000000000000000000000000000000000000000000000000000002 - count for [[1, 2], [3]]
3 - 0000000000000000000000000000000000000000000000000000000000000040 - offset of [1, 2]
4 - 00000000000000000000000000000000000000000000000000000000000000a0 - offset of [3]
5 - 0000000000000000000000000000000000000000000000000000000000000002 - count for [1, 2]
6 - 0000000000000000000000000000000000000000000000000000000000000001 - encoding of 1
7 - 0000000000000000000000000000000000000000000000000000000000000002 - encoding of 2
8 - 0000000000000000000000000000000000000000000000000000000000000001 - count for [3]
9 - 0000000000000000000000000000000000000000000000000000000000000003 - encoding of 3
10 - 0000000000000000000000000000000000000000000000000000000000000003 - count for ["one", "two", "three"]
11 - 0000000000000000000000000000000000000000000000000000000000000060 - offset for "one"
12 - 00000000000000000000000000000000000000000000000000000000000000a0 - offset for "two"
13 - 00000000000000000000000000000000000000000000000000000000000000e0 - offset for "three"
14 - 0000000000000000000000000000000000000000000000000000000000000003 - count for "one"
15 - 6f6e650000000000000000000000000000000000000000000000000000000000 - encoding of "one"
16 - 0000000000000000000000000000000000000000000000000000000000000003 - count for "two"
17 - 74776f0000000000000000000000000000000000000000000000000000000000 - encoding of "two"
18 - 0000000000000000000000000000000000000000000000000000000000000005 - count for "three"
19 - 7468726565000000000000000000000000000000000000000000000000000000 - encoding of "three"
Offset f
points to the start of the content of the array [[1, 2], [3]]
which is line 2 (64 bytes);
thus f = 0x0000000000000000000000000000000000000000000000000000000000000040
.
Offset g
points to the start of the content of the array ["one", "two", "three"]
which is line 10 (320 bytes);
thus g = 0x0000000000000000000000000000000000000000000000000000000000000140
.
Events
Events are an abstraction of the Ethereum logging/event-watching protocol. Log entries provide the contract’s address, a series of up to four topics and some arbitrary length binary data. Events leverage the existing function ABI in order to interpret this (together with an interface spec) as a properly typed structure.
Given an event name and series of event parameters, we split them into two sub-series: those which are indexed and those which are not. Those which are indexed, which may number up to 3 (for non-anonymous events) or 4 (for anonymous ones), are used alongside the Keccak hash of the event signature to form the topics of the log entry. Those which are not indexed form the byte array of the event.
In effect, a log entry using this ABI is described as:
address
: the address of the contract (intrinsically provided by Ethereum);topics[0]
:keccak(EVENT_NAME+"("+EVENT_ARGS.map(canonical_type_of).join(",")+")")
(canonical_type_of
is a function that simply returns the canonical type of a given argument, e.g. foruint indexed foo
, it would returnuint256
). This value is only present intopics[0]
if the event is not declared asanonymous
;topics[n]
:abi_encode(EVENT_INDEXED_ARGS[n - 1])
if the event is not declared asanonymous
orabi_encode(EVENT_INDEXED_ARGS[n])
if it is (EVENT_INDEXED_ARGS
is the series ofEVENT_ARGS
that are indexed);data
: ABI encoding ofEVENT_NON_INDEXED_ARGS
(EVENT_NON_INDEXED_ARGS
is the series ofEVENT_ARGS
that are not indexed,abi_encode
is the ABI encoding function used for returning a series of typed values from a function, as described above).
For all types of length at most 32 bytes, the EVENT_INDEXED_ARGS
array contains
the value directly, padded or sign-extended (for signed integers) to 32 bytes, just as for regular ABI encoding.
However, for all « complex » types or types of dynamic length, including all arrays, string
, bytes
and structs,
EVENT_INDEXED_ARGS
will contain the Keccak hash of a special in-place encoded value
(see Encoding of Indexed Event Parameters), rather than the encoded value directly.
This allows applications to efficiently query for values of dynamic-length types
(by setting the hash of the encoded value as the topic), but leaves applications unable
to decode indexed values they have not queried for. For dynamic-length types,
application developers face a trade-off between fast search for predetermined values
(if the argument is indexed) and legibility of arbitrary values (which requires that
the arguments not be indexed). Developers may overcome this tradeoff and achieve both
efficient search and arbitrary legibility by defining events with two arguments — one
indexed, one not — intended to hold the same value.
Errors
In case of a failure inside a contract, the contract can use a special opcode to abort execution and revert all state changes. In addition to these effects, descriptive data can be returned to the caller. This descriptive data is the encoding of an error and its arguments in the same way as data for a function call.
As an example, let us consider the following contract whose transfer
function always
reverts with a custom error of « insufficient balance »:
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.4;
contract TestToken {
error InsufficientBalance(uint256 available, uint256 required);
function transfer(address /*to*/, uint amount) public pure {
revert InsufficientBalance(0, amount);
}
}
The return data would be encoded in the same way as the function call
InsufficientBalance(0, amount)
to the function InsufficientBalance(uint256,uint256)
,
i.e. 0xcf479181
, uint256(0)
, uint256(amount)
.
The error selectors 0x00000000
and 0xffffffff
are reserved for future use.
Avertissement
Never trust error data. The error data by default bubbles up through the chain of external calls, which means that a contract may receive an error not defined in any of the contracts it calls directly. Furthermore, any contract can fake any error by returning data that matches an error signature, even if the error is not defined anywhere.
JSON
The JSON format for a contract’s interface is given by an array of function, event and error descriptions. A function description is a JSON object with the fields:
type
:"function"
,"constructor"
,"receive"
(the « receive Ether » function) or"fallback"
(the « default » function);name
: the name of the function;inputs
: an array of objects, each of which contains:name
: the name of the parameter.type
: the canonical type of the parameter (more below).components
: used for tuple types (more below).
outputs
: an array of objects similar toinputs
.stateMutability
: a string with one of the following values:pure
(specified to not read blockchain state),view
(specified to not modify the blockchain state),nonpayable
(function does not accept Ether - the default) andpayable
(function accepts Ether).
Constructor and fallback function never have name
or outputs
. Fallback function doesn’t have inputs
either.
Note
Sending non-zero Ether to non-payable function will revert the transaction.
Note
The state mutability nonpayable
is reflected in Solidity by not specifying
a state mutability modifier at all.
An event description is a JSON object with fairly similar fields:
type
: always"event"
name
: the name of the event.inputs
: an array of objects, each of which contains:name
: the name of the parameter.type
: the canonical type of the parameter (more below).components
: used for tuple types (more below).indexed
:true
if the field is part of the log’s topics,false
if it one of the log’s data segment.
anonymous
:true
if the event was declared asanonymous
.
Errors look as follows:
type
: always"error"
name
: the name of the error.inputs
: an array of objects, each of which contains:name
: the name of the parameter.type
: the canonical type of the parameter (more below).components
: used for tuple types (more below).
Note
There can be multiple errors with the same name and even with identical signature in the JSON array, for example if the errors originate from different files in the smart contract or are referenced from another smart contract. For the ABI, only the name of the error itself is relevant and not where it is defined.
For example,
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.4;
contract Test {
constructor() { b = hex"12345678901234567890123456789012"; }
event Event(uint indexed a, bytes32 b);
event Event2(uint indexed a, bytes32 b);
error InsufficientBalance(uint256 available, uint256 required);
function foo(uint a) public { emit Event(a, b); }
bytes32 b;
}
would result in the JSON:
[{
"type":"error",
"inputs": [{"name":"available","type":"uint256"},{"name":"required","type":"uint256"}],
"name":"InsufficientBalance"
}, {
"type":"event",
"inputs": [{"name":"a","type":"uint256","indexed":true},{"name":"b","type":"bytes32","indexed":false}],
"name":"Event"
}, {
"type":"event",
"inputs": [{"name":"a","type":"uint256","indexed":true},{"name":"b","type":"bytes32","indexed":false}],
"name":"Event2"
}, {
"type":"function",
"inputs": [{"name":"a","type":"uint256"}],
"name":"foo",
"outputs": []
}]
Handling tuple types
Despite that names are intentionally not part of the ABI encoding they do make a lot of sense to be included in the JSON to enable displaying it to the end user. The structure is nested in the following way:
An object with members name
, type
and potentially components
describes a typed variable.
The canonical type is determined until a tuple type is reached and the string description up
to that point is stored in type
prefix with the word tuple
, i.e. it will be tuple
followed by
a sequence of []
and [k]
with
integers k
. The components of the tuple are then stored in the member components
,
which is of array type and has the same structure as the top-level object except that
indexed
is not allowed there.
As an example, the code
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.5 <0.9.0;
pragma abicoder v2;
contract Test {
struct S { uint a; uint[] b; T[] c; }
struct T { uint x; uint y; }
function f(S memory, T memory, uint) public pure {}
function g() public pure returns (S memory, T memory, uint) {}
}
would result in the JSON:
[
{
"name": "f",
"type": "function",
"inputs": [
{
"name": "s",
"type": "tuple",
"components": [
{
"name": "a",
"type": "uint256"
},
{
"name": "b",
"type": "uint256[]"
},
{
"name": "c",
"type": "tuple[]",
"components": [
{
"name": "x",
"type": "uint256"
},
{
"name": "y",
"type": "uint256"
}
]
}
]
},
{
"name": "t",
"type": "tuple",
"components": [
{
"name": "x",
"type": "uint256"
},
{
"name": "y",
"type": "uint256"
}
]
},
{
"name": "a",
"type": "uint256"
}
],
"outputs": []
}
]
Strict Encoding Mode
Strict encoding mode is the mode that leads to exactly the same encoding as defined in the formal specification above. This means offsets have to be as small as possible while still not creating overlaps in the data areas and thus no gaps are allowed.
Usually, ABI decoders are written in a straightforward way just following offset pointers, but some decoders might enforce strict mode. The Solidity ABI decoder currently does not enforce strict mode, but the encoder always creates data in strict mode.
Non-standard Packed Mode
Through abi.encodePacked()
, Solidity supports a non-standard packed mode where:
types shorter than 32 bytes are concatenated directly, without padding or sign extension
dynamic types are encoded in-place and without the length.
array elements are padded, but still encoded in-place
Furthermore, structs as well as nested arrays are not supported.
As an example, the encoding of int16(-1), bytes1(0x42), uint16(0x03), string("Hello, world!")
results in:
0xffff42000348656c6c6f2c20776f726c6421
^^^^ int16(-1)
^^ bytes1(0x42)
^^^^ uint16(0x03)
^^^^^^^^^^^^^^^^^^^^^^^^^^ string("Hello, world!") without a length field
More specifically:
During the encoding, everything is encoded in-place. This means that there is no distinction between head and tail, as in the ABI encoding, and the length of an array is not encoded.
The direct arguments of
abi.encodePacked
are encoded without padding, as long as they are not arrays (orstring
orbytes
).The encoding of an array is the concatenation of the encoding of its elements with padding.
Dynamically-sized types like
string
,bytes
oruint[]
are encoded without their length field.The encoding of
string
orbytes
does not apply padding at the end unless it is part of an array or struct (then it is padded to a multiple of 32 bytes).
In general, the encoding is ambiguous as soon as there are two dynamically-sized elements, because of the missing length field.
If padding is needed, explicit type conversions can be used: abi.encodePacked(uint16(0x12)) == hex"0012"
.
Since packed encoding is not used when calling functions, there is no special support for prepending a function selector. Since the encoding is ambiguous, there is no decoding function.
Avertissement
If you use keccak256(abi.encodePacked(a, b))
and both a
and b
are dynamic types,
it is easy to craft collisions in the hash value by moving parts of a
into b
and
vice-versa. More specifically, abi.encodePacked("a", "bc") == abi.encodePacked("ab", "c")
.
If you use abi.encodePacked
for signatures, authentication or data integrity, make
sure to always use the same types and check that at most one of them is dynamic.
Unless there is a compelling reason, abi.encode
should be preferred.
Encoding of Indexed Event Parameters
Indexed event parameters that are not value types, i.e. arrays and structs are not stored directly but instead a keccak256-hash of an encoding is stored. This encoding is defined as follows:
the encoding of a
bytes
andstring
value is just the string contents without any padding or length prefix.the encoding of a struct is the concatenation of the encoding of its members, always padded to a multiple of 32 bytes (even
bytes
andstring
).the encoding of an array (both dynamically- and statically-sized) is the concatenation of the encoding of its elements, always padded to a multiple of 32 bytes (even
bytes
andstring
) and without any length prefix
In the above, as usual, a negative number is padded by sign extension and not zero padded.
bytesNN
types are padded on the right while uintNN
/ intNN
are padded on the left.
Avertissement
The encoding of a struct is ambiguous if it contains more than one dynamically-sized array. Because of that, always re-check the event data and do not rely on the search result based on the indexed parameters alone.
Solidity v0.5.0 Breaking Changes
This section highlights the main breaking changes introduced in Solidity version 0.5.0, along with the reasoning behind the changes and how to update affected code. For the full list check the release changelog.
Note
Contracts compiled with Solidity v0.5.0 can still interface with contracts and even libraries compiled with older versions without recompiling or redeploying them. Changing the interfaces to include data locations and visibility and mutability specifiers suffices. See the Interoperability With Older Contracts section below.
Semantic Only Changes
This section lists the changes that are semantic-only, thus potentially hiding new and different behavior in existing code.
Signed right shift now uses proper arithmetic shift, i.e. rounding towards negative infinity, instead of rounding towards zero. Signed and unsigned shift will have dedicated opcodes in Constantinople, and are emulated by Solidity for the moment.
The
continue
statement in ado...while
loop now jumps to the condition, which is the common behavior in such cases. It used to jump to the loop body. Thus, if the condition is false, the loop terminates.The functions
.call()
,.delegatecall()
and.staticcall()
do not pad anymore when given a singlebytes
parameter.Pure and view functions are now called using the opcode
STATICCALL
instead ofCALL
if the EVM version is Byzantium or later. This disallows state changes on the EVM level.The ABI encoder now properly pads byte arrays and strings from calldata (
msg.data
and external function parameters) when used in external function calls and inabi.encode
. For unpadded encoding, useabi.encodePacked
.The ABI decoder reverts in the beginning of functions and in
abi.decode()
if passed calldata is too short or points out of bounds. Note that dirty higher order bits are still simply ignored.Forward all available gas with external function calls starting from Tangerine Whistle.
Semantic and Syntactic Changes
This section highlights changes that affect syntax and semantics.
The functions
.call()
,.delegatecall()
,staticcall()
,keccak256()
,sha256()
andripemd160()
now accept only a singlebytes
argument. Moreover, the argument is not padded. This was changed to make more explicit and clear how the arguments are concatenated. Change every.call()
(and family) to a.call("")
and every.call(signature, a, b, c)
to use.call(abi.encodeWithSignature(signature, a, b, c))
(the last one only works for value types). Change everykeccak256(a, b, c)
tokeccak256(abi.encodePacked(a, b, c))
. Even though it is not a breaking change, it is suggested that developers changex.call(bytes4(keccak256("f(uint256)")), a, b)
tox.call(abi.encodeWithSignature("f(uint256)", a, b))
.Functions
.call()
,.delegatecall()
and.staticcall()
now return(bool, bytes memory)
to provide access to the return data. Changebool success = otherContract.call("f")
to(bool success, bytes memory data) = otherContract.call("f")
.Solidity now implements C99-style scoping rules for function local variables, that is, variables can only be used after they have been declared and only in the same or nested scopes. Variables declared in the initialization block of a
for
loop are valid at any point inside the loop.
Explicitness Requirements
This section lists changes where the code now needs to be more explicit. For most of the topics the compiler will provide suggestions.
Explicit function visibility is now mandatory. Add
public
to every function and constructor, andexternal
to every fallback or interface function that does not specify its visibility already.Explicit data location for all variables of struct, array or mapping types is now mandatory. This is also applied to function parameters and return variables. For example, change
uint[] x = m_x
touint[] storage x = m_x
, andfunction f(uint[][] x)
tofunction f(uint[][] memory x)
wherememory
is the data location and might be replaced bystorage
orcalldata
accordingly. Note thatexternal
functions require parameters with a data location ofcalldata
.Contract types do not include
address
members anymore in order to separate the namespaces. Therefore, it is now necessary to explicitly convert values of contract type to addresses before using anaddress
member. Example: ifc
is a contract, changec.transfer(...)
toaddress(c).transfer(...)
, andc.balance
toaddress(c).balance
.Explicit conversions between unrelated contract types are now disallowed. You can only convert from a contract type to one of its base or ancestor types. If you are sure that a contract is compatible with the contract type you want to convert to, although it does not inherit from it, you can work around this by converting to
address
first. Example: ifA
andB
are contract types,B
does not inherit fromA
andb
is a contract of typeB
, you can still convertb
to typeA
usingA(address(b))
. Note that you still need to watch out for matching payable fallback functions, as explained below.The
address
type was split intoaddress
andaddress payable
, where onlyaddress payable
provides thetransfer
function. Anaddress payable
can be directly converted to anaddress
, but the other way around is not allowed. Convertingaddress
toaddress payable
is possible via conversion throughuint160
. Ifc
is a contract,address(c)
results inaddress payable
only ifc
has a payable fallback function. If you use the withdraw pattern, you most likely do not have to change your code becausetransfer
is only used onmsg.sender
instead of stored addresses andmsg.sender
is anaddress payable
.Conversions between
bytesX
anduintY
of different size are now disallowed due tobytesX
padding on the right anduintY
padding on the left which may cause unexpected conversion results. The size must now be adjusted within the type before the conversion. For example, you can convert abytes4
(4 bytes) to auint64
(8 bytes) by first converting thebytes4
variable tobytes8
and then touint64
. You get the opposite padding when converting throughuint32
. Before v0.5.0 any conversion betweenbytesX
anduintY
would go throughuint8X
. For exampleuint8(bytes3(0x291807))
would be converted touint8(uint24(bytes3(0x291807)))
(the result is0x07
).Using
msg.value
in non-payable functions (or introducing it via a modifier) is disallowed as a security feature. Turn the function intopayable
or create a new internal function for the program logic that usesmsg.value
.For clarity reasons, the command line interface now requires
-
if the standard input is used as source.
Deprecated Elements
This section lists changes that deprecate prior features or syntax. Note that
many of these changes were already enabled in the experimental mode
v0.5.0
.
Command Line and JSON Interfaces
The command line option
--formal
(used to generate Why3 output for further formal verification) was deprecated and is now removed. A new formal verification module, the SMTChecker, is enabled viapragma experimental SMTChecker;
.The command line option
--julia
was renamed to--yul
due to the renaming of the intermediate languageJulia
toYul
.The
--clone-bin
and--combined-json clone-bin
command line options were removed.Remappings with empty prefix are disallowed.
The JSON AST fields
constant
andpayable
were removed. The information is now present in thestateMutability
field.The JSON AST field
isConstructor
of theFunctionDefinition
node was replaced by a field calledkind
which can have the value"constructor"
,"fallback"
or"function"
.In unlinked binary hex files, library address placeholders are now the first 36 hex characters of the keccak256 hash of the fully qualified library name, surrounded by
$...$
. Previously, just the fully qualified library name was used. This reduces the chances of collisions, especially when long paths are used. Binary files now also contain a list of mappings from these placeholders to the fully qualified names.
Constructors
Constructors must now be defined using the
constructor
keyword.Calling base constructors without parentheses is now disallowed.
Specifying base constructor arguments multiple times in the same inheritance hierarchy is now disallowed.
Calling a constructor with arguments but with wrong argument count is now disallowed. If you only want to specify an inheritance relation without giving arguments, do not provide parentheses at all.
Functions
Function
callcode
is now disallowed (in favor ofdelegatecall
). It is still possible to use it via inline assembly.suicide
is now disallowed (in favor ofselfdestruct
).sha3
is now disallowed (in favor ofkeccak256
).throw
is now disallowed (in favor ofrevert
,require
andassert
).
Conversions
Explicit and implicit conversions from decimal literals to
bytesXX
types is now disallowed.Explicit and implicit conversions from hex literals to
bytesXX
types of different size is now disallowed.
Literals and Suffixes
The unit denomination
years
is now disallowed due to complications and confusions about leap years.Trailing dots that are not followed by a number are now disallowed.
Combining hex numbers with unit denominations (e.g.
0x1e wei
) is now disallowed.The prefix
0X
for hex numbers is disallowed, only0x
is possible.
Variables
Declaring empty structs is now disallowed for clarity.
The
var
keyword is now disallowed to favor explicitness.Assignments between tuples with different number of components is now disallowed.
Values for constants that are not compile-time constants are disallowed.
Multi-variable declarations with mismatching number of values are now disallowed.
Uninitialized storage variables are now disallowed.
Empty tuple components are now disallowed.
Detecting cyclic dependencies in variables and structs is limited in recursion to 256.
Fixed-size arrays with a length of zero are now disallowed.
Syntax
Using
constant
as function state mutability modifier is now disallowed.Boolean expressions cannot use arithmetic operations.
The unary
+
operator is now disallowed.Literals cannot anymore be used with
abi.encodePacked
without prior conversion to an explicit type.Empty return statements for functions with one or more return values are now disallowed.
The « loose assembly » syntax is now disallowed entirely, that is, jump labels, jumps and non-functional instructions cannot be used anymore. Use the new
while
,switch
andif
constructs instead.Functions without implementation cannot use modifiers anymore.
Function types with named return values are now disallowed.
Single statement variable declarations inside if/while/for bodies that are not blocks are now disallowed.
New keywords:
calldata
andconstructor
.New reserved keywords:
alias
,apply
,auto
,copyof
,define
,immutable
,implements
,macro
,mutable
,override
,partial
,promise
,reference
,sealed
,sizeof
,supports
,typedef
andunchecked
.
Interoperability With Older Contracts
It is still possible to interface with contracts written for Solidity versions prior to v0.5.0 (or the other way around) by defining interfaces for them. Consider you have the following pre-0.5.0 contract already deployed:
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.4.25;
// This will report a warning until version 0.4.25 of the compiler
// This will not compile after 0.5.0
contract OldContract {
function someOldFunction(uint8 a) {
//...
}
function anotherOldFunction() constant returns (bool) {
//...
}
// ...
}
This will no longer compile with Solidity v0.5.0. However, you can define a compatible interface for it:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.5.0 <0.9.0;
interface OldContract {
function someOldFunction(uint8 a) external;
function anotherOldFunction() external returns (bool);
}
Note that we did not declare anotherOldFunction
to be view
, despite it being declared constant
in the original
contract. This is due to the fact that starting with Solidity v0.5.0 staticcall
is used to call view
functions.
Prior to v0.5.0 the constant
keyword was not enforced, so calling a function declared constant
with staticcall
may still revert, since the constant
function may still attempt to modify storage. Consequently, when defining an
interface for older contracts, you should only use view
in place of constant
in case you are absolutely sure that
the function will work with staticcall
.
Given the interface defined above, you can now easily use the already deployed pre-0.5.0 contract:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.5.0 <0.9.0;
interface OldContract {
function someOldFunction(uint8 a) external;
function anotherOldFunction() external returns (bool);
}
contract NewContract {
function doSomething(OldContract a) public returns (bool) {
a.someOldFunction(0x42);
return a.anotherOldFunction();
}
}
Similarly, pre-0.5.0 libraries can be used by defining the functions of the library without implementation and supplying the address of the pre-0.5.0 library during linking (see Using the Commandline Compiler for how to use the commandline compiler for linking):
// This will not compile after 0.6.0
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.5.0;
library OldLibrary {
function someFunction(uint8 a) public returns(bool);
}
contract NewContract {
function f(uint8 a) public returns (bool) {
return OldLibrary.someFunction(a);
}
}
Example
The following example shows a contract and its updated version for Solidity v0.5.0 with some of the changes listed in this section.
Old version:
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.4.25;
// This will not compile after 0.5.0
contract OtherContract {
uint x;
function f(uint y) external {
x = y;
}
function() payable external {}
}
contract Old {
OtherContract other;
uint myNumber;
// Function mutability not provided, not an error.
function someInteger() internal returns (uint) { return 2; }
// Function visibility not provided, not an error.
// Function mutability not provided, not an error.
function f(uint x) returns (bytes) {
// Var is fine in this version.
var z = someInteger();
x += z;
// Throw is fine in this version.
if (x > 100)
throw;
bytes memory b = new bytes(x);
y = -3 >> 1;
// y == -1 (wrong, should be -2)
do {
x += 1;
if (x > 10) continue;
// 'Continue' causes an infinite loop.
} while (x < 11);
// Call returns only a Bool.
bool success = address(other).call("f");
if (!success)
revert();
else {
// Local variables could be declared after their use.
int y;
}
return b;
}
// No need for an explicit data location for 'arr'
function g(uint[] arr, bytes8 x, OtherContract otherContract) public {
otherContract.transfer(1 ether);
// Since uint32 (4 bytes) is smaller than bytes8 (8 bytes),
// the first 4 bytes of x will be lost. This might lead to
// unexpected behavior since bytesX are right padded.
uint32 y = uint32(x);
myNumber += y + msg.value;
}
}
New version:
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.5.0;
// This will not compile after 0.6.0
contract OtherContract {
uint x;
function f(uint y) external {
x = y;
}
function() payable external {}
}
contract New {
OtherContract other;
uint myNumber;
// Function mutability must be specified.
function someInteger() internal pure returns (uint) { return 2; }
// Function visibility must be specified.
// Function mutability must be specified.
function f(uint x) public returns (bytes memory) {
// The type must now be explicitly given.
uint z = someInteger();
x += z;
// Throw is now disallowed.
require(x <= 100);
int y = -3 >> 1;
require(y == -2);
do {
x += 1;
if (x > 10) continue;
// 'Continue' jumps to the condition below.
} while (x < 11);
// Call returns (bool, bytes).
// Data location must be specified.
(bool success, bytes memory data) = address(other).call("f");
if (!success)
revert();
return data;
}
using address_make_payable for address;
// Data location for 'arr' must be specified
function g(uint[] memory /* arr */, bytes8 x, OtherContract otherContract, address unknownContract) public payable {
// 'otherContract.transfer' is not provided.
// Since the code of 'OtherContract' is known and has the fallback
// function, address(otherContract) has type 'address payable'.
address(otherContract).transfer(1 ether);
// 'unknownContract.transfer' is not provided.
// 'address(unknownContract).transfer' is not provided
// since 'address(unknownContract)' is not 'address payable'.
// If the function takes an 'address' which you want to send
// funds to, you can convert it to 'address payable' via 'uint160'.
// Note: This is not recommended and the explicit type
// 'address payable' should be used whenever possible.
// To increase clarity, we suggest the use of a library for
// the conversion (provided after the contract in this example).
address payable addr = unknownContract.make_payable();
require(addr.send(1 ether));
// Since uint32 (4 bytes) is smaller than bytes8 (8 bytes),
// the conversion is not allowed.
// We need to convert to a common size first:
bytes4 x4 = bytes4(x); // Padding happens on the right
uint32 y = uint32(x4); // Conversion is consistent
// 'msg.value' cannot be used in a 'non-payable' function.
// We need to make the function payable
myNumber += y + msg.value;
}
}
// We can define a library for explicitly converting ``address``
// to ``address payable`` as a workaround.
library address_make_payable {
function make_payable(address x) internal pure returns (address payable) {
return address(uint160(x));
}
}
Solidity v0.6.0 Breaking Changes
This section highlights the main breaking changes introduced in Solidity version 0.6.0, along with the reasoning behind the changes and how to update affected code. For the full list check the release changelog.
Changes the Compiler Might not Warn About
This section lists changes where the behaviour of your code might change without the compiler telling you about it.
The resulting type of an exponentiation is the type of the base. It used to be the smallest type that can hold both the type of the base and the type of the exponent, as with symmetric operations. Additionally, signed types are allowed for the base of the exponentiation.
Explicitness Requirements
This section lists changes where the code now needs to be more explicit, but the semantics do not change. For most of the topics the compiler will provide suggestions.
Functions can now only be overridden when they are either marked with the
virtual
keyword or defined in an interface. Functions without implementation outside an interface have to be markedvirtual
. When overriding a function or modifier, the new keywordoverride
must be used. When overriding a function or modifier defined in multiple parallel bases, all bases must be listed in parentheses after the keyword like so:override(Base1, Base2)
.Member-access to
length
of arrays is now always read-only, even for storage arrays. It is no longer possible to resize storage arrays by assigning a new value to their length. Usepush()
,push(value)
orpop()
instead, or assign a full array, which will of course overwrite the existing content. The reason behind this is to prevent storage collisions of gigantic storage arrays.The new keyword
abstract
can be used to mark contracts as abstract. It has to be used if a contract does not implement all its functions. Abstract contracts cannot be created using thenew
operator, and it is not possible to generate bytecode for them during compilation.Libraries have to implement all their functions, not only the internal ones.
The names of variables declared in inline assembly may no longer end in
_slot
or_offset
.Variable declarations in inline assembly may no longer shadow any declaration outside the inline assembly block. If the name contains a dot, its prefix up to the dot may not conflict with any declaration outside the inline assembly block.
State variable shadowing is now disallowed. A derived contract can only declare a state variable
x
, if there is no visible state variable with the same name in any of its bases.
Semantic and Syntactic Changes
This section lists changes where you have to modify your code and it does something else afterwards.
Conversions from external function types to
address
are now disallowed. Instead external function types have a member calledaddress
, similar to the existingselector
member.The function
push(value)
for dynamic storage arrays does not return the new length anymore (it returns nothing).The unnamed function commonly referred to as « fallback function » was split up into a new fallback function that is defined using the
fallback
keyword and a receive ether function defined using thereceive
keyword.If present, the receive ether function is called whenever the call data is empty (whether or not ether is received). This function is implicitly
payable
.The new fallback function is called when no other function matches (if the receive ether function does not exist then this includes calls with empty call data). You can make this function
payable
or not. If it is notpayable
then transactions not matching any other function which send value will revert. You should only need to implement the new fallback function if you are following an upgrade or proxy pattern.
New Features
This section lists things that were not possible prior to Solidity 0.6.0 or were more difficult to achieve.
The try/catch statement allows you to react on failed external calls.
struct
andenum
types can be declared at file level.Array slices can be used for calldata arrays, for example
abi.decode(msg.data[4:], (uint, uint))
is a low-level way to decode the function call payload.Natspec supports multiple return parameters in developer documentation, enforcing the same naming check as
@param
.Yul and Inline Assembly have a new statement called
leave
that exits the current function.Conversions from
address
toaddress payable
are now possible viapayable(x)
, wherex
must be of typeaddress
.
Interface Changes
This section lists changes that are unrelated to the language itself, but that have an effect on the interfaces of the compiler. These may change the way how you use the compiler on the command line, how you use its programmable interface, or how you analyze the output produced by it.
New Error Reporter
A new error reporter was introduced, which aims at producing more accessible error messages on the command line.
It is enabled by default, but passing --old-reporter
falls back to the the deprecated old error reporter.
Metadata Hash Options
The compiler now appends the IPFS hash of the metadata file to the end of the bytecode by default
(for details, see documentation on contract metadata). Before 0.6.0, the compiler appended the
Swarm hash by default, and in order to still support this behaviour,
the new command line option --metadata-hash
was introduced. It allows you to select the hash to be produced and
appended, by passing either ipfs
or swarm
as value to the --metadata-hash
command line option.
Passing the value none
completely removes the hash.
These changes can also be used via the Standard JSON Interface and effect the metadata JSON generated by the compiler.
The recommended way to read the metadata is to read the last two bytes to determine the length of the CBOR encoding and perform a proper decoding on that data block as explained in the metadata section.
Yul Optimizer
Together with the legacy bytecode optimizer, the Yul optimizer is now enabled by default when you call the compiler
with --optimize
. It can be disabled by calling the compiler with --no-optimize-yul
.
This mostly affects code that uses ABI coder v2.
C API Changes
The client code that uses the C API of libsolc
is now in control of the memory used by the compiler. To make
this change consistent, solidity_free
was renamed to solidity_reset
, the functions solidity_alloc
and
solidity_free
were added and solidity_compile
now returns a string that must be explicitly freed via
solidity_free()
.
How to update your code
This section gives detailed instructions on how to update prior code for every breaking change.
Change
address(f)
tof.address
forf
being of external function type.Replace
function () external [payable] { ... }
by eitherreceive() external payable { ... }
,fallback() external [payable] { ... }
or both. Prefer using areceive
function only, whenever possible.Change
uint length = array.push(value)
toarray.push(value);
. The new length can be accessed viaarray.length
.Change
array.length++
toarray.push()
to increase, and usepop()
to decrease the length of a storage array.For every named return parameter in a function’s
@dev
documentation define a@return
entry which contains the parameter’s name as the first word. E.g. if you have functionf()
defined likefunction f() public returns (uint value)
and a@dev
annotating it, document its return parameters like so:@return value The return value.
. You can mix named and un-named return parameters documentation so long as the notices are in the order they appear in the tuple return type.Choose unique identifiers for variable declarations in inline assembly that do not conflict with declarations outside the inline assembly block.
Add
virtual
to every non-interface function you intend to override. Addvirtual
to all functions without implementation outside interfaces. For single inheritance, addoverride
to every overriding function. For multiple inheritance, addoverride(A, B, ..)
, where you list all contracts that define the overridden function in the parentheses. When multiple bases define the same function, the inheriting contract must override all conflicting functions.
Solidity v0.7.0 Breaking Changes
This section highlights the main breaking changes introduced in Solidity version 0.7.0, along with the reasoning behind the changes and how to update affected code. For the full list check the release changelog.
Silent Changes of the Semantics
Exponentiation and shifts of literals by non-literals (e.g.
1 << x
or2 ** x
) will always use either the typeuint256
(for non-negative literals) orint256
(for negative literals) to perform the operation. Previously, the operation was performed in the type of the shift amount / the exponent which can be misleading.
Changes to the Syntax
In external function and contract creation calls, Ether and gas is now specified using a new syntax:
x.f{gas: 10000, value: 2 ether}(arg1, arg2)
. The old syntax –x.f.gas(10000).value(2 ether)(arg1, arg2)
– will cause an error.The global variable
now
is deprecated,block.timestamp
should be used instead. The single identifiernow
is too generic for a global variable and could give the impression that it changes during transaction processing, whereasblock.timestamp
correctly reflects the fact that it is just a property of the block.NatSpec comments on variables are only allowed for public state variables and not for local or internal variables.
The token
gwei
is a keyword now (used to specify, e.g.2 gwei
as a number) and cannot be used as an identifier.String literals now can only contain printable ASCII characters and this also includes a variety of escape sequences, such as hexadecimal (
\xff
) and unicode escapes (\u20ac
).Unicode string literals are supported now to accommodate valid UTF-8 sequences. They are identified with the
unicode
prefix:unicode"Hello 😃"
.State Mutability: The state mutability of functions can now be restricted during inheritance: Functions with default state mutability can be overridden by
pure
andview
functions whileview
functions can be overridden bypure
functions. At the same time, public state variables are consideredview
and evenpure
if they are constants.
Inline Assembly
Disallow
.
in user-defined function and variable names in inline assembly. It is still valid if you use Solidity in Yul-only mode.Slot and offset of storage pointer variable
x
are accessed viax.slot
andx.offset
instead ofx_slot
andx_offset
.
Removal of Unused or Unsafe Features
Mappings outside Storage
If a struct or array contains a mapping, it can only be used in storage. Previously, mapping members were silently skipped in memory, which is confusing and error-prone.
Assignments to structs or arrays in storage does not work if they contain mappings. Previously, mappings were silently skipped during the copy operation, which is misleading and error-prone.
Functions and Events
Visibility (
public
/internal
) is not needed for constructors anymore: To prevent a contract from being created, it can be markedabstract
. This makes the visibility concept for constructors obsolete.Type Checker: Disallow
virtual
for library functions: Since libraries cannot be inherited from, library functions should not be virtual.Multiple events with the same name and parameter types in the same inheritance hierarchy are disallowed.
using A for B
only affects the contract it is mentioned in. Previously, the effect was inherited. Now, you have to repeat theusing
statement in all derived contracts that make use of the feature.
Expressions
Shifts by signed types are disallowed. Previously, shifts by negative amounts were allowed, but reverted at runtime.
The
finney
andszabo
denominations are removed. They are rarely used and do not make the actual amount readily visible. Instead, explicit values like1e20
or the very commongwei
can be used.
Declarations
The keyword
var
cannot be used anymore. Previously, this keyword would parse but result in a type error and a suggestion about which type to use. Now, it results in a parser error.
Interface Changes
JSON AST: Mark hex string literals with
kind: "hexString"
.JSON AST: Members with value
null
are removed from JSON output.NatSpec: Constructors and functions have consistent userdoc output.
How to update your code
This section gives detailed instructions on how to update prior code for every breaking change.
Change
x.f.value(...)()
tox.f{value: ...}()
. Similarly(new C).value(...)()
tonew C{value: ...}()
andx.f.gas(...).value(...)()
tox.f{gas: ..., value: ...}()
.Change
now
toblock.timestamp
.Change types of right operand in shift operators to unsigned types. For example change
x >> (256 - y)
tox >> uint(256 - y)
.Repeat the
using A for B
statements in all derived contracts if needed.Remove the
public
keyword from every constructor.Remove the
internal
keyword from every constructor and addabstract
to the contract (if not already present).Change
_slot
and_offset
suffixes in inline assembly to.slot
and.offset
, respectively.
Solidity v0.8.0 Breaking Changes
This section highlights the main breaking changes introduced in Solidity version 0.8.0. For the full list check the release changelog.
Silent Changes of the Semantics
This section lists changes where existing code changes its behaviour without the compiler notifying you about it.
Arithmetic operations revert on underflow and overflow. You can use
unchecked { ... }
to use the previous wrapping behaviour.Checks for overflow are very common, so we made them the default to increase readability of code, even if it comes at a slight increase of gas costs.
ABI coder v2 is activated by default.
You can choose to use the old behaviour using
pragma abicoder v1;
. The pragmapragma experimental ABIEncoderV2;
is still valid, but it is deprecated and has no effect. If you want to be explicit, please usepragma abicoder v2;
instead.Note that ABI coder v2 supports more types than v1 and performs more sanity checks on the inputs. ABI coder v2 makes some function calls more expensive and it can also make contract calls revert that did not revert with ABI coder v1 when they contain data that does not conform to the parameter types.
Exponentiation is right associative, i.e., the expression
a**b**c
is parsed asa**(b**c)
. Before 0.8.0, it was parsed as(a**b)**c
.This is the common way to parse the exponentiation operator.
Failing assertions and other internal checks like division by zero or arithmetic overflow do not use the invalid opcode but instead the revert opcode. More specifically, they will use error data equal to a function call to
Panic(uint256)
with an error code specific to the circumstances.This will save gas on errors while it still allows static analysis tools to distinguish these situations from a revert on invalid input, like a failing
require
.If a byte array in storage is accessed whose length is encoded incorrectly, a panic is caused. A contract cannot get into this situation unless inline assembly is used to modify the raw representation of storage byte arrays.
If constants are used in array length expressions, previous versions of Solidity would use arbitrary precision in all branches of the evaluation tree. Now, if constant variables are used as intermediate expressions, their values will be properly rounded in the same way as when they are used in run-time expressions.
The type
byte
has been removed. It was an alias ofbytes1
.
New Restrictions
This section lists changes that might cause existing contracts to not compile anymore.
There are new restrictions related to explicit conversions of literals. The previous behaviour in the following cases was likely ambiguous:
Explicit conversions from negative literals and literals larger than
type(uint160).max
toaddress
are disallowed.Explicit conversions between literals and an integer type
T
are only allowed if the literal lies betweentype(T).min
andtype(T).max
. In particular, replace usages ofuint(-1)
withtype(uint).max
.Explicit conversions between literals and enums are only allowed if the literal can represent a value in the enum.
Explicit conversions between literals and
address
type (e.g.address(literal)
) have the typeaddress
instead ofaddress payable
. One can get a payable address type by using an explicit conversion, i.e.,payable(literal)
.
Address literals have the type
address
instead ofaddress payable
. They can be converted toaddress payable
by using an explicit conversion, e.g.payable(0xdCad3a6d3569DF655070DEd06cb7A1b2Ccd1D3AF)
.There are new restrictions on explicit type conversions. The conversion is only allowed when there is at most one change in sign, width or type-category (
int
,address
,bytesNN
, etc.). To perform multiple changes, use multiple conversions.Let us use the notation
T(S)
to denote the explicit conversionT(x)
, where,T
andS
are types, andx
is any arbitrary variable of typeS
. An example of such a disallowed conversion would beuint16(int8)
since it changes both width (8 bits to 16 bits) and sign (signed integer to unsigned integer). In order to do the conversion, one has to go through an intermediate type. In the previous example, this would beuint16(uint8(int8))
oruint16(int16(int8))
. Note that the two ways to convert will produce different results e.g., for-1
. The following are some examples of conversions that are disallowed by this rule.address(uint)
anduint(address)
: converting both type-category and width. Replace this byaddress(uint160(uint))
anduint(uint160(address))
respectively.payable(uint160)
,payable(bytes20)
andpayable(integer-literal)
: converting both type-category and state-mutability. Replace this bypayable(address(uint160))
,payable(address(bytes20))
andpayable(address(integer-literal))
respectively. Note thatpayable(0)
is valid and is an exception to the rule.int80(bytes10)
andbytes10(int80)
: converting both type-category and sign. Replace this byint80(uint80(bytes10))
andbytes10(uint80(int80)
respectively.Contract(uint)
: converting both type-category and width. Replace this byContract(address(uint160(uint)))
.
These conversions were disallowed to avoid ambiguity. For example, in the expression
uint16 x = uint16(int8(-1))
, the value ofx
would depend on whether the sign or the width conversion was applied first.Function call options can only be given once, i.e.
c.f{gas: 10000}{value: 1}()
is invalid and has to be changed toc.f{gas: 10000, value: 1}()
.The global functions
log0
,log1
,log2
,log3
andlog4
have been removed.These are low-level functions that were largely unused. Their behaviour can be accessed from inline assembly.
enum
definitions cannot contain more than 256 members.This will make it safe to assume that the underlying type in the ABI is always
uint8
.Declarations with the name
this
,super
and_
are disallowed, with the exception of public functions and events. The exception is to make it possible to declare interfaces of contracts implemented in languages other than Solidity that do permit such function names.Remove support for the
\b
,\f
, and\v
escape sequences in code. They can still be inserted via hexadecimal escapes, e.g.\x08
,\x0c
, and\x0b
, respectively.The global variables
tx.origin
andmsg.sender
have the typeaddress
instead ofaddress payable
. One can convert them intoaddress payable
by using an explicit conversion, i.e.,payable(tx.origin)
orpayable(msg.sender)
.This change was done since the compiler cannot determine whether or not these addresses are payable or not, so it now requires an explicit conversion to make this requirement visible.
Explicit conversion into
address
type always returns a non-payableaddress
type. In particular, the following explicit conversions have the typeaddress
instead ofaddress payable
:address(u)
whereu
is a variable of typeuint160
. One can convertu
into the typeaddress payable
by using two explicit conversions, i.e.,payable(address(u))
.address(b)
whereb
is a variable of typebytes20
. One can convertb
into the typeaddress payable
by using two explicit conversions, i.e.,payable(address(b))
.address(c)
wherec
is a contract. Previously, the return type of this conversion depended on whether the contract can receive Ether (either by having a receive function or a payable fallback function). The conversionpayable(c)
has the typeaddress payable
and is only allowed when the contractc
can receive Ether. In general, one can always convertc
into the typeaddress payable
by using the following explicit conversion:payable(address(c))
. Note thataddress(this)
falls under the same category asaddress(c)
and the same rules apply for it.
The
chainid
builtin in inline assembly is now consideredview
instead ofpure
.Unary negation cannot be used on unsigned integers anymore, only on signed integers.
Interface Changes
The output of
--combined-json
has changed: JSON fieldsabi
,devdoc
,userdoc
andstorage-layout
are sub-objects now. Before 0.8.0 they used to be serialised as strings.The « legacy AST » has been removed (
--ast-json
on the commandline interface andlegacyAST
for standard JSON). Use the « compact AST » (--ast-compact--json
resp.AST
) as replacement.The old error reporter (
--old-reporter
) has been removed.
How to update your code
If you rely on wrapping arithmetic, surround each operation with
unchecked { ... }
.Optional: If you use SafeMath or a similar library, change
x.add(y)
tox + y
,x.mul(y)
tox * y
etc.Add
pragma abicoder v1;
if you want to stay with the old ABI coder.Optionally remove
pragma experimental ABIEncoderV2
orpragma abicoder v2
since it is redundant.Change
byte
tobytes1
.Add intermediate explicit type conversions if required.
Combine
c.f{gas: 10000}{value: 1}()
toc.f{gas: 10000, value: 1}()
.Change
msg.sender.transfer(x)
topayable(msg.sender).transfer(x)
or use a stored variable ofaddress payable
type.Change
x**y**z
to(x**y)**z
.Use inline assembly as a replacement for
log0
, …,log4
.Negate unsigned integers by subtracting them from the maximum value of the type and adding 1 (e.g.
type(uint256).max - x + 1
, while ensuring that x is not zero)
NatSpec Format
Solidity contracts can use a special form of comments to provide rich documentation for functions, return variables and more. This special form is named the Ethereum Natural Language Specification Format (NatSpec).
Note
NatSpec was inspired by Doxygen. While it uses Doxygen-style comments and tags, there is no intention to keep strict compatibility with Doxygen. Please carefully examine the supported tags listed below.
This documentation is segmented into developer-focused messages and end-user-facing messages. These messages may be shown to the end user (the human) at the time that they will interact with the contract (i.e. sign a transaction).
It is recommended that Solidity contracts are fully annotated using NatSpec for all public interfaces (everything in the ABI).
NatSpec includes the formatting for comments that the smart contract author will use, and which are understood by the Solidity compiler. Also detailed below is output of the Solidity compiler, which extracts these comments into a machine-readable format.
NatSpec may also include annotations used by third-party tools. These are most likely
accomplished via the @custom:<name>
tag, and a good use case is analysis and verification
tools.
Documentation Example
Documentation is inserted above each contract
, interface
, library
,
function
, and event
using the Doxygen notation format.
A public
state variable is equivalent to a function
for the purposes of NatSpec.
For Solidity you may choose
///
for single or multi-line comments, or/**
and ending with*/
.For Vyper, use
"""
indented to the inner contents with bare comments. See the Vyper documentation.
The following example shows a contract and a function using all available tags.
Note
The Solidity compiler only interprets tags if they are external or public. You are welcome to use similar comments for your internal and private functions, but those will not be parsed.
This may change in the future.
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.8.2 < 0.9.0;
/// @title A simulator for trees
/// @author Larry A. Gardner
/// @notice You can use this contract for only the most basic simulation
/// @dev All function calls are currently implemented without side effects
/// @custom:experimental This is an experimental contract.
contract Tree {
/// @notice Calculate tree age in years, rounded up, for live trees
/// @dev The Alexandr N. Tetearing algorithm could increase precision
/// @param rings The number of rings from dendrochronological sample
/// @return Age in years, rounded up for partial years
function age(uint256 rings) external virtual pure returns (uint256) {
return rings + 1;
}
/// @notice Returns the amount of leaves the tree has.
/// @dev Returns only a fixed number.
function leaves() external virtual pure returns(uint256) {
return 2;
}
}
contract Plant {
function leaves() external virtual pure returns(uint256) {
return 3;
}
}
contract KumquatTree is Tree, Plant {
function age(uint256 rings) external override pure returns (uint256) {
return rings + 2;
}
/// Return the amount of leaves that this specific kind of tree has
/// @inheritdoc Tree
function leaves() external override(Tree, Plant) pure returns(uint256) {
return 3;
}
}
Documentation Output
When parsed by the compiler, documentation such as the one from the above example will produce two different JSON files. One is meant to be consumed by the end user as a notice when a function is executed and the other to be used by the developer.
If the above contract is saved as ex1.sol
then you can generate the
documentation using:
solc --userdoc --devdoc ex1.sol
And the output is below.
Note
Starting Solidity version 0.6.11 the NatSpec output also contains a version
and a kind
field.
Currently the version
is set to 1
and kind
must be one of user
or dev
.
In the future it is possible that new versions will be introduced, deprecating older ones.
User Documentation
The above documentation will produce the following user documentation JSON file as output:
{
"version" : 1,
"kind" : "user",
"methods" :
{
"age(uint256)" :
{
"notice" : "Calculate tree age in years, rounded up, for live trees"
}
},
"notice" : "You can use this contract for only the most basic simulation"
}
Note that the key by which to find the methods is the function’s canonical signature as defined in the Contract ABI and not simply the function’s name.
Developer Documentation
Apart from the user documentation file, a developer documentation JSON file should also be produced and should look like this:
{
"version" : 1,
"kind" : "dev",
"author" : "Larry A. Gardner",
"details" : "All function calls are currently implemented without side effects",
"custom:experimental" : "This is an experimental contract.",
"methods" :
{
"age(uint256)" :
{
"details" : "The Alexandr N. Tetearing algorithm could increase precision",
"params" :
{
"rings" : "The number of rings from dendrochronological sample"
},
"return" : "age in years, rounded up for partial years"
}
},
"title" : "A simulator for trees"
}
Security Considerations
While it is usually quite easy to build software that works as expected, it is much harder to check that nobody can use it in a way that was not anticipated.
In Solidity, this is even more important because you can use smart contracts to handle tokens or, possibly, even more valuable things. Furthermore, every execution of a smart contract happens in public and, in addition to that, the source code is often available.
Of course you always have to consider how much is at stake: You can compare a smart contract with a web service that is open to the public (and thus, also to malicious actors) and perhaps even open source. If you only store your grocery list on that web service, you might not have to take too much care, but if you manage your bank account using that web service, you should be more careful.
This section will list some pitfalls and general security recommendations but can, of course, never be complete. Also, keep in mind that even if your smart contract code is bug-free, the compiler or the platform itself might have a bug. A list of some publicly known security-relevant bugs of the compiler can be found in the list of known bugs, which is also machine-readable. Note that there is a bug bounty program that covers the code generator of the Solidity compiler.
As always, with open source documentation, please help us extend this section (especially, some examples would not hurt)!
NOTE: In addition to the list below, you can find more security recommendations and best practices in Guy Lando’s knowledge list and the Consensys GitHub repo.
Pitfalls
Private Information and Randomness
Everything you use in a smart contract is publicly visible, even
local variables and state variables marked private
.
Using random numbers in smart contracts is quite tricky if you do not want miners to be able to cheat.
Re-Entrancy
Any interaction from a contract (A) with another contract (B) and any transfer of Ether hands over control to that contract (B). This makes it possible for B to call back into A before this interaction is completed. To give an example, the following code contains a bug (it is just a snippet and not a complete contract):
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.6.0 <0.9.0;
// THIS CONTRACT CONTAINS A BUG - DO NOT USE
contract Fund {
/// @dev Mapping of ether shares of the contract.
mapping(address => uint) shares;
/// Withdraw your share.
function withdraw() public {
if (payable(msg.sender).send(shares[msg.sender]))
shares[msg.sender] = 0;
}
}
The problem is not too serious here because of the limited gas as part
of send
, but it still exposes a weakness: Ether transfer can always
include code execution, so the recipient could be a contract that calls
back into withdraw
. This would let it get multiple refunds and
basically retrieve all the Ether in the contract. In particular, the
following contract will allow an attacker to refund multiple times
as it uses call
which forwards all remaining gas by default:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.6.2 <0.9.0;
// THIS CONTRACT CONTAINS A BUG - DO NOT USE
contract Fund {
/// @dev Mapping of ether shares of the contract.
mapping(address => uint) shares;
/// Withdraw your share.
function withdraw() public {
(bool success,) = msg.sender.call{value: shares[msg.sender]}("");
if (success)
shares[msg.sender] = 0;
}
}
To avoid re-entrancy, you can use the Checks-Effects-Interactions pattern as outlined further below:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.6.0 <0.9.0;
contract Fund {
/// @dev Mapping of ether shares of the contract.
mapping(address => uint) shares;
/// Withdraw your share.
function withdraw() public {
uint share = shares[msg.sender];
shares[msg.sender] = 0;
payable(msg.sender).transfer(share);
}
}
Note that re-entrancy is not only an effect of Ether transfer but of any function call on another contract. Furthermore, you also have to take multi-contract situations into account. A called contract could modify the state of another contract you depend on.
Gas Limit and Loops
Loops that do not have a fixed number of iterations, for example, loops that depend on storage values, have to be used carefully:
Due to the block gas limit, transactions can only consume a certain amount of gas. Either explicitly or just due to
normal operation, the number of iterations in a loop can grow beyond the block gas limit which can cause the complete
contract to be stalled at a certain point. This may not apply to view
functions that are only executed
to read data from the blockchain. Still, such functions may be called by other contracts as part of on-chain operations
and stall those. Please be explicit about such cases in the documentation of your contracts.
Sending and Receiving Ether
Neither contracts nor « external accounts » are currently able to prevent that someone sends them Ether. Contracts can react on and reject a regular transfer, but there are ways to move Ether without creating a message call. One way is to simply « mine to » the contract address and the second way is using
selfdestruct(x)
.If a contract receives Ether (without a function being called), either the receive Ether or the fallback function is executed. If it does not have a receive nor a fallback function, the Ether will be rejected (by throwing an exception). During the execution of one of these functions, the contract can only rely on the « gas stipend » it is passed (2300 gas) being available to it at that time. This stipend is not enough to modify storage (do not take this for granted though, the stipend might change with future hard forks). To be sure that your contract can receive Ether in that way, check the gas requirements of the receive and fallback functions (for example in the « details » section in Remix).
There is a way to forward more gas to the receiving contract using
addr.call{value: x}("")
. This is essentially the same asaddr.transfer(x)
, only that it forwards all remaining gas and opens up the ability for the recipient to perform more expensive actions (and it returns a failure code instead of automatically propagating the error). This might include calling back into the sending contract or other state changes you might not have thought of. So it allows for great flexibility for honest users but also for malicious actors.Use the most precise units to represent the wei amount as possible, as you lose any that is rounded due to a lack of precision.
If you want to send Ether using
address.transfer
, there are certain details to be aware of:If the recipient is a contract, it causes its receive or fallback function to be executed which can, in turn, call back the sending contract.
Sending Ether can fail due to the call depth going above 1024. Since the caller is in total control of the call depth, they can force the transfer to fail; take this possibility into account or use
send
and make sure to always check its return value. Better yet, write your contract using a pattern where the recipient can withdraw Ether instead.Sending Ether can also fail because the execution of the recipient contract requires more than the allotted amount of gas (explicitly by using require, assert, revert or because the operation is too expensive) - it « runs out of gas » (OOG). If you use
transfer
orsend
with a return value check, this might provide a means for the recipient to block progress in the sending contract. Again, the best practice here is to use a « withdraw » pattern instead of a « send » pattern.
Call Stack Depth
External function calls can fail any time because they exceed the maximum call stack size limit of 1024. In such situations, Solidity throws an exception. Malicious actors might be able to force the call stack to a high value before they interact with your contract. Note that, since Tangerine Whistle hardfork, the 63/64 rule makes call stack depth attack impractical. Also note that the call stack and the expression stack are unrelated, even though both have a size limit of 1024 stack slots.
Note that .send()
does not throw an exception if the call stack is
depleted but rather returns false
in that case. The low-level functions
.call()
, .delegatecall()
and .staticcall()
behave in the same way.
tx.origin
Never use tx.origin for authorization. Let’s say you have a wallet contract like this:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
// THIS CONTRACT CONTAINS A BUG - DO NOT USE
contract TxUserWallet {
address owner;
constructor() {
owner = msg.sender;
}
function transferTo(address payable dest, uint amount) public {
// THE BUG IS RIGHT HERE, you must use msg.sender instead of tx.origin
require(tx.origin == owner);
dest.transfer(amount);
}
}
Now someone tricks you into sending Ether to the address of this attack wallet:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
interface TxUserWallet {
function transferTo(address payable dest, uint amount) external;
}
contract TxAttackWallet {
address payable owner;
constructor() {
owner = payable(msg.sender);
}
receive() external payable {
TxUserWallet(msg.sender).transferTo(owner, msg.sender.balance);
}
}
If your wallet had checked msg.sender
for authorization, it would get the address of the attack wallet, instead of the owner address. But by checking tx.origin
, it gets the original address that kicked off the transaction, which is still the owner address. The attack wallet instantly drains all your funds.
Two’s Complement / Underflows / Overflows
As in many programming languages, Solidity’s integer types are not actually integers. They resemble integers when the values are small, but cannot represent arbitrarily large numbers.
The following code causes an overflow because the result of the addition is too large
to be stored in the type uint8
:
uint8 x = 255;
uint8 y = 1;
return x + y;
Solidity has two modes in which it deals with these overflows: Checked and Unchecked or « wrapping » mode.
The default checked mode will detect overflows and cause a failing assertion. You can disable this check
using unchecked { ... }
, causing the overflow to be silently ignored. The above code would return
0
if wrapped in unchecked { ... }
.
Even in checked mode, do not assume you are protected from overflow bugs. In this mode, overflows will always revert. If it is not possible to avoid the overflow, this can lead to a smart contract being stuck in a certain state.
In general, read about the limits of two’s complement representation, which even has some more special edge cases for signed numbers.
Try to use require
to limit the size of inputs to a reasonable range and use the
SMT checker to find potential overflows.
Clearing Mappings
The Solidity type mapping
(see Mapping Types) is a storage-only
key-value data structure that does not keep track of the keys that were
assigned a non-zero value. Because of that, cleaning a mapping without extra
information about the written keys is not possible.
If a mapping
is used as the base type of a dynamic storage array, deleting
or popping the array will have no effect over the mapping
elements. The
same happens, for example, if a mapping
is used as the type of a member
field of a struct
that is the base type of a dynamic storage array. The
mapping
is also ignored in assignments of structs or arrays containing a
mapping
.
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.6.0 <0.9.0;
contract Map {
mapping (uint => uint)[] array;
function allocate(uint _newMaps) public {
for (uint i = 0; i < _newMaps; i++)
array.push();
}
function writeMap(uint _map, uint _key, uint _value) public {
array[_map][_key] = _value;
}
function readMap(uint _map, uint _key) public view returns (uint) {
return array[_map][_key];
}
function eraseMaps() public {
delete array;
}
}
Consider the example above and the following sequence of calls: allocate(10)
,
writeMap(4, 128, 256)
.
At this point, calling readMap(4, 128)
returns 256.
If we call eraseMaps
, the length of state variable array
is zeroed, but
since its mapping
elements cannot be zeroed, their information stays alive
in the contract’s storage.
After deleting array
, calling allocate(5)
allows us to access
array[4]
again, and calling readMap(4, 128)
returns 256 even without
another call to writeMap
.
If your mapping
information must be deleted, consider using a library similar to
iterable mapping,
allowing you to traverse the keys and delete their values in the appropriate mapping
.
Minor Details
Types that do not occupy the full 32 bytes might contain « dirty higher order bits ». This is especially important if you access
msg.data
- it poses a malleability risk: You can craft transactions that call a functionf(uint8 x)
with a raw byte argument of0xff000001
and with0x00000001
. Both are fed to the contract and both will look like the number1
as far asx
is concerned, butmsg.data
will be different, so if you usekeccak256(msg.data)
for anything, you will get different results.
Recommendations
Take Warnings Seriously
If the compiler warns you about something, you should change it. Even if you do not think that this particular warning has security implications, there might be another issue buried beneath it. Any compiler warning we issue can be silenced by slight changes to the code.
Always use the latest version of the compiler to be notified about all recently introduced warnings.
Messages of type info
issued by the compiler are not dangerous, and simply
represent extra suggestions and optional information that the compiler thinks
might be useful to the user.
Restrict the Amount of Ether
Restrict the amount of Ether (or other tokens) that can be stored in a smart contract. If your source code, the compiler or the platform has a bug, these funds may be lost. If you want to limit your loss, limit the amount of Ether.
Keep it Small and Modular
Keep your contracts small and easily understandable. Single out unrelated functionality in other contracts or into libraries. General recommendations about source code quality of course apply: Limit the amount of local variables, the length of functions and so on. Document your functions so that others can see what your intention was and whether it is different than what the code does.
Use the Checks-Effects-Interactions Pattern
Most functions will first perform some checks (who called the function, are the arguments in range, did they send enough Ether, does the person have tokens, etc.). These checks should be done first.
As the second step, if all checks passed, effects to the state variables of the current contract should be made. Interaction with other contracts should be the very last step in any function.
Early contracts delayed some effects and waited for external function calls to return in a non-error state. This is often a serious mistake because of the re-entrancy problem explained above.
Note that, also, calls to known contracts might in turn cause calls to unknown contracts, so it is probably better to just always apply this pattern.
Include a Fail-Safe Mode
While making your system fully decentralised will remove any intermediary, it might be a good idea, especially for new code, to include some kind of fail-safe mechanism:
You can add a function in your smart contract that performs some self-checks like « Has any Ether leaked? », « Is the sum of the tokens equal to the balance of the contract? » or similar things. Keep in mind that you cannot use too much gas for that, so help through off-chain computations might be needed there.
If the self-check fails, the contract automatically switches into some kind of « failsafe » mode, which, for example, disables most of the features, hands over control to a fixed and trusted third party or just converts the contract into a simple « give me back my money » contract.
Ask for Peer Review
The more people examine a piece of code, the more issues are found. Asking people to review your code also helps as a cross-check to find out whether your code is easy to understand - a very important criterion for good smart contracts.
SMTChecker and Formal Verification
Using formal verification it is possible to perform an automated mathematical proof that your source code fulfills a certain formal specification. The specification is still formal (just as the source code), but usually much simpler.
Note that formal verification itself can only help you understand the difference between what you did (the specification) and how you did it (the actual implementation). You still need to check whether the specification is what you wanted and that you did not miss any unintended effects of it.
Solidity implements a formal verification approach based on
SMT (Satisfiability Modulo Theories) and
Horn solving.
The SMTChecker module automatically tries to prove that the code satisfies the
specification given by require
and assert
statements. That is, it considers
require
statements as assumptions and tries to prove that the conditions
inside assert
statements are always true. If an assertion failure is
found, a counterexample may be given to the user showing how the assertion can
be violated. If no warning is given by the SMTChecker for a property,
it means that the property is safe.
The other verification targets that the SMTChecker checks at compile time are:
Arithmetic underflow and overflow.
Division by zero.
Trivial conditions and unreachable code.
Popping an empty array.
Out of bounds index access.
Insufficient funds for a transfer.
All the targets above are automatically checked by default if all engines are enabled, except underflow and overflow for Solidity >=0.8.7.
The potential warnings that the SMTChecker reports are:
<failing property> happens here.
. This means that the SMTChecker proved that a certain property fails. A counterexample may be given, however in complex situations it may also not show a counterexample. This result may also be a false positive in certain cases, when the SMT encoding adds abstractions for Solidity code that is either hard or impossible to express.<failing property> might happen here
. This means that the solver could not prove either case within the given timeout. Since the result is unknown, the SMTChecker reports the potential failure for soundness. This may be solved by increasing the query timeout, but the problem might also simply be too hard for the engine to solve.
To enable the SMTChecker, you must select which engine should run, where the default is no engine. Selecting the engine enables the SMTChecker on all files.
Note
Prior to Solidity 0.8.4, the default way to enable the SMTChecker was via
pragma experimental SMTChecker;
and only the contracts containing the
pragma would be analyzed. That pragma has been deprecated, and although it
still enables the SMTChecker for backwards compatibility, it will be removed
in Solidity 0.9.0. Note also that now using the pragma even in a single file
enables the SMTChecker for all files.
Note
The lack of warnings for a verification target represents an undisputed mathematical proof of correctness, assuming no bugs in the SMTChecker and the underlying solver. Keep in mind that these problems are very hard and sometimes impossible to solve automatically in the general case. Therefore, several properties might not be solved or might lead to false positives for large contracts. Every proven property should be seen as an important achievement. For advanced users, see SMTChecker Tuning to learn a few options that might help proving more complex properties.
Tutorial
Overflow
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.8.0;
contract Overflow {
uint immutable x;
uint immutable y;
function add(uint _x, uint _y) internal pure returns (uint) {
return _x + _y;
}
constructor(uint _x, uint _y) {
(x, y) = (_x, _y);
}
function stateAdd() public view returns (uint) {
return add(x, y);
}
}
The contract above shows an overflow check example.
The SMTChecker does not check underflow and overflow by default for Solidity >=0.8.7,
so we need to use the command line option --model-checker-targets "underflow,overflow"
or the JSON option settings.modelChecker.targets = ["underflow", "overflow"]
.
See this section for targets configuration.
Here, it reports the following:
Warning: CHC: Overflow (resulting value larger than 2**256 - 1) happens here.
Counterexample:
x = 1, y = 115792089237316195423570985008687907853269984665640564039457584007913129639935
= 0
Transaction trace:
Overflow.constructor(1, 115792089237316195423570985008687907853269984665640564039457584007913129639935)
State: x = 1, y = 115792089237316195423570985008687907853269984665640564039457584007913129639935
Overflow.stateAdd()
Overflow.add(1, 115792089237316195423570985008687907853269984665640564039457584007913129639935) -- internal call
--> o.sol:9:20:
|
9 | return _x + _y;
| ^^^^^^^
If we add require
statements that filter out overflow cases,
the SMTChecker proves that no overflow is reachable (by not reporting warnings):
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.8.0;
contract Overflow {
uint immutable x;
uint immutable y;
function add(uint _x, uint _y) internal pure returns (uint) {
return _x + _y;
}
constructor(uint _x, uint _y) {
(x, y) = (_x, _y);
}
function stateAdd() public view returns (uint) {
require(x < type(uint128).max);
require(y < type(uint128).max);
return add(x, y);
}
}
Assert
An assertion represents an invariant in your code: a property that must be true for all transactions, including all input and storage values, otherwise there is a bug.
The code below defines a function f
that guarantees no overflow.
Function inv
defines the specification that f
is monotonically increasing:
for every possible pair (_a, _b)
, if _b > _a
then f(_b) > f(_a)
.
Since f
is indeed monotonically increasing, the SMTChecker proves that our
property is correct. You are encouraged to play with the property and the function
definition to see what results come out!
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.8.0;
contract Monotonic {
function f(uint _x) internal pure returns (uint) {
require(_x < type(uint128).max);
return _x * 42;
}
function inv(uint _a, uint _b) public pure {
require(_b > _a);
assert(f(_b) > f(_a));
}
}
We can also add assertions inside loops to verify more complicated properties. The following code searches for the maximum element of an unrestricted array of numbers, and asserts the property that the found element must be greater or equal every element in the array.
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.8.0;
contract Max {
function max(uint[] memory _a) public pure returns (uint) {
uint m = 0;
for (uint i = 0; i < _a.length; ++i)
if (_a[i] > m)
m = _a[i];
for (uint i = 0; i < _a.length; ++i)
assert(m >= _a[i]);
return m;
}
}
Note that in this example the SMTChecker will automatically try to prove three properties:
++i
in the first loop does not overflow.++i
in the second loop does not overflow.The assertion is always true.
Note
The properties involve loops, which makes it much much harder than the previous examples, so beware of loops!
All the properties are correctly proven safe. Feel free to change the properties and/or add restrictions on the array to see different results. For example, changing the code to
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.8.0;
contract Max {
function max(uint[] memory _a) public pure returns (uint) {
require(_a.length >= 5);
uint m = 0;
for (uint i = 0; i < _a.length; ++i)
if (_a[i] > m)
m = _a[i];
for (uint i = 0; i < _a.length; ++i)
assert(m > _a[i]);
return m;
}
}
gives us:
Warning: CHC: Assertion violation happens here.
Counterexample:
_a = [0, 0, 0, 0, 0]
= 0
Transaction trace:
Test.constructor()
Test.max([0, 0, 0, 0, 0])
--> max.sol:14:4:
|
14 | assert(m > _a[i]);
State Properties
So far the examples only demonstrated the use of the SMTChecker over pure code, proving properties about specific operations or algorithms. A common type of properties in smart contracts are properties that involve the state of the contract. Multiple transactions might be needed to make an assertion fail for such a property.
As an example, consider a 2D grid where both axis have coordinates in the range (-2^128, 2^128 - 1). Let us place a robot at position (0, 0). The robot can only move diagonally, one step at a time, and cannot move outside the grid. The robot’s state machine can be represented by the smart contract below.
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.8.0;
contract Robot {
int x = 0;
int y = 0;
modifier wall {
require(x > type(int128).min && x < type(int128).max);
require(y > type(int128).min && y < type(int128).max);
_;
}
function moveLeftUp() wall public {
--x;
++y;
}
function moveLeftDown() wall public {
--x;
--y;
}
function moveRightUp() wall public {
++x;
++y;
}
function moveRightDown() wall public {
++x;
--y;
}
function inv() public view {
assert((x + y) % 2 == 0);
}
}
Function inv
represents an invariant of the state machine that x + y
must be even.
The SMTChecker manages to prove that regardless how many commands we give the
robot, even if infinitely many, the invariant can never fail. The interested
reader may want to prove that fact manually as well. Hint: this invariant is
inductive.
We can also trick the SMTChecker into giving us a path to a certain position we think might be reachable. We can add the property that (2, 4) is not reachable, by adding the following function.
function reach_2_4() public view {
assert(!(x == 2 && y == 4));
}
This property is false, and while proving that the property is false, the SMTChecker tells us exactly how to reach (2, 4):
Warning: CHC: Assertion violation happens here.
Counterexample:
x = 2, y = 4
Transaction trace:
Robot.constructor()
State: x = 0, y = 0
Robot.moveLeftUp()
State: x = (- 1), y = 1
Robot.moveRightUp()
State: x = 0, y = 2
Robot.moveRightUp()
State: x = 1, y = 3
Robot.moveRightUp()
State: x = 2, y = 4
Robot.reach_2_4()
--> r.sol:35:4:
|
35 | assert(!(x == 2 && y == 4));
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^
Note that the path above is not necessarily deterministic, as there are other paths that could reach (2, 4). The choice of which path is shown might change depending on the used solver, its version, or just randomly.
External Calls and Reentrancy
Every external call is treated as a call to unknown code by the SMTChecker. The reasoning behind that is that even if the code of the called contract is available at compile time, there is no guarantee that the deployed contract will indeed be the same as the contract where the interface came from at compile time.
In some cases, it is possible to automatically infer properties over state variables that are still true even if the externally called code can do anything, including reenter the caller contract.
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.8.0;
interface Unknown {
function run() external;
}
contract Mutex {
uint x;
bool lock;
Unknown immutable unknown;
constructor(Unknown _u) {
require(address(_u) != address(0));
unknown = _u;
}
modifier mutex {
require(!lock);
lock = true;
_;
lock = false;
}
function set(uint _x) mutex public {
x = _x;
}
function run() mutex public {
uint xPre = x;
unknown.run();
assert(xPre == x);
}
}
The example above shows a contract that uses a mutex flag to forbid reentrancy.
The solver is able to infer that when unknown.run()
is called, the contract
is already « locked », so it would not be possible to change the value of x
,
regardless of what the unknown called code does.
If we « forget » to use the mutex
modifier on function set
, the
SMTChecker is able to synthesize the behaviour of the externally called code so
that the assertion fails:
Warning: CHC: Assertion violation happens here.
Counterexample:
x = 1, lock = true, unknown = 1
Transaction trace:
Mutex.constructor(1)
State: x = 0, lock = false, unknown = 1
Mutex.run()
unknown.run() -- untrusted external call, synthesized as:
Mutex.set(1) -- reentrant call
--> m.sol:32:3:
|
32 | assert(xPre == x);
| ^^^^^^^^^^^^^^^^^
SMTChecker Options and Tuning
Timeout
The SMTChecker uses a hardcoded resource limit (rlimit
) chosen per solver,
which is not precisely related to time. We chose the rlimit
option as the default
because it gives more determinism guarantees than time inside the solver.
This options translates roughly to « a few seconds timeout » per query. Of course many properties
are very complex and need a lot of time to be solved, where determinism does not matter.
If the SMTChecker does not manage to solve the contract properties with the default rlimit
,
a timeout can be given in milliseconds via the CLI option --model-checker-timeout <time>
or
the JSON option settings.modelChecker.timeout=<time>
, where 0 means no timeout.
Verification Targets
The types of verification targets created by the SMTChecker can also be
customized via the CLI option --model-checker-target <targets>
or the JSON
option settings.modelChecker.targets=<targets>
.
In the CLI case, <targets>
is a no-space-comma-separated list of one or
more verification targets, and an array of one or more targets as strings in
the JSON input.
The keywords that represent the targets are:
Assertions:
assert
.Arithmetic underflow:
underflow
.Arithmetic overflow:
overflow
.Division by zero:
divByZero
.Trivial conditions and unreachable code:
constantCondition
.Popping an empty array:
popEmptyArray
.Out of bounds array/fixed bytes index access:
outOfBounds
.Insufficient funds for a transfer:
balance
.All of the above:
default
(CLI only).
A common subset of targets might be, for example:
--model-checker-targets assert,overflow
.
All targets are checked by default, except underflow and overflow for Solidity >=0.8.7.
There is no precise heuristic on how and when to split verification targets, but it can be useful especially when dealing with large contracts.
Unproved Targets
If there are any unproved targets, the SMTChecker issues one warning stating
how many unproved targets there are. If the user wishes to see all the specific
unproved targets, the CLI option --model-checker-show-unproved
and
the JSON option settings.modelChecker.showUnproved = true
can be used.
Verified Contracts
By default all the deployable contracts in the given sources are analyzed separately as the one that will be deployed. This means that if a contract has many direct and indirect inheritance parents, all of them will be analyzed on their own, even though only the most derived will be accessed directly on the blockchain. This causes an unnecessary burden on the SMTChecker and the solver. To aid cases like this, users can specify which contracts should be analyzed as the deployed one. The parent contracts are of course still analyzed, but only in the context of the most derived contract, reducing the complexity of the encoding and generated queries. Note that abstract contracts are by default not analyzed as the most derived by the SMTChecker.
The chosen contracts can be given via a comma-separated list (whitespace is not
allowed) of <source>:<contract> pairs in the CLI:
--model-checker-contracts "<source1.sol:contract1>,<source2.sol:contract2>,<source2.sol:contract3>"
,
and via the object settings.modelChecker.contracts
in the JSON input,
which has the following form:
"contracts": {
"source1.sol": ["contract1"],
"source2.sol": ["contract2", "contract3"]
}
Reported Inferred Inductive Invariants
For properties that were proved safe with the CHC engine, the SMTChecker can retrieve inductive invariants that were inferred by the Horn solver as part of the proof. Currently two types of invariants can be reported to the user:
Contract Invariants: these are properties over the contract’s state variables that are true before and after every possible transaction that the contract may ever run. For example,
x >= y
, wherex
andy
are a contract’s state variables.Reentrancy Properties: they represent the behavior of the contract in the presence of external calls to unknown code. These properties can express a relation between the value of the state variables before and after the external call, where the external call is free to do anything, including making reentrant calls to the analyzed contract. Primed variables represent the state variables” values after said external call. Example:
lock -> x = x'
.
The user can choose the type of invariants to be reported using the CLI option --model-checker-invariants "contract,reentrancy"
or as an array in the field settings.modelChecker.invariants
in the JSON input.
By default the SMTChecker does not report invariants.
Division and Modulo With Slack Variables
Spacer, the default Horn solver used by the SMTChecker, often dislikes division
and modulo operations inside Horn rules. Because of that, by default the
Solidity division and modulo operations are encoded using the constraint
a = b * d + m
where d = a / b
and m = a % b
.
However, other solvers, such as Eldarica, prefer the syntactically precise operations.
The command line flag --model-checker-div-mod-no-slacks
and the JSON option
settings.modelChecker.divModNoSlacks
can be used to toggle the encoding
depending on the used solver preferences.
Natspec Function Abstraction
Certain functions including common math methods such as pow
and sqrt
may be too complex to be analyzed in a fully automated way.
These functions can be annotated with Natspec tags that indicate to the
SMTChecker that these functions should be abstracted. This means that the
body of the function is not used, and when called, the function will:
Return a nondeterministic value, and either keep the state variables unchanged if the abstracted function is view/pure, or also set the state variables to nondeterministic values otherwise. This can be used via the annotation
/// @custom:smtchecker abstract-function-nondet
.Act as an uninterpreted function. This means that the semantics of the function (given by the body) are ignored, and the only property this function has is that given the same input it guarantees the same output. This is currently under development and will be available via the annotation
/// @custom:smtchecker abstract-function-uf
.
Model Checking Engines
The SMTChecker module implements two different reasoning engines, a Bounded Model Checker (BMC) and a system of Constrained Horn Clauses (CHC). Both engines are currently under development, and have different characteristics. The engines are independent and every property warning states from which engine it came. Note that all the examples above with counterexamples were reported by CHC, the more powerful engine.
By default both engines are used, where CHC runs first, and every property that
was not proven is passed over to BMC. You can choose a specific engine via the CLI
option --model-checker-engine {all,bmc,chc,none}
or the JSON option
settings.modelChecker.engine={all,bmc,chc,none}
.
Bounded Model Checker (BMC)
The BMC engine analyzes functions in isolation, that is, it does not take the overall behavior of the contract over multiple transactions into account when analyzing each function. Loops are also ignored in this engine at the moment. Internal function calls are inlined as long as they are not recursive, directly or indirectly. External function calls are inlined if possible. Knowledge that is potentially affected by reentrancy is erased.
The characteristics above make BMC prone to reporting false positives, but it is also lightweight and should be able to quickly find small local bugs.
Constrained Horn Clauses (CHC)
A contract’s Control Flow Graph (CFG) is modelled as a system of Horn clauses, where the life cycle of the contract is represented by a loop that can visit every public/external function non-deterministically. This way, the behavior of the entire contract over an unbounded number of transactions is taken into account when analyzing any function. Loops are fully supported by this engine. Internal function calls are supported, and external function calls assume the called code is unknown and can do anything.
The CHC engine is much more powerful than BMC in terms of what it can prove, and might require more computing resources.
SMT and Horn solvers
The two engines detailed above use automated theorem provers as their logical backends. BMC uses an SMT solver, whereas CHC uses a Horn solver. Often the same tool can act as both, as seen in z3, which is primarily an SMT solver and makes Spacer available as a Horn solver, and Eldarica which does both.
The user can choose which solvers should be used, if available, via the CLI
option --model-checker-solvers {all,cvc4,smtlib2,z3}
or the JSON option
settings.modelChecker.solvers=[smtlib2,z3]
, where:
cvc4
is only available if thesolc
binary is compiled with it. Only BMC usescvc4
.smtlib2
outputs SMT/Horn queries in the smtlib2 format. These can be used together with the compiler’s callback mechanism so that any solver binary from the system can be employed to synchronously return the results of the queries to the compiler. This is currently the only way to use Eldarica, for example, since it does not have a C++ API. This can be used by both BMC and CHC depending on which solvers are called.z3
is availableif
solc
is compiled with it;if a dynamic
z3
library of version 4.8.x is installed in a Linux system (from Solidity 0.7.6);statically in
soljson.js
(from Solidity 0.6.9), that is, the Javascript binary of the compiler.
Since both BMC and CHC use z3
, and z3
is available in a greater variety
of environments, including in the browser, most users will almost never need to be
concerned about this option. More advanced users might apply this option to try
alternative solvers on more complex problems.
Please note that certain combinations of chosen engine and solver will lead to
the SMTChecker doing nothing, for example choosing CHC and cvc4
.
Abstraction and False Positives
The SMTChecker implements abstractions in an incomplete and sound way: If a bug is reported, it might be a false positive introduced by abstractions (due to erasing knowledge or using a non-precise type). If it determines that a verification target is safe, it is indeed safe, that is, there are no false negatives (unless there is a bug in the SMTChecker).
If a target cannot be proven you can try to help the solver by using the tuning
options in the previous section.
If you are sure of a false positive, adding require
statements in the code
with more information may also give some more power to the solver.
SMT Encoding and Types
The SMTChecker encoding tries to be as precise as possible, mapping Solidity types and expressions to their closest SMT-LIB representation, as shown in the table below.
Solidity type |
SMT sort |
Theories |
---|---|---|
Boolean |
Bool |
Bool |
intN, uintN, address, bytesN, enum, contract |
Integer |
LIA, NIA |
array, mapping, bytes, string |
Tuple (Array elements, Integer length) |
Datatypes, Arrays, LIA |
struct |
Tuple |
Datatypes |
other types |
Integer |
LIA |
Types that are not yet supported are abstracted by a single 256-bit unsigned integer, where their unsupported operations are ignored.
For more details on how the SMT encoding works internally, see the paper SMT-based Verification of Solidity Smart Contracts.
Function Calls
In the BMC engine, function calls to the same contract (or base contracts) are inlined when possible, that is, when their implementation is available. Calls to functions in other contracts are not inlined even if their code is available, since we cannot guarantee that the actual deployed code is the same.
The CHC engine creates nonlinear Horn clauses that use summaries of the called functions to support internal function calls. External function calls are treated as calls to unknown code, including potential reentrant calls.
Complex pure functions are abstracted by an uninterpreted function (UF) over the arguments.
Functions |
BMC/CHC behavior |
---|---|
|
Verification target. |
|
Assumption. |
internal call |
BMC: Inline function call. CHC: Function summaries. |
external call to known code |
BMC: Inline function call or erase knowledge about state variables and local storage references. CHC: Assume called code is unknown. Try to infer invariants that hold after the call returns. |
Storage array push/pop |
Supported precisely. Checks whether it is popping an empty array. |
ABI functions |
Abstracted with UF. |
|
Supported precisely. |
|
Abstracted with UF. |
pure functions without implementation (external or complex) |
Abstracted with UF |
external functions without implementation |
BMC: Erase state knowledge and assume result is nondeterminisc. CHC: Nondeterministic summary. Try to infer invariants that hold after the call returns. |
transfer |
BMC: Checks whether the contract’s balance is sufficient. CHC: does not yet perform the check. |
others |
Currently unsupported |
Using abstraction means loss of precise knowledge, but in many cases it does not mean loss of proving power.
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.8.0;
contract Recover
{
function f(
bytes32 hash,
uint8 _v1, uint8 _v2,
bytes32 _r1, bytes32 _r2,
bytes32 _s1, bytes32 _s2
) public pure returns (address) {
address a1 = ecrecover(hash, _v1, _r1, _s1);
require(_v1 == _v2);
require(_r1 == _r2);
require(_s1 == _s2);
address a2 = ecrecover(hash, _v2, _r2, _s2);
assert(a1 == a2);
return a1;
}
}
In the example above, the SMTChecker is not expressive enough to actually
compute ecrecover
, but by modelling the function calls as uninterpreted
functions we know that the return value is the same when called on equivalent
parameters. This is enough to prove that the assertion above is always true.
Abstracting a function call with an UF can be done for functions known to be deterministic, and can be easily done for pure functions. It is however difficult to do this with general external functions, since they might depend on state variables.
Reference Types and Aliasing
Solidity implements aliasing for reference types with the same data location. That means one variable may be modified through a reference to the same data area. The SMTChecker does not keep track of which references refer to the same data. This implies that whenever a local reference or state variable of reference type is assigned, all knowledge regarding variables of the same type and data location is erased. If the type is nested, the knowledge removal also includes all the prefix base types.
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.8.0;
contract Aliasing
{
uint[] array1;
uint[][] array2;
function f(
uint[] memory a,
uint[] memory b,
uint[][] memory c,
uint[] storage d
) internal {
array1[0] = 42;
a[0] = 2;
c[0][0] = 2;
b[0] = 1;
// Erasing knowledge about memory references should not
// erase knowledge about state variables.
assert(array1[0] == 42);
// However, an assignment to a storage reference will erase
// storage knowledge accordingly.
d[0] = 2;
// Fails as false positive because of the assignment above.
assert(array1[0] == 42);
// Fails because `a == b` is possible.
assert(a[0] == 2);
// Fails because `c[i] == b` is possible.
assert(c[0][0] == 2);
assert(d[0] == 2);
assert(b[0] == 1);
}
function g(
uint[] memory a,
uint[] memory b,
uint[][] memory c,
uint x
) public {
f(a, b, c, array2[x]);
}
}
After the assignment to b[0]
, we need to clear knowledge about a
since
it has the same type (uint[]
) and data location (memory). We also need to
clear knowledge about c
, since its base type is also a uint[]
located
in memory. This implies that some c[i]
could refer to the same data as
b
or a
.
Notice that we do not clear knowledge about array
and d
because they
are located in storage, even though they also have type uint[]
. However,
if d
was assigned, we would need to clear knowledge about array
and
vice-versa.
Contract Balance
A contract may be deployed with funds sent to it, if msg.value
> 0 in the
deployment transaction.
However, the contract’s address may already have funds before deployment,
which are kept by the contract.
Therefore, the SMTChecker assumes that address(this).balance >= msg.value
in the constructor in order to be consistent with the EVM rules.
The contract’s balance may also increase without triggering any calls to the
contract, if
selfdestruct
is executed by another contract with the analyzed contract as the target of the remaining funds,the contract is the coinbase (i.e.,
block.coinbase
) of some block.
To model this properly, the SMTChecker assumes that at every new transaction
the contract’s balance may grow by at least msg.value
.
Real World Assumptions
Some scenarios can be expressed in Solidity and the EVM, but are expected to
never occur in practice.
One of such cases is the length of a dynamic storage array overflowing during a
push: If the push
operation is applied to an array of length 2^256 - 1, its
length silently overflows.
However, this is unlikely to happen in practice, since the operations required
to grow the array to that point would take billions of years to execute.
Another similar assumption taken by the SMTChecker is that an address” balance
can never overflow.
A similar idea was presented in EIP-1985.
Resources
General Resources
Integrated (Ethereum) Development Environments
- Brownie
Python-based development and testing framework for smart contracts targeting the Ethereum Virtual Machine.
- Dapp
Tool for building, testing and deploying smart contracts from the command line.
- Embark
Developer platform for building and deploying decentralized applications.
- Foundry
Fast, portable and modular toolkit for Ethereum application development written in Rust.
- Hardhat
Ethereum development environment with local Ethereum network, debugging features and plugin ecosystem.
- Remix
Browser-based IDE with integrated compiler and Solidity runtime environment without server-side components.
- Truffle
Ethereum development framework.
Editor Integrations
Atom
- Etheratom
Plugin for the Atom editor that features syntax highlighting, compilation and a runtime environment (Backend node & VM compatible).
- Atom Solidity Linter
Plugin for the Atom editor that provides Solidity linting.
- Atom Solium Linter
Configurable Solidity linter for Atom using Solium (now Ethlint) as a base.
Emacs
- Emacs Solidity
Plugin for the Emacs editor providing syntax highlighting and compilation error reporting.
IntelliJ
- IntelliJ IDEA plugin
Solidity plugin for IntelliJ IDEA (and all other JetBrains IDEs)
Sublime
- Package for SublimeText - Solidity language syntax
Solidity syntax highlighting for SublimeText editor.
Vim
- Vim Solidity
Plugin for the Vim editor providing syntax highlighting.
- Vim Syntastic
Plugin for the Vim editor providing compile checking.
Visual Studio Code
- Visual Studio Code extension
Solidity plugin for Microsoft Visual Studio Code that includes syntax highlighting and the Solidity compiler.
- Solidity Visual Auditor extension
Adds security centric syntax and semantic highlighting to Visual Studio Code.
Solidity Tools
- ABI to Solidity interface converter
A script for generating contract interfaces from the ABI of a smart contract.
- abi-to-sol
Tool to generate Solidity interface source from a given ABI JSON.
- Doxity
Documentation Generator for Solidity.
- Ethlint
Linter to identify and fix style and security issues in Solidity.
- evmdis
EVM Disassembler that performs static analysis on the bytecode to provide a higher level of abstraction than raw EVM operations.
- EVM Lab
Rich tool package to interact with the EVM. Includes a VM, Etherchain API, and a trace-viewer with gas cost display.
- hevm
EVM debugger and symbolic execution engine.
- leafleth
A documentation generator for Solidity smart-contracts.
- PIET
A tool to develop, audit and use Solidity smart contracts through a simple graphical interface.
- Scaffold-ETH
Forkable Ethereum development stack focused on fast product iterations.
- sol2uml
Unified Modeling Language (UML) class diagram generator for Solidity contracts.
- solc-select
A script to quickly switch between Solidity compiler versions.
- Solidity prettier plugin
A Prettier Plugin for Solidity.
- Solidity REPL
Try Solidity instantly with a command-line Solidity console.
- solgraph
Visualize Solidity control flow and highlight potential security vulnerabilities.
- Solhint
Solidity linter that provides security, style guide and best practice rules for smart contract validation.
- Sourcify
Decentralized automated contract verification service and public repository of contract metadata.
- Sūrya
Utility tool for smart contract systems, offering a number of visual outputs and information about the contracts” structure. Also supports querying the function call graph.
- Universal Mutator
A tool for mutation generation, with configurable rules and support for Solidity and Vyper.
Third-Party Solidity Parsers and Grammars
- Solidity Parser for JavaScript
A Solidity parser for JS built on top of a robust ANTLR4 grammar.
Import Path Resolution
In order to be able to support reproducible builds on all platforms, the Solidity compiler has to abstract away the details of the filesystem where source files are stored. Paths used in imports must work the same way everywhere while the command-line interface must be able to work with platform-specific paths to provide good user experience. This section aims to explain in detail how Solidity reconciles these requirements.
Virtual Filesystem
The compiler maintains an internal database (virtual filesystem or VFS for short) where each source unit is assigned a unique source unit name which is an opaque and unstructured identifier. When you use the import statement, you specify an import path that references a source unit name.
Import Callback
The VFS is initially populated only with files the compiler has received as input. Additional files can be loaded during compilation using an import callback, which is different depending on the type of compiler you use (see below). If the compiler does not find any source unit name matching the import path in the VFS, it invokes the callback, which is responsible for obtaining the source code to be placed under that name. An import callback is free to interpret source unit names in an arbitrary way, not just as paths. If there is no callback available when one is needed or if it fails to locate the source code, compilation fails.
The command-line compiler provides the Host Filesystem Loader - a rudimentary callback that interprets a source unit name as a path in the local filesystem. The JavaScript interface does not provide any by default, but one can be provided by the user. This mechanism can be used to obtain source code from locations other then the local filesystem (which may not even be accessible, e.g. when the compiler is running in a browser). For example the Remix IDE provides a versatile callback that lets you import files from HTTP, IPFS and Swarm URLs or refer directly to packages in NPM registry.
Note
Host Filesystem Loader’s file lookup is platform-dependent. For example backslashes in a source unit name can be interpreted as directory separators or not and the lookup can be case-sensitive or not, depending on the underlying platform.
For portability it is recommended to avoid using import paths that will work correctly only with a specific import callback or only on one platform. For example you should always use forward slashes since they work as path separators also on platforms that support backslashes.
Initial Content of the Virtual Filesystem
The initial content of the VFS depends on how you invoke the compiler:
solc / command-line interface
When you compile a file using the command-line interface of the compiler, you provide one or more paths to files containing Solidity code:
solc contract.sol /usr/local/dapp-bin/token.sol
The source unit name of a file loaded this way is constructed by converting its path to a canonical form and, if possible, making it relative to either the base path or one of the include paths. See CLI Path Normalization and Stripping for a detailed description of this process.
Standard JSON
When using the Standard JSON API (via either the JavaScript interface or the
--standard-json
command-line option) you provide input in JSON format, containing, among other things, the content of all your source files:{ "language": "Solidity", "sources": { "contract.sol": { "content": "import \"./util.sol\";\ncontract C {}" }, "util.sol": { "content": "library Util {}" }, "/usr/local/dapp-bin/token.sol": { "content": "contract Token {}" } }, "settings": {"outputSelection": {"*": { "*": ["metadata", "evm.bytecode"]}}} }
The
sources
dictionary becomes the initial content of the virtual filesystem and its keys are used as source unit names.Standard JSON (via import callback)
With Standard JSON it is also possible to tell the compiler to use the import callback to obtain the source code:
{ "language": "Solidity", "sources": { "/usr/local/dapp-bin/token.sol": { "urls": [ "/projects/mytoken.sol", "https://example.com/projects/mytoken.sol" ] } }, "settings": {"outputSelection": {"*": { "*": ["metadata", "evm.bytecode"]}}} }
If an import callback is available, the compiler will give it the strings specified in
urls
one by one, until one is loaded successfully or the end of the list is reached.The source unit names are determined the same way as when using
content
- they are keys of thesources
dictionary and the content ofurls
does not affect them in any way.Standard input
On the command line it is also possible to provide the source by sending it to compiler’s standard input:
echo 'import "./util.sol"; contract C {}' | solc -
-
used as one of the arguments instructs the compiler to place the content of the standard input in the virtual filesystem under a special source unit name:<stdin>
.
Once the VFS is initialized, additional files can still be added to it only through the import callback.
Imports
The import statement specifies an import path. Based on how the import path is specified, we can divide imports into two categories:
Direct imports, where you specify the full source unit name directly.
Relative imports, where you specify a path starting with
./
or../
to be combined with the source unit name of the importing file.
import "./math/math.sol";
import "contracts/tokens/token.sol";
In the above ./math/math.sol
and contracts/tokens/token.sol
are import paths while the
source unit names they translate to are contracts/math/math.sol
and contracts/tokens/token.sol
respectively.
Direct Imports
An import that does not start with ./
or ../
is a direct import.
import "/project/lib/util.sol"; // source unit name: /project/lib/util.sol
import "lib/util.sol"; // source unit name: lib/util.sol
import "@openzeppelin/address.sol"; // source unit name: @openzeppelin/address.sol
import "https://example.com/token.sol"; // source unit name: https://example.com/token.sol
After applying any import remappings the import path simply becomes the source unit name.
Note
A source unit name is just an identifier and even if its value happens to look like a path, it
is not subject to the normalization rules you would typically expect in a shell.
Any /./
or /../
seguments or sequences of multiple slashes remain a part of it.
When the source is provided via Standard JSON interface it is entirely possible to associate
different content with source unit names that would refer to the same file on disk.
When the source is not available in the virtual filesystem, the compiler passes the source unit name
to the import callback.
The Host Filesystem Loader will attempt to use it as a path and look up the file on disk.
At this point the platform-specific normalization rules kick in and names that were considered
different in the VFS may actually result in the same file being loaded.
For example /project/lib/math.sol
and /project/lib/../lib///math.sol
are considered
completely different in the VFS even though they refer to the same file on disk.
Note
Even if an import callback ends up loading source code for two different source unit names from the same file on disk, the compiler will still see them as separate source units. It is the source unit name that matters, not the physical location of the code.
Relative Imports
An import starting with ./
or ../
is a relative import.
Such imports specify a path relative to the source unit name of the importing source unit:
import "./util.sol" as util; // source unit name: /project/lib/util.sol
import "../token.sol" as token; // source unit name: /project/token.sol
import "./util.sol" as util; // source unit name: lib/util.sol
import "../token.sol" as token; // source unit name: token.sol
Note
Relative imports always start with ./
or ../
so import "util.sol"
, unlike
import "./util.sol"
, is a direct import.
While both paths would be considered relative in the host filesystem, util.sol
is actually
absolute in the VFS.
Let us define a path segment as any non-empty part of the path that does not contain a separator
and is bounded by two path separators.
A separator is a forward slash or the beginning/end of the string.
For example in ./abc/..//
there are three path segments: .
, abc
and ..
.
The compiler computes a source unit name from the import path in the following way:
First a prefix is computed
Prefix is initialized with the source unit name of the importing source unit.
The last path segment with preceding slashes is removed from the prefix.
Then, the leading part of the normalized import path, consisting only of
/
and.
characters is considered. For every..
segment found in this part the last path segment with preceding slashes is removed from the prefix.
Then the prefix is prepended to the normalized import path. If the prefix is non-empty, a single slash is inserted between it and the import path.
The removal of the last path segment with preceding slashes is understood to work as follows:
Everything past the last slash is removed (i.e.
a/b//c.sol
becomesa/b//
).All trailing slashes are removed (i.e.
a/b//
becomesa/b
).
The normalization rules are the same as for UNIX paths, namely:
All the internal
.
segments are removed.Every internal
..
segment backtracks one level up in the hierarchy.Multiple slashes are squashed into a single one.
Note that normalization is performed only on the import path.
The source unit name of the importing module that is used for the prefix remains unnormalized.
This ensures that the protocol://
part does not turn into protocol:/
if the importing file
is identified with a URL.
If your import paths are already normalized, you can expect the above algorithm to produce very intuitive results. Here are some examples of what you can expect if they are not:
import "./util/./util.sol"; // source unit name: lib/src/../util/util.sol
import "./util//util.sol"; // source unit name: lib/src/../util/util.sol
import "../util/../array/util.sol"; // source unit name: lib/src/array/util.sol
import "../.././../util.sol"; // source unit name: util.sol
import "../../.././../util.sol"; // source unit name: util.sol
Note
The use of relative imports containing leading ..
segments is not recommended.
The same effect can be achieved in a more reliable way by using direct imports with
base path and include paths.
Base Path and Include Paths
The base path and include paths represent directories that the Host Filesystem Loader will load files from. When a source unit name is passed to the loader, it prepends the base path to it and performs a filesystem lookup. If the lookup does not succeed, the same is done with all directories on the include path list.
It is recommended to set the base path to the root directory of your project and use include paths to
specify additional locations that may contain libraries your project depends on.
This lets you import from these libraries in a uniform way, no matter where they are located in the
filesystem relative to your project.
For example, if you use npm to install packages and your contract imports
@openzeppelin/contracts/utils/Strings.sol
, you can use these options to tell the compiler that
the library can be found in one of the npm package directories:
solc contract.sol \
--base-path . \
--include-path node_modules/ \
--include-path /usr/local/lib/node_modules/
Your contract will compile (with the same exact metadata) no matter whether you install the library in the local or global package directory or even directly under your project root.
By default the base path is empty, which leaves the source unit name unchanged. When the source unit name is a relative path, this results in the file being looked up in the directory the compiler has been invoked from. It is also the only value that results in absolute paths in source unit names being actually interpreted as absolute paths on disk. If the base path itself is relative, it is interpreted as relative to the current working directory of the compiler.
Note
Include paths cannot have empty values and must be used together with a non-empty base path.
Note
Include paths and base path can overlap as long as it does not make import resolution ambiguous. For example, you can specify a directory inside base path as an include directory or have an include directory that is a subdirectory of another include directory. The compiler will only issue an error if the source unit name passed to the Host Filesystem Loader represents an existing path when combined with multiple include paths or an include path and base path.
CLI Path Normalization and Stripping
On the command line the compiler behaves just as you would expect from any other program: it accepts paths in a format native to the platform and relative paths are relative to the current working directory. The source unit names assigned to files whose paths are specified on the command line, however, should not change just because the project is being compiled on a different platform or because the compiler happens to have been invoked from a different directory. To achieve this, paths to source files coming from the command line must be converted to a canonical form, and, if possible, made relative to the base path or one of the include paths.
The normalization rules are as follows:
If a path is relative, it is made absolute by prepending the current working directory to it.
Internal
.
and..
segments are collapsed.Platform-specific path separators are replaced with forward slashes.
Sequences of multiple consecutive path separators are squashed into a single separator (unless they are the leading slashes of an UNC path).
If the path includes a root name (e.g. a drive letter on Windows) and the root is the same as the root of the current working directory, the root is replaced with
/
.Symbolic links in the path are not resolved.
The only exception is the path to the current working directory prepended to relative paths in the process of making them absolute. On some platforms the working directory is reported always with symbolic links resolved so for consistency the compiler resolves them everywhere.
The original case of the path is preserved even if the filesystem is case-insensitive but case-preserving and the actual case on disk is different.
Note
There are situations where paths cannot be made platform-independent.
For example on Windows the compiler can avoid using drive letters by referring to the root
directory of the current drive as /
but drive letters are still necessary for paths leading
to other drives.
You can avoid such situations by ensuring that all the files are available within a single
directory tree on the same drive.
After normalization the compiler attempts to make the source file path relative.
It tries the base path first and then the include paths in the order they were given.
If the base path is empty or not specified, it is treated as if it was equal to the path to the
current working directory (with all symbolic links resolved).
The result is accepted only if the normalized directory path is the exact prefix of the normalized
file path.
Otherwise the file path remains absolute.
This makes the conversion unambiguous and ensures that the relative path does not start with ../
.
The resulting file path becomes the source unit name.
Note
The relative path produced by stripping must remain unique within the base path and include paths.
For example the compiler will issue an error for the following command if both
/project/contract.sol
and /lib/contract.sol
exist:
solc /project/contract.sol --base-path /project --include-path /lib
Note
Prior to version 0.8.8, CLI path stripping was not performed and the only normalization applied was the conversion of path separators. When working with older versions of the compiler it is recommended to invoke the compiler from the base path and to only use relative paths on the command line.
Allowed Paths
As a security measure, the Host Filesystem Loader will refuse to load files from outside of a few locations that are considered safe by default:
Outside of Standard JSON mode:
The directories containing input files listed on the command line.
The directories used as remapping targets. If the target is not a directory (i.e does not end with
/
,/.
or/..
) the directory containing the target is used instead.Base path and include paths.
In Standard JSON mode:
Base path and include paths.
Additional directories can be whitelisted using the --allow-paths
option.
The option accepts a comma-separated list of paths:
cd /home/user/project/
solc token/contract.sol \
lib/util.sol=libs/util.sol \
--base-path=token/ \
--include-path=/lib/ \
--allow-paths=../utils/,/tmp/libraries
When the compiler is invoked with the command shown above, the Host Filesystem Loader will allow importing files from the following directories:
/home/user/project/token/
(becausetoken/
contains the input file and also because it is the base path),/lib/
(because/lib/
is one of the include paths),/home/user/project/libs/
(becauselibs/
is a directory containing a remapping target),/home/user/utils/
(because of../utils/
passed to--allow-paths
),/tmp/libraries/
(because of/tmp/libraries
passed to--allow-paths
),
Note
The working directory of the compiler is one of the paths allowed by default only if it happens to be the base path (or the base path is not specified or has an empty value).
Note
The compiler does not check if allowed paths actually exist and whether they are directories. Non-existent or empty paths are simply ignored. If an allowed path matches a file rather than a directory, the file is considered whitelisted, too.
Note
Allowed paths are case-sensitive even if the filesystem is not.
The case must exactly match the one used in your imports.
For example --allow-paths tokens
will not match import "Tokens/IERC20.sol"
.
Avertissement
Files and directories only reachable through symbolic links from allowed directories are not
automatically whitelisted.
For example if token/contract.sol
in the example above was actually a symlink pointing at
/etc/passwd
the compiler would refuse to load it unless /etc/
was one of the allowed
paths too.
Import Remapping
Import remapping allows you to redirect imports to a different location in the virtual filesystem.
The mechanism works by changing the translation between import paths and source unit names.
For example you can set up a remapping so that any import from the virtual directory
github.com/ethereum/dapp-bin/library/
would be seen as an import from dapp-bin/library/
instead.
You can limit the scope of a remapping by specifying a context. This allows creating remappings that apply only to imports located in a specific library or a specific file. Without a context a remapping is applied to every matching import in all the files in the virtual filesystem.
Import remappings have the form of context:prefix=target
:
context
must match the beginning of the source unit name of the file containing the import.prefix
must match the beginning of the source unit name resulting from the import.target
is the value the prefix is replaced with.
For example, if you clone https://github.com/ethereum/dapp-bin/ locally to /project/dapp-bin
and run the compiler with:
solc github.com/ethereum/dapp-bin/=dapp-bin/ --base-path /project source.sol
you can use the following in your source file:
import "github.com/ethereum/dapp-bin/library/math.sol"; // source unit name: dapp-bin/library/math.sol
The compiler will look for the file in the VFS under dapp-bin/library/math.sol
.
If the file is not available there, the source unit name will be passed to the Host Filesystem
Loader, which will then look in /project/dapp-bin/library/iterable_mapping.sol
.
Avertissement
Information about remappings is stored in contract metadata. Since the binary produced by the compiler has a hash of the metadata embedded in it, any modification to the remappings will result in different bytecode.
For this reason you should be careful not to include any local information in remapping targets.
For example if your library is located in /home/user/packages/mymath/math.sol
, a remapping
like @math/=/home/user/packages/mymath/
would result in your home directory being included in
the metadata.
To be able to reproduce the same bytecode with such a remapping on a different machine, you
would need to recreate parts of your local directory structure in the VFS and (if you rely on
Host Filesystem Loader) also in the host filesystem.
To avoid having your local directory structure embedded in the metadata, it is recommended to
designate the directories containing libraries as include paths instead.
For example, in the example above --include-path /home/user/packages/
would let you use
imports starting with mymath/
.
Unlike remapping, the option on its own will not make mymath
appear as @math
but this
can be achieved by creating a symbolic link or renaming the package subdirectory.
As a more complex example, suppose you rely on a module that uses an old version of dapp-bin that
you checked out to /project/dapp-bin_old
, then you can run:
solc module1:github.com/ethereum/dapp-bin/=dapp-bin/ \
module2:github.com/ethereum/dapp-bin/=dapp-bin_old/ \
--base-path /project \
source.sol
This means that all imports in module2
point to the old version but imports in module1
point to the new version.
Here are the detailed rules governing the behaviour of remappings:
Remappings only affect the translation between import paths and source unit names.
Source unit names added to the VFS in any other way cannot be remapped. For example the paths you specify on the command-line and the ones in
sources.urls
in Standard JSON are not affected.solc /project/=/contracts/ /project/contract.sol # source unit name: /project/contract.sol
In the example above the compiler will load the source code from
/project/contract.sol
and place it under that exact source unit name in the VFS, not under/contract/contract.sol
.Context and prefix must match source unit names, not import paths.
This means that you cannot remap
./
or../
directly since they are replaced during the translation to source unit name but you can remap the part of the name they are replaced with:solc ./=a/ /project/=b/ /project/contract.sol # source unit name: /project/contract.sol
import "./util.sol" as util; // source unit name: b/util.sol
You cannot remap base path or any other part of the path that is only added internally by an import callback:
solc /project/=/contracts/ /project/contract.sol --base-path /project # source unit name: contract.sol
import "util.sol" as util; // source unit name: util.sol
Target is inserted directly into the source unit name and does not necessarily have to be a valid path.
It can be anything as long as the import callback can handle it. In case of the Host Filesystem Loader this includes also relative paths. When using the JavaScript interface you can even use URLs and abstract identifiers if your callback can handle them.
Remapping happens after relative imports have already been resolved into source unit names. This means that targets starting with
./
and../
have no special meaning and are relative to the base path rather than to the location of the source file.Remapping targets are not normalized so
@root/=./a/b//
will remap@root/contract.sol
to./a/b//contract.sol
and nota/b/contract.sol
.If the target does not end with a slash, the compiler will not add one automatically:
solc /project/=/contracts /project/contract.sol # source unit name: /project/contract.sol
import "/project/util.sol" as util; // source unit name: /contractsutil.sol
Context and prefix are patterns and matches must be exact.
a//b=c
will not matcha/b
.source unit names are not normalized so
a/b=c
will not matcha//b
either.Parts of file and directory names can match as well.
/newProject/con:/new=old
will match/newProject/contract.sol
and remap it tooldProject/contract.sol
.
At most one remapping is applied to a single import.
If multiple remappings match the same source unit name, the one with the longest matching prefix is chosen.
If prefixes are identical, the one specified last wins.
Remappings do not work on other remappings. For example
a=b b=c c=d
will not result ina
being remapped tod
.
Prefix cannot be empty but context and target are optional.
If
target
is the empty string,prefix
is simply removed from import paths.Empty
context
means that the remapping applies to all imports in all source units.
Using URLs in imports
Most URL prefixes such as https://
or data://
have no special meaning in import paths.
The only exception is file://
which is stripped from source unit names by the Host Filesystem
Loader.
When compiling locally you can use import remapping to replace the protocol and domain part with a local path:
solc :https://github.com/ethereum/dapp-bin=/usr/local/dapp-bin contract.sol
Note the leading :
, which is necessary when the remapping context is empty.
Otherwise the https:
part would be interpreted by the compiler as the context.
Yul
Yul (previously also called JULIA or IULIA) is an intermediate language that can be compiled to bytecode for different backends.
Support for EVM 1.0, EVM 1.5 and Ewasm is planned, and it is designed to be a usable common denominator of all three platforms. It can already be used in stand-alone mode and for « inline assembly » inside Solidity and there is an experimental implementation of the Solidity compiler that uses Yul as an intermediate language. Yul is a good target for high-level optimisation stages that can benefit all target platforms equally.
Motivation and High-level Description
The design of Yul tries to achieve several goals:
Programs written in Yul should be readable, even if the code is generated by a compiler from Solidity or another high-level language.
Control flow should be easy to understand to help in manual inspection, formal verification and optimization.
The translation from Yul to bytecode should be as straightforward as possible.
Yul should be suitable for whole-program optimization.
In order to achieve the first and second goal, Yul provides high-level constructs
like for
loops, if
and switch
statements and function calls. These should
be sufficient for adequately representing the control flow for assembly programs.
Therefore, no explicit statements for SWAP
, DUP
, JUMPDEST
, JUMP
and JUMPI
are provided, because the first two obfuscate the data flow
and the last two obfuscate control flow. Furthermore, functional statements of
the form mul(add(x, y), 7)
are preferred over pure opcode statements like
7 y x add mul
because in the first form, it is much easier to see which
operand is used for which opcode.
Even though it was designed for stack machines, Yul does not expose the complexity of the stack itself. The programmer or auditor should not have to worry about the stack.
The third goal is achieved by compiling the higher level constructs to bytecode in a very regular way. The only non-local operation performed by the assembler is name lookup of user-defined identifiers (functions, variables, …) and cleanup of local variables from the stack.
To avoid confusions between concepts like values and references, Yul is statically typed. At the same time, there is a default type (usually the integer word of the target machine) that can always be omitted to help readability.
To keep the language simple and flexible, Yul does not have any built-in operations, functions or types in its pure form. These are added together with their semantics when specifying a dialect of Yul, which allows specializing Yul to the requirements of different target platforms and feature sets.
Currently, there is only one specified dialect of Yul. This dialect uses
the EVM opcodes as builtin functions
(see below) and defines only the type u256
, which is the native 256-bit
type of the EVM. Because of that, we will not provide types in the examples below.
Simple Example
The following example program is written in the EVM dialect and computes exponentiation.
It can be compiled using solc --strict-assembly
. The builtin functions
mul
and div
compute product and division, respectively.
{
function power(base, exponent) -> result
{
switch exponent
case 0 { result := 1 }
case 1 { result := base }
default
{
result := power(mul(base, base), div(exponent, 2))
switch mod(exponent, 2)
case 1 { result := mul(base, result) }
}
}
}
It is also possible to implement the same function using a for-loop
instead of with recursion. Here, lt(a, b)
computes whether a
is less than b
.
less-than comparison.
{
function power(base, exponent) -> result
{
result := 1
for { let i := 0 } lt(i, exponent) { i := add(i, 1) }
{
result := mul(result, base)
}
}
}
At the end of the section, a complete implementation of the ERC-20 standard can be found.
Stand-Alone Usage
You can use Yul in its stand-alone form in the EVM dialect using the Solidity compiler.
This will use the Yul object notation so that it is possible to refer
to code as data to deploy contracts. This Yul mode is available for the commandline compiler
(use --strict-assembly
) and for the standard-json interface:
{
"language": "Yul",
"sources": { "input.yul": { "content": "{ sstore(0, 1) }" } },
"settings": {
"outputSelection": { "*": { "*": ["*"], "": [ "*" ] } },
"optimizer": { "enabled": true, "details": { "yul": true } }
}
}
Avertissement
Yul is in active development and bytecode generation is only fully implemented for the EVM dialect of Yul with EVM 1.0 as target.
Informal Description of Yul
In the following, we will talk about each individual aspect of the Yul language. In examples, we will use the default EVM dialect.
Syntax
Yul parses comments, literals and identifiers in the same way as Solidity,
so you can e.g. use //
and /* */
to denote comments.
There is one exception: Identifiers in Yul can contain dots: .
.
Yul can specify « objects » that consist of code, data and sub-objects. Please see Yul Objects below for details on that. In this section, we are only concerned with the code part of such an object. This code part always consists of a curly-braces delimited block. Most tools support specifying just a code block where an object is expected.
Inside a code block, the following elements can be used (see the later sections for more details):
literals, i.e.
0x123
,42
or"abc"
(strings up to 32 characters)calls to builtin functions, e.g.
add(1, mload(0))
variable declarations, e.g.
let x := 7
,let x := add(y, 3)
orlet x
(initial value of 0 is assigned)identifiers (variables), e.g.
add(3, x)
assignments, e.g.
x := add(y, 3)
blocks where local variables are scoped inside, e.g.
{ let x := 3 { let y := add(x, 1) } }
if statements, e.g.
if lt(a, b) { sstore(0, 1) }
switch statements, e.g.
switch mload(0) case 0 { revert() } default { mstore(0, 1) }
for loops, e.g.
for { let i := 0} lt(i, 10) { i := add(i, 1) } { mstore(i, 7) }
function definitions, e.g.
function f(a, b) -> c { c := add(a, b) }
Multiple syntactical elements can follow each other simply separated by
whitespace, i.e. there is no terminating ;
or newline required.
Literals
As literals, you can use:
Integer constants in decimal or hexadecimal notation.
ASCII strings (e.g.
"abc"
), which may contain hex escapes\xNN
and Unicode escapes\uNNNN
whereN
are hexadecimal digits.Hex strings (e.g.
hex"616263"
).
In the EVM dialect of Yul, literals represent 256-bit words as follows:
Decimal or hexadecimal constants must be less than
2**256
. They represent the 256-bit word with that value as an unsigned integer in big endian encoding.An ASCII string is first viewed as a byte sequence, by viewing a non-escape ASCII character as a single byte whose value is the ASCII code, an escape
\xNN
as single byte with that value, and an escape\uNNNN
as the UTF-8 sequence of bytes for that code point. The byte sequence must not exceed 32 bytes. The byte sequence is padded with zeros on the right to reach 32 bytes in length; in other words, the string is stored left-aligned. The padded byte sequence represents a 256-bit word whose most significant 8 bits are the ones from the first byte, i.e. the bytes are interpreted in big endian form.A hex string is first viewed as a byte sequence, by viewing each pair of contiguous hex digits as a byte. The byte sequence must not exceed 32 bytes (i.e. 64 hex digits), and is treated as above.
When compiling for the EVM, this will be translated into an
appropriate PUSHi
instruction. In the following example,
3
and 2
are added resulting in 5 and then the
bitwise and
with the string « abc » is computed.
The final value is assigned to a local variable called x
.
The 32-byte limit above does not apply to string literals passed to builtin functions that require
literal arguments (e.g. setimmutable
or loadimmutable
). Those strings never end up in the
generated bytecode.
let x := and("abc", add(3, 2))
Unless it is the default type, the type of a literal has to be specified after a colon:
// This will not compile (u32 and u256 type not implemented yet)
let x := and("abc":u32, add(3:u256, 2:u256))
Function Calls
Both built-in and user-defined functions (see below) can be called in the same way as shown in the previous example. If the function returns a single value, it can be directly used inside an expression again. If it returns multiple values, they have to be assigned to local variables.
function f(x, y) -> a, b { /* ... */ }
mstore(0x80, add(mload(0x80), 3))
// Here, the user-defined function `f` returns two values.
let x, y := f(1, mload(0))
For built-in functions of the EVM, functional expressions
can be directly translated to a stream of opcodes:
You just read the expression from right to left to obtain the
opcodes. In the case of the first line in the example, this
is PUSH1 3 PUSH1 0x80 MLOAD ADD PUSH1 0x80 MSTORE
.
For calls to user-defined functions, the arguments are also
put on the stack from right to left and this is the order
in which argument lists are evaluated. The return values,
though, are expected on the stack from left to right,
i.e. in this example, y
is on top of the stack and x
is below it.
Variable Declarations
You can use the let
keyword to declare variables.
A variable is only visible inside the
{...}
-block it was defined in. When compiling to the EVM,
a new stack slot is created that is reserved
for the variable and automatically removed again when the end of the block
is reached. You can provide an initial value for the variable.
If you do not provide a value, the variable will be initialized to zero.
Since variables are stored on the stack, they do not directly
influence memory or storage, but they can be used as pointers
to memory or storage locations in the built-in functions
mstore
, mload
, sstore
and sload
.
Future dialects might introduce specific types for such pointers.
When a variable is referenced, its current value is copied.
For the EVM, this translates to a DUP
instruction.
{
let zero := 0
let v := calldataload(zero)
{
let y := add(sload(v), 1)
v := y
} // y is "deallocated" here
sstore(v, zero)
} // v and zero are "deallocated" here
If the declared variable should have a type different from the default type, you denote that following a colon. You can also declare multiple variables in one statement when you assign from a function call that returns multiple values.
// This will not compile (u32 and u256 type not implemented yet)
{
let zero:u32 := 0:u32
let v:u256, t:u32 := f()
let x, y := g()
}
Depending on the optimiser settings, the compiler can free the stack slots already after the variable has been used for the last time, even though it is still in scope.
Assignments
Variables can be assigned to after their definition using the
:=
operator. It is possible to assign multiple
variables at the same time. For this, the number and types of the
values have to match.
If you want to assign the values returned from a function that has
multiple return parameters, you have to provide multiple variables.
The same variable may not occur multiple times on the left-hand side of
an assignment, e.g. x, x := f()
is invalid.
let v := 0
// re-assign v
v := 2
let t := add(v, 2)
function f() -> a, b { }
// assign multiple values
v, t := f()
If
The if statement can be used for conditionally executing code. No « else » block can be defined. Consider using « switch » instead (see below) if you need multiple alternatives.
if lt(calldatasize(), 4) { revert(0, 0) }
The curly braces for the body are required.
Switch
You can use a switch statement as an extended version of the if statement.
It takes the value of an expression and compares it to several literal constants.
The branch corresponding to the matching constant is taken.
Contrary to other programming languages, for safety reasons, control flow does
not continue from one case to the next. There can be a fallback or default
case called default
which is taken if none of the literal constants matches.
{
let x := 0
switch calldataload(4)
case 0 {
x := calldataload(0x24)
}
default {
x := calldataload(0x44)
}
sstore(0, div(x, 2))
}
The list of cases is not enclosed by curly braces, but the body of a case does require them.
Loops
Yul supports for-loops which consist of a header containing an initializing part, a condition, a post-iteration part and a body. The condition has to be an expression, while the other three are blocks. If the initializing part declares any variables at the top level, the scope of these variables extends to all other parts of the loop.
The break
and continue
statements can be used in the body to exit the loop
or skip to the post-part, respectively.
The following example computes the sum of an area in memory.
{
let x := 0
for { let i := 0 } lt(i, 0x100) { i := add(i, 0x20) } {
x := add(x, mload(i))
}
}
For loops can also be used as a replacement for while loops: Simply leave the initialization and post-iteration parts empty.
{
let x := 0
let i := 0
for { } lt(i, 0x100) { } { // while(i < 0x100)
x := add(x, mload(i))
i := add(i, 0x20)
}
}
Function Declarations
Yul allows the definition of functions. These should not be confused with functions in Solidity since they are never part of an external interface of a contract and are part of a namespace separate from the one for Solidity functions.
For the EVM, Yul functions take their arguments (and a return PC) from the stack and also put the results onto the stack. User-defined functions and built-in functions are called in exactly the same way.
Functions can be defined anywhere and are visible in the block they are declared in. Inside a function, you cannot access local variables defined outside of that function.
Functions declare parameters and return variables, similar to Solidity. To return a value, you assign it to the return variable(s).
If you call a function that returns multiple values, you have to assign
them to multiple variables using a, b := f(x)
or let a, b := f(x)
.
The leave
statement can be used to exit the current function. It
works like the return
statement in other languages just that it does
not take a value to return, it just exits the functions and the function
will return whatever values are currently assigned to the return variable(s).
Note that the EVM dialect has a built-in function called return
that
quits the full execution context (internal message call) and not just
the current yul function.
The following example implements the power function by square-and-multiply.
{
function power(base, exponent) -> result {
switch exponent
case 0 { result := 1 }
case 1 { result := base }
default {
result := power(mul(base, base), div(exponent, 2))
switch mod(exponent, 2)
case 1 { result := mul(base, result) }
}
}
}
Specification of Yul
This chapter describes Yul code formally. Yul code is usually placed inside Yul objects, which are explained in their own chapter.
Block = '{' Statement* '}'
Statement =
Block |
FunctionDefinition |
VariableDeclaration |
Assignment |
If |
Expression |
Switch |
ForLoop |
BreakContinue |
Leave
FunctionDefinition =
'function' Identifier '(' TypedIdentifierList? ')'
( '->' TypedIdentifierList )? Block
VariableDeclaration =
'let' TypedIdentifierList ( ':=' Expression )?
Assignment =
IdentifierList ':=' Expression
Expression =
FunctionCall | Identifier | Literal
If =
'if' Expression Block
Switch =
'switch' Expression ( Case+ Default? | Default )
Case =
'case' Literal Block
Default =
'default' Block
ForLoop =
'for' Block Expression Block Block
BreakContinue =
'break' | 'continue'
Leave = 'leave'
FunctionCall =
Identifier '(' ( Expression ( ',' Expression )* )? ')'
Identifier = [a-zA-Z_$] [a-zA-Z_$0-9.]*
IdentifierList = Identifier ( ',' Identifier)*
TypeName = Identifier
TypedIdentifierList = Identifier ( ':' TypeName )? ( ',' Identifier ( ':' TypeName )? )*
Literal =
(NumberLiteral | StringLiteral | TrueLiteral | FalseLiteral) ( ':' TypeName )?
NumberLiteral = HexNumber | DecimalNumber
StringLiteral = '"' ([^"\r\n\\] | '\\' .)* '"'
TrueLiteral = 'true'
FalseLiteral = 'false'
HexNumber = '0x' [0-9a-fA-F]+
DecimalNumber = [0-9]+
Restrictions on the Grammar
Apart from those directly imposed by the grammar, the following restrictions apply:
Switches must have at least one case (including the default case).
All case values need to have the same type and distinct values.
If all possible values of the expression type are covered, a default case is
not allowed (i.e. a switch with a bool
expression that has both a
true and a false case do not allow a default case).
Every expression evaluates to zero or more values. Identifiers and Literals evaluate to exactly one value and function calls evaluate to a number of values equal to the number of return variables of the function called.
In variable declarations and assignments, the right-hand-side expression (if present) has to evaluate to a number of values equal to the number of variables on the left-hand-side. This is the only situation where an expression evaluating to more than one value is allowed. The same variable name cannot occur more than once in the left-hand-side of an assignment or variable declaration.
Expressions that are also statements (i.e. at the block level) have to evaluate to zero values.
In all other situations, expressions have to evaluate to exactly one value.
A continue
or break
statement can only be used inside the body of a for-loop, as follows.
Consider the innermost loop that contains the statement.
The loop and the statement must be in the same function, or both must be at the top level.
The statement must be in the loop’s body block;
it cannot be in the loop’s initialization block or update block.
It is worth emphasizing that this restriction applies just
to the innermost loop that contains the continue
or break
statement:
this innermost loop, and therefore the continue
or break
statement,
may appear anywhere in an outer loop, possibly in an outer loop’s initialization block or update block.
For example, the following is legal,
because the break
occurs in the body block of the inner loop,
despite also occurring in the update block of the outer loop:
for {} true { for {} true {} { break } }
{
}
The condition part of the for-loop has to evaluate to exactly one value.
The leave
statement can only be used inside a function.
Functions cannot be defined anywhere inside for loop init blocks.
Literals cannot be larger than their type. The largest type defined is 256-bit wide.
During assignments and function calls, the types of the respective values have to match. There is no implicit type conversion. Type conversion in general can only be achieved if the dialect provides an appropriate built-in function that takes a value of one type and returns a value of a different type.
Scoping Rules
Scopes in Yul are tied to Blocks (exceptions are functions and the for loop
as explained below) and all declarations
(FunctionDefinition
, VariableDeclaration
)
introduce new identifiers into these scopes.
Identifiers are visible in
the block they are defined in (including all sub-nodes and sub-blocks):
Functions are visible in the whole block (even before their definitions) while
variables are only visible starting from the statement after the VariableDeclaration
.
In particular, variables cannot be referenced in the right hand side of their own variable declaration. Functions can be referenced already before their declaration (if they are visible).
As an exception to the general scoping rule, the scope of the « init » part of the for-loop (the first block) extends across all other parts of the for loop. This means that variables (and functions) declared in the init part (but not inside a block inside the init part) are visible in all other parts of the for-loop.
Identifiers declared in the other parts of the for loop respect the regular syntactical scoping rules.
This means a for-loop of the form for { I... } C { P... } { B... }
is equivalent
to { I... for {} C { P... } { B... } }
.
The parameters and return parameters of functions are visible in the function body and their names have to be distinct.
Inside functions, it is not possible to reference a variable that was declared outside of that function.
Shadowing is disallowed, i.e. you cannot declare an identifier at a point where another identifier with the same name is also visible, even if it is not possible to reference it because it was declared outside the current function.
Formal Specification
We formally specify Yul by providing an evaluation function E overloaded on the various nodes of the AST. As builtin functions can have side effects, E takes two state objects and the AST node and returns two new state objects and a variable number of other values. The two state objects are the global state object (which in the context of the EVM is the memory, storage and state of the blockchain) and the local state object (the state of local variables, i.e. a segment of the stack in the EVM).
If the AST node is a statement, E returns the two state objects and a « mode »,
which is used for the break
, continue
and leave
statements.
If the AST node is an expression, E returns the two state objects and
as many values as the expression evaluates to.
The exact nature of the global state is unspecified for this high level
description. The local state L
is a mapping of identifiers i
to values v
,
denoted as L[i] = v
.
For an identifier v
, let $v
be the name of the identifier.
We will use a destructuring notation for the AST nodes.
E(G, L, <{St1, ..., Stn}>: Block) =
let G1, L1, mode = E(G, L, St1, ..., Stn)
let L2 be a restriction of L1 to the identifiers of L
G1, L2, mode
E(G, L, St1, ..., Stn: Statement) =
if n is zero:
G, L, regular
else:
let G1, L1, mode = E(G, L, St1)
if mode is regular then
E(G1, L1, St2, ..., Stn)
otherwise
G1, L1, mode
E(G, L, FunctionDefinition) =
G, L, regular
E(G, L, <let var_1, ..., var_n := rhs>: VariableDeclaration) =
E(G, L, <var_1, ..., var_n := rhs>: Assignment)
E(G, L, <let var_1, ..., var_n>: VariableDeclaration) =
let L1 be a copy of L where L1[$var_i] = 0 for i = 1, ..., n
G, L1, regular
E(G, L, <var_1, ..., var_n := rhs>: Assignment) =
let G1, L1, v1, ..., vn = E(G, L, rhs)
let L2 be a copy of L1 where L2[$var_i] = vi for i = 1, ..., n
G, L2, regular
E(G, L, <for { i1, ..., in } condition post body>: ForLoop) =
if n >= 1:
let G1, L, mode = E(G, L, i1, ..., in)
// mode has to be regular or leave due to the syntactic restrictions
if mode is leave then
G1, L1 restricted to variables of L, leave
otherwise
let G2, L2, mode = E(G1, L1, for {} condition post body)
G2, L2 restricted to variables of L, mode
else:
let G1, L1, v = E(G, L, condition)
if v is false:
G1, L1, regular
else:
let G2, L2, mode = E(G1, L, body)
if mode is break:
G2, L2, regular
otherwise if mode is leave:
G2, L2, leave
else:
G3, L3, mode = E(G2, L2, post)
if mode is leave:
G2, L3, leave
otherwise
E(G3, L3, for {} condition post body)
E(G, L, break: BreakContinue) =
G, L, break
E(G, L, continue: BreakContinue) =
G, L, continue
E(G, L, leave: Leave) =
G, L, leave
E(G, L, <if condition body>: If) =
let G0, L0, v = E(G, L, condition)
if v is true:
E(G0, L0, body)
else:
G0, L0, regular
E(G, L, <switch condition case l1:t1 st1 ... case ln:tn stn>: Switch) =
E(G, L, switch condition case l1:t1 st1 ... case ln:tn stn default {})
E(G, L, <switch condition case l1:t1 st1 ... case ln:tn stn default st'>: Switch) =
let G0, L0, v = E(G, L, condition)
// i = 1 .. n
// Evaluate literals, context doesn't matter
let _, _, v1 = E(G0, L0, l1)
...
let _, _, vn = E(G0, L0, ln)
if there exists smallest i such that vi = v:
E(G0, L0, sti)
else:
E(G0, L0, st')
E(G, L, <name>: Identifier) =
G, L, L[$name]
E(G, L, <fname(arg1, ..., argn)>: FunctionCall) =
G1, L1, vn = E(G, L, argn)
...
G(n-1), L(n-1), v2 = E(G(n-2), L(n-2), arg2)
Gn, Ln, v1 = E(G(n-1), L(n-1), arg1)
Let <function fname (param1, ..., paramn) -> ret1, ..., retm block>
be the function of name $fname visible at the point of the call.
Let L' be a new local state such that
L'[$parami] = vi and L'[$reti] = 0 for all i.
Let G'', L'', mode = E(Gn, L', block)
G'', Ln, L''[$ret1], ..., L''[$retm]
E(G, L, l: StringLiteral) = G, L, str(l),
where str is the string evaluation function,
which for the EVM dialect is defined in the section 'Literals' above
E(G, L, n: HexNumber) = G, L, hex(n)
where hex is the hexadecimal evaluation function,
which turns a sequence of hexadecimal digits into their big endian value
E(G, L, n: DecimalNumber) = G, L, dec(n),
where dec is the decimal evaluation function,
which turns a sequence of decimal digits into their big endian value
EVM Dialect
The default dialect of Yul currently is the EVM dialect for the currently selected version of the EVM.
with a version of the EVM. The only type available in this dialect
is u256
, the 256-bit native type of the Ethereum Virtual Machine.
Since it is the default type of this dialect, it can be omitted.
The following table lists all builtin functions (depending on the EVM version) and provides a short description of the semantics of the function / opcode. This document does not want to be a full description of the Ethereum virtual machine. Please refer to a different document if you are interested in the precise semantics.
Opcodes marked with -
do not return a result and all others return exactly one value.
Opcodes marked with F
, H
, B
, C
, I
and L
are present since Frontier, Homestead,
Byzantium, Constantinople, Istanbul or London respectively.
In the following, mem[a...b)
signifies the bytes of memory starting at position a
up to
but not including position b
and storage[p]
signifies the storage contents at slot p
.
Since Yul manages local variables and control-flow,
opcodes that interfere with these features are not available. This includes
the dup
and swap
instructions as well as jump
instructions, labels and the push
instructions.
Instruction |
Explanation |
||
---|---|---|---|
stop() |
- |
F |
stop execution, identical to return(0, 0) |
add(x, y) |
F |
x + y |
|
sub(x, y) |
F |
x - y |
|
mul(x, y) |
F |
x * y |
|
div(x, y) |
F |
x / y or 0 if y == 0 |
|
sdiv(x, y) |
F |
x / y, for signed numbers in two’s complement, 0 if y == 0 |
|
mod(x, y) |
F |
x % y, 0 if y == 0 |
|
smod(x, y) |
F |
x % y, for signed numbers in two’s complement, 0 if y == 0 |
|
exp(x, y) |
F |
x to the power of y |
|
not(x) |
F |
bitwise « not » of x (every bit of x is negated) |
|
lt(x, y) |
F |
1 if x < y, 0 otherwise |
|
gt(x, y) |
F |
1 if x > y, 0 otherwise |
|
slt(x, y) |
F |
1 if x < y, 0 otherwise, for signed numbers in two’s complement |
|
sgt(x, y) |
F |
1 if x > y, 0 otherwise, for signed numbers in two’s complement |
|
eq(x, y) |
F |
1 if x == y, 0 otherwise |
|
iszero(x) |
F |
1 if x == 0, 0 otherwise |
|
and(x, y) |
F |
bitwise « and » of x and y |
|
or(x, y) |
F |
bitwise « or » of x and y |
|
xor(x, y) |
F |
bitwise « xor » of x and y |
|
byte(n, x) |
F |
nth byte of x, where the most significant byte is the 0th byte |
|
shl(x, y) |
C |
logical shift left y by x bits |
|
shr(x, y) |
C |
logical shift right y by x bits |
|
sar(x, y) |
C |
signed arithmetic shift right y by x bits |
|
addmod(x, y, m) |
F |
(x + y) % m with arbitrary precision arithmetic, 0 if m == 0 |
|
mulmod(x, y, m) |
F |
(x * y) % m with arbitrary precision arithmetic, 0 if m == 0 |
|
signextend(i, x) |
F |
sign extend from (i*8+7)th bit counting from least significant |
|
keccak256(p, n) |
F |
keccak(mem[p…(p+n))) |
|
pc() |
F |
current position in code |
|
pop(x) |
- |
F |
discard value x |
mload(p) |
F |
mem[p…(p+32)) |
|
mstore(p, v) |
- |
F |
mem[p…(p+32)) := v |
mstore8(p, v) |
- |
F |
mem[p] := v & 0xff (only modifies a single byte) |
sload(p) |
F |
storage[p] |
|
sstore(p, v) |
- |
F |
storage[p] := v |
msize() |
F |
size of memory, i.e. largest accessed memory index |
|
gas() |
F |
gas still available to execution |
|
address() |
F |
address of the current contract / execution context |
|
balance(a) |
F |
wei balance at address a |
|
selfbalance() |
I |
equivalent to balance(address()), but cheaper |
|
caller() |
F |
call sender (excluding |
|
callvalue() |
F |
wei sent together with the current call |
|
calldataload(p) |
F |
call data starting from position p (32 bytes) |
|
calldatasize() |
F |
size of call data in bytes |
|
calldatacopy(t, f, s) |
- |
F |
copy s bytes from calldata at position f to mem at position t |
codesize() |
F |
size of the code of the current contract / execution context |
|
codecopy(t, f, s) |
- |
F |
copy s bytes from code at position f to mem at position t |
extcodesize(a) |
F |
size of the code at address a |
|
extcodecopy(a, t, f, s) |
- |
F |
like codecopy(t, f, s) but take code at address a |
returndatasize() |
B |
size of the last returndata |
|
returndatacopy(t, f, s) |
- |
B |
copy s bytes from returndata at position f to mem at position t |
extcodehash(a) |
C |
code hash of address a |
|
create(v, p, n) |
F |
create new contract with code mem[p…(p+n)) and send v wei and return the new address; returns 0 on error |
|
create2(v, p, n, s) |
C |
create new contract with code mem[p…(p+n)) at address
keccak256(0xff . this . s . keccak256(mem[p…(p+n)))
and send v wei and return the new address, where |
|
call(g, a, v, in, insize, out, outsize) |
F |
call contract at address a with input mem[in…(in+insize)) providing g gas and v wei and output area mem[out…(out+outsize)) returning 0 on error (eg. out of gas) and 1 on success See more |
|
callcode(g, a, v, in, insize, out, outsize) |
F |
identical to |
|
delegatecall(g, a, in, insize, out, outsize) |
H |
identical to |
|
staticcall(g, a, in, insize, out, outsize) |
B |
identical to |
|
return(p, s) |
- |
F |
end execution, return data mem[p…(p+s)) |
revert(p, s) |
- |
B |
end execution, revert state changes, return data mem[p…(p+s)) |
selfdestruct(a) |
- |
F |
end execution, destroy current contract and send funds to a |
invalid() |
- |
F |
end execution with invalid instruction |
log0(p, s) |
- |
F |
log without topics and data mem[p…(p+s)) |
log1(p, s, t1) |
- |
F |
log with topic t1 and data mem[p…(p+s)) |
log2(p, s, t1, t2) |
- |
F |
log with topics t1, t2 and data mem[p…(p+s)) |
log3(p, s, t1, t2, t3) |
- |
F |
log with topics t1, t2, t3 and data mem[p…(p+s)) |
log4(p, s, t1, t2, t3, t4) |
- |
F |
log with topics t1, t2, t3, t4 and data mem[p…(p+s)) |
chainid() |
I |
ID of the executing chain (EIP-1344) |
|
basefee() |
L |
current block’s base fee (EIP-3198 and EIP-1559) |
|
origin() |
F |
transaction sender |
|
gasprice() |
F |
gas price of the transaction |
|
blockhash(b) |
F |
hash of block nr b - only for last 256 blocks excluding current |
|
coinbase() |
F |
current mining beneficiary |
|
timestamp() |
F |
timestamp of the current block in seconds since the epoch |
|
number() |
F |
current block number |
|
difficulty() |
F |
difficulty of the current block |
|
gaslimit() |
F |
block gas limit of the current block |
Note
The call*
instructions use the out
and outsize
parameters to define an area in memory where
the return or failure data is placed. This area is written to depending on how many bytes the called contract returns.
If it returns more data, only the first outsize
bytes are written. You can access the rest of the data
using the returndatacopy
opcode. If it returns less data, then the remaining bytes are not touched at all.
You need to use the returndatasize
opcode to check which part of this memory area contains the return data.
The remaining bytes will retain their values as of before the call.
In some internal dialects, there are additional functions:
datasize, dataoffset, datacopy
The functions datasize(x)
, dataoffset(x)
and datacopy(t, f, l)
are used to access other parts of a Yul object.
datasize
and dataoffset
can only take string literals (the names of other objects)
as arguments and return the size and offset in the data area, respectively.
For the EVM, the datacopy
function is equivalent to codecopy
.
setimmutable, loadimmutable
The functions setimmutable(offset, "name", value)
and loadimmutable("name")
are
used for the immutable mechanism in Solidity and do not nicely map to pure Yul.
The call to setimmutable(offset, "name", value)
assumes that the runtime code of the contract
containing the given named immutable was copied to memory at offset offset
and will write value
to all
positions in memory (relative to offset
) that contain the placeholder that was generated for calls
to loadimmutable("name")
in the runtime code.
linkersymbol
The function linkersymbol("library_id")
is a placeholder for an address literal to be substituted
by the linker.
Its first and only argument must be a string literal and uniquely represents the address to be inserted.
Identifiers can be arbitrary but when the compiler produces Yul code from Solidity sources,
it uses a library name qualified with the name of the source unit that defines that library.
To link the code with a particular library address, the same identifier must be provided to the
--libraries
option on the command line.
For example this code
let a := linkersymbol("file.sol:Math")
is equivalent to
let a := 0x1234567890123456789012345678901234567890
when the linker is invoked with --libraries "file.sol:Math=0x1234567890123456789012345678901234567890
option.
See Using the Commandline Compiler for details about the Solidity linker.
memoryguard
This function is available in the EVM dialect with objects. The caller of
let ptr := memoryguard(size)
(where size
has to be a literal number)
promises that they only use memory in either the range [0, size)
or the
unbounded range starting at ptr
.
Since the presence of a memoryguard
call indicates that all memory access
adheres to this restriction, it allows the optimizer to perform additional
optimization steps, for example the stack limit evader, which attempts to move
stack variables that would otherwise be unreachable to memory.
The Yul optimizer promises to only use the memory range [size, ptr)
for its purposes.
If the optimizer does not need to reserve any memory, it holds that ptr == size
.
memoryguard
can be called multiple times, but needs to have the same literal as argument
within one Yul subobject. If at least one memoryguard
call is found in a subobject,
the additional optimiser steps will be run on it.
verbatim
The set of verbatim...
builtin functions lets you create bytecode for opcodes
that are not known to the Yul compiler. It also allows you to create
bytecode sequences that will not be modified by the optimizer.
The functions are verbatim_<n>i_<m>o("<data>", ...)
, where
n
is a decimal between 0 and 99 that specifies the number of input stack slots / variablesm
is a decimal between 0 and 99 that specifies the number of output stack slots / variablesdata
is a string literal that contains the sequence of bytes
If you for example want to define a function that multiplies the input by two, without the optimizer touching the constant two, you can use
let x := calldataload(0)
let double := verbatim_1i_1o(hex"600202", x)
This code will result in a dup1
opcode to retrieve x
(the optimizer might directly re-use result of the
calldataload
opcode, though)
directly followed by 600202
. The code is assumed to
consume the copied value of x
and produce the result
on the top of the stack. The compiler then generates code
to allocate a stack slot for double
and store the result there.
As with all opcodes, the arguments are arranged on the stack with the leftmost argument on the top, while the return values are assumed to be laid out such that the rightmost variable is at the top of the stack.
Since verbatim
can be used to generate arbitrary opcodes
or even opcodes unknown to the Solidity compiler, care has to be taken
when using verbatim
together with the optimizer. Even when the
optimizer is switched off, the code generator has to determine
the stack layout, which means that e.g. using verbatim
to modify
the stack height can lead to undefined behaviour.
The following is a non-exhaustive list of restrictions on verbatim bytecode that are not checked by the compiler. Violations of these restrictions can result in undefined behaviour.
Control-flow should not jump into or out of verbatim blocks, but it can jump within the same verbatim block.
Stack contents apart from the input and output parameters should not be accessed.
The stack height difference should be exactly
m - n
(output slots minus input slots).Verbatim bytecode cannot make any assumptions about the surrounding bytecode. All required parameters have to be passed in as stack variables.
The optimizer does not analyze verbatim bytecode and always
assumes that it modifies all aspects of state and thus can only
do very few optimizations across verbatim
function calls.
The optimizer treats verbatim bytecode as an opaque block of code. It will not split it but might move, duplicate or combine it with identical verbatim bytecode blocks. If a verbatim bytecode block is unreachable by the control-flow, it can be removed.
Avertissement
During discussions about whether or not EVM improvements
might break existing smart contracts, features inside verbatim
cannot receive the same consideration as those used by the Solidity
compiler itself.
Note
To avoid confusion, all identifiers starting with the string verbatim
are reserved
and cannot be used for user-defined identifiers.
Specification of Yul Object
Yul objects are used to group named code and data sections.
The functions datasize
, dataoffset
and datacopy
can be used to access these sections from within code.
Hex strings can be used to specify data in hex encoding,
regular strings in native encoding. For code,
datacopy
will access its assembled binary representation.
Object = 'object' StringLiteral '{' Code ( Object | Data )* '}'
Code = 'code' Block
Data = 'data' StringLiteral ( HexLiteral | StringLiteral )
HexLiteral = 'hex' ('"' ([0-9a-fA-F]{2})* '"' | '\'' ([0-9a-fA-F]{2})* '\'')
StringLiteral = '"' ([^"\r\n\\] | '\\' .)* '"'
Above, Block
refers to Block
in the Yul code grammar explained in the previous chapter.
Note
Data objects or sub-objects whose names contain a .
can be defined
but it is not possible to access them through datasize
,
dataoffset
or datacopy
because .
is used as a separator
to access objects inside another object.
Note
The data object called ".metadata"
has a special meaning:
It cannot be accessed from code and is always appended to the very end of the
bytecode, regardless of its position in the object.
Other data objects with special significance might be added in the
future, but their names will always start with a .
.
An example Yul Object is shown below:
// A contract consists of a single object with sub-objects representing
// the code to be deployed or other contracts it can create.
// The single "code" node is the executable code of the object.
// Every (other) named object or data section is serialized and
// made accessible to the special built-in functions datacopy / dataoffset / datasize
// The current object, sub-objects and data items inside the current object
// are in scope.
object "Contract1" {
// This is the constructor code of the contract.
code {
function allocate(size) -> ptr {
ptr := mload(0x40)
if iszero(ptr) { ptr := 0x60 }
mstore(0x40, add(ptr, size))
}
// first create "Contract2"
let size := datasize("Contract2")
let offset := allocate(size)
// This will turn into codecopy for EVM
datacopy(offset, dataoffset("Contract2"), size)
// constructor parameter is a single number 0x1234
mstore(add(offset, size), 0x1234)
pop(create(offset, add(size, 32), 0))
// now return the runtime object (the currently
// executing code is the constructor code)
size := datasize("runtime")
offset := allocate(size)
// This will turn into a memory->memory copy for Ewasm and
// a codecopy for EVM
datacopy(offset, dataoffset("runtime"), size)
return(offset, size)
}
data "Table2" hex"4123"
object "runtime" {
code {
function allocate(size) -> ptr {
ptr := mload(0x40)
if iszero(ptr) { ptr := 0x60 }
mstore(0x40, add(ptr, size))
}
// runtime code
mstore(0, "Hello, World!")
return(0, 0x20)
}
}
// Embedded object. Use case is that the outside is a factory contract,
// and Contract2 is the code to be created by the factory
object "Contract2" {
code {
// code here ...
}
object "runtime" {
code {
// code here ...
}
}
data "Table1" hex"4123"
}
}
Yul Optimizer
The Yul optimizer operates on Yul code and uses the same language for input, output and intermediate states. This allows for easy debugging and verification of the optimizer.
Please refer to the general optimizer documentation for more details about the different optimization stages and how to use the optimizer.
If you want to use Solidity in stand-alone Yul mode, you activate the optimizer using --optimize
and optionally specify the expected number of contract executions with
--optimize-runs
:
solc --strict-assembly --optimize --optimize-runs 200
In Solidity mode, the Yul optimizer is activated together with the regular optimizer.
Optimization Step Sequence
By default the Yul optimizer applies its predefined sequence of optimization steps to the generated assembly.
You can override this sequence and supply your own using the --yul-optimizations
option:
solc --optimize --ir-optimized --yul-optimizations 'dhfoD[xarrscLMcCTU]uljmul'
The order of steps is significant and affects the quality of the output.
Moreover, applying a step may uncover new optimization opportunities for others that were already
applied so repeating steps is often beneficial.
By enclosing part of the sequence in square brackets ([]
) you tell the optimizer to repeatedly
apply that part until it no longer improves the size of the resulting assembly.
You can use brackets multiple times in a single sequence but they cannot be nested.
The following optimization steps are available:
Abbreviation |
Full name |
---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Some steps depend on properties ensured by BlockFlattener
, FunctionGrouper
, ForLoopInitRewriter
.
For this reason the Yul optimizer always applies them before applying any steps supplied by the user.
The ReasoningBasedSimplifier is an optimizer step that is currently not enabled in the default set of steps. It uses an SMT solver to simplify arithmetic expressions and boolean conditions. It has not received thorough testing or validation yet and can produce non-reproducible results, so please use with care!
Complete ERC20 Example
object "Token" {
code {
// Store the creator in slot zero.
sstore(0, caller())
// Deploy the contract
datacopy(0, dataoffset("runtime"), datasize("runtime"))
return(0, datasize("runtime"))
}
object "runtime" {
code {
// Protection against sending Ether
require(iszero(callvalue()))
// Dispatcher
switch selector()
case 0x70a08231 /* "balanceOf(address)" */ {
returnUint(balanceOf(decodeAsAddress(0)))
}
case 0x18160ddd /* "totalSupply()" */ {
returnUint(totalSupply())
}
case 0xa9059cbb /* "transfer(address,uint256)" */ {
transfer(decodeAsAddress(0), decodeAsUint(1))
returnTrue()
}
case 0x23b872dd /* "transferFrom(address,address,uint256)" */ {
transferFrom(decodeAsAddress(0), decodeAsAddress(1), decodeAsUint(2))
returnTrue()
}
case 0x095ea7b3 /* "approve(address,uint256)" */ {
approve(decodeAsAddress(0), decodeAsUint(1))
returnTrue()
}
case 0xdd62ed3e /* "allowance(address,address)" */ {
returnUint(allowance(decodeAsAddress(0), decodeAsAddress(1)))
}
case 0x40c10f19 /* "mint(address,uint256)" */ {
mint(decodeAsAddress(0), decodeAsUint(1))
returnTrue()
}
default {
revert(0, 0)
}
function mint(account, amount) {
require(calledByOwner())
mintTokens(amount)
addToBalance(account, amount)
emitTransfer(0, account, amount)
}
function transfer(to, amount) {
executeTransfer(caller(), to, amount)
}
function approve(spender, amount) {
revertIfZeroAddress(spender)
setAllowance(caller(), spender, amount)
emitApproval(caller(), spender, amount)
}
function transferFrom(from, to, amount) {
decreaseAllowanceBy(from, caller(), amount)
executeTransfer(from, to, amount)
}
function executeTransfer(from, to, amount) {
revertIfZeroAddress(to)
deductFromBalance(from, amount)
addToBalance(to, amount)
emitTransfer(from, to, amount)
}
/* ---------- calldata decoding functions ----------- */
function selector() -> s {
s := div(calldataload(0), 0x100000000000000000000000000000000000000000000000000000000)
}
function decodeAsAddress(offset) -> v {
v := decodeAsUint(offset)
if iszero(iszero(and(v, not(0xffffffffffffffffffffffffffffffffffffffff)))) {
revert(0, 0)
}
}
function decodeAsUint(offset) -> v {
let pos := add(4, mul(offset, 0x20))
if lt(calldatasize(), add(pos, 0x20)) {
revert(0, 0)
}
v := calldataload(pos)
}
/* ---------- calldata encoding functions ---------- */
function returnUint(v) {
mstore(0, v)
return(0, 0x20)
}
function returnTrue() {
returnUint(1)
}
/* -------- events ---------- */
function emitTransfer(from, to, amount) {
let signatureHash := 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef
emitEvent(signatureHash, from, to, amount)
}
function emitApproval(from, spender, amount) {
let signatureHash := 0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925
emitEvent(signatureHash, from, spender, amount)
}
function emitEvent(signatureHash, indexed1, indexed2, nonIndexed) {
mstore(0, nonIndexed)
log3(0, 0x20, signatureHash, indexed1, indexed2)
}
/* -------- storage layout ---------- */
function ownerPos() -> p { p := 0 }
function totalSupplyPos() -> p { p := 1 }
function accountToStorageOffset(account) -> offset {
offset := add(0x1000, account)
}
function allowanceStorageOffset(account, spender) -> offset {
offset := accountToStorageOffset(account)
mstore(0, offset)
mstore(0x20, spender)
offset := keccak256(0, 0x40)
}
/* -------- storage access ---------- */
function owner() -> o {
o := sload(ownerPos())
}
function totalSupply() -> supply {
supply := sload(totalSupplyPos())
}
function mintTokens(amount) {
sstore(totalSupplyPos(), safeAdd(totalSupply(), amount))
}
function balanceOf(account) -> bal {
bal := sload(accountToStorageOffset(account))
}
function addToBalance(account, amount) {
let offset := accountToStorageOffset(account)
sstore(offset, safeAdd(sload(offset), amount))
}
function deductFromBalance(account, amount) {
let offset := accountToStorageOffset(account)
let bal := sload(offset)
require(lte(amount, bal))
sstore(offset, sub(bal, amount))
}
function allowance(account, spender) -> amount {
amount := sload(allowanceStorageOffset(account, spender))
}
function setAllowance(account, spender, amount) {
sstore(allowanceStorageOffset(account, spender), amount)
}
function decreaseAllowanceBy(account, spender, amount) {
let offset := allowanceStorageOffset(account, spender)
let currentAllowance := sload(offset)
require(lte(amount, currentAllowance))
sstore(offset, sub(currentAllowance, amount))
}
/* ---------- utility functions ---------- */
function lte(a, b) -> r {
r := iszero(gt(a, b))
}
function gte(a, b) -> r {
r := iszero(lt(a, b))
}
function safeAdd(a, b) -> r {
r := add(a, b)
if or(lt(r, a), lt(r, b)) { revert(0, 0) }
}
function calledByOwner() -> cbo {
cbo := eq(owner(), caller())
}
function revertIfZeroAddress(addr) {
require(addr)
}
function require(condition) {
if iszero(condition) { revert(0, 0) }
}
}
}
}
Style Guide
Introduction
This guide is intended to provide coding conventions for writing Solidity code. This guide should be thought of as an evolving document that will change over time as useful conventions are found and old conventions are rendered obsolete.
Many projects will implement their own style guides. In the event of conflicts, project specific style guides take precedence.
The structure and many of the recommendations within this style guide were taken from python’s pep8 style guide.
The goal of this guide is not to be the right way or the best way to write Solidity code. The goal of this guide is consistency. A quote from python’s pep8 captures this concept well.
Note
A style guide is about consistency. Consistency with this style guide is important. Consistency within a project is more important. Consistency within one module or function is most important.
But most importantly: know when to be inconsistent – sometimes the style guide just doesn’t apply. When in doubt, use your best judgment. Look at other examples and decide what looks best. And don’t hesitate to ask!
Code Layout
Indentation
Use 4 spaces per indentation level.
Tabs or Spaces
Spaces are the preferred indentation method.
Mixing tabs and spaces should be avoided.
Blank Lines
Surround top level declarations in Solidity source with two blank lines.
Yes:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.0 <0.9.0;
contract A {
// ...
}
contract B {
// ...
}
contract C {
// ...
}
No:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.0 <0.9.0;
contract A {
// ...
}
contract B {
// ...
}
contract C {
// ...
}
Within a contract surround function declarations with a single blank line.
Blank lines may be omitted between groups of related one-liners (such as stub functions for an abstract contract)
Yes:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.6.0 <0.9.0;
abstract contract A {
function spam() public virtual pure;
function ham() public virtual pure;
}
contract B is A {
function spam() public pure override {
// ...
}
function ham() public pure override {
// ...
}
}
No:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.6.0 <0.9.0;
abstract contract A {
function spam() virtual pure public;
function ham() public virtual pure;
}
contract B is A {
function spam() public pure override {
// ...
}
function ham() public pure override {
// ...
}
}
Maximum Line Length
Keeping lines under the PEP 8 recommendation to a maximum of 79 (or 99) characters helps readers easily parse the code.
Wrapped lines should conform to the following guidelines.
The first argument should not be attached to the opening parenthesis.
One, and only one, indent should be used.
Each argument should fall on its own line.
The terminating element,
);
, should be placed on the final line by itself.
Function Calls
Yes:
thisFunctionCallIsReallyLong(
longArgument1,
longArgument2,
longArgument3
);
No:
thisFunctionCallIsReallyLong(longArgument1,
longArgument2,
longArgument3
);
thisFunctionCallIsReallyLong(longArgument1,
longArgument2,
longArgument3
);
thisFunctionCallIsReallyLong(
longArgument1, longArgument2,
longArgument3
);
thisFunctionCallIsReallyLong(
longArgument1,
longArgument2,
longArgument3
);
thisFunctionCallIsReallyLong(
longArgument1,
longArgument2,
longArgument3);
Assignment Statements
Yes:
thisIsALongNestedMapping[being][set][to_some_value] = someFunction(
argument1,
argument2,
argument3,
argument4
);
No:
thisIsALongNestedMapping[being][set][to_some_value] = someFunction(argument1,
argument2,
argument3,
argument4);
Event Definitions and Event Emitters
Yes:
event LongAndLotsOfArgs(
address sender,
address recipient,
uint256 publicKey,
uint256 amount,
bytes32[] options
);
LongAndLotsOfArgs(
sender,
recipient,
publicKey,
amount,
options
);
No:
event LongAndLotsOfArgs(address sender,
address recipient,
uint256 publicKey,
uint256 amount,
bytes32[] options);
LongAndLotsOfArgs(sender,
recipient,
publicKey,
amount,
options);
Source File Encoding
UTF-8 or ASCII encoding is preferred.
Imports
Import statements should always be placed at the top of the file.
Yes:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.0 <0.9.0;
import "./Owned.sol";
contract A {
// ...
}
contract B is Owned {
// ...
}
No:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.0 <0.9.0;
contract A {
// ...
}
import "./Owned.sol";
contract B is Owned {
// ...
}
Order of Functions
Ordering helps readers identify which functions they can call and to find the constructor and fallback definitions easier.
Functions should be grouped according to their visibility and ordered:
constructor
receive function (if exists)
fallback function (if exists)
external
public
internal
private
Within a grouping, place the view
and pure
functions last.
Yes:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
contract A {
constructor() {
// ...
}
receive() external payable {
// ...
}
fallback() external {
// ...
}
// External functions
// ...
// External functions that are view
// ...
// External functions that are pure
// ...
// Public functions
// ...
// Internal functions
// ...
// Private functions
// ...
}
No:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
contract A {
// External functions
// ...
fallback() external {
// ...
}
receive() external payable {
// ...
}
// Private functions
// ...
// Public functions
// ...
constructor() {
// ...
}
// Internal functions
// ...
}
Whitespace in Expressions
Avoid extraneous whitespace in the following situations:
Immediately inside parenthesis, brackets or braces, with the exception of single line function declarations.
Yes:
spam(ham[1], Coin({name: "ham"}));
No:
spam( ham[ 1 ], Coin( { name: "ham" } ) );
Exception:
function singleLine() public { spam(); }
Immediately before a comma, semicolon:
Yes:
function spam(uint i, Coin coin) public;
No:
function spam(uint i , Coin coin) public ;
More than one space around an assignment or other operator to align with another:
Yes:
x = 1;
y = 2;
long_variable = 3;
No:
x = 1;
y = 2;
long_variable = 3;
Don’t include a whitespace in the receive and fallback functions:
Yes:
receive() external payable {
...
}
fallback() external {
...
}
No:
receive () external payable {
...
}
fallback () external {
...
}
Control Structures
The braces denoting the body of a contract, library, functions and structs should:
open on the same line as the declaration
close on their own line at the same indentation level as the beginning of the declaration.
The opening brace should be preceded by a single space.
Yes:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.0 <0.9.0;
contract Coin {
struct Bank {
address owner;
uint balance;
}
}
No:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.0 <0.9.0;
contract Coin
{
struct Bank {
address owner;
uint balance;
}
}
The same recommendations apply to the control structures if
, else
, while
,
and for
.
Additionally there should be a single space between the control structures
if
, while
, and for
and the parenthetic block representing the
conditional, as well as a single space between the conditional parenthetic
block and the opening brace.
Yes:
if (...) {
...
}
for (...) {
...
}
No:
if (...)
{
...
}
while(...){
}
for (...) {
...;}
For control structures whose body contains a single statement, omitting the braces is ok if the statement is contained on a single line.
Yes:
if (x < 10)
x += 1;
No:
if (x < 10)
someArray.push(Coin({
name: 'spam',
value: 42
}));
For if
blocks which have an else
or else if
clause, the else
should be
placed on the same line as the if
’s closing brace. This is an exception compared
to the rules of other block-like structures.
Yes:
if (x < 3) {
x += 1;
} else if (x > 7) {
x -= 1;
} else {
x = 5;
}
if (x < 3)
x += 1;
else
x -= 1;
No:
if (x < 3) {
x += 1;
}
else {
x -= 1;
}
Function Declaration
For short function declarations, it is recommended for the opening brace of the function body to be kept on the same line as the function declaration.
The closing brace should be at the same indentation level as the function declaration.
The opening brace should be preceded by a single space.
Yes:
function increment(uint x) public pure returns (uint) {
return x + 1;
}
function increment(uint x) public pure onlyOwner returns (uint) {
return x + 1;
}
No:
function increment(uint x) public pure returns (uint)
{
return x + 1;
}
function increment(uint x) public pure returns (uint){
return x + 1;
}
function increment(uint x) public pure returns (uint) {
return x + 1;
}
function increment(uint x) public pure returns (uint) {
return x + 1;}
The modifier order for a function should be:
Visibility
Mutability
Virtual
Override
Custom modifiers
Yes:
function balance(uint from) public view override returns (uint) {
return balanceOf[from];
}
function shutdown() public onlyOwner {
selfdestruct(owner);
}
No:
function balance(uint from) public override view returns (uint) {
return balanceOf[from];
}
function shutdown() onlyOwner public {
selfdestruct(owner);
}
For long function declarations, it is recommended to drop each argument onto its own line at the same indentation level as the function body. The closing parenthesis and opening bracket should be placed on their own line as well at the same indentation level as the function declaration.
Yes:
function thisFunctionHasLotsOfArguments(
address a,
address b,
address c,
address d,
address e,
address f
)
public
{
doSomething();
}
No:
function thisFunctionHasLotsOfArguments(address a, address b, address c,
address d, address e, address f) public {
doSomething();
}
function thisFunctionHasLotsOfArguments(address a,
address b,
address c,
address d,
address e,
address f) public {
doSomething();
}
function thisFunctionHasLotsOfArguments(
address a,
address b,
address c,
address d,
address e,
address f) public {
doSomething();
}
If a long function declaration has modifiers, then each modifier should be dropped to its own line.
Yes:
function thisFunctionNameIsReallyLong(address x, address y, address z)
public
onlyOwner
priced
returns (address)
{
doSomething();
}
function thisFunctionNameIsReallyLong(
address x,
address y,
address z
)
public
onlyOwner
priced
returns (address)
{
doSomething();
}
No:
function thisFunctionNameIsReallyLong(address x, address y, address z)
public
onlyOwner
priced
returns (address) {
doSomething();
}
function thisFunctionNameIsReallyLong(address x, address y, address z)
public onlyOwner priced returns (address)
{
doSomething();
}
function thisFunctionNameIsReallyLong(address x, address y, address z)
public
onlyOwner
priced
returns (address) {
doSomething();
}
Multiline output parameters and return statements should follow the same style recommended for wrapping long lines found in the Maximum Line Length section.
Yes:
function thisFunctionNameIsReallyLong(
address a,
address b,
address c
)
public
returns (
address someAddressName,
uint256 LongArgument,
uint256 Argument
)
{
doSomething()
return (
veryLongReturnArg1,
veryLongReturnArg2,
veryLongReturnArg3
);
}
No:
function thisFunctionNameIsReallyLong(
address a,
address b,
address c
)
public
returns (address someAddressName,
uint256 LongArgument,
uint256 Argument)
{
doSomething()
return (veryLongReturnArg1,
veryLongReturnArg1,
veryLongReturnArg1);
}
For constructor functions on inherited contracts whose bases require arguments, it is recommended to drop the base constructors onto new lines in the same manner as modifiers if the function declaration is long or hard to read.
Yes:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
// Base contracts just to make this compile
contract B {
constructor(uint) {
}
}
contract C {
constructor(uint, uint) {
}
}
contract D {
constructor(uint) {
}
}
contract A is B, C, D {
uint x;
constructor(uint param1, uint param2, uint param3, uint param4, uint param5)
B(param1)
C(param2, param3)
D(param4)
{
// do something with param5
x = param5;
}
}
No:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
// Base contracts just to make this compile
contract B {
constructor(uint) {
}
}
contract C {
constructor(uint, uint) {
}
}
contract D {
constructor(uint) {
}
}
contract A is B, C, D {
uint x;
constructor(uint param1, uint param2, uint param3, uint param4, uint param5)
B(param1)
C(param2, param3)
D(param4) {
x = param5;
}
}
contract X is B, C, D {
uint x;
constructor(uint param1, uint param2, uint param3, uint param4, uint param5)
B(param1)
C(param2, param3)
D(param4) {
x = param5;
}
}
When declaring short functions with a single statement, it is permissible to do it on a single line.
Permissible:
function shortFunction() public { doSomething(); }
These guidelines for function declarations are intended to improve readability. Authors should use their best judgment as this guide does not try to cover all possible permutations for function declarations.
Mappings
In variable declarations, do not separate the keyword mapping
from its
type by a space. Do not separate any nested mapping
keyword from its type by
whitespace.
Yes:
mapping(uint => uint) map;
mapping(address => bool) registeredAddresses;
mapping(uint => mapping(bool => Data[])) public data;
mapping(uint => mapping(uint => s)) data;
No:
mapping (uint => uint) map;
mapping( address => bool ) registeredAddresses;
mapping (uint => mapping (bool => Data[])) public data;
mapping(uint => mapping (uint => s)) data;
Variable Declarations
Declarations of array variables should not have a space between the type and the brackets.
Yes:
uint[] x;
No:
uint [] x;
Other Recommendations
Strings should be quoted with double-quotes instead of single-quotes.
Yes:
str = "foo";
str = "Hamlet says, 'To be or not to be...'";
No:
str = 'bar';
str = '"Be yourself; everyone else is already taken." -Oscar Wilde';
Surround operators with a single space on either side.
Yes:
x = 3;
x = 100 / 10;
x += 3 + 4;
x |= y && z;
No:
x=3;
x = 100/10;
x += 3+4;
x |= y&&z;
Operators with a higher priority than others can exclude surrounding whitespace in order to denote precedence. This is meant to allow for improved readability for complex statements. You should always use the same amount of whitespace on either side of an operator:
Yes:
x = 2**3 + 5;
x = 2*y + 3*z;
x = (a+b) * (a-b);
No:
x = 2** 3 + 5;
x = y+z;
x +=1;
Order of Layout
Layout contract elements in the following order:
Pragma statements
Import statements
Interfaces
Libraries
Contracts
Inside each contract, library or interface, use the following order:
Type declarations
State variables
Events
Modifiers
Functions
Note
It might be clearer to declare types close to their use in events or state variables.
Naming Conventions
Naming conventions are powerful when adopted and used broadly. The use of different conventions can convey significant meta information that would otherwise not be immediately available.
The naming recommendations given here are intended to improve the readability, and thus they are not rules, but rather guidelines to try and help convey the most information through the names of things.
Lastly, consistency within a codebase should always supersede any conventions outlined in this document.
Naming Styles
To avoid confusion, the following names will be used to refer to different naming styles.
b
(single lowercase letter)B
(single uppercase letter)lowercase
lower_case_with_underscores
UPPERCASE
UPPER_CASE_WITH_UNDERSCORES
CapitalizedWords
(or CapWords)mixedCase
(differs from CapitalizedWords by initial lowercase character!)Capitalized_Words_With_Underscores
Note
When using initialisms in CapWords, capitalize all the letters of the initialisms. Thus HTTPServerError is better than HttpServerError. When using initialisms in mixedCase, capitalize all the letters of the initialisms, except keep the first one lower case if it is the beginning of the name. Thus xmlHTTPRequest is better than XMLHTTPRequest.
Names to Avoid
l
- Lowercase letter elO
- Uppercase letter ohI
- Uppercase letter eye
Never use any of these for single letter variable names. They are often indistinguishable from the numerals one and zero.
Contract and Library Names
Contracts and libraries should be named using the CapWords style. Examples:
SimpleToken
,SmartBank
,CertificateHashRepository
,Player
,Congress
,Owned
.Contract and library names should also match their filenames.
If a contract file includes multiple contracts and/or libraries, then the filename should match the core contract. This is not recommended however if it can be avoided.
As shown in the example below, if the contract name is Congress
and the library name is Owned
, then their associated filenames should be Congress.sol
and Owned.sol
.
Yes:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
// Owned.sol
contract Owned {
address public owner;
constructor() {
owner = msg.sender;
}
modifier onlyOwner {
require(msg.sender == owner);
_;
}
function transferOwnership(address newOwner) public onlyOwner {
owner = newOwner;
}
}
and in Congress.sol
:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.0 <0.9.0;
import "./Owned.sol";
contract Congress is Owned, TokenRecipient {
//...
}
No:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
// owned.sol
contract owned {
address public owner;
constructor() {
owner = msg.sender;
}
modifier onlyOwner {
require(msg.sender == owner);
_;
}
function transferOwnership(address newOwner) public onlyOwner {
owner = newOwner;
}
}
and in Congress.sol
:
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.7.0;
import "./owned.sol";
contract Congress is owned, tokenRecipient {
//...
}
Struct Names
Structs should be named using the CapWords style. Examples: MyCoin
, Position
, PositionXY
.
Event Names
Events should be named using the CapWords style. Examples: Deposit
, Transfer
, Approval
, BeforeTransfer
, AfterTransfer
.
Function Names
Functions should use mixedCase. Examples: getBalance
, transfer
, verifyOwner
, addMember
, changeOwner
.
Function Argument Names
Function arguments should use mixedCase. Examples: initialSupply
, account
, recipientAddress
, senderAddress
, newOwner
.
When writing library functions that operate on a custom struct, the struct
should be the first argument and should always be named self
.
Local and State Variable Names
Use mixedCase. Examples: totalSupply
, remainingSupply
, balancesOf
, creatorAddress
, isPreSale
, tokenExchangeRate
.
Constants
Constants should be named with all capital letters with underscores separating
words. Examples: MAX_BLOCKS
, TOKEN_NAME
, TOKEN_TICKER
, CONTRACT_VERSION
.
Modifier Names
Use mixedCase. Examples: onlyBy
, onlyAfter
, onlyDuringThePreSale
.
Enums
Enums, in the style of simple type declarations, should be named using the CapWords style. Examples: TokenGroup
, Frame
, HashStyle
, CharacterLocation
.
Avoiding Naming Collisions
single_trailing_underscore_
This convention is suggested when the desired name collides with that of a built-in or otherwise reserved name.
NatSpec
Solidity contracts can also contain NatSpec comments. They are written with a
triple slash (///
) or a double asterisk block (/** ... */
) and
they should be used directly above function declarations or statements.
For example, the contract from a simple smart contract with the comments added looks like the one below:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.16 <0.9.0;
/// @author The Solidity Team
/// @title A simple storage example
contract SimpleStorage {
uint storedData;
/// Store `x`.
/// @param x the new value to store
/// @dev stores the number in the state variable `storedData`
function set(uint x) public {
storedData = x;
}
/// Return the stored value.
/// @dev retrieves the value of the state variable `storedData`
/// @return the stored value
function get() public view returns (uint) {
return storedData;
}
}
It is recommended that Solidity contracts are fully annotated using NatSpec for all public interfaces (everything in the ABI).
Please see the section about NatSpec for a detailed explanation.
Common Patterns
Withdrawal from Contracts
The recommended method of sending funds after an effect
is using the withdrawal pattern. Although the most intuitive
method of sending Ether, as a result of an effect, is a
direct transfer
call, this is not recommended as it
introduces a potential security risk. You may read
more about this on the Security Considerations page.
The following is an example of the withdrawal pattern in practice in a contract where the goal is to send the most money to the contract in order to become the « richest », inspired by King of the Ether.
In the following contract, if you are no longer the richest, you receive the funds of the person who is now the richest.
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.4;
contract WithdrawalContract {
address public richest;
uint public mostSent;
mapping (address => uint) pendingWithdrawals;
/// The amount of Ether sent was not higher than
/// the currently highest amount.
error NotEnoughEther();
constructor() payable {
richest = msg.sender;
mostSent = msg.value;
}
function becomeRichest() public payable {
if (msg.value <= mostSent) revert NotEnoughEther();
pendingWithdrawals[richest] += msg.value;
richest = msg.sender;
mostSent = msg.value;
}
function withdraw() public {
uint amount = pendingWithdrawals[msg.sender];
// Remember to zero the pending refund before
// sending to prevent re-entrancy attacks
pendingWithdrawals[msg.sender] = 0;
payable(msg.sender).transfer(amount);
}
}
This is as opposed to the more intuitive sending pattern:
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.4;
contract SendContract {
address payable public richest;
uint public mostSent;
/// The amount of Ether sent was not higher than
/// the currently highest amount.
error NotEnoughEther();
constructor() payable {
richest = payable(msg.sender);
mostSent = msg.value;
}
function becomeRichest() public payable {
if (msg.value <= mostSent) revert NotEnoughEther();
// This line can cause problems (explained below).
richest.transfer(msg.value);
richest = payable(msg.sender);
mostSent = msg.value;
}
}
Notice that, in this example, an attacker could trap the
contract into an unusable state by causing richest
to be
the address of a contract that has a receive or fallback function
which fails (e.g. by using revert()
or by just
consuming more than the 2300 gas stipend transferred to them). That way,
whenever transfer
is called to deliver funds to the
« poisoned » contract, it will fail and thus also becomeRichest
will fail, with the contract being stuck forever.
In contrast, if you use the « withdraw » pattern from the first example, the attacker can only cause his or her own withdraw to fail and not the rest of the contract’s workings.
Restricting Access
Restricting access is a common pattern for contracts. Note that you can never restrict any human or computer from reading the content of your transactions or your contract’s state. You can make it a bit harder by using encryption, but if your contract is supposed to read the data, so will everyone else.
You can restrict read access to your contract’s state
by other contracts. That is actually the default
unless you declare your state variables public
.
Furthermore, you can restrict who can make modifications to your contract’s state or call your contract’s functions and this is what this section is about.
The use of function modifiers makes these restrictions highly readable.
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.4;
contract AccessRestriction {
// These will be assigned at the construction
// phase, where `msg.sender` is the account
// creating this contract.
address public owner = msg.sender;
uint public creationTime = block.timestamp;
// Now follows a list of errors that
// this contract can generate together
// with a textual explanation in special
// comments.
/// Sender not authorized for this
/// operation.
error Unauthorized();
/// Function called too early.
error TooEarly();
/// Not enough Ether sent with function call.
error NotEnoughEther();
// Modifiers can be used to change
// the body of a function.
// If this modifier is used, it will
// prepend a check that only passes
// if the function is called from
// a certain address.
modifier onlyBy(address _account)
{
if (msg.sender != _account)
revert Unauthorized();
// Do not forget the "_;"! It will
// be replaced by the actual function
// body when the modifier is used.
_;
}
/// Make `_newOwner` the new owner of this
/// contract.
function changeOwner(address _newOwner)
public
onlyBy(owner)
{
owner = _newOwner;
}
modifier onlyAfter(uint _time) {
if (block.timestamp < _time)
revert TooEarly();
_;
}
/// Erase ownership information.
/// May only be called 6 weeks after
/// the contract has been created.
function disown()
public
onlyBy(owner)
onlyAfter(creationTime + 6 weeks)
{
delete owner;
}
// This modifier requires a certain
// fee being associated with a function call.
// If the caller sent too much, he or she is
// refunded, but only after the function body.
// This was dangerous before Solidity version 0.4.0,
// where it was possible to skip the part after `_;`.
modifier costs(uint _amount) {
if (msg.value < _amount)
revert NotEnoughEther();
_;
if (msg.value > _amount)
payable(msg.sender).transfer(msg.value - _amount);
}
function forceOwnerChange(address _newOwner)
public
payable
costs(200 ether)
{
owner = _newOwner;
// just some example condition
if (uint160(owner) & 0 == 1)
// This did not refund for Solidity
// before version 0.4.0.
return;
// refund overpaid fees
}
}
A more specialised way in which access to function calls can be restricted will be discussed in the next example.
State Machine
Contracts often act as a state machine, which means that they have certain stages in which they behave differently or in which different functions can be called. A function call often ends a stage and transitions the contract into the next stage (especially if the contract models interaction). It is also common that some stages are automatically reached at a certain point in time.
An example for this is a blind auction contract which starts in the stage « accepting blinded bids », then transitions to « revealing bids » which is ended by « determine auction outcome ».
Function modifiers can be used in this situation to model the states and guard against incorrect usage of the contract.
Example
In the following example,
the modifier atStage
ensures that the function can
only be called at a certain stage.
Automatic timed transitions
are handled by the modifier timedTransitions
, which
should be used for all functions.
Note
Modifier Order Matters. If atStage is combined with timedTransitions, make sure that you mention it after the latter, so that the new stage is taken into account.
Finally, the modifier transitionNext
can be used
to automatically go to the next stage when the
function finishes.
Note
Modifier May be Skipped. This only applies to Solidity before version 0.4.0: Since modifiers are applied by simply replacing code and not by using a function call, the code in the transitionNext modifier can be skipped if the function itself uses return. If you want to do that, make sure to call nextStage manually from those functions. Starting with version 0.4.0, modifier code will run even if the function explicitly returns.
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.4;
contract StateMachine {
enum Stages {
AcceptingBlindedBids,
RevealBids,
AnotherStage,
AreWeDoneYet,
Finished
}
/// Function cannot be called at this time.
error FunctionInvalidAtThisStage();
// This is the current stage.
Stages public stage = Stages.AcceptingBlindedBids;
uint public creationTime = block.timestamp;
modifier atStage(Stages _stage) {
if (stage != _stage)
revert FunctionInvalidAtThisStage();
_;
}
function nextStage() internal {
stage = Stages(uint(stage) + 1);
}
// Perform timed transitions. Be sure to mention
// this modifier first, otherwise the guards
// will not take the new stage into account.
modifier timedTransitions() {
if (stage == Stages.AcceptingBlindedBids &&
block.timestamp >= creationTime + 10 days)
nextStage();
if (stage == Stages.RevealBids &&
block.timestamp >= creationTime + 12 days)
nextStage();
// The other stages transition by transaction
_;
}
// Order of the modifiers matters here!
function bid()
public
payable
timedTransitions
atStage(Stages.AcceptingBlindedBids)
{
// We will not implement that here
}
function reveal()
public
timedTransitions
atStage(Stages.RevealBids)
{
}
// This modifier goes to the next stage
// after the function is done.
modifier transitionNext()
{
_;
nextStage();
}
function g()
public
timedTransitions
atStage(Stages.AnotherStage)
transitionNext
{
}
function h()
public
timedTransitions
atStage(Stages.AreWeDoneYet)
transitionNext
{
}
function i()
public
timedTransitions
atStage(Stages.Finished)
{
}
}
List of Known Bugs
Below, you can find a JSON-formatted list of some of the known security-relevant bugs in the Solidity compiler. The file itself is hosted in the Github repository. The list stretches back as far as version 0.3.0, bugs known to be present only in versions preceding that are not listed.
There is another file called bugs_by_version.json, which can be used to check which bugs affect a specific version of the compiler.
Contract source verification tools and also other tools interacting with contracts should consult this list according to the following criteria:
It is mildly suspicious if a contract was compiled with a nightly compiler version instead of a released version. This list does not keep track of unreleased or nightly versions.
It is also mildly suspicious if a contract was compiled with a version that was not the most recent at the time the contract was created. For contracts created from other contracts, you have to follow the creation chain back to a transaction and use the date of that transaction as creation date.
It is highly suspicious if a contract was compiled with a compiler that contains a known bug and the contract was created at a time where a newer compiler version containing a fix was already released.
The JSON file of known bugs below is an array of objects, one for each bug, with the following keys:
- uid
Unique identifier given to the bug in the form of
SOL-<year>-<number>
. It is possible that multiple entries exists with the same uid. This means multiple version ranges are affected by the same bug.- name
Unique name given to the bug
- summary
Short description of the bug
- description
Detailed description of the bug
- link
URL of a website with more detailed information, optional
- introduced
The first published compiler version that contained the bug, optional
- fixed
The first published compiler version that did not contain the bug anymore
- publish
The date at which the bug became known publicly, optional
- severity
Severity of the bug: very low, low, medium, high. Takes into account discoverability in contract tests, likelihood of occurrence and potential damage by exploits.
- conditions
Conditions that have to be met to trigger the bug. The following keys can be used:
optimizer
, Boolean value which means that the optimizer has to be switched on to enable the bug.evmVersion
, a string that indicates which EVM version compiler settings trigger the bug. The string can contain comparison operators. For example,">=constantinople"
means that the bug is present when the EVM version is set toconstantinople
or later. If no conditions are given, assume that the bug is present.- check
This field contains different checks that report whether the smart contract contains the bug or not. The first type of check are Javascript regular expressions that are to be matched against the source code (« source-regex ») if the bug is present. If there is no match, then the bug is very likely not present. If there is a match, the bug might be present. For improved accuracy, the checks should be applied to the source code after stripping comments. The second type of check are patterns to be checked on the compact AST of the Solidity program (« ast-compact-json-path »). The specified search query is a JsonPath expression. If at least one path of the Solidity AST matches the query, the bug is likely present.
[
{
"uid": "SOL-2021-4",
"name": "UserDefinedValueTypesBug",
"summary": "User defined value types with underlying type shorter than 32 bytes used incorrect storage layout and wasted storage",
"description": "The compiler did not correctly compute the storage layout of user defined value types based on types that are shorter than 32 bytes. It would always use a full storage slot for these types, even if the underlying type was shorter. This was wasteful and might have problems with tooling or contract upgrades.",
"link": "https://blog.soliditylang.org/2021/09/29/user-defined-value-types-bug/",
"introduced": "0.8.8",
"fixed": "0.8.9",
"severity": "very low"
},
{
"uid": "SOL-2021-3",
"name": "SignedImmutables",
"summary": "Immutable variables of signed integer type shorter than 256 bits can lead to values with invalid higher order bits if inline assembly is used.",
"description": "When immutable variables of signed integer type shorter than 256 bits are read, their higher order bits were unconditionally set to zero. The correct operation would be to sign-extend the value, i.e. set the higher order bits to one if the sign bit is one. This sign-extension is performed by Solidity just prior to when it matters, i.e. when a value is stored in memory, when it is compared or when a division is performed. Because of that, to our knowledge, the only way to access the value in its unclean state is by reading it through inline assembly.",
"link": "https://blog.soliditylang.org/2021/09/29/signed-immutables-bug/",
"introduced": "0.6.5",
"fixed": "0.8.9",
"severity": "very low"
},
{
"uid": "SOL-2021-2",
"name": "ABIDecodeTwoDimensionalArrayMemory",
"summary": "If used on memory byte arrays, result of the function ``abi.decode`` can depend on the contents of memory outside of the actual byte array that is decoded.",
"description": "The ABI specification uses pointers to data areas for everything that is dynamically-sized. When decoding data from memory (instead of calldata), the ABI decoder did not properly validate some of these pointers. More specifically, it was possible to use large values for the pointers inside arrays such that computing the offset resulted in an undetected overflow. This could lead to these pointers targeting areas in memory outside of the actual area to be decoded. This way, it was possible for ``abi.decode`` to return different values for the same encoded byte array.",
"link": "https://blog.soliditylang.org/2021/04/21/decoding-from-memory-bug/",
"introduced": "0.4.16",
"fixed": "0.8.4",
"conditions": {
"ABIEncoderV2": true
},
"severity": "very low"
},
{
"uid": "SOL-2021-1",
"name": "KeccakCaching",
"summary": "The bytecode optimizer incorrectly re-used previously evaluated Keccak-256 hashes. You are unlikely to be affected if you do not compute Keccak-256 hashes in inline assembly.",
"description": "Solidity's bytecode optimizer has a step that can compute Keccak-256 hashes, if the contents of the memory are known during compilation time. This step also has a mechanism to determine that two Keccak-256 hashes are equal even if the values in memory are not known during compile time. This mechanism had a bug where Keccak-256 of the same memory content, but different sizes were considered equal. More specifically, ``keccak256(mpos1, length1)`` and ``keccak256(mpos2, length2)`` in some cases were considered equal if ``length1`` and ``length2``, when rounded up to nearest multiple of 32 were the same, and when the memory contents at ``mpos1`` and ``mpos2`` can be deduced to be equal. You maybe affected if you compute multiple Keccak-256 hashes of the same content, but with different lengths inside inline assembly. You are unaffected if your code uses ``keccak256`` with a length that is not a compile-time constant or if it is always a multiple of 32.",
"link": "https://blog.soliditylang.org/2021/03/23/keccak-optimizer-bug/",
"fixed": "0.8.3",
"conditions": {
"optimizer": true
},
"severity": "medium"
},
{
"uid": "SOL-2020-11",
"name": "EmptyByteArrayCopy",
"summary": "Copying an empty byte array (or string) from memory or calldata to storage can result in data corruption if the target array's length is increased subsequently without storing new data.",
"description": "The routine that copies byte arrays from memory or calldata to storage stores unrelated data from after the source array in the storage slot if the source array is empty. If the storage array's length is subsequently increased either by using ``.push()`` or by assigning to its ``.length`` attribute (only before 0.6.0), the newly created byte array elements will not be zero-initialized, but contain the unrelated data. You are not affected if you do not assign to ``.length`` and do not use ``.push()`` on byte arrays, or only use ``.push(<arg>)`` or manually initialize the new elements.",
"link": "https://blog.soliditylang.org/2020/10/19/empty-byte-array-copy-bug/",
"fixed": "0.7.4",
"severity": "medium"
},
{
"uid": "SOL-2020-10",
"name": "DynamicArrayCleanup",
"summary": "When assigning a dynamically-sized array with types of size at most 16 bytes in storage causing the assigned array to shrink, some parts of deleted slots were not zeroed out.",
"description": "Consider a dynamically-sized array in storage whose base-type is small enough such that multiple values can be packed into a single slot, such as `uint128[]`. Let us define its length to be `l`. When this array gets assigned from another array with a smaller length, say `m`, the slots between elements `m` and `l` have to be cleaned by zeroing them out. However, this cleaning was not performed properly. Specifically, after the slot corresponding to `m`, only the first packed value was cleaned up. If this array gets resized to a length larger than `m`, the indices corresponding to the unclean parts of the slot contained the original value, instead of 0. The resizing here is performed by assigning to the array `length`, by a `push()` or via inline assembly. You are not affected if you are only using `.push(<arg>)` or if you assign a value (even zero) to the new elements after increasing the length of the array.",
"link": "https://blog.soliditylang.org/2020/10/07/solidity-dynamic-array-cleanup-bug/",
"fixed": "0.7.3",
"severity": "medium"
},
{
"uid": "SOL-2020-9",
"name": "FreeFunctionRedefinition",
"summary": "The compiler does not flag an error when two or more free functions with the same name and parameter types are defined in a source unit or when an imported free function alias shadows another free function with a different name but identical parameter types.",
"description": "In contrast to functions defined inside contracts, free functions with identical names and parameter types did not create an error. Both definition of free functions with identical name and parameter types and an imported free function with an alias that shadows another function with a different name but identical parameter types were permitted due to which a call to either the multiply defined free function or the imported free function alias within a contract led to the execution of that free function which was defined first within the source unit. Subsequently defined identical free function definitions were silently ignored and their code generation was skipped.",
"introduced": "0.7.1",
"fixed": "0.7.2",
"severity": "low"
},
{
"uid": "SOL-2020-8",
"name": "UsingForCalldata",
"summary": "Function calls to internal library functions with calldata parameters called via ``using for`` can result in invalid data being read.",
"description": "Function calls to internal library functions using the ``using for`` mechanism copied all calldata parameters to memory first and passed them on like that, regardless of whether it was an internal or an external call. Due to that, the called function would receive a memory pointer that is interpreted as a calldata pointer. Since dynamically sized arrays are passed using two stack slots for calldata, but only one for memory, this can lead to stack corruption. An affected library call will consider the JUMPDEST to which it is supposed to return as part of its arguments and will instead jump out to whatever was on the stack before the call.",
"introduced": "0.6.9",
"fixed": "0.6.10",
"severity": "very low"
},
{
"uid": "SOL-2020-7",
"name": "MissingEscapingInFormatting",
"summary": "String literals containing double backslash characters passed directly to external or encoding function calls can lead to a different string being used when ABIEncoderV2 is enabled.",
"description": "When ABIEncoderV2 is enabled, string literals passed directly to encoding functions or external function calls are stored as strings in the intemediate code. Characters outside the printable range are handled correctly, but backslashes are not escaped in this procedure. This leads to double backslashes being reduced to single backslashes and consequently re-interpreted as escapes potentially resulting in a different string being encoded.",
"introduced": "0.5.14",
"fixed": "0.6.8",
"severity": "very low",
"conditions": {
"ABIEncoderV2": true
}
},
{
"uid": "SOL-2020-6",
"name": "ArraySliceDynamicallyEncodedBaseType",
"summary": "Accessing array slices of arrays with dynamically encoded base types (e.g. multi-dimensional arrays) can result in invalid data being read.",
"description": "For arrays with dynamically sized base types, index range accesses that use a start expression that is non-zero will result in invalid array slices. Any index access to such array slices will result in data being read from incorrect calldata offsets. Array slices are only supported for dynamic calldata types and all problematic type require ABIEncoderV2 to be enabled.",
"introduced": "0.6.0",
"fixed": "0.6.8",
"severity": "very low",
"conditions": {
"ABIEncoderV2": true
}
},
{
"uid": "SOL-2020-5",
"name": "ImplicitConstructorCallvalueCheck",
"summary": "The creation code of a contract that does not define a constructor but has a base that does define a constructor did not revert for calls with non-zero value.",
"description": "Starting from Solidity 0.4.5 the creation code of contracts without explicit payable constructor is supposed to contain a callvalue check that results in contract creation reverting, if non-zero value is passed. However, this check was missing in case no explicit constructor was defined in a contract at all, but the contract has a base that does define a constructor. In these cases it is possible to send value in a contract creation transaction or using inline assembly without revert, even though the creation code is supposed to be non-payable.",
"introduced": "0.4.5",
"fixed": "0.6.8",
"severity": "very low"
},
{
"uid": "SOL-2020-4",
"name": "TupleAssignmentMultiStackSlotComponents",
"summary": "Tuple assignments with components that occupy several stack slots, i.e. nested tuples, pointers to external functions or references to dynamically sized calldata arrays, can result in invalid values.",
"description": "Tuple assignments did not correctly account for tuple components that occupy multiple stack slots in case the number of stack slots differs between left-hand-side and right-hand-side. This can either happen in the presence of nested tuples or if the right-hand-side contains external function pointers or references to dynamic calldata arrays, while the left-hand-side contains an omission.",
"introduced": "0.1.6",
"fixed": "0.6.6",
"severity": "very low"
},
{
"uid": "SOL-2020-3",
"name": "MemoryArrayCreationOverflow",
"summary": "The creation of very large memory arrays can result in overlapping memory regions and thus memory corruption.",
"description": "No runtime overflow checks were performed for the length of memory arrays during creation. In cases for which the memory size of an array in bytes, i.e. the array length times 32, is larger than 2^256-1, the memory allocation will overflow, potentially resulting in overlapping memory areas. The length of the array is still stored correctly, so copying or iterating over such an array will result in out-of-gas.",
"link": "https://blog.soliditylang.org/2020/04/06/memory-creation-overflow-bug/",
"introduced": "0.2.0",
"fixed": "0.6.5",
"severity": "low"
},
{
"uid": "SOL-2020-1",
"name": "YulOptimizerRedundantAssignmentBreakContinue",
"summary": "The Yul optimizer can remove essential assignments to variables declared inside for loops when Yul's continue or break statement is used. You are unlikely to be affected if you do not use inline assembly with for loops and continue and break statements.",
"description": "The Yul optimizer has a stage that removes assignments to variables that are overwritten again or are not used in all following control-flow branches. This logic incorrectly removes such assignments to variables declared inside a for loop if they can be removed in a control-flow branch that ends with ``break`` or ``continue`` even though they cannot be removed in other control-flow branches. Variables declared outside of the respective for loop are not affected.",
"introduced": "0.6.0",
"fixed": "0.6.1",
"severity": "medium",
"conditions": {
"yulOptimizer": true
}
},
{
"uid": "SOL-2020-2",
"name": "privateCanBeOverridden",
"summary": "Private methods can be overridden by inheriting contracts.",
"description": "While private methods of base contracts are not visible and cannot be called directly from the derived contract, it is still possible to declare a function of the same name and type and thus change the behaviour of the base contract's function.",
"introduced": "0.3.0",
"fixed": "0.5.17",
"severity": "low"
},
{
"uid": "SOL-2020-1",
"name": "YulOptimizerRedundantAssignmentBreakContinue0.5",
"summary": "The Yul optimizer can remove essential assignments to variables declared inside for loops when Yul's continue or break statement is used. You are unlikely to be affected if you do not use inline assembly with for loops and continue and break statements.",
"description": "The Yul optimizer has a stage that removes assignments to variables that are overwritten again or are not used in all following control-flow branches. This logic incorrectly removes such assignments to variables declared inside a for loop if they can be removed in a control-flow branch that ends with ``break`` or ``continue`` even though they cannot be removed in other control-flow branches. Variables declared outside of the respective for loop are not affected.",
"introduced": "0.5.8",
"fixed": "0.5.16",
"severity": "low",
"conditions": {
"yulOptimizer": true
}
},
{
"uid": "SOL-2019-10",
"name": "ABIEncoderV2LoopYulOptimizer",
"summary": "If both the experimental ABIEncoderV2 and the experimental Yul optimizer are activated, one component of the Yul optimizer may reuse data in memory that has been changed in the meantime.",
"description": "The Yul optimizer incorrectly replaces ``mload`` and ``sload`` calls with values that have been previously written to the load location (and potentially changed in the meantime) if all of the following conditions are met: (1) there is a matching ``mstore`` or ``sstore`` call before; (2) the contents of memory or storage is only changed in a function that is called (directly or indirectly) in between the first store and the load call; (3) called function contains a for loop where the same memory location is changed in the condition or the post or body block. When used in Solidity mode, this can only happen if the experimental ABIEncoderV2 is activated and the experimental Yul optimizer has been activated manually in addition to the regular optimizer in the compiler settings.",
"introduced": "0.5.14",
"fixed": "0.5.15",
"severity": "low",
"conditions": {
"ABIEncoderV2": true,
"optimizer": true,
"yulOptimizer": true
}
},
{
"uid": "SOL-2019-9",
"name": "ABIEncoderV2CalldataStructsWithStaticallySizedAndDynamicallyEncodedMembers",
"summary": "Reading from calldata structs that contain dynamically encoded, but statically-sized members can result in incorrect values.",
"description": "When a calldata struct contains a dynamically encoded, but statically-sized member, the offsets for all subsequent struct members are calculated incorrectly. All reads from such members will result in invalid values. Only calldata structs are affected, i.e. this occurs in external functions with such structs as argument. Using affected structs in storage or memory or as arguments to public functions on the other hand works correctly.",
"introduced": "0.5.6",
"fixed": "0.5.11",
"severity": "low",
"conditions": {
"ABIEncoderV2": true
}
},
{
"uid": "SOL-2019-8",
"name": "SignedArrayStorageCopy",
"summary": "Assigning an array of signed integers to a storage array of different type can lead to data corruption in that array.",
"description": "In two's complement, negative integers have their higher order bits set. In order to fit into a shared storage slot, these have to be set to zero. When a conversion is done at the same time, the bits to set to zero were incorrectly determined from the source and not the target type. This means that such copy operations can lead to incorrect values being stored.",
"link": "https://blog.soliditylang.org/2019/06/25/solidity-storage-array-bugs/",
"introduced": "0.4.7",
"fixed": "0.5.10",
"severity": "low/medium"
},
{
"uid": "SOL-2019-7",
"name": "ABIEncoderV2StorageArrayWithMultiSlotElement",
"summary": "Storage arrays containing structs or other statically-sized arrays are not read properly when directly encoded in external function calls or in abi.encode*.",
"description": "When storage arrays whose elements occupy more than a single storage slot are directly encoded in external function calls or using abi.encode*, their elements are read in an overlapping manner, i.e. the element pointer is not properly advanced between reads. This is not a problem when the storage data is first copied to a memory variable or if the storage array only contains value types or dynamically-sized arrays.",
"link": "https://blog.soliditylang.org/2019/06/25/solidity-storage-array-bugs/",
"introduced": "0.4.16",
"fixed": "0.5.10",
"severity": "low",
"conditions": {
"ABIEncoderV2": true
}
},
{
"uid": "SOL-2019-6",
"name": "DynamicConstructorArgumentsClippedABIV2",
"summary": "A contract's constructor that takes structs or arrays that contain dynamically-sized arrays reverts or decodes to invalid data.",
"description": "During construction of a contract, constructor parameters are copied from the code section to memory for decoding. The amount of bytes to copy was calculated incorrectly in case all parameters are statically-sized but contain dynamically-sized arrays as struct members or inner arrays. Such types are only available if ABIEncoderV2 is activated.",
"introduced": "0.4.16",
"fixed": "0.5.9",
"severity": "very low",
"conditions": {
"ABIEncoderV2": true
}
},
{
"uid": "SOL-2019-5",
"name": "UninitializedFunctionPointerInConstructor",
"summary": "Calling uninitialized internal function pointers created in the constructor does not always revert and can cause unexpected behaviour.",
"description": "Uninitialized internal function pointers point to a special piece of code that causes a revert when called. Jump target positions are different during construction and after deployment, but the code for setting this special jump target only considered the situation after deployment.",
"introduced": "0.5.0",
"fixed": "0.5.8",
"severity": "very low"
},
{
"uid": "SOL-2019-5",
"name": "UninitializedFunctionPointerInConstructor_0.4.x",
"summary": "Calling uninitialized internal function pointers created in the constructor does not always revert and can cause unexpected behaviour.",
"description": "Uninitialized internal function pointers point to a special piece of code that causes a revert when called. Jump target positions are different during construction and after deployment, but the code for setting this special jump target only considered the situation after deployment.",
"introduced": "0.4.5",
"fixed": "0.4.26",
"severity": "very low"
},
{
"uid": "SOL-2019-4",
"name": "IncorrectEventSignatureInLibraries",
"summary": "Contract types used in events in libraries cause an incorrect event signature hash",
"description": "Instead of using the type `address` in the hashed signature, the actual contract name was used, leading to a wrong hash in the logs.",
"introduced": "0.5.0",
"fixed": "0.5.8",
"severity": "very low"
},
{
"uid": "SOL-2019-4",
"name": "IncorrectEventSignatureInLibraries_0.4.x",
"summary": "Contract types used in events in libraries cause an incorrect event signature hash",
"description": "Instead of using the type `address` in the hashed signature, the actual contract name was used, leading to a wrong hash in the logs.",
"introduced": "0.3.0",
"fixed": "0.4.26",
"severity": "very low"
},
{
"uid": "SOL-2019-3",
"name": "ABIEncoderV2PackedStorage",
"summary": "Storage structs and arrays with types shorter than 32 bytes can cause data corruption if encoded directly from storage using the experimental ABIEncoderV2.",
"description": "Elements of structs and arrays that are shorter than 32 bytes are not properly decoded from storage when encoded directly (i.e. not via a memory type) using ABIEncoderV2. This can cause corruption in the values themselves but can also overwrite other parts of the encoded data.",
"link": "https://blog.soliditylang.org/2019/03/26/solidity-optimizer-and-abiencoderv2-bug/",
"introduced": "0.5.0",
"fixed": "0.5.7",
"severity": "low",
"conditions": {
"ABIEncoderV2": true
}
},
{
"uid": "SOL-2019-3",
"name": "ABIEncoderV2PackedStorage_0.4.x",
"summary": "Storage structs and arrays with types shorter than 32 bytes can cause data corruption if encoded directly from storage using the experimental ABIEncoderV2.",
"description": "Elements of structs and arrays that are shorter than 32 bytes are not properly decoded from storage when encoded directly (i.e. not via a memory type) using ABIEncoderV2. This can cause corruption in the values themselves but can also overwrite other parts of the encoded data.",
"link": "https://blog.soliditylang.org/2019/03/26/solidity-optimizer-and-abiencoderv2-bug/",
"introduced": "0.4.19",
"fixed": "0.4.26",
"severity": "low",
"conditions": {
"ABIEncoderV2": true
}
},
{
"uid": "SOL-2019-2",
"name": "IncorrectByteInstructionOptimization",
"summary": "The optimizer incorrectly handles byte opcodes whose second argument is 31 or a constant expression that evaluates to 31. This can result in unexpected values.",
"description": "The optimizer incorrectly handles byte opcodes that use the constant 31 as second argument. This can happen when performing index access on bytesNN types with a compile-time constant value (not index) of 31 or when using the byte opcode in inline assembly.",
"link": "https://blog.soliditylang.org/2019/03/26/solidity-optimizer-and-abiencoderv2-bug/",
"introduced": "0.5.5",
"fixed": "0.5.7",
"severity": "very low",
"conditions": {
"optimizer": true
}
},
{
"uid": "SOL-2019-1",
"name": "DoubleShiftSizeOverflow",
"summary": "Double bitwise shifts by large constants whose sum overflows 256 bits can result in unexpected values.",
"description": "Nested logical shift operations whose total shift size is 2**256 or more are incorrectly optimized. This only applies to shifts by numbers of bits that are compile-time constant expressions.",
"link": "https://blog.soliditylang.org/2019/03/26/solidity-optimizer-and-abiencoderv2-bug/",
"introduced": "0.5.5",
"fixed": "0.5.6",
"severity": "low",
"conditions": {
"optimizer": true,
"evmVersion": ">=constantinople"
}
},
{
"uid": "SOL-2018-4",
"name": "ExpExponentCleanup",
"summary": "Using the ** operator with an exponent of type shorter than 256 bits can result in unexpected values.",
"description": "Higher order bits in the exponent are not properly cleaned before the EXP opcode is applied if the type of the exponent expression is smaller than 256 bits and not smaller than the type of the base. In that case, the result might be larger than expected if the exponent is assumed to lie within the value range of the type. Literal numbers as exponents are unaffected as are exponents or bases of type uint256.",
"link": "https://blog.soliditylang.org/2018/09/13/solidity-bugfix-release/",
"fixed": "0.4.25",
"severity": "medium/high",
"check": {"regex-source": "[^/]\\*\\* *[^/0-9 ]"}
},
{
"uid": "SOL-2018-3",
"name": "EventStructWrongData",
"summary": "Using structs in events logged wrong data.",
"description": "If a struct is used in an event, the address of the struct is logged instead of the actual data.",
"link": "https://blog.soliditylang.org/2018/09/13/solidity-bugfix-release/",
"introduced": "0.4.17",
"fixed": "0.4.25",
"severity": "very low",
"check": {"ast-compact-json-path": "$..[?(@.nodeType === 'EventDefinition')]..[?(@.nodeType === 'UserDefinedTypeName' && @.typeDescriptions.typeString.startsWith('struct'))]"}
},
{
"uid": "SOL-2018-2",
"name": "NestedArrayFunctionCallDecoder",
"summary": "Calling functions that return multi-dimensional fixed-size arrays can result in memory corruption.",
"description": "If Solidity code calls a function that returns a multi-dimensional fixed-size array, array elements are incorrectly interpreted as memory pointers and thus can cause memory corruption if the return values are accessed. Calling functions with multi-dimensional fixed-size arrays is unaffected as is returning fixed-size arrays from function calls. The regular expression only checks if such functions are present, not if they are called, which is required for the contract to be affected.",
"link": "https://blog.soliditylang.org/2018/09/13/solidity-bugfix-release/",
"introduced": "0.1.4",
"fixed": "0.4.22",
"severity": "medium",
"check": {"regex-source": "returns[^;{]*\\[\\s*[^\\] \\t\\r\\n\\v\\f][^\\]]*\\]\\s*\\[\\s*[^\\] \\t\\r\\n\\v\\f][^\\]]*\\][^{;]*[;{]"}
},
{
"uid": "SOL-2018-1",
"name": "OneOfTwoConstructorsSkipped",
"summary": "If a contract has both a new-style constructor (using the constructor keyword) and an old-style constructor (a function with the same name as the contract) at the same time, one of them will be ignored.",
"description": "If a contract has both a new-style constructor (using the constructor keyword) and an old-style constructor (a function with the same name as the contract) at the same time, one of them will be ignored. There will be a compiler warning about the old-style constructor, so contracts only using new-style constructors are fine.",
"introduced": "0.4.22",
"fixed": "0.4.23",
"severity": "very low"
},
{
"uid": "SOL-2017-5",
"name": "ZeroFunctionSelector",
"summary": "It is possible to craft the name of a function such that it is executed instead of the fallback function in very specific circumstances.",
"description": "If a function has a selector consisting only of zeros, is payable and part of a contract that does not have a fallback function and at most five external functions in total, this function is called instead of the fallback function if Ether is sent to the contract without data.",
"fixed": "0.4.18",
"severity": "very low"
},
{
"uid": "SOL-2017-4",
"name": "DelegateCallReturnValue",
"summary": "The low-level .delegatecall() does not return the execution outcome, but converts the value returned by the functioned called to a boolean instead.",
"description": "The return value of the low-level .delegatecall() function is taken from a position in memory, where the call data or the return data resides. This value is interpreted as a boolean and put onto the stack. This means if the called function returns at least 32 zero bytes, .delegatecall() returns false even if the call was successful.",
"introduced": "0.3.0",
"fixed": "0.4.15",
"severity": "low"
},
{
"uid": "SOL-2017-3",
"name": "ECRecoverMalformedInput",
"summary": "The ecrecover() builtin can return garbage for malformed input.",
"description": "The ecrecover precompile does not properly signal failure for malformed input (especially in the 'v' argument) and thus the Solidity function can return data that was previously present in the return area in memory.",
"fixed": "0.4.14",
"severity": "medium"
},
{
"uid": "SOL-2017-2",
"name": "SkipEmptyStringLiteral",
"summary": "If \"\" is used in a function call, the following function arguments will not be correctly passed to the function.",
"description": "If the empty string literal \"\" is used as an argument in a function call, it is skipped by the encoder. This has the effect that the encoding of all arguments following this is shifted left by 32 bytes and thus the function call data is corrupted.",
"fixed": "0.4.12",
"severity": "low"
},
{
"uid": "SOL-2017-1",
"name": "ConstantOptimizerSubtraction",
"summary": "In some situations, the optimizer replaces certain numbers in the code with routines that compute different numbers.",
"description": "The optimizer tries to represent any number in the bytecode by routines that compute them with less gas. For some special numbers, an incorrect routine is generated. This could allow an attacker to e.g. trick victims about a specific amount of ether, or function calls to call different functions (or none at all).",
"link": "https://blog.soliditylang.org/2017/05/03/solidity-optimizer-bug/",
"fixed": "0.4.11",
"severity": "low",
"conditions": {
"optimizer": true
}
},
{
"uid": "SOL-2016-11",
"name": "IdentityPrecompileReturnIgnored",
"summary": "Failure of the identity precompile was ignored.",
"description": "Calls to the identity contract, which is used for copying memory, ignored its return value. On the public chain, calls to the identity precompile can be made in a way that they never fail, but this might be different on private chains.",
"severity": "low",
"fixed": "0.4.7"
},
{
"uid": "SOL-2016-10",
"name": "OptimizerStateKnowledgeNotResetForJumpdest",
"summary": "The optimizer did not properly reset its internal state at jump destinations, which could lead to data corruption.",
"description": "The optimizer performs symbolic execution at certain stages. At jump destinations, multiple code paths join and thus it has to compute a common state from the incoming edges. Computing this common state was simplified to just use the empty state, but this implementation was not done properly. This bug can cause data corruption.",
"severity": "medium",
"introduced": "0.4.5",
"fixed": "0.4.6",
"conditions": {
"optimizer": true
}
},
{
"uid": "SOL-2016-9",
"name": "HighOrderByteCleanStorage",
"summary": "For short types, the high order bytes were not cleaned properly and could overwrite existing data.",
"description": "Types shorter than 32 bytes are packed together into the same 32 byte storage slot, but storage writes always write 32 bytes. For some types, the higher order bytes were not cleaned properly, which made it sometimes possible to overwrite a variable in storage when writing to another one.",
"link": "https://blog.soliditylang.org/2016/11/01/security-alert-solidity-variables-can-overwritten-storage/",
"severity": "high",
"introduced": "0.1.6",
"fixed": "0.4.4"
},
{
"uid": "SOL-2016-8",
"name": "OptimizerStaleKnowledgeAboutSHA3",
"summary": "The optimizer did not properly reset its knowledge about SHA3 operations resulting in some hashes (also used for storage variable positions) not being calculated correctly.",
"description": "The optimizer performs symbolic execution in order to save re-evaluating expressions whose value is already known. This knowledge was not properly reset across control flow paths and thus the optimizer sometimes thought that the result of a SHA3 operation is already present on the stack. This could result in data corruption by accessing the wrong storage slot.",
"severity": "medium",
"fixed": "0.4.3",
"conditions": {
"optimizer": true
}
},
{
"uid": "SOL-2016-7",
"name": "LibrariesNotCallableFromPayableFunctions",
"summary": "Library functions threw an exception when called from a call that received Ether.",
"description": "Library functions are protected against sending them Ether through a call. Since the DELEGATECALL opcode forwards the information about how much Ether was sent with a call, the library function incorrectly assumed that Ether was sent to the library and threw an exception.",
"severity": "low",
"introduced": "0.4.0",
"fixed": "0.4.2"
},
{
"uid": "SOL-2016-6",
"name": "SendFailsForZeroEther",
"summary": "The send function did not provide enough gas to the recipient if no Ether was sent with it.",
"description": "The recipient of an Ether transfer automatically receives a certain amount of gas from the EVM to handle the transfer. In the case of a zero-transfer, this gas is not provided which causes the recipient to throw an exception.",
"severity": "low",
"fixed": "0.4.0"
},
{
"uid": "SOL-2016-5",
"name": "DynamicAllocationInfiniteLoop",
"summary": "Dynamic allocation of an empty memory array caused an infinite loop and thus an exception.",
"description": "Memory arrays can be created provided a length. If this length is zero, code was generated that did not terminate and thus consumed all gas.",
"severity": "low",
"fixed": "0.3.6"
},
{
"uid": "SOL-2016-4",
"name": "OptimizerClearStateOnCodePathJoin",
"summary": "The optimizer did not properly reset its internal state at jump destinations, which could lead to data corruption.",
"description": "The optimizer performs symbolic execution at certain stages. At jump destinations, multiple code paths join and thus it has to compute a common state from the incoming edges. Computing this common state was not done correctly. This bug can cause data corruption, but it is probably quite hard to use for targeted attacks.",
"severity": "low",
"fixed": "0.3.6",
"conditions": {
"optimizer": true
}
},
{
"uid": "SOL-2016-3",
"name": "CleanBytesHigherOrderBits",
"summary": "The higher order bits of short bytesNN types were not cleaned before comparison.",
"description": "Two variables of type bytesNN were considered different if their higher order bits, which are not part of the actual value, were different. An attacker might use this to reach seemingly unreachable code paths by providing incorrectly formatted input data.",
"severity": "medium/high",
"fixed": "0.3.3"
},
{
"uid": "SOL-2016-2",
"name": "ArrayAccessCleanHigherOrderBits",
"summary": "Access to array elements for arrays of types with less than 32 bytes did not correctly clean the higher order bits, causing corruption in other array elements.",
"description": "Multiple elements of an array of values that are shorter than 17 bytes are packed into the same storage slot. Writing to a single element of such an array did not properly clean the higher order bytes and thus could lead to data corruption.",
"severity": "medium/high",
"fixed": "0.3.1"
},
{
"uid": "SOL-2016-1",
"name": "AncientCompiler",
"summary": "This compiler version is ancient and might contain several undocumented or undiscovered bugs.",
"description": "The list of bugs is only kept for compiler versions starting from 0.3.0, so older versions might contain undocumented bugs.",
"severity": "high",
"fixed": "0.3.0"
}
]
Contributing
Help is always welcome and there are plenty of options how you can contribute to Solidity.
In particular, we appreciate support in the following areas:
Reporting issues.
Fixing and responding to Solidity’s GitHub issues, especially those tagged as « good first issue » which are meant as introductory issues for external contributors.
Improving the documentation.
Translating the documentation into more languages.
Responding to questions from other users on StackExchange and the Solidity Gitter Chat.
Getting involved in the language design process by proposing language changes or new features in the Solidity forum and providing feedback.
To get started, you can try Compilation à partir des sources in order to familiarize yourself with the components of Solidity and the build process. Also, it may be useful to become well-versed at writing smart-contracts in Solidity.
Please note that this project is released with a Contributor Code of Conduct. By participating in this project - in the issues, pull requests, or Gitter channels - you agree to abide by its terms.
Team Calls
If you have issues or pull requests to discuss, or are interested in hearing what the team and contributors are working on, you can join our public team calls:
Mondays at 3pm CET/CEST.
Wednesdays at 2pm CET/CEST.
Both calls take place on Jitsi.
How to Report Issues
To report an issue, please use the GitHub issues tracker. When reporting issues, please mention the following details:
Solidity version.
Source code (if applicable).
Operating system.
Steps to reproduce the issue.
Actual vs. expected behaviour.
Reducing the source code that caused the issue to a bare minimum is always very helpful and sometimes even clarifies a misunderstanding.
Workflow for Pull Requests
In order to contribute, please fork off of the develop
branch and make your
changes there. Your commit messages should detail why you made your change
in addition to what you did (unless it is a tiny change).
If you need to pull in any changes from develop
after making your fork (for
example, to resolve potential merge conflicts), please avoid using git merge
and instead, git rebase
your branch. This will help us review your change
more easily.
Additionally, if you are writing a new feature, please ensure you add appropriate
test cases under test/
(see below).
However, if you are making a larger change, please consult with the Solidity Development Gitter channel (different from the one mentioned above, this one is focused on compiler and language development instead of language usage) first.
New features and bugfixes should be added to the Changelog.md
file: please
follow the style of previous entries, when applicable.
Finally, please make sure you respect the coding style for this project. Also, even though we do CI testing, please test your code and ensure that it builds locally before submitting a pull request.
Thank you for your help!
Running the Compiler Tests
Prerequisites
For running all compiler tests you may want to optionally install a few dependencies (evmone, libz3, and libhera).
On macOS some of the testing scripts expect GNU coreutils to be installed.
This can be easiest accomplished using Homebrew: brew install coreutils
.
On Windows systems make sure that you have a privilege to create symlinks, otherwise several tests may fail. Administrators should have that privilege, but you may also grant it to other users or enable Developer Mode.
Running the Tests
Solidity includes different types of tests, most of them bundled into the
Boost C++ Test Framework application soltest
.
Running build/test/soltest
or its wrapper scripts/soltest.sh
is sufficient for most changes.
The ./scripts/tests.sh
script executes most Solidity tests automatically,
including those bundled into the Boost C++ Test Framework
application soltest
(or its wrapper scripts/soltest.sh
), as well as command line tests and
compilation tests.
The test system automatically tries to discover the location of the evmone for running the semantic tests.
The evmone
library must be located in the deps
or deps/lib
directory relative to the
current working directory, to its parent or its parent’s parent. Alternatively an explicit location
for the evmone
shared object can be specified via the ETH_EVMONE
environment variable.
evmone
is needed mainly for running semantic and gas tests.
If you do not have it installed, you can skip these tests by passing the --no-semantic-tests
flag to scripts/soltest.sh
.
Running Ewasm tests is disabled by default and can be explicitly enabled
via ./scripts/soltest.sh --ewasm
and requires hera
to be found by soltest
.
The mechanism for locating the hera
library is the same as for evmone
, except that the
variable for specifying an explicit location is called ETH_HERA
.
The evmone
and hera
libraries should both end with the file name
extension .so
on Linux, .dll
on Windows systems and .dylib
on macOS.
For running SMT tests, the libz3
library must be installed and locatable
by cmake
during compiler configure stage.
If the libz3
library is not installed on your system, you should disable the
SMT tests by exporting SMT_FLAGS=--no-smt
before running ./scripts/tests.sh
or
running ./scripts/soltest.sh --no-smt
.
These tests are libsolidity/smtCheckerTests
and libsolidity/smtCheckerTestsJSON
.
Note
To get a list of all unit tests run by Soltest, run ./build/test/soltest --list_content=HRF
.
For quicker results you can run a subset of, or specific tests.
To run a subset of tests, you can use filters:
./scripts/soltest.sh -t TestSuite/TestName
,
where TestName
can be a wildcard *
.
Or, for example, to run all the tests for the yul disambiguator:
./scripts/soltest.sh -t "yulOptimizerTests/disambiguator/*" --no-smt
.
./build/test/soltest --help
has extensive help on all of the options available.
See especially:
show_progress (-p) to show test completion,
run_test (-t) to run specific tests cases, and
report-level (-r) give a more detailed report.
Note
Those working in a Windows environment wanting to run the above basic sets
without libz3. Using Git Bash, you use: ./build/test/Release/soltest.exe -- --no-smt
.
If you are running this in plain Command Prompt, use .\build\test\Release\soltest.exe -- --no-smt
.
If you want to debug using GDB, make sure you build differently than the « usual ».
For example, you could run the following command in your build
folder:
.. code-block:: bash
cmake -DCMAKE_BUILD_TYPE=Debug .. make
This creates symbols so that when you debug a test using the --debug
flag,
you have access to functions and variables in which you can break or print with.
The CI runs additional tests (including solc-js
and testing third party Solidity
frameworks) that require compiling the Emscripten target.
Writing and Running Syntax Tests
Syntax tests check that the compiler generates the correct error messages for invalid code
and properly accepts valid code.
They are stored in individual files inside the tests/libsolidity/syntaxTests
folder.
These files must contain annotations, stating the expected result(s) of the respective test.
The test suite compiles and checks them against the given expectations.
For example: ./test/libsolidity/syntaxTests/double_stateVariable_declaration.sol
contract test {
uint256 variable;
uint128 variable;
}
// ----
// DeclarationError: (36-52): Identifier already declared.
A syntax test must contain at least the contract under test itself, followed by the separator // ----
. The comments that follow the separator are used to describe the
expected compiler errors or warnings. The number range denotes the location in the source where the error occurred.
If you want the contract to compile without any errors or warning you can leave
out the separator and the comments that follow it.
In the above example, the state variable variable
was declared twice, which is not allowed. This results in a DeclarationError
stating that the identifier was already declared.
The isoltest
tool is used for these tests and you can find it under ./build/test/tools/
. It is an interactive tool which allows
editing of failing contracts using your preferred text editor. Let’s try to break this test by removing the second declaration of variable
:
contract test {
uint256 variable;
}
// ----
// DeclarationError: (36-52): Identifier already declared.
Running ./build/test/tools/isoltest
again results in a test failure:
syntaxTests/double_stateVariable_declaration.sol: FAIL
Contract:
contract test {
uint256 variable;
}
Expected result:
DeclarationError: (36-52): Identifier already declared.
Obtained result:
Success
isoltest
prints the expected result next to the obtained result, and also
provides a way to edit, update or skip the current contract file, or quit the application.
It offers several options for failing tests:
edit
:isoltest
tries to open the contract in an editor so you can adjust it. It either uses the editor given on the command line (asisoltest --editor /path/to/editor
), in the environment variableEDITOR
or just/usr/bin/editor
(in that order).update
: Updates the expectations for contract under test. This updates the annotations by removing unmet expectations and adding missing expectations. The test is then run again.skip
: Skips the execution of this particular test.quit
: Quitsisoltest
.
All of these options apply to the current contract, expect quit
which stops the entire testing process.
Automatically updating the test above changes it to
contract test {
uint256 variable;
}
// ----
and re-run the test. It now passes again:
Re-running test case...
syntaxTests/double_stateVariable_declaration.sol: OK
Note
Choose a name for the contract file that explains what it tests, e.g. double_variable_declaration.sol
.
Do not put more than one contract into a single file, unless you are testing inheritance or cross-contract calls.
Each file should test one aspect of your new feature.
Running the Fuzzer via AFL
Fuzzing is a technique that runs programs on more or less random inputs to find exceptional execution
states (segmentation faults, exceptions, etc). Modern fuzzers are clever and run a directed search
inside the input. We have a specialized binary called solfuzzer
which takes source code as input
and fails whenever it encounters an internal compiler error, segmentation fault or similar, but
does not fail if e.g., the code contains an error. This way, fuzzing tools can find internal problems in the compiler.
We mainly use AFL for fuzzing. You need to download and
install the AFL packages from your repositories (afl, afl-clang) or build them manually.
Next, build Solidity (or just the solfuzzer
binary) with AFL as your compiler:
cd build
# if needed
make clean
cmake .. -DCMAKE_C_COMPILER=path/to/afl-gcc -DCMAKE_CXX_COMPILER=path/to/afl-g++
make solfuzzer
At this stage you should be able to see a message similar to the following:
Scanning dependencies of target solfuzzer
[ 98%] Building CXX object test/tools/CMakeFiles/solfuzzer.dir/fuzzer.cpp.o
afl-cc 2.52b by <lcamtuf@google.com>
afl-as 2.52b by <lcamtuf@google.com>
[+] Instrumented 1949 locations (64-bit, non-hardened mode, ratio 100%).
[100%] Linking CXX executable solfuzzer
If the instrumentation messages did not appear, try switching the cmake flags pointing to AFL’s clang binaries:
# if previously failed
make clean
cmake .. -DCMAKE_C_COMPILER=path/to/afl-clang -DCMAKE_CXX_COMPILER=path/to/afl-clang++
make solfuzzer
Otherwise, upon execution the fuzzer halts with an error saying binary is not instrumented:
afl-fuzz 2.52b by <lcamtuf@google.com>
... (truncated messages)
[*] Validating target binary...
[-] Looks like the target binary is not instrumented! The fuzzer depends on
compile-time instrumentation to isolate interesting test cases while
mutating the input data. For more information, and for tips on how to
instrument binaries, please see /usr/share/doc/afl-doc/docs/README.
When source code is not available, you may be able to leverage QEMU
mode support. Consult the README for tips on how to enable this.
(It is also possible to use afl-fuzz as a traditional, "dumb" fuzzer.
For that, you can use the -n option - but expect much worse results.)
[-] PROGRAM ABORT : No instrumentation detected
Location : check_binary(), afl-fuzz.c:6920
Next, you need some example source files. This makes it much easier for the fuzzer to find errors. You can either copy some files from the syntax tests or extract test files from the documentation or the other tests:
mkdir /tmp/test_cases
cd /tmp/test_cases
# extract from tests:
path/to/solidity/scripts/isolate_tests.py path/to/solidity/test/libsolidity/SolidityEndToEndTest.cpp
# extract from documentation:
path/to/solidity/scripts/isolate_tests.py path/to/solidity/docs
The AFL documentation states that the corpus (the initial input files) should not be
too large. The files themselves should not be larger than 1 kB and there should be
at most one input file per functionality, so better start with a small number of.
There is also a tool called afl-cmin
that can trim input files
that result in similar behaviour of the binary.
Now run the fuzzer (the -m
extends the size of memory to 60 MB):
afl-fuzz -m 60 -i /tmp/test_cases -o /tmp/fuzzer_reports -- /path/to/solfuzzer
The fuzzer creates source files that lead to failures in /tmp/fuzzer_reports
.
Often it finds many similar source files that produce the same error. You can
use the tool scripts/uniqueErrors.sh
to filter out the unique errors.
Whiskers
Whiskers is a string templating system similar to Mustache. It is used by the compiler in various places to aid readability, and thus maintainability and verifiability, of the code.
The syntax comes with a substantial difference to Mustache. The template markers {{
and }}
are
replaced by <
and >
in order to aid parsing and avoid conflicts with Yul
(The symbols <
and >
are invalid in inline assembly, while {
and }
are used to delimit blocks).
Another limitation is that lists are only resolved one depth and they do not recurse. This may change in the future.
A rough specification is the following:
Any occurrence of <name>
is replaced by the string-value of the supplied variable name
without any
escaping and without iterated replacements. An area can be delimited by <#name>...</name>
. It is replaced
by as many concatenations of its contents as there were sets of variables supplied to the template system,
each time replacing any <inner>
items by their respective value. Top-level variables can also be used
inside such areas.
There are also conditionals of the form <?name>...<!name>...</name>
, where template replacements
continue recursively either in the first or the second segment depending on the value of the boolean
parameter name
. If <?+name>...<!+name>...</+name>
is used, then the check is whether
the string parameter name
is non-empty.
Documentation Style Guide
In the following section you find style recommendations specifically focusing on documentation contributions to Solidity.
English Language
Use English, with British English spelling preferred, unless using project or brand names. Try to reduce the usage of local slang and references, making your language as clear to all readers as possible. Below are some references to help:
Note
While the official Solidity documentation is written in English, there are community contributed Traductions in other languages available. Please refer to the translation guide for information on how to contribute to the community translations.
Title Case for Headings
Use title case for headings. This means capitalise all principal words in titles, but not articles, conjunctions, and prepositions unless they start the title.
For example, the following are all correct:
Title Case for Headings.
For Headings Use Title Case.
Local and State Variable Names.
Order of Layout.
Expand Contractions
Use expanded contractions for words, for example:
« Do not » instead of « Don’t ».
« Can not » instead of « Can’t ».
Active and Passive Voice
Active voice is typically recommended for tutorial style documentation as it helps the reader understand who or what is performing a task. However, as the Solidity documentation is a mixture of tutorials and reference content, passive voice is sometimes more applicable.
As a summary:
Use passive voice for technical reference, for example language definition and internals of the Ethereum VM.
Use active voice when describing recommendations on how to apply an aspect of Solidity.
For example, the below is in passive voice as it specifies an aspect of Solidity:
Functions can be declared
pure
in which case they promise not to read from or modify the state.
For example, the below is in active voice as it discusses an application of Solidity:
When invoking the compiler, you can specify how to discover the first element of a path, and also path prefix remappings.
Common Terms
« Function parameters » and « return variables », not input and output parameters.
Code Examples
A CI process tests all code block formatted code examples that begin with pragma solidity
, contract
, library
or interface
using the ./test/cmdlineTests.sh
script when you create a PR. If you are adding new code examples,
ensure they work and pass tests before creating the PR.
Ensure that all code examples begin with a pragma
version that spans the largest where the contract code is valid.
For example pragma solidity >=0.4.0 <0.9.0;
.
Running Documentation Tests
Make sure your contributions pass our documentation tests by running ./scripts/docs.sh
that installs dependencies
needed for documentation and checks for any problems such as broken links or syntax issues.
Solidity Language Design
To actively get involved in the language design process and share your ideas concerning the future of Solidity, please join the Solidity forum.
The Solidity forum serves as the place to propose and discuss new language features and their implementation in the early stages of ideation or modifications of existing features.
As soon as proposals get more tangible, their implementation will also be discussed in the Solidity GitHub repository in the form of issues.
In addition to the forum and issue discussions, we regularly host language design discussion calls in which selected topics, issues or feature implementations are debated in detail. The invitation to those calls is shared via the forum.
We are also sharing feedback surveys and other content that is relevant to language design in the forum.
If you want to know where the team is standing in terms or implementing new features, you can follow the implementation status in the Solidity Github project. Issues in the design backlog need further specification and will either be discussed in a language design call or in a regular team call. You can see the upcoming changes for the next breaking release by changing from the default branch (develop) to the breaking branch.
For ad-hoc cases and questions you can reach out to us via the Solidity-dev Gitter channel, a dedicated chatroom for conversations around the Solidity compiler and language development.
We are happy to hear your thoughts on how we can improve the language design process to be even more collaborative and transparent.
Solidity Brand Guide
This brand guide features information on Solidity’s brand policy and logo usage guidelines.
The Solidity Brand
The Solidity programming language is an open-source, community project governed by a core team. The core team is sponsored by the Ethereum Foundation.
This document aims to provide information about how to best use the Solidity brand name and logo.
We encourage you to read this document carefully before using the brand name or the logo. Your cooperation is highly appreciated!
Solidity Brand Name
« Solidity » should be used to refer to the Solidity programming language solely.
Please do not use « Solidity »:
To refer to any other programming language.
In a way that is misleading or may imply association of unrelated modules, tools, documentation, or other resources with the Solidity programming language.
In ways that confuse the community as to whether the Solidity programming language is open-source and free to use.
Solidity Logo License
The Solidity logo is distributed and licensed under a Creative Commons Attribution 4.0 International License.
This is the most permissive Creative Commons license and allows reuse and modifications for any purpose.
You are free to:
Share — Copy and redistribute the material in any medium or format.
Adapt — Remix, transform, and build upon the material for any purpose, even commercially.
Under the following terms:
Attribution — You must give appropriate credit, provide a link to the license, and indicate if changes were made. You may do so in any reasonable manner, but not in any way that suggests that the Solidity core team endorses you or your use.
When using the Solidity logo, please respect the Solidity logo guidelines.
Solidity Logo Guidelines
(Right click on the logo to download it.)
Please do not:
Change the ratio of the logo (do not stretch it or cut it).
Change the colors of the logo, unless it is absolutely necessary.
Credits
This document was, in parts, derived from the Python Software Foundation Trademark Usage Policy and the Rust Media Guide.
Language Influences
Solidity is a curly-bracket language that has been influenced and inspired by several well-known programming languages.
Solidity is most profoundly influenced by C++, but also borrowed concepts from languages like Python, JavaScript, and others.
The influence from C++ can be seen in the syntax for variable declarations, for loops, the concept of overloading functions, implicit and explicit type conversions and many other details.
In the early days of the language, Solidity used to be partly influenced by JavaScript.
This was due to function-level scoping of variables and the use of the keyword var
.
The JavaScript influence was reduced starting from version 0.4.0.
Now, the main remaining similarity to JavaScript is that functions are defined using the keyword
function
. Solidity also supports import syntax and semantics that
are similar to those available in JavaScript. Besides those points, Solidity looks like
most other curly-bracket languages and has no major JavaScript influence anymore.
Another influence to Solidity was Python. Solidity’s modifiers were added trying to model
Python’s decorators with a much more restricted functionality. Furthermore, multiple inheritance, C3 linearization,
and the super
keyword are taken from Python as well as the general assignment and copy semantics of value
and reference types.