Passer au contenu principal
Change page

Tester les contrats intelligents

Dernière modification: @Im-SpiETH(opens in a new tab), 15 juin 2024

Les blockchains publiques comme Ethereum sont immuables, ce qui rend difficile le changement de code des contrats intelligents après le déploiement. Les modèles de mise à niveau du contrat pour effectuer des « mises à niveau virtuelles » existent, or celles-ci sont difficiles à mettre en œuvre et nécessitent un consensus social. De plus, une mise à jour ne peut corriger une erreur qu'après sa découverte. Si un attaquant découvre la vulnérabilité en premier, votre contrat intelligent risque d'être exploité.

Pour ces raisons, tester les contrats intelligents avant le déploiement sur le réseau principal est une exigence minimale pour la sécurité. Il existe de nombreuses techniques pour tester les contrats et évaluer la justesse du code ; ce que vous choisissez dépend de vos besoins. Néanmoins, une suite de tests composée d'outils et d'approches différents est idéale pour attraper des défauts de sécurité mineurs et majeurs dans le code contractuel.

Prérequis

Cette page explique comment tester les contrats intelligents avant le déploiement sur le réseau Ethereum. Cela suppose que vous êtes familier avec les contrats intelligents.

Qu'est-ce que le test de contrats intelligents ?

Les tests de contrats intelligents sont le processus de vérification que le code d'un contrat intelligent fonctionne comme prévu. Les tests sont utiles pour vérifier si un contrat intelligent particulier satisfait aux exigences de fiabilité, d'utilisabilité et de sécurité.

Bien que les méthodes de test soient différentes, la plupart des méthodes de test nécessitent l'exécution d'un contrat intelligent avec un petit échantillon de données qu'il est censé gérer. Si le contrat produit des résultats corrects pour les données d'échantillonnage, il est supposé fonctionner correctement. La plupart des outils de test fournissent des ressources pour écrire et exécuter des cas de test(opens in a new tab) pour vérifier si une exécution de contrats correspond aux résultats attendus.

Pourquoi est-il important de tester les contrats intelligents ?

Comme les contrats intelligents gèrent souvent les actifs financiers de grande valeur, des erreurs de programmation mineures peuvent entraîner et entraînent souvent des pertes massives pour les utilisateurs(opens in a new tab). Des tests rigoureux peuvent toutefois vous aider à découvrir rapidement les défauts d'un code de contrats intelligents et à les corriger avant de se lancer sur le réseau principal.

Tant qu'il est possible de mettre à jour un contrat si un bogue est découvert, les mises à jour sont complexes et peuvent entraîner des erreurs(opens in a new tab) si elles sont mal gérées. La mise à niveau d'un contrat annule encore le principe de l'immutabilité et charge les utilisateurs avec des hypothèses de confiance supplémentaires. Inversement, un plan complet de test de votre contrat atténue les risques de sécurité des contrats intelligents et réduit le besoin d'effectuer des mises à niveau logiques complexes après le déploiement.

Méthodes pour tester les contrats intelligents

Les méthodes de test des contrats intelligents sur Ethereum se distinguent en deux grandes catégories : les tests automatisés et les tests manuels. Les tests automatisés et les tests manuels offrent des avantages et des compromis uniques, mais vous pouvez combiner les deux pour créer un plan solide pour l'analyse de vos contrats.

Les tests automatisés

Les tests automatisés utilisent des outils qui vérifient automatiquement un code de contrats intelligents pour détecter des erreurs dans l'exécution. L'avantage des tests automatisés vient de l'utilisation de scripts(opens in a new tab) pour guider l'évaluation des fonctionnalités contractuelles. Les tests scriptés peuvent être programmés aux fins d'une exécution répétée avec une intervention humaine minimale, rendant les tests automatisés plus efficaces que les approches manuelles de test.

Les tests automatisés sont particulièrement utiles lorsque les tests sont répétitifs et prennent du temps ; difficile à exécuter manuellement ; sensible aux erreurs humaines ; ou nécessitant une évaluation des fonctions critiques du contrat. Mais les outils de test automatisés peuvent avoir des inconvénients : ils peuvent manquer certains bogues et produire beaucoup de faux positifs(opens in a new tab). Par conséquent, l'association des tests automatisés avec des tests manuels pour les contrats intelligents est idéale.

