Contrats

Les contrats dans Solidity sont similaires aux classes dans les langages orientés objet. Ils contiennent des données persistantes dans des variables d’état, et des fonctions qui peuvent modifier ces variables. L’appel d’une fonction sur un contrat (instance) différent va effectuer un appel de fonction EVM et donc un changement de contexte de telle sorte que les variables d’état dans le contrat appelant sont inaccessibles. Un contrat et ses fonctions doivent être appelés pour que quelque chose se produise. Il n’y a pas de concept de « cron » dans Ethereum pour appeler une fonction à un événement particulier automatiquement.

Création de contrats

Les contrats peuvent être créés « de l’extérieur » via des transactions Ethereum ou à partir de contrats Solidity.

Des IDE, tels que Remix, rendent le processus de création transparent à l’aide d’éléments d’interface utilisateur.

Une façon de créer des contrats de façon programmatique sur Ethereum est via l’API JavaScript web3.js. Elle dispose d’une fonction appelée web3.eth.Contract pour faciliter la création de contrats.

Lorsqu’un contrat est créé, son constructeur (une fonction déclarée avec la fonction le mot-clé constructor) est exécutée une fois.

Un constructeur est facultatif. Un seul constructeur est autorisé, ce qui signifie que la surcharge n’est pas supportée.

Après l’exécution du constructeur, le code final du contrat est stocké sur la blockchain. Ce code comprend toutes les fonctions publiques et externes ainsi que toutes les fonctions qui sont accessibles à partir de là par des appels de fonction. Le code déployé n’inclut pas le code du constructeur ou les fonctions internes appelées uniquement depuis le constructeur.

En interne, les arguments des constructeurs sont passés ABI encodé 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 souhaite 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 qui est défini ci-dessous.
    // Il est possible d'y faire référence tant qu'il n'est pas utilisé
    // pour créer un nouveau contrat.
    TokenCreator creator;
    address owner;
    bytes32 name;

    // Il s'agit du constructeur qui enregistre le
    // créateur et le nom attribué.
    constructor(bytes32 _name) {
        // Les variables d'état sont accessibles via leur nom
        // et non pas via, par exemple, `this.owner`. Les fonctions peuvent
        // être accédées directement ou via `this.f`,
        // mais ce dernier fournit une vue externe
        // à la fonction. En particulier dans le constructeur,
        // vous ne devriez pas accéder aux fonctions de manière externe,
        // car la fonction n'existe pas encore.
        // Voir la section suivante pour plus de détails.
        owner = msg.sender;

        // Nous effectuons une conversion de type explicite de `address`
        // vers `TokenCreator` et nous supposons que le type de
        // contrat appelant est `TokenCreator`, mais il n'existe
        // aucun moyen réel de le vérifier.
        // Cette opération ne crée pas de nouveau contrat.
        creator = TokenCreator(msg.sender);
        name = _name;
    }

    function changeName(bytes32 newName) public {
        // Seul le créateur peut modifier le nom.
        // Nous comparons le contrat en fonction de son
        // adresse qui peut être récupérée par
        // conversion explicite en adresse.
        if (msg.sender == address(creator))
            name = newName;
    }

    function transfer(address newOwner) public {
        // Seul le propriétaire actuel peut transférer le jeton.
        if (msg.sender != owner) return;

        // Nous demandons au contrat de création si le transfert
        // doit avoir lieu en utilisant une fonction du
        // contrat `TokenCreator` défini ci-dessous. Si
        // l'appel échoue (par exemple à cause d'une panne sèche),
        // l'exécution échoue également ici.
        if (creator.isTokenTransferOK(owner, newOwner))
            owner = newOwner;
    }
}


contract TokenCreator {
    function createToken(bytes32 name)
        public
        returns (OwnedToken tokenAddress)
    {
        // Crée un nouveau contrat `Token` et retourne son adresse.
        // Du côté de JavaScript, le type de retour
        // de cette fonction est `address`, puisque 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 `address`.
        tokenAddress.changeName(name);
    }

    // Effectuer des vérifications pour déterminer si le transfert d'un jeton vers
    // le contrat `OwnedToken` doit être effectué.
    function isTokenTransferOK(address currentOwner, address newOwner)
        public
        pure
        returns (bool ok)
    {
        // Vérifier une condition arbitraire pour voir si le transfert doit avoir lieu.
        return keccak256(abi.encodePacked(currentOwner, newOwner))[0] == 0x7f;
    }
}

Visibilité et Getters

Solidity connaît deux types d’appels de fonction : internes qui ne créent pas d’appel EVM réel (également appelé « appel de message ») et les externes qui le font. Pour cette raison, il existe quatre types de visibilité 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 depuis d’autres contrats et via des transactions. Une fonction externe f ne peut pas être appelée en interne (c’est-à-dire que f() ne fonctionne pas, mais this.f() fonctionne).

public

Les fonctions publiques font partie de l’interface du contrat et peuvent être appelées soit en interne, soit 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 peuvent être accessibles qu’en interne (c’est à dire depuis le contrat en cours ou des contrats qui en dérivent), sans utiliser this. C’est le niveau de visibilité par défaut des variables d’état.

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 des contrats dérivés.

Note

Tout ce qui est à l’intérieur d’un contrat est visible pour tous les observateurs externes à la blockchain. Rendre quelque chose private empêche seulement les autres contrats de lire ou de modifier l’information, mais elle sera toujours visible pour le monde entier en dehors 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 de 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.setData() pour récupérer la valeur de data dans le stockage 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; }
}

// Cela ne compilera 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 au 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 publiques. Pour le contrat donné ci-dessous, le compilateur générera une fonction appelée data qui ne prend aucun arguments 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é comme une variable d’état. S’il est accédé en externe (c’est-à-dire avec this.), il est évalué comme une fonction.

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.0 <0.9.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 tableau, alors vous pouvez seulement récupérer les éléments uniques du tableau via la fonction getter générée. Ce mécanisme existe pour éviter des coûts de gaz élevés lors du retour d’un tableau entier. Vous pouvez utiliser pour spécifier l’élément individuel à retourner, par exemple myArray(0). Si vous voulez retourner un tableau entier en un seul appel, vous devez alors é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) public view returns (uint) {
        return myArray[i];
    }
    */

    // fonction qui retourne le tableau entier
    function getArray() public view 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;
}

Il génère une fonction de la forme suivante. Le mappage et les tableaux (à l’exception des tableaux d’octets) dans la structure sont omis parce qu’il n’y a pas de bonne façon de sélectionner les membres individuels de la structure ou de fournir une clé pour le mappage :

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 fonction

Les modificateurs peuvent être utilisés pour changer le comportement des fonctions de manière déclarative. Par exemple, vous pouvez utiliser un modificateur pour 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 remplacées par des contrats dérivés, mais uniquement s’ils sont marqués virtual. Pour plus de détails, veuillez consulter 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 définit uniquement un modificateur mais ne l'utilise pas.
    // mais ne l'utilise pas : il sera utilisé dans les contrats dérivés.
    // Le corps de la fonction est inséré là où apparaît le symbole spécial
    // `_;` dans la définition d'un modificateur.
    // Cela signifie que si le propriétaire appelle cette fonction,
    // la fonction est exécutée et sinon, une exception est
    // levée.
    modifier onlyOwner {
        require(
            msg.sender == owner,
            "Seul le propriétaire peut appeler cette fonction."
        );
        _;
    }
}

contract destructible is owned {
    // Ce contrat hérite du modificateur `onlyOwner` de la fonction
    // `owned` et l'applique à la fonction `destroy`, qui
    // fait que les appels à `destroy` n'ont d'effet que si
    // ils sont effectués par le propriétaire stocké.
    function destroy() public onlyOwner {
        selfdestruct(owner);
    }
}

contract priced {
    // Les modificateurs peuvent recevoir des arguments :
    modifier costs(uint price) {
        if (msg.value >= price) {
            _;
        }
    }
}

contract Register is priced, destructible {
    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
    // rejetera automatiquement tout l'Ether qui lui sera envoyé.
    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 réentrants provenant de `msg.sender.call` ne peuvent pas appeler `f` à nouveau.
    /// L'instruction `return 7` attribue 7 à la valeur de retour mais
    /// exécute l'instruction `locked = false` dans le modificateur.
    function f() public noReentrancy returns (uint) {
        (bool success,) = msg.sender.call("");
        require(success);
        return 7;
    }
}

