ITSkillsCenter
Développement Web

Écrire et tester un premier smart contract Solidity avec Foundry

13 min de lecture
Guide principal : Développer des smart contracts sur Ethereum : Solidity, outils et web3. Commencez par ce guide pour situer chaque tutoriel dans l’ensemble.

Un smart contract n’est rien d’autre qu’un programme déposé sur une blockchain : une fois publié, son code est public, immuable, et s’exécute exactement comme il a été écrit, sans serveur à administrer. Avant de toucher au moindre testnet ou à un portefeuille, la première compétence à acquérir est de savoir écrire un contrat, le compiler, puis prouver qu’il fonctionne avec des tests automatisés. C’est précisément ce que nous faisons ici, en local, sans dépenser un centime, avec Foundry — la chaîne d’outils de référence pour développer en Solidity.

À la fin de ce tutoriel, vous aurez un projet Foundry complet contenant un contrat de fidélité fonctionnel et une suite de tests verte. Ce contrat servira de fondation aux tutoriels suivants : nous le déploierons sur un réseau de test, puis le ferons évoluer en jeton et en interface web.

🎯 Ce que vous allez apprendre

  • Installer Foundry et initialiser un projet Solidity propre.
  • Écrire un contrat avec variables d’état, mapping, événements, erreurs personnalisées et contrôle d’accès.
  • Compiler le contrat et lire les messages du compilateur.
  • Écrire des tests en Solidity qui vérifient les cas nominaux et les cas d’échec attendus.
  • Interpréter le rapport de forge test et déboguer une assertion qui casse.

🛠️ Ce que vous allez construire

Un contrat RegistreFidelite qui tient le compte des points de fidélité d’une boutique : le gérant crédite des points à un client, n’importe qui peut consulter un solde, et chaque crédit émet un événement traçable. C’est un cas réel et minimal — assez simple pour tenir en trente lignes, assez complet pour rencontrer les briques fondamentales de Solidity.

Prérequis

  • Un système Linux, macOS, ou Windows avec WSL2 (Foundry s’installe nativement sur les systèmes Unix).
  • Une connaissance de base de la ligne de commande et des notions de programmation (variables, fonctions, conditions).
  • Aucun portefeuille ni cryptomonnaie n’est requis : tout se passe en local.
  • ⏱️ Temps estimé : 40 à 60 minutes.

Test express : si vous savez ouvrir un terminal et exécuter une commande, vous êtes prêt. La syntaxe de Solidity rappelle celle de JavaScript et de C : aucune expérience préalable de la blockchain n’est nécessaire.

Étape 1 — Installer Foundry

Foundry regroupe quatre outils : forge (compilation et tests), cast (interaction avec une chaîne), anvil (un nœud Ethereum local) et chisel (un REPL Solidity). On les installe tous d’un coup via le script officiel foundryup, qui gère ensuite les mises à jour.

curl -L https://foundry.paradigm.xyz | bash

Cette commande télécharge le gestionnaire foundryup et l’ajoute à votre PATH. Fermez puis rouvrez le terminal (ou rechargez votre profil shell), puis lancez l’installation des binaires :

foundryup

Vous devriez voir défiler le téléchargement de forge, cast, anvil et chisel, suivi d’un message de fin. Vérifiez que tout est en place :

forge --version

La sortie affiche un numéro de version et un hash de commit. Si le terminal répond command not found, c’est que le PATH n’a pas été rechargé : ouvrez un nouveau terminal et réessayez. À ce stade, l’environnement de développement est prêt.

Étape 2 — Initialiser le projet

Foundry sait générer un squelette de projet complet avec une arborescence conventionnelle. On crée le projet du registre de fidélité :

forge init registre-fidelite
cd registre-fidelite

Cette commande crée plusieurs dossiers : src/ pour les contrats, test/ pour les tests, script/ pour les scripts de déploiement, et lib/ où atterrissent les dépendances (la bibliothèque de test forge-std y est déjà installée). Le fichier foundry.toml centralise la configuration. Foundry place aussi un exemple Counter.sol que nous allons remplacer.

Supprimez les fichiers d’exemple pour repartir propre :