Les tests manuels

Les tests manuels sont aidés par l'homme et impliquent l'exécution de chaque cas de test dans votre suite de tests l'un après l'autre lors de l'analyse de la justesse des contrats intelligents. Ceci est différent des tests automatisés, où vous pouvez exécuter simultanément plusieurs tests isolés sur un contrat et obtenir un rapport montrant tous les échecs et réussissant les tests.

Les tests manuels peuvent être effectués par un seul individu, suivant un plan d'essai écrit qui couvre différents scénarios d'essais. Vous pourriez également avoir plusieurs personnes ou groupes qui interagissent avec un contrat intelligent sur une période déterminée dans le cadre de tests manuels. Les testeurs compareront le comportement réel du contrat avec le comportement attendu, signalant toute différence comme un bug.

Des tests manuels efficaces requièrent des ressources considérables (compétences, temps, argent et efforts), et il est possible, en raison d'erreurs humaines, de manquer certaines erreurs lors de l'exécution des tests. Mais les tests manuels peuvent aussi être bénéfiques — par exemple, un test humain (p. ex. un vérificateur) peut utiliser l'intuition pour détecter les cas où un outil de test automatique manquerait.

Les méthodes de test automatiques pour les contrats intelligents

Tests unitaires

Les tests unitaires évaluent séparément les fonctions contractuelles et vérifient que chaque composant fonctionne correctement. Un test unitaire est simple, rapide à exécuter et donne une idée précise de ce qui s'est mal passé si le test échoue.

Les tests unitaires sont utiles pour vérifier que les fonctions retournent les valeurs attendues et que le stockage du contrat est correctement mis à jour après l'exécution de la fonction. En outre, l'exécution de tests unitaires après avoir apporté des modifications à une base de code de contrats garantit que l'ajout de nouvelles logiques n'introduit pas d'erreurs. Voici quelques directives pour exécuter des tests unitaires efficaces :

Lignes directrices pour le test unitaire des contrats intelligents

1. Comprendre la logique et le flux de travail de vos contrats

Avant d'écrire des tests unitaires, il aide à savoir quelles fonctionnalités un contrat intelligent offre et comment les utilisateurs accèderont et utiliseront ces fonctions. Ceci est particulièrement utile pour exécuter des tests de chemin heureux(opens in a new tab) qui déterminent si les fonctions dans un contrat retournent la sortie correcte pour les entrées utilisateur valides. Nous allons expliquer ce concept en utilisant cet exemple (abrégé) d'un contrat de vente aux enchères(opens in a new tab)

1constructor(
2 uint biddingTime,
3 address payable beneficiaryAddress
4 ) {
5 beneficiary = beneficiaryAddress;
6 auctionEndTime = block.timestamp + biddingTime;
7 }
8
9function bid() external payable {
10
11 if (block.timestamp > auctionEndTime)
12 revert AuctionAlreadyEnded();
13
14 if (msg.value <= highestBid)
15 revert BidNotHighEnough(highestBid);
16
17 if (highestBid != 0) {
18 pendingReturns[highestBidder] += highestBid;
19 }
20 highestBidder = msg.sender;
21 highestBid = msg.value;
22 emit HighestBidIncreased(msg.sender, msg.value);
23 }
24
25 function withdraw() external returns (bool) {
26 uint amount = pendingReturns[msg.sender];
27 if (amount > 0) {
28 pendingReturns[msg.sender] = 0;
29
30 if (!payable(msg.sender).send(amount)) {
31 pendingReturns[msg.sender] = amount;
32 return false;
33 }
34 }
35 return true;
36 }
37
38function auctionEnd() external {
39 if (block.timestamp < auctionEndTime)
40 revert AuctionNotYetEnded();
41 if (ended)
42 revert AuctionEndAlreadyCalled();
43
44 ended = true;
45 emit AuctionEnded(highestBidder, highestBid);
46
47 beneficiary.transfer(highestBid);
48 }
49}
Afficher tout

Il s'agit d'un simple contrat de vente aux enchères conçu pour recevoir des offres pendant la période d'enchère. Si le highestBid augmente, le précédent plus offrant reçoit son argent ; une fois la période d'enchères terminée, le beneficiary appelle le contrat pour récupérer son argent.