Si vous voulez accéder à un modificateur m défini dans un contrat C, vous pouvez utiliser C.m pour le le référencer sans recherche virtuelle. Il est seulement possible d’utiliser les modificateurs définis dans le contrat actuel ou ses contrats de base. Les modificateurs peuvent aussi être définis dans des bibliothèques, mais leur utilisation est limitée aux fonctions de la même bibliothèque.

Plusieurs modificateurs sont appliqués à une fonction en les spécifiant dans une séparée par des espaces et sont évaluées dans l’ordre présenté.

Les modificateurs ne peuvent pas accéder ou modifier implicitement les arguments et les valeurs de retour des fonctions qu’ils modifient. Leurs valeurs ne peuvent leur être transmises que de manière explicite au moment de l’invocation.

Les retours explicites d’un modificateur ou d’un corps de fonction ne quittent que le modificateur ou du corps de la fonction actuelle. Les variables de retour sont assignées et le flux de contrôle continue après le _ du modificateur précédent.

Avertissement

Dans une version antérieure de Solidity, les instructions return dans les fonctions ayant des modificateurs se comportaient différemment.

Un retour explicite d’un modificateur avec return; n’affecte pas les valeurs retournées par la fonction. Le modificateur peut toutefois choisir de ne pas exécuter du tout le corps de la fonction et, dans ce cas, les variables return sont placées à leur valeur par défaut comme si la fonction avait un corps vide.

Le symbole _ peut apparaître plusieurs fois dans le modificateur. Chaque occurrence est remplacée par le corps de la fonction.

Les expressions arbitraires sont autorisées pour les arguments du modificateur et dans ce contexte, tous les symboles visibles de la fonction sont visibles dans le modificateur. Les symboles introduits dans le modificateur ne sont pas visibles dans la fonction (car ils pourraient être modifiés par la surcharge).

Variables d’état constantes et immuables

Les variables d’état peuvent être déclarées comme constant ou immutable. Dans les deux cas, les variables ne peuvent pas être modifiées après la construction du contrat. Pour les variables constant, la valeur doit être fixée à la compilation, alors que pour les variables immutables, elle peut encore être assignée au moment de la construction.

Il est également possible de définir des variables constant au niveau du fichier.

Le compilateur ne réserve pas d’emplacement pour ces variables, et chaque occurrence est remplacée par la valeur correspondante.

Comparé aux variables d’état régulières, les coûts de gaz des variables constantes et immuables sont beaucoup plus faibles. Pour une variable constante, l’expression qui lui est assignée est copiée à tous les endroits où elle est accédée et est également réévaluée à chaque fois. Cela permet des optimisations locales. Les variables immuables sont évaluées une seule fois au moment de la construction et leur valeur est copiée à tous les endroits du code où elles sont accédées. Pour ces valeurs, 32 octets sont réservés, même si elles pourraient tenir dans moins d’octets. Pour cette raison, les valeurs constantes peuvent parfois être moins chères que les valeurs immuables.

Tous les types de constantes et d’immuables ne sont pas encore implémentés. Les seuls types supportés sont strings (uniquement pour les constantes) et value types.

// 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;
        // Les affectations aux immuables peuvent 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 constant, la valeur doit être une constante au moment de la compilation et elle doit être assignée à l’endroit où la variable est déclarée. Toute expression qui accède au stockage, aux données de la blockchain (par exemple, block.timestamp, address(this).balance ou block.number) ou aux données d’exécution (msg.value ou gasleft()) ou fait des appels à des contrats externes est interdit. Les expressions qui pourraient avoir un effet secondaire sur l’allocation de mémoire sont autorisées, mais celles qui pourraient 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, à l’exception de keccak256, ils appellent des contrats externes).

La raison pour laquelle les effets secondaires sur l’allocateur de mémoire sont autorisés est qu’il devrait être possible de construire des objets complexes comme par exemple des tables de consultation. Cette fonctionnalité n’est pas encore totalement utilisable.

Immutable

Les variables déclarées comme immutables sont un peu moins restreintes que celles déclarées comme constant : Les variables immuables peuvent se voir attribuer une valeur arbitraire dans le constructeur du contrat ou au moment de leur déclaration. Elles ne peuvent être assignées qu’une seule fois et peuvent, à partir de ce moment, être lues même pendant la construction.

Le code de création du contrat généré par le compilateur modifiera le code d’exécution du contrat avant qu’il ne soit retourné en remplaçant toutes les références aux immutables par les valeurs qui leur sont attribuées. Ceci est important si vous comparez le code d’exécution généré par le compilateur avec celui réellement stocké dans la blockchain.

Note

Les immutables qui sont affectés lors de leur déclaration ne sont considérés comme initialisés que lorsque le constructeur du contrat s’exécute. Cela signifie que vous ne pouvez pas initialiser les immutables en ligne avec une valeur qui dépend d’un autre immuable. Vous pouvez cependant le faire à l’intérieur du constructeur du contrat.

Il s’agit d’une protection contre les différentes interprétations concernant l’ordre de l’initialisation des variables d’état et de l’exécution du constructeur, en particulier en ce qui concerne l’héritage.

Fonctions

Les fonctions peuvent être définies à l’intérieur et à l’extérieur des contrats.

Les fonctions hors contrat, aussi appelées « fonctions libres », ont toujours une valeur implicite internal. visibilité implicite. Leur code est inclus dans tous les contrats qui les appellent, comme pour les fonctions internes des bibliothèques.

// 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 {
        // Cela appelle la fonction free en interne.
        // Le compilateur ajoutera son code au contrat.
        uint s = sum(_arr);
        require(s >= 10);
        found = true;
    }
}

Note

Les fonctions définies en dehors d’un contrat sont toujours exécutées dans le contexte d’un contrat. Elles ont toujours accès à la variable this, peuvent appeler d’autres contrats, leur envoyer de l’Ether et détruire le contrat qui les a appelées, entre autres choses. La principale différence avec les fonctions définies à l’intérieur d’un contrat est que les fonctions libres n’ont pas d’accès direct aux variables de stockage et aux fonctions qui ne sont pas dans leur portée.

Paramètres des fonctions et variables de retour

Les fonctions prennent des paramètres typés en entrée et peuvent, contrairement à beaucoup d’autres langages, renvoyer un nombre arbitraire de valeurs en sortie.

Paramètres des fonctions

Les paramètres de fonction sont déclarés de la même manière que les variables, et le nom des paramètres non utilisés peuvent être omis.

Par exemple, si vous voulez que votre contrat accepte un type d’appel externe avec deux entiers, vous utiliserez quelque chose comme ce qui suit :

// 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;
    }
}

Les paramètres de fonction peuvent être utilisés comme n’importe quelle autre variable locale et ils peuvent également être affectés.

Note

Une fonction externe ne peut pas accepter un tableau multidimensionnel comme paramètre d’entrée. Cette fonctionnalité est possible si vous activez le codeur ABI v2 en ajoutant pragma abicoder v2; à votre fichier source.

Une fonction interne peut accepter un tableau multidimensionnel sans activer la fonction.

Variables de retour

Les variables de retour de fonction sont déclarées avec la même syntaxe après le mot-clé returns.

Par exemple, supposons que vous vouliez renvoyer deux résultats : la somme et le produit de deux entiers passés comme paramètres de la fonction, vous utiliserez quelque chose comme :

// 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;
    }
}

Les noms des variables de retour peuvent être omis. Les variables de retour peuvent être utilisées comme toute autre variable locales et sont initialisées avec leur valeur par défaut et ont cette valeur jusqu’à ce qu’elles soient (ré)assignées.

Vous pouvez soit assigner explicitement aux variables de retour et ensuite laisser la fonction comme ci-dessus, ou vous pouvez fournir des valeurs de retour (soit une seule, soit multiple ones) directement avec l’instruction return.

// 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);
    }
}

Si vous utilisez un return précoce pour quitter une fonction qui a des variables de retour, vous devez fournir des valeurs de retour avec l’instruction return.