rm src/Counter.sol test/Counter.t.sol

Le projet est désormais vide de tout exemple, mais conserve sa structure et sa bibliothèque de test. Nous pouvons écrire notre propre contrat.

Étape 3 — Écrire le contrat RegistreFidelite

Avant de coder, posons le modèle mental. Un contrat ressemble à une classe : il a des variables d’état (stockées de façon permanente sur la chaîne), des fonctions (qui lisent ou modifient cet état) et des événements (des journaux que les applications peuvent écouter). Notre registre a besoin de retenir qui possède combien de points, de savoir qui a le droit de créditer, et d’annoncer chaque crédit.

Créez le fichier src/RegistreFidelite.sol avec ce contenu :

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;

contract RegistreFidelite {
    address public proprietaire;
    mapping(address => uint256) private soldes;

    event PointsCredites(address indexed client, uint256 montant, uint256 nouveauSolde);

    error NonAutorise();
    error MontantNul();

    constructor() {
        proprietaire = msg.sender;
    }

    modifier seulementProprietaire() {
        if (msg.sender != proprietaire) revert NonAutorise();
        _;
    }

    function crediter(address client, uint256 montant) external seulementProprietaire {
        if (montant == 0) revert MontantNul();
        soldes[client] += montant;
        emit PointsCredites(client, montant, soldes[client]);
    }

    function soldeDe(address client) external view returns (uint256) {
        return soldes[client];
    }
}

Décortiquons. La première ligne déclare la licence (le compilateur émet un avertissement si elle manque) et pragma solidity ^0.8.28; fixe la version du compilateur acceptée. La variable proprietaire est public, ce qui génère automatiquement une fonction de lecture. Le mapping associe une adresse à un solde, comme un dictionnaire. Le constructor s’exécute une seule fois, au déploiement, et fige le déployeur comme propriétaire via msg.sender (l’adresse qui appelle).

Le modifier seulementProprietaire est un garde réutilisable : il s’exécute avant le corps de la fonction (le _; marque l’endroit où ce corps s’insère) et annule la transaction si l’appelant n’est pas le propriétaire. Les error personnalisées (introduites en Solidity 0.8.4) remplacent les anciennes chaînes require : elles coûtent moins de gaz et sont plus faciles à tester. Enfin, view sur soldeDe signale une fonction de lecture pure, gratuite à appeler depuis l’extérieur.

Étape 4 — Compiler

Compiler traduit le Solidity en bytecode exécutable par la machine virtuelle Ethereum (l’EVM), et vérifie au passage la syntaxe et les types. C’est le premier filet de sécurité.

forge build

Si tout est correct, vous lisez Compiler run successful et Foundry écrit les artefacts (ABI et bytecode) dans le dossier out/. En cas d’erreur, le compilateur pointe le fichier et la ligne fautive : lisez le message de bas en haut, la première erreur est souvent la cause des suivantes. Une erreur fréquente au début est un point-virgule manquant ou une version de pragma incompatible.

Étape 5 — Écrire les tests

En Foundry, on teste le Solidity… avec du Solidity. Chaque test est une fonction dont le nom commence par test, regroupée dans un contrat qui hérite de Test (fourni par forge-std). Cet héritage donne accès à assertEq pour les assertions et à l’objet vm (le « cheat code ») qui permet de simuler des comptes ou d’attendre une annulation. On veut couvrir deux familles de cas : ce qui doit marcher, et ce qui doit échouer.

Créez test/RegistreFidelite.t.sol :

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;

import {Test} from "forge-std/Test.sol";
import {RegistreFidelite} from "../src/RegistreFidelite.sol";

contract RegistreFideliteTest is Test {
    RegistreFidelite private registre;
    address private client = address(0xBEEF);

    function setUp() public {
        registre = new RegistreFidelite();
    }

    function test_CrediteEtLitLeSolde() public {
        registre.crediter(client, 150);
        assertEq(registre.soldeDe(client), 150);
    }

    function test_CumuleLesCredits() public {
        registre.crediter(client, 100);
        registre.crediter(client, 50);
        assertEq(registre.soldeDe(client), 150);
    }

    function test_RevertSi_NonProprietaire() public {
        vm.prank(client);
        vm.expectRevert(RegistreFidelite.NonAutorise.selector);
        registre.crediter(client, 100);
    }

    function test_RevertSi_MontantNul() public {
        vm.expectRevert(RegistreFidelite.MontantNul.selector);
        registre.crediter(client, 0);
    }
}