Les tests unitaires pour un contrat comme celui-ci couvriraient différentes fonctions qu'un utilisateur peut appeler lorsqu'il interagit avec le contrat. Il peut s'agir, par exemple, d'un test unitaire qui vérifie si un utilisateur peut placer une enchère pendant que l'enchère est en cours (c'est-à-dire que les appels à bid() réussissent) ou d'un test qui vérifie si un utilisateur peut placer une enchère plus élevée que le highestBid actuel.

Comprendre le flux de travail opérationnel d'un contrat facilite également la rédaction de tests unitaires qui vérifient si l'exécution répond aux exigences. Par exemple, le contrat d'enchère spécifie que les utilisateurs ne peuvent pas placer d'enchères une fois l'enchère terminée (c'est-à-dire lorsque auctionEndTime est inférieur à block.timestamp). Ainsi, un développeur peut exécuter un test unitaire qui vérifie si les appels à la fonction bid() réussissent ou échouent lorsque la vente aux enchères est terminée (à savoir, quand auctionEndTime > block.timestamp).

2. Évaluer toutes les hypothèses liées à l'exécution du contrat

Il est important de documenter toutes les hypothèses concernant l'exécution d'un contrat et d'écrire des tests unitaires pour vérifier la validité de ces hypothèses. En plus d'offrir une protection contre une exécution inattendue, tester les assertions vous force à penser à des opérations qui pourraient rompre un modèle de sécurité de contrats intelligents. Une astuce utile est d'aller au-delà des « tests utilisateurs heureux » et d'écrire des tests négatifs qui vérifient si une fonction échoue pour les mauvaises entrées.

De nombreux frameworks de tests unitaires vous permettent de créer des assertions – des déclarations simples qui indiquent ce qu'un contrat peut ou ne peut pas faire – et d'exécuter des tests pour voir si ces assertions sont en cours d'exécution. Un développeur travaillant sur le contrat de vente aux enchères décrit précédemment pourrait établir les affirmations suivantes à propos de son comportement avant d'exécuter des tests négatifs :

  • Les utilisateurs ne peuvent pas placer d'enchères lorsque la vente aux enchères est terminée ou n'a pas commencé.

  • Le contrat de vente aux enchères se rétablit si une offre est inférieure au seuil acceptable.

  • Les utilisateurs qui ne gagnent pas l'enchère sont crédités de leurs fonds

Remarque : Une autre façon de tester les hypothèses consiste à écrire des tests qui déclenchent des modificateurs de fonction(opens in a new tab) dans un contrat, en particulier les instructions require, assert et if…else.

3. Mesurer la couverture du code

La couverture de code(opens in a new tab) est une métrique de test qui suit le nombre de branches, de lignes et d'instructions dans votre code exécuté lors des tests. Les tests doivent avoir une bonne couverture du code, sinon vous risquez d'obtenir des « faux négatifs », ce qui se produit lorsqu'un contrat réussit tous les tests, mais des vulnérabilités existent toujours dans le code. L'enregistrement d'une couverture de code élevée donne toutefois l'assurance que toutes les déclarations/fonctions d'un contrat intelligent ont été suffisamment testées pour être correctes.

4. Utiliser des frameworks de test bien développés

La qualité des outils utilisés pour exécuter des tests unitaires pour vos contrats intelligents est cruciale. Un framework de test idéal est celui qui est régulièrement maintenu ; fournit des fonctionnalités utiles (par ex. des capacités de journalisation et de signalement) ; et doit avoir été largement utilisé et contrôlé par d'autres développeurs.

Les frameworks de test unitaires pour les contrats intelligents Solidity sont disponibles en différents langages (principalement JavaScript, Python et Rust). Voir certains des guides ci-dessous pour plus d'informations sur la façon de commencer à exécuter des tests unitaires avec différents frameworks de test :

Tests d'intégration

Alors que les tests unitaires déboguent les fonctions du contrat de manière isolée, les tests d'intégration évaluent les composants d'un contrat intelligent dans son ensemble. Les tests d'intégration peuvent détecter les problèmes liés aux appels intercontractuels ou aux interactions entre différentes fonctions dans le même contrat intelligent. Par exemple, les tests d'intégration peuvent aider à vérifier si des choses comme l'héritage(opens in a new tab) et l'injection de dépendance fonctionnent correctement.