Note

Vous ne pouvez pas retourner certains types à partir de fonctions non internes, notamment les tableaux dynamiques multidimensionnels et les structs. Si vous activez le ABI coder v2 en ajoutant pragma abicoder v2; à votre fichier source, alors plus de types sont disponibles, mais les types mapping sont toujours limités à l’intérieur d’un seul contrat et vous ne pouvez pas les transférer.

Renvoi de valeurs multiples

Lorsqu’une fonction possède plusieurs types de retour, l’instruction return (v0, v1, ..., vn) peut être utilisée pour retourner plusieurs valeurs. Le nombre de composants doit être le même que le nombre de variables de retour et leurs types doivent correspondre, éventuellement après une conversion implicite.

Mutabilité de l’État

Voir les fonctions

Les fonctions peuvent être déclarées vues, auquel cas elles promettent de ne pas modifier l’état.

Note

Si la cible EVM du compilateur est Byzantium ou plus récente (par défaut), l’opcode STATICCALL est utilisé lorsque les fonctions view sont appelées, ce qui impose à l’état de rester non modifié dans le cadre de l’exécution de l’EVM. Pour les fonctions de bibliothèque view, DELEGATECALL est utilisé, car il n’existe pas de combinaison de DELEGATECALL et de STATICCALL. Cela signifie que les fonctions de la bibliothèque view n’ont pas de contrôles d’exécution qui empêchent les états. Ceci ne devrait pas avoir d’impact négatif sur la sécurité car le code de la bibliothèque est généralement connu au moment de la compilation et le vérificateur statique effectue des vérifications au moment de la compilation.

Les instructions suivantes sont considérées comme modifiant l’état :

  1. Écriture dans les variables d’état

  2. Émettre des événements

  3. Créer d’autres contrats

  4. Utiliser selfdestruct

  5. Envoyer de l’Ether via des appels

  6. Appeler une fonction qui n’est pas marquée view ou pure

  7. Utiliser des appels de bas niveau

  8. Utilisation d’un assemblage en ligne contenant 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 des modifications d’état dans les fonctions view par l’utilisation de conversions de types explicites invalides. En utilisant STATICCALL pour les fonctions view, les modifications de l’état sont empêchées au niveau de l’EVM.

Fonctions pures

Les fonctions peuvent être déclarées pure, auquel cas elles promettent de ne pas lire ou modifier l’état. En particulier, il devrait être possible d’évaluer une fonction pure à la compilation seulement ses entrées et msg.data, mais sans aucune connaissance de l’état actuel de la blockchain. Cela signifie que la lecture de variables immutable peut être une opération non pure.

Note

Si la cible EVM du compilateur est Byzantium ou plus récente (par défaut), l’opcode STATICCALL est utilisé, 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 instructions modifiant l’état expliquée ci-dessus, les suivantes sont considérées comme lisant l’état :

  1. Lecture des variables d’état

  2. Accès à adresse(this).balance ou <adresse>.balance

  3. Accéder à l’un des membres de block, tx, msg (à l’exception de msg.sig et msg.data)

  4. L’appel de toute fonction non marquée pure

  5. L’utilisation d’un assemblage en ligne 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);
    }
}

Les fonctions pures sont en mesure d’utiliser les fonctions revert() et require() pour revenir sur des changements d’état potentiels lorsqu’une erreur se produit.

Revenir en arrière sur un changement d’état n’est pas considéré comme une « modification d’état », car seuls les changements d’état effectuées précédemment dans du code qui n’avait pas la restriction view ou pure sont inversées et ce code a la possibilité d’attraper le revert et de ne pas le transmettre.

Ce comportement est également en accord avec l’opcode STATICCALL.

Avertissement

Il est impossible 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 imposé au niveau de l’EVM, pure ne peut pas).

Note

Avant la version 0.5.0, le compilateur n’utilisait pas l’opcode STATICCALL pour les programmes pure. Ceci permettait des modifications d’état dans les fonctions pures par l’utilisation de conversions de types explicites invalides. En utilisant STATICCALL pour les fonctions pures, les modifications de l’état sont empêchées au niveau de l’EVM.

Note

Avant la version 0.4.17, le compilateur n’imposait pas que pure ne lise pas l’état. C’est un contrôle de type à la compilation, qui peut être contourné en faisant des conversions explicites invalides entre les types de contrat, car le compilateur peut vérifier que le type du contrat ne fait pas d’opérations de changement d’état, mais il ne peut pas vérifier que le contrat qui sera appelé au moment de l’exécution est effectivement de ce type.

Fonctions spéciales

Fonction de réception d’Ether

Un contrat peut avoir au maximum une fonction receive, déclarée à l’aide des éléments suivants receive() external payable { ... } (sans le mot-clé function). Cette fonction ne peut pas avoir d’arguments, ne peut rien retourner et doit avoir une une visibilité external et une mutabilité de l’état payable. Elle peut être virtuelle, peut être surchargée et peut avoir des modificateurs.

La fonction de réception est exécutée lors d’un appel au contrat avec des données d’appel vides. C’est la fonction qui est exécutée lors des transferts d’Ether (par exemple via .send() ou .transfer()). Si cette fonction n’existe pas, mais qu’une fonction payable de repli existe, la fonction de repli sera appelée lors d’un transfert d’Ether simple. Si aucune fonction de réception d’Ether ni aucune fonction de repli payable n’est présente, le contrat ne peut pas recevoir d’Ether par le biais de transactions normales et lance une exception.

Dans le pire des cas, la fonction receive ne peut compter que sur le fait que 2300 gaz soient disponible (par exemple lorsque send ou transfer est utilisé), ce qui laisse peu de place pour effectuer d’autres opérations que la journalisation de base. Les opérations suivantes consommeront plus de gaz que l’allocation de 2300 gaz :

  • Écriture dans le stockage

  • Création d’un contrat

  • Appeler une fonction externe qui consomme une grande quantité de gaz

  • Envoi d’éther

Avertissement

Les contrats qui reçoivent de l’Ether directement (sans appel de fonction, c’est-à-dire en utilisant send ou transfer) mais qui ne définissent pas de fonction de réception d’Ether ou de fonction de repli payable, lancer une exception en renvoyant l’Ether (ceci était différent avant Solidity v0.4.0). Donc si vous voulez que votre contrat reçoive de l’Ether, vous devez implémenter une fonction de réception d’Ether (l’utilisation de fonctions de repli payantes pour recevoir de l’éther n’est pas recommandée, car elle n’échouerait pas en cas de confusion d’interface).

Avertissement

Un contrat sans fonction de réception d’Ether peut recevoir de l’Ether en tant que destinataire d’une transaction coinbase (c.à.d récompense de bloc miner) ou en tant que destination d’une selfdestruct.

Un contrat ne peut pas réagir à de tels transferts d’Ether et ne peut donc pas les rejeter. Il s’agit d’un choix de conception de l’EVM, Solidity ne peut pas le contourner.

Cela signifie également que address(this).balance peut être plus élevé que la somme d’une comptabilité manuelle implémentée dans un contrat (par exemple, en ayant un compteur mis à jour dans la fonction de réception d’Ether).

Ci-dessous vous pouvez voir un exemple d’un contrat Sink qui utilise la fonction receive.

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.6.0 <0.9.0;

// Ce contrat garde tout l'Ether qui lui est envoyé sans aucun moyen
// de le récupérer.
contract Sink {
    event Received(address, uint);
    receive() external payable {
        emit Received(msg.sender, msg.value);
    }
}

Fonction de repli

Un contrat peut avoir au maximum une fonction fallback, déclarée en utilisant soit fallback () external [payable], soit fallback (bytes calldata _input) external [payable] returns (bytes memory _output) (dans les deux cas sans le mot-clé function). Cette fonction doit avoir une visibilité external. Une fonction de repli peut être virtuelle, peut remplacer et peut avoir des modificateurs.

La fonction de repli est exécutée lors d’un appel au contrat si aucune des autres fonction ne correspond à la signature de la fonction donnée, ou si aucune donnée n’est fournie et qu’il n’existe pas de fonction de réception d’éther. La fonction de repli reçoit toujours des données, mais pour recevoir également de l’Ether elle doit être marquée payable.

