Contrats intelligents et Vyper

Bannière Amazon du livre Maîtriser Ethereum

Vyper est un langage de programmation expérimental orienté contrat pour la machine virtuelle Ethereum qui s’efforce de fournir une auditabilité supérieure, en permettant aux développeurs de produire plus facilement du code intelligible. En fait, l’un des principes de Vyper est de rendre pratiquement impossible pour les développeurs d’écrire du code trompeur.

Dans ce chapitre, nous examinerons les problèmes courants liés aux contrats intelligents, présenterons le langage de programmation de contrats Vyper et le comparerons à Solidity, en démontrant les différences.

Vulnérabilités et Vyper

Une étude récente a analysé près d’un million de contrats intelligents Ethereum déployés et a constaté que nombre de ces contrats contenaient de graves vulnérabilités. Au cours de leur analyse, les chercheurs ont défini trois catégories de base de vulnérabilités en trace:

Contrats suicidaires

Contrats intelligents qui peuvent être tués par des adresses arbitraires

Contrats cupides

Contrats intelligents pouvant atteindre un état dans lequel ils ne peuvent pas libérer d’ether

Contrats prodigues

Contrats intelligents pouvant être conclus pour libérer de l’ether à des adresses arbitraires

Les vulnérabilités sont introduites dans les contrats intelligents via le code. Il peut être fortement soutenu que ces vulnérabilités et d’autres ne sont pas introduites intentionnellement, mais quoi qu’il en soit, un code de contrat intelligent indésirable entraîne évidemment une perte de fonds inattendue pour les utilisateurs d’Ethereum, et ce n’est pas idéal. Vyper est conçu pour faciliter l’écriture de code sécurisé, ou également pour rendre plus difficile l’écriture accidentelle de code trompeur ou vulnérable.

Comparaison avec Solidity

L’une des façons dont Vyper essaie de rendre le code dangereux plus difficile à écrire est en omettant délibérément certaines fonctionnalités de Solidity. Il est important pour ceux qui envisagent de développer des contrats intelligents dans Vyper de comprendre quelles fonctionnalités Vyper n’a pas, et pourquoi. Par conséquent, dans cette section, nous allons explorer ces fonctionnalités et expliquer pourquoi elles ont été omises.

Modificateurs

Comme nous l’avons vu dans le chapitre précédent, dans Solidity vous pouvez écrire une fonction à l’aide de modificateurs (modifiers). Par exemple, la fonction suivante, changeOwner, exécutera le code dans un modificateur appelé onlyBy dans le cadre de son exécution :

function changeOwner(address _newOwner)
    public
    onlyBy(owner)
{
    owner = _newOwner;
}

Ce modificateur applique une règle relative à la propriété. Comme vous pouvez le voir, ce modificateur particulier agit comme un mécanisme pour effectuer une pré-vérification au nom de la fonction changeOwner :

modifier onlyBy(address _account)
{
    require(msg.sender == _account);
    _;
}

Mais les modificateurs ne sont pas là uniquement pour effectuer des vérifications, comme illustré ici. En fait, en tant que modificateurs, ils peuvent modifier considérablement l’environnement d’un contrat intelligent, dans le contexte de la fonction appelante. En termes simples, les modificateurs sont omniprésents.

Regardons un autre exemple de style Solidity :

enum Stages {
    SafeStage,
    DangerStage,
    FinalStage
}

uint public creationTime = now;
Stages public stage = Stages.SafeStage;

function nextStage() internal {
    stage = Stages(uint(stage) + 1);
}

modifier stageTimeConfirmation() {
    if (stage == Stages.SafeStage &&
                now >= creationTime + 10 days)
        nextStage();
    _;
}

function a()
    public
    stageTimeConfirmation
    // Plus de code va ici
{
}

D’une part, les développeurs doivent toujours vérifier tout autre code que leur propre code appelle. Cependant, il est possible que dans certaines situations (comme lorsque les contraintes de temps ou l’épuisement entraînent un manque de concentration), un développeur oublie une seule ligne de code. Cela est encore plus probable si le développeur doit se déplacer dans un fichier volumineux tout en gardant mentalement une trace de la hiérarchie des appels de fonction et en mémorisant l’état des variables de contrat intelligent.