Les tests d'intégration sont utiles si votre contrat adopte une architecture modulaire ou des interfaces avec d'autres contrats en chaîne pendant l'exécution. Une façon d'exécuter des tests d'intégration consiste à à une hauteur spécifique (en utilisant un outil tel que Forge(opens in a new tab) ou Hardhat(opens in a new tab)) et simuler les interactions entre votre contrat et les contrats déployés.

La blockchain forkée se comportera de la même manière que le réseau principal et aura des comptes avec des états et des soldes associés. Mais cela ne fonctionne que comme un environnement de développement local en bac à sable, ce qui signifie que vous n'aurez pas besoin d'un véritable ETH pour les transactions, par exemple, vos changements n'affecteront pas non plus le véritable protocole Ethereum.

Fuzzing orienté propriétés

Les tests basés sur les propriétés sont le processus permettant de vérifier qu'un contrat intelligent satisfait à une propriété définie. Les propriétés affirment des faits sur le comportement d'un contrat qui devraient rester vrais dans différents scénarios. Un exemple de propriété de contrat intelligent pourrait être « Les opérations arithmétiques dans le contrat ne soupassent ou ne dépassent jamais ».

L'analyse statique et l'analyse dynamique sont deux techniques communes pour exécuter des tests basés sur la propriété, et les deux peuvent vérifier que le code d'un programme (un contrat intelligent dans ce cas) satisfait certaines propriétés prédéfinies. Certains outils de test fondés sur la propriété sont fournis avec des règles prédéfinies sur les propriétés de contrat attendues et vérifient le code par rapport à ces règles, tandis que d'autres vous permettent de créer des propriétés personnalisées pour un contrat intelligent.

Analyse statique

Un analyseur statique prend comme intrants le code source d'un contrat intelligent et les résultats indiquant si un contrat satisfait ou non une propriété. Contrairement à l’analyse dynamique, l’analyse statique n’implique pas d’exécuter un contrat pour l’analyser à des fins d'exactitude. L'analyse statique explique plutôt tous les chemins possibles qu'un contrat intelligent pourrait prendre pendant l'exécution (c'est-à-dire en examinant la structure du code source pour déterminer ce que cela signifierait pour l'opération des contrats à l'exécution).

Linting(opens in a new tab) et tests statiques(opens in a new tab) sont des méthodes courantes pour exécuter des analyses statiques sur des contrats. Les deux nécessitent l'analyse de représentations de bas niveau d'une exécution de contrats telles que les arbres de syntaxe abstraits(opens in a new tab) et les courbes de flux de contrôle(opens in a new tab) sorties par le compilateur.

Dans la plupart des cas, l'analyse statique est utile pour détecter les problèmes de sécurité tels que l'utilisation de constructions non sûres, les erreurs de syntaxe, ou les violations des normes de codage dans un code de contrats. Cependant, les analyseurs statiques sont généralement peu sains lorsqu'ils détectent des vulnérabilités plus profondes et peuvent produire des faux positifs excessifs.

Analyse dynamique

L'analyse dynamique génère des entrées symboliques (par exemple, en exécution symbolique(opens in a new tab)) ou des entrées concrètes (par ex. dans fuzzing(opens in a new tab)) vers les fonctions d'un contrat intelligent pour voir si une trace d'exécution enfreint des propriétés spécifiques. Cette forme de tests fondés sur des propriétés diffère des tests unitaires dans ces cas de tests couvrant plusieurs scénarios et un programme gère la génération de cas de test.

Fuzzing(opens in a new tab) est un exemple de technique d'analyse dynamique pour vérifier des propriétés arbitraires dans des contrats intelligents. Un fuzzer invoque des fonctions dans un contrat cible avec des variations aléatoires ou malformées d'une valeur d'input définie. Si le contrat intelligent entre un état d'erreur (par ex. où une assertion échoue), le problème est marqué et les entrées qui conduisent l'exécution vers le chemin vulnérable sont produites dans un rapport.