Si la version avec paramètres est utilisée, _input contiendra les données complètes envoyées au contrat (égal à msg.data) et peut retourner des données dans _output. Les données retournées ne seront pas codées par l’ABI. Au lieu de cela, elles seront retournées sans modifications (même pas de remplissage).

Dans le pire des cas, si une fonction de repli payable est également utilisée à la place d’une fonction de réception, elle ne peut compter que sur le gaz 2300 disponible (voir fonction de réception d’éther pour une brève description des implications de ceci).

Comme toute fonction, la fonction de repli peut exécuter des opérations complexes tant qu’il y a suffisamment de gaz qui lui est transmis.

Avertissement

Une fonction de repli payable est également exécutée pour les transferts d’Ether simples, si aucune fonction de réception d’Ether n’est présente. Il est recommandé de toujours définir une fonction de réception Ether de réception, si vous définissez une fonction de repli payable afin de distinguer les transferts Ether des confusions d’interface.

Note

Si vous voulez décoder les données d’entrée, vous pouvez vérifier les quatre premiers octets pour le sélecteur de fonction et ensuite vous pouvez utiliser abi.decode avec la syntaxe array slice pour décoder les données codées par ABI : (c, d) = abi.decode(_input[4 :], (uint256, uint256)); Notez que cette méthode ne doit être utilisée qu’en dernier recours, et que les fonctions appropriées doivent être utilisées à la place.

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.6.2 <0.9.0;

contract Test {
    uint x;
    // Cette fonction est appelée pour tous les messages envoyés à
    // ce contrat (il n'y a pas d'autre fonction).
    // L'envoi d'Ether à ce contrat provoquera une exception,
    // car la fonction de repli n'a pas le modificateur `payable`.
    fallback() external { x = 1; }
}

contract TestPayable {
    uint x;
    uint y;
    // Cette fonction est appelée pour tous les messages envoyés à
    // ce contrat, sauf les transferts Ether simples
    // (il n'y a pas d'autre fonction que la fonction de réception).
    // Tout appel à ce contrat avec des calldata non vides exécutera
    // la fonction de repli (même si Ether est envoyé avec l'appel).
    fallback() external payable { x = 1; y = msg.value; }

    // Cette fonction est appelée pour les transferts Ether simples, c'est à dire
    // pour chaque appel avec des données d'appel vides.
    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);
        // il en résulte que test.x devient == 1.

        // address(test) ne permettra pas d'appeler directement ``send``, puisque ``test`` n'a pas de payable
        // fonction de repli.
        // Il doit être converti en adresse payable pour pouvoir appeler ``send``.
        address payable testPayable = payable(address(test));

        // Si quelqu'un envoie de l'Ether à ce contrat,
        // le transfert échouera, c'est à dire que cela renvoie false ici.
        return testPayable.send(2 ether);
    }

    function callTestPayable(TestPayable test) public returns (bool) {
        (bool success,) = address(test).call(abi.encodeWithSignature("nonExistingFunction()"));
        require(success);
        // le résultat est que test.x devient == 1 et test.y devient 0.
        (success,) = address(test).call{value: 1}(abi.encodeWithSignature("nonExistingFunction()"));
        require(success);
        // le résultat est que test.x devient == 1 et test.y devient 1.

        // Si quelqu'un envoie de l'Ether à ce contrat, la fonction de réception de TestPayable sera appelée.
        // Comme cette fonction écrit dans le stockage, elle prend plus d'éther que ce qui est disponible avec un
        // simple ``send`` ou ``transfer``. Pour cette raison, nous devons utiliser un appel de bas niveau.
        (success,) = address(test).call{value: 2 ether}("");
        require(success);
        // le résultat est que test.x devient == 2 et test.y devient 2 ether.

        return true;
    }
}

Surcharge des 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 la portée 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 (uint out) {
        if (_really)
            out = _in;
    }
}

Les 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 leurs types Solidity mais pas par leurs types externes.

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.16 <0.9.0;

// This will not compile
contract A {
    function f(B _in) public pure returns (B out) {
        out = _in;
    }

    function f(address _in) public pure returns (address out) {
        out = _in;
    }
}

contract B {
}

Les deux surcharges de fonction f ci-dessus finissent par accepter le type d’adresse pour l’ABI bien qu’ils ils sont considérés comme différents dans Solidity.

Résolution des surcharges et correspondance des arguments

Les fonctions surchargées sont sélectionnées en faisant correspondre les déclarations de fonction dans la portée actuelle aux arguments fournis dans l’appel de fonction. Les fonctions sont sélectionnées comme candidates à la surcharge si tous les arguments peuvent être implicitement convertis dans les types attendus. S’il n’y a pas exactement un candidat, la résolution échoue.

Note

Les paramètres de retour ne sont pas pris en compte pour 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;
    }
}

Appeler f(50) créerait une erreur de type puisque 50 peut être implicitement converti à la fois en types uint8 et uint256. D’un autre côté, f(256) se résoudrait en une surcharge f(uint256) puisque 256 ne peut pas être implicitement converti en uint8.

Événements

Les événements Solidity offrent une abstraction au-dessus de la fonctionnalité de journalisation de l’EVM. Les applications peuvent s’abonner 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 dans le journal de la transaction, une structure de données spéciale dans la blockchain. Ces journaux sont associés à l’adresse du contrat, sont incorporés dans la blockchain, et y restent aussi longtemps qu’un bloc est accessible (pour toujours à partir de maintenant, mais cela pourrait changer avec Serenity). Le journal et ses données d’événement ne sont pas accessibles à partir des contrats (même pas depuis le contrat qui les a créés).

Il est possible de demander une preuve Merkle pour les journaux. Si une entité externe fournit une telle preuve à un contrat, celui-ci peut vérifier que le journal existe réellement dans la blockchain. Vous devez fournir des en-têtes de bloc car le contrat ne peut voir que les 256 derniers hachages de blocs.

Vous pouvez ajouter l’attribut indexed à un maximum de trois paramètres qui les ajoutent à une structure de données spéciale appelée « topics » au lieu de la partie données du journal. Un topic ne peut contenir qu’un seul mot (32 octets), donc si vous utilisez un type de référence pour un argument indexé, le hachage Keccak-256 de la valeur est stocké comme un sujet à la place.

Tous les paramètres sans l’attribut indexed sont ABI-encodés dans la partie données du journal.

Les sujets vous permettent de rechercher des événements, par exemple en filtrant une séquence de blocs pour certains événements. Vous pouvez également filtrer les événements en fonction de l’adresse du contrat qui a émis l’événement.

Par exemple, le code ci-dessous utilise le contrat web3.js subscribe("logs"). La méthode pour filtrer les journaux 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 hachage de la signature de l’événement est l’un des sujets, sauf si vous avez déclaré l’événement avec le spécificateur anonymous. Cela signifie qu’il n’est pas possible de filtrer les événements anonymes spécifiques par nom, vous pouvez seulement filtrer par l’adresse du contrat. L’avantage des événements anonymes est qu’ils sont moins chers à déployer et à appeler. Ils vous permettent également de déclarer quatre arguments indexés au lieu de trois.

Note

Comme le journal des transactions ne stocke que les données de l’événement et non le type, vous devez connaître le type de l’événement, y compris le paramètre qui est indexé et si l’événement est anonyme afin d’interpréter correctement les données. En particulier, il est possible de « falsifier » la signature d’un autre événement en utilisant un événement anonyme.

// 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 en utilisant `emit`, suivi par
        // le nom de l'événement et les arguments
        // (le cas échéant) entre parenthèses. Toute invocation de ce type
        // (même profondément imbriquée) peut être détectée à partir de
        // l'API JavaScript en filtrant pour `Deposit`.
        emit Deposit(msg.sender, _id, msg.value);
    }
}

L’utilisation dans l’API JavaScript est la suivante :

var abi = /* abi tel que généré par le compilateur */;
var ClientReceipt = web3.eth.contract(abi);
var clientReceipt = ClientReceipt.at("0x1234...ab67" /* address */);

var depositEvent = clientReceipt.Deposit();

// surveiller les changements
depositEvent.watch(function(error, result){
    // le résultat contient des arguments non indexés et des sujets
    // donnés à l'appel `Deposit`.
    if (!error)
        console.log(result);
});