Regardons l’exemple précédent un peu plus en profondeur. Imaginez qu’un développeur écrive une fonction publique appelée "a". Le développeur est nouveau dans ce contrat et utilise un modificateur écrit par quelqu’un d’autre. En un coup d’œil, il semble que le modificateur stageTimeConfirmation effectue simplement quelques vérifications concernant l’âge du contrat par rapport à la fonction appelante. Ce que le développeur peut ne pas réaliser, c’est que le modificateur appelle également une autre fonction, nextStage. Dans ce scénario de démonstration simpliste, le simple fait d’appeler la fonction publique "a" entraîne le déplacement de la variable "stage" du contrat intelligent de "SafeStage" à "DangerStage".

Vyper a complètement supprimé les modificateurs. Les recommandations de Vyper sont les suivantes : si vous n’exécutez que des assertions avec des modificateurs, utilisez simplement des vérifications et des assertions en ligne dans le cadre de la fonction ; si vous modifiez l’état du contrat intelligent, etc., intégrez à nouveau ces modifications explicitement à la fonction. Cela améliore l’auditabilité et la lisibilité, car le lecteur n’a pas à "envelopper" mentalement (ou manuellement) le code du modificateur autour de la fonction pour voir ce qu’elle fait.

Héritage de classe

L’héritage permet aux programmeurs pour exploiter la puissance du code pré-écrit en acquérant des fonctionnalités, des propriétés et des comportements préexistants à partir de bibliothèques de logiciels existantes. L’héritage est puissant et favorise la réutilisation du code. Solidity prend en charge l’héritage multiple ainsi que le polymorphisme, mais bien que ce soient des fonctionnalités clés de la programmation orientée objet, Vyper ne les prend pas en charge. Vyper soutient que la mise en œuvre de l’héritage nécessite que les codeurs et les auditeurs sautent entre plusieurs fichiers afin de comprendre ce que fait le programme. Vyper considère également que l’héritage multiple peut rendre le code trop compliqué à comprendre - un point de vue tacitement admis par la documentation Solidity, qui donne un exemple de la façon dont l’héritage multiple peut être problématique.

Assemblage en ligne

L’assemblage en ligne donne aux développeurs un accès de bas niveau à Ethereum Virtual Machine, permettant aux programmes Solidity d’effectuer des opérations en accédant directement aux instructions EVM. Par exemple, le code assembleur en ligne suivant ajoute 3 à l’emplacement mémoire 0x80 :

3 0x80 mload add 0x80 mstore

Vyper considère que la perte de lisibilité est un prix trop élevé à payer pour la puissance supplémentaire et ne prend donc pas en charge l’assemblage en ligne.

Surcharge de fonction

La surcharge de fonction permet aux développeurs d’écrire plusieurs fonctions du même nom. La fonction utilisée à une occasion donnée dépend des types d’arguments fournis. Prenons par exemple les deux fonctions suivantes :

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

function f(uint _in, bytes32 _key) public pure returns (uint out) {
    out = 2;
}

La première fonction (nommée f)accepte un argument d’entrée de type uint ; la deuxième fonction (également nommée f)accepte deux arguments, un de type uint et un de type bytes32. Avoir plusieurs définitions de fonction avec le même nom prenant des arguments différents peut être déroutant, donc Vyper ne prend pas en charge la surcharge de fonction.

Conversion de type variable

Il existe deux types de typage : implicite et explicite

Le transtypage implicite est souvent effectué au moment de la compilation. Par exemple, si une conversion de type est sémantiquement correcte et qu’aucune information n’est susceptible d’être perdue, le compilateur peut effectuer une conversion implicite, telle que la conversion d’une variable de type uint8 en uint16. Les premières versions de Vyper autorisaient le transtypage implicite des variables, mais pas les versions récentes.

Les transtypages explicites peuvent être insérés dans Solidity. Malheureusement, ils peuvent entraîner des comportements inattendus. Par exemple, convertir un uint32 en un type plus petit uint16 supprime simplement les bits d’ordre supérieur, comme illustré ici :

uint32 a = 0x12345678;
uint16 b = uint16(a);
// La variable b est 0x5678 maintenant