La fonction setUp s’exécute avant chaque test et redéploie un registre neuf, garantissant des tests indépendants. Comme c’est le contrat de test qui déploie, il devient le propriétaire — d’où la réussite des crédits directs. Le test test_RevertSi_NonProprietaire utilise vm.prank(client) pour que l’appel suivant provienne de l’adresse du client (et non du propriétaire), puis vm.expectRevert avec le selector de l’erreur pour exiger l’annulation. C’est la manière idiomatique de tester un contrôle d’accès.

Étape 6 — Lancer les tests et lire le rapport

On exécute toute la suite d’un coup :

forge test

Foundry compile, déploie les contrats en mémoire, exécute chaque fonction de test et affiche un récapitulatif. La sortie attendue ressemble à Suite result: ok. 4 passed; 0 failed, avec le coût en gaz de chaque test. Une ligne verte par test signifie que le comportement observé correspond à vos assertions.

Pour comprendre ce qui se passe quand un test échoue — ou simplement pour observer les traces — augmentez la verbosité :

forge test -vvv

Le drapeau -vvv affiche les traces d’appel et, en cas d’échec, la valeur attendue face à la valeur obtenue. C’est l’outil de débogage numéro un : si assertEq casse, vous voyez immédiatement les deux nombres comparés et pouvez remonter à la cause.

Point d’étape — Votre projet doit compiler sans erreur et afficher 4 tests au vert. Pour vérifier : relancez forge test. Si un test échoue, lisez la trace avec -vvv et comparez la valeur attendue à la valeur obtenue avant de toucher au contrat.

Étape 7 — Vérifier l’émission d’un événement

Les événements sont essentiels : c’est par eux qu’une interface web saura qu’un crédit a eu lieu. Foundry permet de vérifier qu’un événement est bien émis, avec les bonnes valeurs, grâce à vm.expectEmit. Ajoutez ce test au contrat de test :

function test_EmetLevenement() public {
    vm.expectEmit(true, false, false, true);
    emit RegistreFidelite.PointsCredites(client, 75, 75);
    registre.crediter(client, 75);
}

Les quatre booléens de vm.expectEmit indiquent quels champs vérifier : les trois premiers correspondent aux paramètres indexed (ici seul client l’est, donc true, false, false) et le dernier aux données non indexées (true pour comparer montant et nouveauSolde). On « déclare » d’abord l’événement attendu, puis on appelle la fonction réelle. Relancez forge test : vous devez maintenant compter 5 tests au vert. Si le test échoue, c’est que les valeurs émises ne correspondent pas — un excellent moyen de détecter une régression silencieuse.

Comprendre ce qui se passe vraiment

Pour bien tester, il faut un modèle mental clair de l’exécution. Sur Ethereum, deux types d’appels coexistent. Les fonctions qui modifient l’état — comme crediter — exigent une transaction : elle est signée par un compte, propagée au réseau, exécutée par chaque nœud, puis inscrite dans un bloc. Chaque opération consomme du gaz, une unité qui mesure le travail de calcul et que l’émetteur paie. À l’inverse, les fonctions view comme soldeDe ne changent rien : on peut les interroger gratuitement, sans transaction, car le nœud répond à partir de l’état qu’il détient déjà.

Cette distinction explique pourquoi nos tests sont fiables. L’EVM est déterministe : pour un même état de départ et une même transaction, le résultat est toujours identique, sur n’importe quelle machine. Foundry exploite cette propriété en rejouant vos contrats dans une EVM en mémoire — d’où sa rapidité et sa reproductibilité. Quand un test passe chez vous, il passera de la même façon en intégration continue. Et quand crediter est appelé par la mauvaise adresse, l’annulation (revert) restaure intégralement l’état d’avant l’appel : aucune modification partielle n’est jamais enregistrée. C’est cette logique du « tout ou rien » qui rend les smart contracts prévisibles, et c’est précisément ce que vos tests doivent verrouiller avant tout déploiement.