// Ou passez un callback pour commencer à regarder immédiatement.
var depositEvent = clientReceipt.Deposit(function(error, result) {
    if (!error)
        console.log(result);
});

Le résultat de l’opération ci-dessus ressemble à ce qui suit (découpé) :

{
   "returnValues": {
       "_from": "0x1111…FFFFCCCC",
       "_id": "0x50…sd5adb20",
       "_value": "0x420042"
   },
   "raw": {
       "data": "0x7f…91385",
       "topics": ["0xfd4…b4ead7", "0x7f…1a91385"]
   }
}

Ressources supplémentaires pour comprendre les événements

Les erreurs et la déclaration de retour en arrière

Les erreurs dans Solidity fournissent un moyen pratique et efficace d’expliquer à l’utilisateur pourquoi une opération a échoué. Elles peuvent être définies à l’intérieur et à l’extérieur des contrats (y compris les interfaces et les bibliothèques).

Elles doivent être utilisées conjointement avec l’instruction revert qui provoque toutes les modifications de l’appel en cours et renvoie les données d’erreur à l’appelant.

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.4;

/// Solde insuffisant pour le transfert. Nécessaire `required` mais seulement
/// `available` disponible.
/// @param available disponible disponible.
/// @param required montant demandé pour le transfert.
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;
    }
    // ...
}

Les erreurs ne peuvent pas être surchargées ou remplacées mais sont héritées. La même erreur peut être définie à plusieurs endroits, à condition que les champs d’application soient distincts. Les instances d’erreurs ne peuvent être créées qu’en utilisant les instructions revert.

L’erreur crée des données qui sont ensuite transmises à l’appelant avec l’opération revert, afin de retourner au composant hors chaîne ou de l’attraper dans une instruction try/catch. Notez qu’une erreur ne peut être attrapée que si elle provient d’un appel externe, les retours se produisant dans des appels internes ou à l’intérieur de la même fonction ne peuvent pas être attrapés.

Si vous ne fournissez pas de paramètres, l’erreur ne nécessite que quatre octets de données et vous pouvez utiliser NatSpec comme ci-dessus pour expliquer plus en détail les raisons de l’erreur, qui ne sont pas stockées dans la chaîne. Cela en fait une fonctionnalité de signalement d’erreur très bon marché et pratique à la fois.

Plus précisément, une instance d’erreur est codée par ABI de la même manière que un appel à une fonction du même nom et du même type le serait et est ensuite utilisé comme données de retour dans l’opcode revert. Cela signifie que les données consistent en un sélecteur de 4 octets suivi de données ABI-encodées. Le sélecteur est constitué des quatre premiers octets du keccak256-hash de la signature du type d’erreur.

Note

Il est possible qu’un contrat soit révoqué avec des erreurs différentes du même nom ou même avec des erreurs définies à des endroits différents qui sont indiscernables par l’appelant. Pour l’extérieur, c’est-à-dire l’ABI, seul le nom de l’erreur est pertinent, pas le contrat ou le fichier où elle est définie.

L’instruction require(condition, "description"); serait équivalente à if (!condition) revert Error("description") si vous pouviez définir error Error(string). Notez cependant que Error est un type intégré et ne peut être défini dans un code fourni par l’utilisateur.

De même, un échec de assert ou des conditions similaires se retourneront avec une erreur du type intégré Panic(uint256).

Note

Les données d’erreur ne doivent être utilisées que pour donner une indication de l’échec, mais pas comme un moyen pour le flux de contrôle. La raison en est que les données de retour des appels internes sont propagées en retour dans la chaîne des appels externes par défaut. Cela signifie qu’un appel interne peut « forger » des données de retour qui semblent pouvoir provenir du contrat qui l’a appelé.

Héritage

Solidity prend en charge l’héritage multiple, y compris le polymorphisme.

Le polymorphisme signifie qu’un appel de fonction (interne et externe) exécute toujours la fonction du même nom (et des types de paramètres) dans le contrat le plus dérivé de la hiérarchie d’héritage. Ceci doit être explicitement activé sur chaque fonction de la hiérarchie en utilisant les mots-clés virtual et override. Voir Remplacement de fonctions pour plus de détails.

Il est possible d’appeler des fonctions plus haut dans la hiérarchie d’héritage en interne, en spécifiant explicitement le contrat en utilisant ContractName.functionName() ou en utilisant super.functionName() si vous souhaitez appeler la fonction à un niveau supérieur dans la hiérarchie d’héritage aplatie (voir ci-dessous).

Lorsqu’un contrat hérite d’autres contrats, un seul contrat unique est créé sur la blockchain, et le code de tous les contrats de base est compilé dans le contrat créé. Cela signifie que tous les appels internes aux fonctions des contrats de base utilisent également des appels de fonctions internes (super.f(..) utilisera JUMP et non un appel de message).

Le shadowing de variables d’état est considéré comme une erreur. Un contrat dérivé peut seulement déclarer une variable d’état x, s’il n’y a pas de variable d’état visible avec le même nom dans l’une de ses bases.

Le système d’héritage général est très similaire à celui de Python <https://docs.python.org/3/tutorial/classes.html#inheritance>`_, surtout en ce qui concerne l’héritage multiple, mais il y a aussi quelques différences.

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 `s` 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. Ceux-ci ne peuvent pas être
// accessibles en externe via `this`.
contract Destructible is Owned {
    // Le mot clé `virtual` signifie que la fonction peut modifier
    // son comportement dans les classes dérivées ("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 une 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 `Destructible`, mais il n'y a qu'une seule instance de `owned`.
// Pourtant, il n'existe 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/types d'entrées. Si la fonction de remplacement a différents
    // types de paramètres de sortie différents, cela entraîne une erreur.
    // Les appels de fonction locaux et par message tiennent compte de ces surcharges.
    // Si vous voulez que la fonction soit prioritaire, vous devez utiliser le
    // mot-clé `override`. Vous devez à nouveau spécifier le mot-clé `virtual`
    // si vous voulez que cette fonction soit à nouveau surchargée.
    function destroy() public virtual override {
        if (msg.sender == owner) {
            Config config = Config(0xD5f9D8D94886E70b06E474c3fB14Fd43E2f23970);
            NameReg(config.lookup(1)).unregister();
            // Il est toujours possible d'appeler une
            // fonction spécifique surchargée.
            Destructible.destroy();
        }
    }
}


// Si un constructeur prend un argument, il doit être
// fourni dans l'en-tête ou le modificateur-invocation-style à
// 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;
    }

    // Ici, nous ne spécifions que `override` et non `virtual`.
    // Cela signifie que les contrats dérivant de `PriceFeed`
    // ne peuvent plus modifier le comportement de `destroy`.
    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 « faire suivre » la demande de destruction. La manière dont cela est fait est problématique, comme le montre 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() fera appel à Base2.destroy parce que nous le spécifions explicitement dans la surcharge finale, mais cette fonction contournera Base1.destroy. Le moyen de contourner 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 l’un de ses contrats de base. Au contraire, elle appelle plutôt cette fonction sur le contrat de base suivant dans le d’héritage final, il appellera donc Base1.destroy() (notez que la séquence d’héritage finale est – en commençant par le contrat le plus contrat le plus dérivé : Final, Base2, Base1, Destructible, owned). La fonction réelle qui est appelée lors de l’utilisation de super 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 ordinaire de recherche de méthode virtuelle ordinaire.

Remplacement des fonctions

Les fonctions de base peuvent être surchargées par les contrats hérités pour changer leur comportement si elles sont marquées comme virtual. La fonction de remplacement doit alors utiliser le mot-clé override dans l’en-tête de la fonction. La fonction de remplacement ne peut que changer la visibilité de la fonction de remplacement de externe à public. La mutabilité peut être changée en une mutabilité plus stricte en suivant l’ordre : nonpayable peut être remplacé par view et pure. view peut être remplacé par pure. payable est une exception et ne peut pas être changé en une autre mutabilité.

L’exemple suivant démontre la modification de la mutabilité et de la visibilité :

// 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 {}
}