Vyper a à la place une fonction convert pour effectuer des transtypages explicites. La fonction convert (trouvée à la ligne 82 de convert.py) :

def convert(expr, context):
    output_type = expr.args[1].s
    if output_type in conversion_table:
        return conversion_table[output_type](expr, context)
    else:
        raise Exception("Conversion to {} is invalid.".format(output_type))

Notez l’utilisation de conversion_table (trouvé à la ligne 90 du même fichier), qui ressemble à ceci :

conversion_table = {
    'int128': to_int128,
    'uint256': to_unint256,
    'decimal': to_decimal,
    'bytes32': to_bytes32,
}

Lorsqu’un développeur appelle convert, il fait référence à conversion_table, ce qui garantit que la conversion appropriée est effectuée. Par exemple, si un développeur passe un int128 à la fonction convert, la fonction to_int128 à la ligne 26 du même fichier (convert.py) sera exécutée. La fonction to_int128 est la suivante :

@signature(('int128', 'uint256', 'bytes32', 'bytes'), 'str_literal')
def to_int128(expr, args, kwargs, context):
    in_node = args[0]
    typ, len = get_type(in_node)
    if typ in ('int128', 'uint256', 'bytes32'):
        if in_node.typ.is_literal
            and not SizeLimits.MINNUM <= in_node.value <= SizeLimits.MAXNUM:
            raise InvalidLiteralException(
                "Number out of range: {}".format(in_node.value), expr
            )
        return LLLnode.from_list(
            ['clamp', ['mload', MemoryPositions.MINNUM], in_node,
            ['mload', MemoryPositions.MAXNUM]], typ=BaseType('int128'),
            pos=getpos(expr)
        )
    else:
        return byte_array_to_num(in_node, expr, 'int128')

Comme vous pouvez le constater, le processus de conversion garantit qu’aucune information ne peut être perdue. si c’est possible, une exception est levée. Le code de conversion empêche la troncation ainsi que d’autres anomalies qui seraient normalement autorisées par un transtypage implicite.

Choisir un transtypage explicite plutôt qu’implicite signifie que le développeur est responsable de l’exécution de tous les transtypages. Bien que cette approche produise un code plus détaillé, elle améliore également la sécurité et la vérifiabilité des contrats intelligents.

Préconditions et Postconditions

Vyper gère explicitement les préconditions, les postconditions et les changements d’état. Bien que cela produise un code redondant, cela permet également une lisibilité et une sécurité maximales. Lors de la rédaction d’un contrat intelligent dans Vyper, un développeur doit observer les trois points suivants :

Condition

Quel est l’état/condition actuel des variables d’état Ethereum ?

Effets

Quels effets ce code de contrat intelligent aura-t-il sur la condition des variables d’état lors de l’exécution ? Autrement dit, qu’est-ce qui sera affecté et qu’est-ce qui ne sera pas affecté ? Ces effets sont-ils conformes aux intentions du contrat intelligent ?

Interaction

Une fois que les deux premières considérations ont été traitées de manière exhaustive, il est temps d’exécuter le code. Avant le déploiement, parcourez logiquement le code et examinez tous les résultats permanents possibles, les conséquences et les scénarios d’exécution du code, y compris les interactions avec d’autres contrats.

Idéalement, chacun de ces points devrait être soigneusement examiné puis documenté de manière approfondie dans le code. Cela améliorera la conception du code, le rendant finalement plus lisible et auditable.

Décorateurs

Les décorateurs suivants peuvent être utilisés au début de chaque fonction :

@private

Le décorateur @private rend la fonction inaccessible depuis l’extérieur du contrat.

@public

Le décorateur @public rend la fonction à la fois visible et exécutable publiquement. Par exemple, même le portefeuille Ethereum affichera de telles fonctions lors de la visualisation du contrat.

@constant

Les fonctions avec le décorateur @constant ne sont pas autorisées à modifier les variables d’état. En fait, le compilateur rejettera le programme entier (avec une erreur appropriée) si la fonction essaie de changer une variable d’état.

@payable

Seules les fonctions avec le décorateur @payable sont autorisées à transférer de la valeur.