Le fuzzing est utile pour évaluer un mécanisme de validation des entrées des contrats intelligents car la mauvaise gestion des entrées inattendues peut entraîner une exécution imprévue et produire des effets dangereux. Cette forme de tests fondés sur les propriétés peut être idéale pour de nombreuses raisons :

  1. L'écriture de cas de test pour couvrir de nombreux scénarios est difficile. Un test de propriété nécessite seulement que vous définissiez un comportement et une plage de données pour tester le comportement — le programme génère automatiquement des cas de test basés sur la propriété définie.

  2. Votre suite de tests ne peut pas couvrir suffisamment tous les chemins possibles dans le programme. Même avec une couverture de 100 %, il est possible de passer à côté de cas extrêmes.

  3. Les tests unitaires prouvent qu'un contrat s'exécute correctement pour des données d'échantillon, mais si le contrat s'exécute correctement pour des entrées en dehors de l'échantillon reste inconnu. Les tests de propriété exécutent un contrat cible avec de multiples variations d'une valeur d'entrée donnée pour trouver des traces d'exécution qui causent des défaillances d'assertion. Ainsi, un test de propriété fournit plus de garanties qu'un contrat s'exécute correctement pour une vaste classe de données d'intrants.

Lignes directrices pour l'exécution de tests fondés sur les propriétés pour les contrats intelligents

L'exécution de tests basés sur les propriétés commence généralement par la définition d'une propriété (par ex. absence de dépassements d'entier(opens in a new tab)) ou de collections de propriétés que vous voulez vérifier dans un contrat intelligent. Vous pouvez également avoir besoin de définir une plage de valeurs dans laquelle le programme peut générer des données pour les entrées de transaction lors de l'écriture de tests de propriété.

Une fois configuré correctement, l'outil de test de propriété exécutera vos fonctions de contrats intelligents avec des entrées générées aléatoirement. S'il y a des violations d'assertion, vous devriez obtenir un rapport avec des données d'entrée concrètes qui enfreint la propriété en cours d'évaluation. Consultez certains des guides ci-dessous pour commencer avec des tests basés sur les propriétés en cours d'exécution avec différents outils :

Test manuel pour les contrats intelligents

Les tests manuels des contrats intelligents arrivent souvent plus tard dans le cycle de développement après avoir exécuté des tests automatisés. Un système évalue le contrat intelligent comme un produit totalement intégré pour voir s'il se déroule comme spécifié dans les exigences techniques.

Tester les contrats sur une blockchain locale

Alors que les tests automatisés effectués dans un environnement de développement local peuvent fournir des informations de débogage utiles, vous aimeriez savoir comment votre contrat intelligent se comporte dans un environnement de production. Cependant, le déploiement dans la chaîne principale Ethereum entraîne des frais de gaz, sans parler du fait que vous ou vos utilisateurs pouvez perdre de l'argent si votre contrat intelligent comporte toujours des bogues.

Tester votre contrat sur une blockchain locale (également connue sous le nom de réseau de développement) est une alternative recommandée aux tests sur le réseau principal. Une blockchain locale est une copie de la blockchain Ethereum fonctionnant localement sur votre ordinateur qui simule le comportement de la couche d'exécution d'Ethereum. À ce titre, vous pouvez programmer des transactions pour interagir avec un contrat sans encourir de frais généraux importants.

Exécuter des contrats sur une blockchain locale pourrait être utile comme une forme de test d’intégration manuelle. Les contrats intelligents sont hautement composables, vous permettant de vous intégrer aux protocoles existants, mais vous devrez quand même vous assurer que ces interactions complexes sur la chaîne produisent les bons résultats.

En savoir plus sur les réseaux de développement.

Tests de contrats sur les réseaux de test

Un réseau de test fonctionne exactement comme le réseau principal d'Ethereum, sauf qu'il utilise Ether (ETH) sans valeur de monde réel. Déployer votre contrat sur un réseau de test signifie que n'importe qui peut interagir avec lui (par exemple, via le frontend de la DAPP) sans mettre en péril les fonds.

Cette forme de test manuel est utile pour évaluer le flux de bout en bout de votre application du point de vue de l'utilisateur. Ici, les utilisateurs finaux peuvent exécuter des essais et rapporter tous les problèmes avec la logique commerciale du contrat et les fonctionnalités générales.

Déployer sur un réseau de test après avoir testé sur une blockchain locale est idéal car la première est plus proche du comportement de la Machine virtuelle Ethereum. Par conséquent, il est fréquent que de nombreux projets Ethereum-native déploient des DApps sur des réseaux de test pour évaluer une opération de contrats intelligents dans des conditions réelles.

En savoir plus sur les réseaux de test Ethereum.

Test contre vérification formelle

Bien que les tests permettent de confirmer qu'un contrat renvoie les résultats attendus pour certaines entrées de données, ils ne peuvent pas prouver de manière concluante la même chose pour les entrées non utilisées lors des tests. Tester un contrat intelligent ne peut donc pas garantir « l'exactitude fonctionnelle » (c'est-à-dire qu'il ne peut pas montrer qu'un programme se comporte comme requis pour tous les ensembles de valeurs d'entrée).

La vérification formelle est une approche permettant d'évaluer la justesse des logiciels en vérifiant si un modèle formel du programme correspond à la spécification formelle. Un modèle formel est une représentation mathématique abstraite d'un programme, tandis qu'une spécification formelle définit les propriétés d'un programme (c'est-à-dire des assertions logiques à propos de l'exécution du programme).

Parce que les propriétés sont écrites en termes mathématiques, il devient possible de vérifier qu'un modèle formel (mathématique) du système satisfait une spécification en utilisant des règles d'inférence logiques. Ainsi, on dit que les outils de vérification formels produisent des « preuves mathématiques » de l’exactitude d’un système.

Contrairement aux tests, la vérification formelle peut être utilisée pour vérifier qu'une exécution de contrats intelligents satisfait une spécification formelle pour toutes les exécutions (c'est-à-dire qu'il n'a pas de bogues) sans avoir besoin de l'exécuter avec des exemples de données. Non seulement cela réduit le temps passé à exécuter des dizaines de tests unitaires, mais il est aussi plus efficace pour attraper des vulnérabilités cachées. Cela dit, les techniques de vérification formelle reposent sur un spectre en fonction de leur difficulté de mise en œuvre et de leur utilité.