Pour l’héritage multiple, les contrats de base les plus dérivés qui définissent la même doivent être spécifiés explicitement après le mot-clé override. En d’autres termes, vous devez spécifier tous les contrats de base qui définissent la même fonction et qui n’ont pas encore été remplacés par un autre contrat de base (sur un chemin quelconque du graphe d’héritage). De plus, si un contrat hérite de la même fonction à partir de plusieurs bases (sans lien), il doit explicitement la remplacer :

// 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
{
    // Dérive de plusieurs bases définissant foo(), nous devons donc explicitement
    // le surcharger
    function foo() public override(Base1, Base2) {}
}

Un spécificateur de surcharge explicite n’est pas nécessaire si la fonction est définie dans un contrat de base commun ou s’il existe une fonction unique dans un contrat de base commun qui prévaut déjà sur toutes les autres fonctions.

// 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 {}
// Aucune surcharge explicite n'est requise
contract D is B, C {}

Plus formellement, il n’est pas nécessaire de surcharger une fonction (directement ou indirectement) héritée de bases multiples s’il existe un contrat de base qui fait partie de tous les chemins de surcharge pour la signature, et (1) cette base implémente la fonction et qu’aucun chemin depuis le contrat actuel vers la base ne mentionne une fonction avec cette signature ou (2) cette base n’implémente pas la fonction et il y a au plus une mention de la fonction dans tous les chemins allant du contrat actuel à cette base.

Dans ce sens, un chemin de surcharge pour une signature est un chemin à travers le graphe d’héritage qui commence au contrat considéré et se termine par un contrat mentionnant une fonction avec cette signature qui n’est pas surchargée.

Si vous n’indiquez pas qu’une fonction qui surcharge est virtual, les contrats dérivés ne peuvent plus modifier le comportement de cette fonction.

Note

Les fonctions ayant la visibilité private ne peuvent pas être virtual.

Note

Les fonctions sans implémentation doivent être marquées virtual en dehors des interfaces. Dans les interfaces, toutes les fonctions sont automatiquement considérées comme virtual.

Note

A partir de Solidity 0.8.8, le mot-clé override n’est pas nécessaire pour remplacer une fonction, au cas où la fonction est définie dans plusieurs bases.

Les variables d’état publiques peuvent remplacer les fonctions externes si les types de paramètres et de retour de la fonction correspondent à la fonction getter de la 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

Si les variables d’état publiques peuvent remplacer les fonctions externes, elles ne peuvent pas elles-mêmes être surchargées.

Remplacement d’un modificateur

Les modificateurs de fonction peuvent se substituer les uns aux autres. Cela fonctionne de la même manière que la superposition de fonctions (sauf qu’il n’y a pas de surcharge pour les modificateurs). Le mot-clé virtual doit être utilisé sur le modificateur surchargé et le mot-clé override doit être utilisé dans le modificateur de surcharge :

// 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 {_;}
}

En cas d’héritage multiple, tous les contrats de base directs doivent être spécifiés explicitement :

// 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 facultative déclarée avec le mot-clé constructor qui est exécutée lors de la création du contrat, et dans laquelle vous pouvez exécuter le code d’initialisation du contrat.

Avant que le code du constructeur ne soit exécuté, les variables d’état sont initialisées à leur valeur spécifiée si vous les initialisez en ligne, ou leur valeur par défaut si vous ne le faites pas.

Après l’exécution du constructeur, le code définitif du contrat est déployé sur la blockchain. Le déploiement du code coûte un gaz supplémentaire linéaire à la longueur du code. Ce code comprend toutes les fonctions qui font partie de l’interface publique et toutes les fonctions qui sont accessibles à partir de celle-ci par des appels de fonction. Il ne comprend pas le code du constructeur ni les fonctions internes qui ne sont appelées uniquement depuis le constructeur.

S’il n’y a pas de constructeur, le contrat prendra en charge le constructeur par défaut, 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() {}
}

Vous pouvez utiliser des paramètres internes dans un constructeur (par exemple des pointeurs de stockage). Dans ce cas, le contrat doit être marqué abstract, parce que ces paramètres ne peuvent pas se voir attribuer de valeurs valides de l’extérieur, mais uniquement par le biais des constructeurs des contrats dérivés.

Avertissement

Avant la version 0.4.22, les constructeurs étaient 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

Avant la version 0.7.0, vous deviez spécifier la visibilité des constructeurs comme étant soit internal ou public.

Arguments pour les constructeurs de base

Les constructeurs de tous les contrats de base seront appelés en suivant les règles de linéarisation expliquées ci-dessous. Si les constructeurs de base ont des arguments, les contrats dérivés doivent tous les spécifier. Ceci peut être fait de deux manières :

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;

contract Base {
    uint x;
    constructor(uint _x) { x = _x; }
}

// Soit spécifier directement dans la liste d'héritage...
contract Derived1 is Base(7) {
    constructor() {}
}

// ou par un "modificateur" du constructeur dérivé.
contract Derived2 is Base {
    constructor(uint _y) Base(_y * _y) {}
}

L’une des façons est directement dans la liste d’héritage (est Base(7)). L’autre est dans la façon dont un modificateur est invoqué dans le cadre du constructeur dérivé (Base(_y * _y)). La première façon est plus pratique si l’argument du constructeur est une constante et définit le comportement du contrat ou le le décrit. La deuxième façon doit être utilisée si les arguments du constructeur de la base dépendent de ceux du contrat dérivé. Les arguments doivent être donnés soit dans la liste d’héritage ou dans le style modificateur dans le constructeur dérivé. Spécifier les arguments aux deux endroits est une erreur.

Si un contrat dérivé ne spécifie pas les arguments de tous les constructeurs de ses contrats de base, il sera considéré comme un contrat abstrait.

Héritage multiple et linéarisation

Les langages qui autorisent l’héritage multiple doivent faire face à plusieurs problèmes. L’un d’entre eux est le problème du diamant. Solidity est similaire à Python en ce qu’il utilise la « C3 Linearization » pour forcer un ordre spécifique dans le graphe acyclique dirigé (DAG) des classes de base. Cette propriété souhaitable de la monotonicité, mais désapprouve certains graphes d’héritage. En particulier, l’ordre dans lequel dans lequel les classes de base sont données dans la directive s est important : Vous devez lister les contrats de base directs dans l’ordre de « le plus similaire à la base » à « le plus dérivé ». Notez que cet ordre est l’inverse de celui utilisé en Python.

Une autre façon simplifiée d’expliquer ceci est que lorsqu’une fonction est appelée qui est définie plusieurs fois dans différents contrats, les bases données sont recherchées de droite à gauche (de gauche à droite en Python) de manière approfondie, s’arrêtant à la première correspondance. Si un contrat de base a déjà été recherché, il est ignoré.

Dans le code suivant, Solidity donnera l’erreur suivante erreur « Linearization of inheritance graph impossible » (« Linéarisation du graphe d’héritage impossible »).

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.0 <0.9.0;

contract X {}
contract A is X {}
// Cela ne compilera pas
contract C is A, X {}

La raison en est que C demande à X de supplanter A (en spécifiant A, X dans cet ordre), mais A lui-même demande d’outrepasser X, ce qui est une contradiction qui ne peut être résolue.

En raison du fait que vous devez explicitement surcharger une fonction qui est héritée de plusieurs bases sans une surcharge unique, la linéarisation de C3 n’est pas trop importante en pratique.

Un domaine où la linéarisation de l’héritage est particulièrement importante et peut-être pas aussi claire est lorsqu’il y a plusieurs constructeurs dans la hiérarchie de l’héritage. Les constructeurs seront toujours exécutés dans l’ordre linéarisé, quel que soit l’ordre dans lequel leurs arguments sont fournis dans le constructeur du contrat hérité. Par exemple :

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;

contract Base1 {
    constructor() {}
}

contract Base2 {
    constructor() {}
}

// Les constructeurs sont exécutés dans l'ordre suivant :
//  1 - Base1
//  2 - Base2
//  3 - Derived1
contract Derived1 is Base1, Base2 {
    constructor() Base1() Base2() {}
}

// Les constructeurs sont exécutés dans l'ordre suivant :
//  1 - Base2
//  2 - Base1
//  3 - Derived2
contract Derived2 is Base2, Base1 {
    constructor() Base2() Base1() {}
}