Vyper implémente explicitement la logique des décorateurs. Par exemple, le processus de compilation de Vyper échouera si une fonction a à la fois un décorateur @payable et un décorateur @constant. Cela a du sens car une fonction qui transfère une valeur a par définition mis à jour l’état, elle ne peut donc pas être @constant. Chaque fonction Vyper doit être décorée avec @public ou @private (mais pas les deux !).

Ordre des fonctions et des variables

Chaque contrat intelligent Vyper individuel se compose d’un seul fichier Vyper uniquement. En d’autres termes, tout le code d’un contrat intelligent Vyper donné, y compris toutes les fonctions, variables, etc., existe au même endroit. Vyper exige que la fonction et les déclarations de variables de chaque contrat intelligent soient écrites physiquement dans un ordre particulier. Solidity n’a pas du tout cette exigence. Jetons un coup d’œil à un exemple Solidity :

pragma solidity ^0.4.0;

contract ordering {

    function topFunction()
    external
    returns (bool) {
        initiatizedBelowTopFunction = this.lowerFunction();
        return initiatizedBelowTopFunction;
    }

    bool initiatizedBelowTopFunction;
    bool lowerFunctionVar;

    function lowerFunction()
    external
    returns (bool) {
        lowerFunctionVar = true;
        return lowerFunctionVar;
    }

}

Dans cet exemple, la fonction appelée topFunction appelle une autre fonction, lowerFunction. topFunction attribue également une valeur à une variable appelée initiatizedBelowTopFunction. Comme vous pouvez le voir, Solidity n’exige pas que ces fonctions et variables soient physiquement déclarées avant d’être appelées par le code d’exécution. Il s’agit d’un code Solidity valide qui se compilera avec succès.

Les exigences de commande de Vyper ne sont pas une nouveauté ; en fait, ces exigences de commande ont toujours été présentes dans la programmation Python. La commande requise par Vyper est simple et logique, comme illustré dans cet exemple suivant :

# Déclarez une variable appelée theBool
theBool: public(bool)

# Déclarez une fonction appelée topFunction
@public
def topFunction() -> bool:
# Attribuez une valeur à la fonction déjà déclarée appelée theBool
    self.theBool = True
    return self.theBool

# Déclarez une fonction appelée lowerFunction
@public
def lowerFunction():
# Appelez la fonction déjà déclarée appelée topFunction
    assert self.topFunction()

Cela montre le bon ordre des fonctions et des variables dans un contrat intelligent Vyper. Notez comment la variable theBool et la fonction topFunction sont déclarées avant qu’elles ne reçoivent une valeur et ne soient appelées, respectivement. Si theBool était déclaré sous topFunction ou si topFunction était déclaré sous lowerFunction, ce contrat ne serait pas compilé.

Compilation

Vyper a son propre éditeur de code et compilateur en ligne, qui vous permet d’écrire puis compilez vos contrats intelligents en code intermédiaire, ABI et LLL en utilisant uniquement votre navigateur Web. Le compilateur en ligne Vyper propose une variété de contrats intelligents pré-écrits pour votre commodité, y compris des contrats pour une simple enchère ouverte, des achats à distance sécurisés, des jetons ERC20, etc. Cet outil, propose une seule version du logiciel de compilation. Il est mis à jour régulièrement mais ne garantit pas toujours la dernière version. Etherscan a un compilateur Vyper en ligne qui vous permet de sélectionner la version du compilateur. De plus, Remix, conçu à l’origine pour les contrats intelligents Solidity, dispose désormais d’un plug-in Vyper disponible dans l’onglet Paramètres.

Note

Vyper implémente ERC20 en tant que contrat précompilé, ce qui permet d’utiliser facilement ces contrats intelligents prêts à l’emploi. Les contrats dans Vyper doivent être déclarés en tant que variables globales. Un exemple de déclaration de la variable ERC20 est le suivant :

token: address(ERC20)

Vous pouvez également compiler un contrat à l’aide de la ligne de commande. Chaque contrat Vyper est enregistré dans un seul fichier avec l’extension .vy. Une fois installé, vous pouvez compiler un contrat avec Vyper en exécutant la commande suivante :

vyper ~/hello_world.vy

La description ABI lisible par l’homme (au format JSON) peut ensuite être obtenue en exécutant la commande suivante :

vyper -f json ~/hello_world.v.py