En savoir plus sur la vérification formelle des contrats intelligents.

Tester vs audits et récompenses de bugs

Comme mentionné, des tests rigoureux peuvent rarement garantir l'absence de bogues dans un contrat ; les approches de vérification formelle peuvent fournir des garanties plus fortes d'exactitude, mais elles sont actuellement difficiles à utiliser et entraînent des coûts considérables.

Néanmoins, vous pouvez encore augmenter la possibilité de capturer des vulnérabilités de contrat en obtenant une révision de code indépendante. Les audits de contrats intelligents(opens in a new tab) et les récompenses de bugs(opens in a new tab) sont deux façons d'amener d'autres personnes à analyser vos contrats.

Les vérifications sont effectuées par des vérificateurs expérimentés dans la recherche de cas de défauts de sécurité et de mauvaises pratiques de développement dans les contrats intelligents. Un audit comprendra généralement des tests (et éventuellement une vérification officielle) ainsi qu'une révision manuelle de l'ensemble du codebase.

Inversement, un programme de prime aux bogues implique généralement d'offrir une récompense financière à une personne (communément décrite comme hackers whitehat(opens in a new tab)) qui découvre une vulnérabilité dans un contrat intelligent et la divulgue aux développeurs. Ces chasses à la prime ont des points communs avec les audits étant donné que les deux méthodes impliquent le recours à l'expertise de tierces personnes pour découvrir des vulnérabilités dans les contrats intelligents.

La principale différence est que les programmes de primes aux bogues sont ouverts à la communauté plus large des développeurs et des hackers et attirent une large classe de hackers éthiques et de professionnels de la sécurité indépendants possédant des compétences et une expérience uniques. Cela peut être un avantage par rapport aux audits de contrats intelligents qui reposent principalement sur des équipes qui peuvent posséder une expertise plus limitée ou étroite.

Outils de test et bibliothèques

Outils pour tests unitaires

Outils de test fondés sur les propriétés

Outils d'analyse statique

Outils d'analyse dynamique

  • Une vue d'ensemble et une comparaison de produits de test différents _
  • Comment utiliser Echidna pour tester les contrats intelligents
  • Comment utiliser Manticore pour trouver les bogues dans les contrats intelligents
  • Comment utiliser Slither pour trouver des bugs de contrat intelligent
  • Comment simuler des contrats Solidity pour les tests
  • Comment exécuter des tests unitaires dans Solidity en utilisant Foundry(opens in a new tab)

Complément d'information

Cet article vous a été utile ?