// Les constructeurs sont toujours exécutés dans l'ordre suivant :
//  1 - Base2
//  2 - Base1
//  3 - Derived3
contract Derived3 is Base2, Base1 {
    constructor() Base1() Base2() {}
}

Hériter de différents types de membres portant le même nom

C’est une erreur lorsque l’une des paires suivantes dans un contrat porte le même nom en raison de l’héritage :
  • une fonction et un modificateur

  • une fonction et un événement

  • un événement et un modificateur

À titre d’exception, un getter de variable d’état peut remplacer une fonction externe.

Contrats abstraits

Les contrats doivent être marqués comme abstraits lorsqu’au moins une de leurs fonctions n’est pas implémentée. Les contrats peuvent être marqués comme abstraits même si toutes les fonctions sont implémentées.

Cela peut être fait en utilisant le mot-clé abstract comme le montre l’exemple suivant. Notez que ce contrat doit être défini comme abstrait, car la fonction utterance() a été définie, mais aucune implémentation n’a été fournie (aucun corps d’implémentation { } n’a été donné).

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.6.0 <0.9.0;

abstract contract Feline {
    function utterance() public virtual returns (bytes32);
}

Ces contrats abstraits ne peuvent pas être instanciés directement. Cela est également vrai si un contrat abstrait met en œuvre toutes les fonctions définies. L’utilisation d’un contrat abstrait comme classe de base est illustrée dans l’exemple suivant :

// 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"; }
}

Si un contrat hérite d’un contrat abstrait et qu’il n’implémente pas toutes les fonctions non implémentées en les surchargeant, il doit également être marqué comme abstrait.

Notez qu’une fonction sans implémentation est différente d’une Fonction Type, même si leur syntaxe est très similaire.

Exemple de fonction sans implémentation (une déclaration de fonction) :

function foo(address) external returns (address);

Exemple de déclaration d’une variable dont le type est un type de fonction :

function(address) external returns (address) foo;

Les contrats abstraits découplent la définition d’un contrat de son implémentation fournissant une meilleure extensibilité et auto-documentation et facilitant les modèles comme la méthode Template et supprimant la duplication du code. Les contrats abstraits sont utiles de la même façon que définir des méthodes dans une interface est utile. C’est un moyen pour le concepteur du contrat abstrait de dire « tout enfant de moi doit implémenter cette méthode ».

Note

Les contrats abstraits ne peuvent pas remplacer une fonction virtuelle implémentée par une fonction virtuelle non implémentée.

Interfaces

Les interfaces sont similaires aux contrats abstraits, mais aucune fonction ne peut y être implémentée. Il existe d’autres restrictions :

  • Elles ne peuvent pas hériter d’autres contrats, mais elles peuvent hériter d’autres interfaces.

  • Toutes les fonctions déclarées doivent être externes.

  • Elles ne peuvent pas déclarer de constructeur.

  • Elles ne peuvent pas déclarer de variables d’état.

  • Elles ne peuvent pas déclarer de modificateurs.

Certaines de ces restrictions peuvent être levées à l’avenir.

Les interfaces sont fondamentalement limitées à ce que l’ABI du contrat peut représenter, et la conversion entre l’ABI et une interface devrait être possible sans aucune perte d’information.

Les interfaces sont désignées par leur propre mot-clé :

// 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;
}

Les contrats peuvent hériter d’interfaces comme ils le feraient pour d’autres contrats.

Toutes les fonctions déclarées dans les interfaces sont implicitement virtual, les fonctions qui les surchargent n’ont pas besoin du mot-clé override. Cela ne signifie pas automatiquement qu’une fonction surchargée peut être à nouveau surchargée. Cela n’est possible que si la fonction qui la surcharge est marquée virtual.

Les interfaces peuvent hériter d’autres interfaces. Les règles sont les mêmes que pour l’héritage normal.

// 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 {
    // Doit redéfinir test afin d'affirmer que les parents
    // sont compatibles.
    function test() external override(ParentA, ParentB) returns (uint256);
}

Les types définis dans les interfaces et autres structures de type contrat sont accessibles à partir d’autres contrats : Token.TokenType ou Token.Coin.

Avertissement

Les interfaces supportent les types enum depuis Solidity version 0.5.0, soyez sûr que le pragma version spécifie cette version au minimum.

Bibliothèques

Les bibliothèques sont similaires aux contrats, mais leur but est d’être déployées une seule fois à une adresse spécifique et leur code est réutilisé en utilisant le DELEGATECALL (CALLCODE jusqu’à Homestead) de l’EVM. Cela signifie que si des fonctions de bibliothèque sont appelées, leur code est exécuté dans le contexte du contrat d’appel, c’est-à-dire que this pointe vers le contrat appelant, et surtout le stockage du contrat appelant est accessible. Comme une bibliothèque est un morceau de code source isolé, elle ne peut accéder aux variables d’état du contrat d’appel que si elles sont explicitement fournies (elle n’aurait aucun moyen de les nommer, sinon). Les fonctions des bibliothèques ne peuvent être appelées directement (c’est-à-dire sans l’utilisation de DELEGATECALL) que si elles ne modifient pas l’état (c’est-à-dire si ce sont des fonctions view ou pure), parce que les bibliothèques sont supposées être sans état. En particulier, il n’est possible de détruire une bibliothèque.

Note

Jusqu’à la version 0.4.20, il était possible de détruire des bibliothèques en contournant le système de types de Solidity. A partir de cette version, les librairies contiennent un mécanisme qui empêche les fonctions modifiant l’état d’être appelées directement (c’est-à-dire sans DELEGATECALL).

Les bibliothèques peuvent être vues comme des contrats de base implicites des contrats qui les utilisent. Elles ne seront pas explicitement visibles dans la hiérarchie de l’héritage, mais les appels aux fonctions des bibliothèques ressemblent aux appels aux fonctions des contrats de base explicites (en utilisant un accès qualifié comme L.f()). Bien sûr, les appels aux fonctions internes utilisent la convention d’appel interne, ce qui signifie que tous les types internes peuvent être passés et les types stockés en mémoire seront passés par référence et non copiés. Pour réaliser cela dans l’EVM, le code des fonctions de bibliothèques internes qui sont appelées à partir d’un contrat ainsi que toutes les fonctions appelées à partir de celui-ci seront incluses dans le contrat et un appel régulier JUMP sera utilisé au lieu d’un DELEGATECALL.

Note

L’analogie avec l’héritage s’effondre lorsqu’il s’agit de fonctions publiques. L’appel d’une fonction de bibliothèque publique avec L.f() entraîne un appel externe (DELEGATECALL pour être précis). En revanche, A.f() est un appel interne lorsque A est un contrat de base du contrat actuel.

L’exemple suivant illustre comment utiliser les bibliothèques (mais en utilisant une méthode manuelle, ne manquez pas de consulter utiliser for pour un exemple plus avancé pour implémenter un ensemble).

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.6.0 <0.9.0;


// Nous définissons un nouveau type de données struct qui sera utilisé pour
// contenir ses données dans le contrat d'appel.
struct Data {
    mapping(uint => bool) flags;
}

library Set {
    // Notez que le premier paramètre est de type
    // "référence de stockage" et donc seulement son adresse de stockage et pas
    // son contenu est transmis dans le cadre de l'appel. Il s'agit d'une
    // particularité des fonctions de bibliothèque. Il est idiomatique
    // d'appeler le premier paramètre `self` si la fonction peut
    // être vue comme une méthode de cet objet.
    function insert(Data storage self, uint value)
        public
        returns (bool)
    {
        if (self.flags[value])
            return false; // déjà là
        self.flags[value] = true;
        return true;
    }

    function remove(Data storage self, uint value)
        public
        returns (bool)
    {
        if (!self.flags[value])
            return false; // pas là
        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 {
        // Les fonctions de la bibliothèque peuvent être appelées sans une
        // instance spécifique de la bibliothèque, puisque
        // l'"instance" sera le contrat en cours.
        require(Set.insert(knownValues, value));
    }
    // In this contract, we can also directly access knownValues.flags, if we want.
}

Bien sûr, vous n’êtes pas obligé de suivre cette voie pour utiliser des bibliothèques : elles peuvent aussi être utilisées sans définir de type de données struct. Les fonctions fonctionnent également sans paramètres de de référence de stockage, et elles peuvent avoir plusieurs paramètres de référence et dans n’importe quelle position.

Les appels à Set.contains, Set.insert et Set.remove sont tous compilés en tant qu’appels (DELEGATECALL) à un contrat/librairie externe. Si vous utilisez des bibliothèques, soyez conscient qu’un appel à une fonction externe réelle est effectué. msg.sender, msg.value et this garderont leurs valeurs dans cet appel. (avant Homestead, à cause de l’utilisation de CALLCODE, msg.sender et msg.value changeaient, cependant).

L’exemple suivant montre comment utiliser les types stockés dans la mémoire et les fonctions internes des bibliothèques afin d’implémenter des types personnalisés sans la surcharge des appels de fonctions externes :

// 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) {
            // dommage, nous devons ajouter un membre
            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);
    }
}