Protection contre les erreurs de dépassement au niveau du compilateur

Les erreurs de dépassement dans le logiciel peut être catastrophique lorsqu’il s’agit de valeur réelle. Par exemple, une transaction de la mi-avril 2018 montre le transfert malveillant de plus de 57 896 044 618 658 100 000 000 000 000 000 000 000 000,000 000 000 000 000 000 tokens ou jetons BEC. Cette transaction était le résultat d’un problème de dépassement d’entier dans le contrat de jeton ERC20 de BeautyChain (BecToken.sol). Les développeurs de Solidity ont accès à des bibliothèques comme SafeMathainsi qu’à des outils d’analyse de la sécurité des contrats intelligents Ethereum comme Mythril OSS. Cependant, les développeurs ne sont pas obligés d’utiliser les outils de sécurité. En termes simples, si la sécurité n’est pas appliquée par le langage, les développeurs peuvent écrire du code non sécurisé qui se compilera avec succès et s’exécutera plus tard "avec succès".

Vyper dispose d’une protection intégrée contre les débordements, mise en œuvre selon une approche à deux volets. Tout d’abord, Vyper fournit un SafeMath équivalent qui inclut les cas d’exception nécessaires pour l’arithmétique entière. Deuxièmement, Vyper utilise des pinces chaque fois qu’une constante littérale est chargée, qu’une valeur est transmise à une fonction ou qu’une variable est affectée. Les pinces sont implémentées via des fonctions personnalisées dans le compilateur LLL (Low-level Lisp-like Language) et ne peuvent pas être désactivées. (Le compilateur Vyper génère LLL plutôt que le code intermédiaire EVM ; cela simplifie le développement de Vyper lui-même.)

Lecture et écriture de données

Bien qu’il soit coûteux de stocker, lire et modifier des données, ces opérations de stockage sont une composante nécessaire de la plupart des contrats intelligents. Les contrats intelligents peuvent écrire des données à deux endroits :

État global

Les variables d’état d’un contrat intelligent donné sont stockées dans le trie d’état global d’Ethereum ; un contrat intelligent ne peut stocker, lire et modifier que des données relatives à l’adresse de ce contrat particulier (c’est-à-dire que les contrats intelligents ne peuvent pas lire ou écrire dans d’autres contrats intelligents).

Journaux

Un contrat intelligent peut également écrire dans les données de la chaîne d’Ethereum via des événements de journal. Alors que Vyper utilisait initialement la syntaxe __log__ pour déclarer ces événements, une mise à jour a été effectuée pour aligner davantage sa déclaration d’événement sur la syntaxe d’origine de Solidity. Par exemple, la déclaration par Vyper d’un événement appelé MyLog était à l’origine MyLog: __log__({arg1: indexed(bytes[3])}). La syntaxe est maintenant devenue MyLog: event({arg1: indexed(bytes[3])}). Il est important de noter que l’exécution de l’événement de journalisation dans Vyper était, et est toujours, comme suit : log.MyLog("123").

Bien que les contrats intelligents puissent écrire dans les données de la chaîne d’Ethereum (via des événements de journal), ils ne peuvent pas lire les événements de journal en chaîne qu’ils ont créés. Néanmoins, l’un des avantages de l’écriture dans les données de la chaîne d’Ethereum via des événements de journal est que les journaux peuvent être découverts et lus, sur la chaîne publique, par des clients légers. Par exemple, la valeur logsBloom dans un bloc extrait peut indiquer si un événement de journal est présent ou non. Une fois que l’existence d’événements de journal a été établie, les données de journal peuvent être obtenues à partir d’un reçu de transaction donné.

Conclusion

Vyper est un nouveau langage de programmation orienté contrat puissant et intéressant. Sa conception est biaisée vers "l’exactitude", au détriment d’une certaine flexibilité. Cela peut permettre aux programmeurs de rédiger de meilleurs contrats intelligents et d’éviter certains pièges qui provoquent l’apparition de graves vulnérabilités. Ensuite, nous examinerons plus en détail la sécurité des contrats intelligents. Certaines des nuances de la conception de Vyper peuvent devenir plus apparentes une fois que vous avez lu tous les problèmes de sécurité possibles qui peuvent survenir dans les contrats intelligents.