🐞 Pièges fréquents

Symptôme / erreur Cause probable Correctif
forge: command not found PATH non rechargé après l’installation Ouvrir un nouveau terminal ou recharger le profil shell, puis relancer foundryup
Source file requires different compiler version Le pragma exige une version absente Lancer foundryup pour mettre Foundry à jour, ou ajuster le pragma
Error (2018): Function state mutability can be restricted to view Une fonction ne modifie pas l’état mais n’est pas marquée view Ajouter view à la signature
Test d’accès qui passe alors qu’il devrait échouer Oubli de vm.prank : l’appel vient du propriétaire Préfixer l’appel par vm.prank(autreAdresse)
EvmError: Revert inattendu Une condition revert est déclenchée Relancer avec -vvv pour voir quelle erreur personnalisée a été levée

✅ Récapitulatif

Vous êtes parti d’un environnement vide et vous disposez maintenant d’un projet Foundry opérationnel, d’un contrat RegistreFidelite qui gère des soldes avec contrôle d’accès et événements, et d’une suite de cinq tests couvrant les cas nominaux et les échecs attendus. Vous savez compiler, exécuter les tests, et utiliser -vvv pour déboguer. Ces réflexes — écrire, compiler, tester, lire les traces — sont exactement ceux d’un développeur de smart contracts au quotidien.

🧾 Aide-mémoire

Commande / élément Rôle
foundryup Installer ou mettre à jour les outils Foundry
forge init <nom> Créer un projet avec son arborescence
forge build Compiler les contrats
forge test Exécuter la suite de tests
forge test -vvv Tests avec traces détaillées pour déboguer
vm.prank(addr) Simuler que le prochain appel vient de addr
vm.expectRevert(...) Exiger que l’appel suivant échoue
vm.expectEmit(...) Vérifier l’émission d’un événement

💪 À vous de jouer

Ajoutez une fonction debiter(address client, uint256 montant) qui retire des points, réservée au propriétaire, et qui annule avec une erreur SoldeInsuffisant si le client n’a pas assez de points. Écrivez ensuite les tests correspondants : un débit valide, et un débit qui dépasse le solde.

Voir une solution
error SoldeInsuffisant();

function debiter(address client, uint256 montant) external seulementProprietaire {
    if (soldes[client] < montant) revert SoldeInsuffisant();
    soldes[client] -= montant;
    emit PointsCredites(client, montant, soldes[client]);
}

// Test
function test_RevertSi_SoldeInsuffisant() public {
    registre.crediter(client, 10);
    vm.expectRevert(RegistreFidelite.SoldeInsuffisant.selector);
    registre.debiter(client, 50);
}

Pensez à un événement dédié au débit si vous voulez distinguer les deux opérations dans une interface.

Tutoriels frères

Pour aller plus loin

FAQ

Foundry ou Hardhat pour débuter ?
Les deux sont d’excellents choix. Foundry a l’avantage de tester en Solidity (un seul langage à apprendre) et d’être très rapide. Hardhat, en JavaScript/TypeScript, s’intègre naturellement à un projet web. Ce tutoriel privilégie Foundry pour sa simplicité d’entrée.

Pourquoi des erreurs personnalisées plutôt que require avec un message ?
Depuis Solidity 0.8.4, les erreurs personnalisées consomment moins de gaz que les chaînes de caractères et se testent par leur selector, ce qui rend les tests plus robustes face aux changements de formulation.

Mes tests passent : mon contrat est-il sûr ?
Des tests verts prouvent que les comportements testés sont corrects, pas qu’il n’existe aucune faille. La sécurité d’un contrat se travaille à part — c’est l’objet d’un tutoriel dédié de cette série.

Partager
Service ITSkillsCenter

Application mobile Android et iOS

Création d'application mobile Android et iOS. À partir de 350 000 FCFA.

Démarrer mon projet
Publicité