Il est possible d’obtenir l’adresse d’une bibliothèque en convertissant le type de la bibliothèque en type adress, c’est-à-dire en utilisant address(LibraryName).

Comme le compilateur ne connaît pas l’adresse à laquelle la bibliothèque sera déployée, le code hexadécimal compilé contiendra des caractères de remplacement de la forme __$30bbc0abd4d6364515865950d3e0d10953$__. Le caractère de remplacement est un préfixe de 34 caractères de l’encodage hexadécimal du hachage keccak256 du nom de bibliothèque pleinement qualifié, qui serait par exemple libraries/bigint.sol:BigInt si la bibliothèque était stockée dans un fichier appelé bigint.sol dans un répertoire libraries/. Un tel bytecode est incomplet et ne devrait pas être déployé. Les placeholders doivent être remplacés par des adresses réelles. Vous pouvez le faire soit en passant au compilateur lors de la compilation de la bibliothèque ou en utilisant l’éditeur de liens pour mettre à jour un binaire déjà compilé. Voir Liens entre les bibliothèques pour des informations sur la façon d’utiliser le compilateur en ligne de commande pour la liaison.

Par rapport aux contrats, les bibliothèques sont limitées de la manière suivante :

  • elles ne peuvent pas avoir de variables d’état

  • elles ne peuvent ni hériter ni être héritées

  • elles ne peuvent pas recevoir d’éther

  • elles ne peuvent pas être détruites

(Ces restrictions pourraient être levées ultérieurement).

Signatures de fonction et sélecteurs dans les bibliothèques

Bien que les appels externes à des fonctions de bibliothèques publiques ou externes soient possibles, la convention d’appel pour de tels appels est considérée comme interne à Solidity et n’est pas la même que celle spécifiée pour la fonction ordinaire du contrat ABI. Les fonctions de bibliothèque externes supportent plus de types d’arguments que les fonctions de contrat externes, par exemple les structs récursifs et les pointeurs de stockage. Pour cette raison, les signatures de fonctions utilisées pour calculer le sélecteur à 4 octets sont calculées selon un schéma de dénomination interne et les arguments de types non pris en charge par l’ABI du contrat utilisent un encodage interne.

Les identifiants suivants sont utilisés pour les types dans les signatures :

  • Les types de valeurs, les string non stockées et les bytes non stockés utilisent les mêmes identifiants que dans l’ABI du contrat.

  • Les types de tableaux non stockés suivent la même convention que dans l’ABI du contrat, c’est-à-dire <type>[] pour les tableaux dynamiques et <type>[M] pour les tableaux de taille fixe de M éléments.

  • Les structures non stockées sont désignées par leur nom complet, c’est-à-dire C.S pour contrat C { struct S { ... } }.

  • Les mappages de pointeurs de stockage utilisent mapping(<keyType> => <valueType>) storage<keyType> et <valueType> sont sont les identificateurs des types de clé et de valeur du mappage, respectivement.

  • Les autres types de pointeurs de stockage utilisent l’identificateur de type de leur type non stocké correspondant, mais ajoutent un espace unique suivi de storage.

Le codage des arguments est le même que pour l’ABI des contrats ordinaires, sauf pour les pointeurs de stockage, qui sont codés en tant que uint256 faisant référence à l’emplacement de stockage vers lequel ils pointent.

Comme pour l’ABI du contrat, le sélecteur est constitué des quatre premiers octets du Keccak256-hash de la signature. Sa valeur peut être obtenue à partir de Solidity en utilisant le membre .selector comme suit :

// 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;
    }
}

Protection d’appel pour les bibliothèques

Comme mentionné dans l’introduction, si le code d’une bibliothèque est exécuté en utilisant un CALL au lieu d’un DELEGATECALL ou CALLCODE, il se réverbère sauf si une fonction view ou pure est appelée.

L’EVM ne fournit pas de moyen direct pour qu’un contrat puisse détecter s’il a été appelé en utilisant CALL ou non, mais un contrat mais un contrat peut utiliser l’opcode ADDRESS pour savoir « où » il est actuellement en cours d’exécution. Le code généré compare cette adresse à l’adresse utilisée au moment de la construction pour déterminer le mode d’appel.

Plus spécifiquement, le code d’exécution d’une bibliothèque commence toujours par une instruction push, qui est un zéro de 20 octets au moment de la compilation. Lorsque le code déployé s’exécute, cette constante est remplacée en mémoire par l’adresse actuelle et ce code modifié est stocké dans le contrat. Au moment de l’exécution, cela fait en sorte que l’adresse du moment du déploiement soit la première constante à être poussée sur la pile et le code du distributeur compare l’adresse actuelle à cette constante pour toute fonction non-visible et non pure.

Cela signifie que le code réel stocké sur la chaîne pour une bibliothèque est différent du code rapporté par le compilateur en tant que deployedBytecode.

Utiliser For

La directive using A for B; peut être utilisée pour attacher des fonctions (de la bibliothèque A) à n’importe quel type (B) dans le contexte d’un contrat. Ces fonctions recevront l’objet sur lequel elles sont appelées comme premier paramètre (comme la variable self en Python).

L’effet de using A for *; est que les fonctions de la bibliothèque A sont attachées à tout type.

Dans les deux cas, toutes les fonctions de la bibliothèque sont attachées, même celles pour lesquelles le type du premier paramètre ne correspond pas au type de l’objet. Le type est vérifié au moment où la fonction est appelée et la résolution de surcharge de fonction est effectuée.

La directive using A pour B; n’est active que dans le contrat actuel, y compris au sein de toutes ses fonctions, et n’a aucun effet en dehors du contrat dans lequel elle est utilisée. La directive ne peut être utilisée qu’à l’intérieur d’un contrat, et non à l’intérieur de l’une de ses fonctions.

Réécrivons l’exemple de l’ensemble à partir de la directive Bibliothèques de cette manière :

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.6.0 <0.9.0;


// Il s'agit du même code que précédemment, mais sans commentaires.
struct Data { mapping(uint => bool) flags; }

library Set {
    function insert(Data storage self, uint value)
        public
        returns (bool)
    {
        if (self.flags[value])
            return false; // déjà là
        self.flags[value] = true;
        return true;
    }

    function remove(Data storage self, uint value)
        public
        returns (bool)
    {
        if (!self.flags[value])
            return false; // pas là
        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; // c'est le changement crucial
    Data knownValues;

    function register(uint value) public {
        // Ici, toutes les variables de type Data ont
        // des fonctions membres correspondantes.
        // L'appel de fonction suivant est identique à
        // `Set.insert(knownValues, value)`
        require(knownValues.insert(value));
    }
}

Il est également possible d’étendre les types élémentaires de cette manière :

// 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 {
        // Cette opération effectue l'appel de la fonction de bibliothèque
        uint index = data.indexOf(_old);
        if (index == type(uint).max)
            data.push(_new);
        else
            data[index] = _new;
    }
}

Notez que tous les appels de bibliothèque externes sont des appels de fonction EVM réels. Cela signifie que si vous passez des types de mémoire ou de valeur, une copie sera effectuée, même de la variable self. La seule situation où aucune copie ne sera effectuée est l’utilisation de variables de référence de stockage ou l’appel de fonctions de bibliothèque internes sont appelées.