Pour apprendre Solidity, la voie la plus rapide consiste à mener un seul petit contrat jusqu’au bout : l’écrire, le compiler, le tester, l’attaquer, le corriger. C’est le programme de cette leçon. En une heure environ, sur votre propre machine, vous aurez une tirelire qui garde de l’ether pour chaque utilisateur, une suite de tests qui vérifie les dépôts, les retraits, les erreurs et les droits d’accès, un test de fuzzing qui lui lance des montants au hasard, et une copie volontairement défectueuse qu’un second contrat vide par une attaque de réentrance. Pas de wallet, pas d’argent réel, pas de réseau.
Une règle encadre tout le reste. Un smart contract est un code public qui détient de l’argent et qu’on ne corrige pas discrètement une fois déployé : un bogue y est une erreur publique, définitive et souvent coûteuse. C’est pourquoi les tests et les attaques arrivent dès la première heure, et non après la syntaxe.
Tout s’exécute sur une chaîne locale. Le déploiement sur un réseau de test public, et le branchement d’une page web sur le contrat, relèvent d’une prochaine leçon de ce cours.
Ce qu’il faut savoir avant d’apprendre le langage Solidity
C’est la leçon 3 du parcours métiers, et elle suppose la précédente : notre guide pour devenir développeur blockchain explique pourquoi Solidity est le langage d’Ethereum et des chaînes qui copient sa machine virtuelle, et ce qu’blockchain that runs exactly as written when it is called, holding and moving funds according to its code, without an intermediary able to change the outcome.">smart contract holding reserves of two or more tokens, supplied by users, that a decentralized exchange uses to execute trades automatically at a price set by a formula.">pool diverge, compared with simply holding the same tokens in a wallet. It becomes permanent when the provider withdraws.">il faut apprendre d’abord. Concrètement, vous devez être à l’aise avec un langage de programmation (JavaScript, Python, Go ou Rust se transposent bien), avec un terminal et avec Git. Les formations françaises et les écoles sont déjà passées en revue dans cette première leçon : nous n’y revenons pas ici.
Il vous faut aussi un modèle mental de la chaîne. Un contrat s’exécute sur Ethereum ou sur un réseau compatible ; il ne fait quelque chose que lorsqu’une transaction ou un autre contrat l’appelle ; et chaque appel est signé par un compte qui détient une clé privée. Si msg.sender, plus bas, vous paraît abstrait, lisez d’abord comment une transaction crypto est construite et signée. Sur un vrai réseau, chaque étape qui écrit des données coûte des frais de gas ; sur la chaîne de test locale utilisée ici, le gas est bien mesuré mais payé avec de l’ether fictif, donc rien ne vous coûte un centime.
Un mot sur la langue. Tout l’outillage de cette leçon (Foundry, OpenZeppelin, Slither) est documenté en anglais, et les messages du compilateur sont en anglais. La documentation officielle de Solidity existe en revanche en français, à l’adresse docs.soliditylang.org/fr/latest : elle est utile pour lire la référence du langage dans votre langue. Une traduction peut toutefois avoir du retard sur la version anglaise, alors regardez le bandeau de version en haut de page avant de vous fier à un détail. Dans cette leçon, les identifiants et les commandes restent en anglais, comme dans tout projet réel.
Installer un environnement Solidity : Remix dans le navigateur, Foundry sur votre machine
Deux installations méritent d’être connues. Remix ne demande rien et reste le moyen le plus rapide de voir un contrat tourner. Foundry est une boîte à outils en ligne de commande et sert de base au projet de cette leçon, parce que ses tests s’écrivent en Solidity.
Remix : essayer un contrat en 20 minutes sans rien installer
Remix est un environnement de développement qui tourne dans le navigateur, sur remix.ethereum.org (une application de bureau existe aussi). Vous créez un fichier, vous y collez un contrat, vous le compilez depuis le panneau latéral et vous le déployez sur la Remix VM, que la documentation de Remix décrit comme un bac à sable blockchain dans le navigateur. L’état de ce bac à sable est conservé entre deux rechargements de la page, sauf si vous désactivez l’option dans les paramètres, mais le stockage du navigateur est fragile : gardez ce qui compte dans un dépôt Git.
Utilisez Remix pour tester une idée ou suivre un court tutoriel. Pour un projet avec des tests, du contrôle de version et des bibliothèques, passez à une boîte à outils locale.
Foundry : installer forge, cast, anvil et chisel avec foundryup
Foundry rassemble quatre outils en ligne de commande : forge compile et teste les contrats, cast dialogue avec une chaîne depuis le terminal, anvil fait tourner un nœud Ethereum local et chisel est un interpréteur Solidity pour essayer des lignes isolées. La page d’installation officielle (en anglais) donne une installation en deux temps sous macOS et Linux :
curl -L https://foundry.paradigm.xyz | bash
Ouvrez ensuite un nouveau terminal (ou rechargez le profil de votre shell, par exemple avec source ~/.zshrc) et lancez :
foundryup
forge --version
La première ligne télécharge l’installeur foundryup ; foundryup récupère ensuite la dernière version stable des quatre outils et, d’après la documentation, vérifie l’empreinte SHA-256 de chaque binaire par rapport à l’attestation de la version sur GitHub. Faire passer un script d’Internet directement dans son shell est un choix de confiance : l’adresse ci-dessus est celle que donne la documentation de Foundry, vous pouvez ouvrir le script et le lire avant, ou utiliser plutôt l’image Docker officielle (ghcr.io/foundry-rs/foundry:latest).
Sous Windows, la documentation demande Git Bash ou WSL ; PowerShell et l’invite de commandes ne sont pas pris en charge. Au 27 octobre 2026, la dernière version stable de Foundry sur GitHub est la v1.8.6 (21 octobre 2026, une version corrective pour des mises à jour de sécurité de dépendances), et des versions nightly sortent chaque jour. Relancez foundryup de temps en temps pour mettre à jour.
Pourquoi cette leçon utilise Foundry plutôt que Hardhat
Hardhat est l’autre grande boîte à outils pour les contrats Ethereum, et la leçon précédente présentait les deux. Ce sont deux outils sérieux et maintenus, et le choix relève du goût. La raison retenue ici est pratique : dans Foundry, un test est simplement un autre contrat Solidity, si bien que chaque test écrit vous fait pratiquer le langage que vous apprenez, sans couche JavaScript à installer à côté. Rien de ce que vous apprenez ici n’est perdu si un futur employeur utilise Hardhat : le code du contrat est le même.
Créer le projet et lire ce que forge init a produit
Créez le dossier du projet. La première forme conserve le contrat d’exemple Counter de Foundry, pratique pour un premier coup d’œil ; la seconde part d’un dossier vide.
forge init piggybank
ou, sans les fichiers d’exemple :
forge init --empty piggybank
cd piggybank
forge build
forge test
Nous avons lu le code source de forge init dans Foundry v1.8.6 pour vérifier ce qu’il crée. Vous obtenez src/ pour les contrats, test/ pour les tests, script/ pour les scripts de déploiement, lib/ avec la bibliothèque de test forge-std installée, un fichier de configuration foundry.toml et un README. Sauf si vous passez --no-git, la commande fait aussi du dossier un dépôt Git et ajoute un .gitignore et un workflow GitHub Actions qui lance les tests, quand ces fichiers n’existent pas encore. Sans --empty, les fichiers d’exemple sont src/Counter.sol, test/Counter.t.sol et script/Counter.s.sol : lancez une fois forge test dessus pour vérifier que l’installation fonctionne, puis supprimez-les.
Une mise en garde sur les anciens tutoriels : beaucoup montrent forge init --no-commit ou forge install --no-commit. Les versions récentes de Foundry ne créent plus de commit par défaut et la référence actuelle ne liste que --commit : abandonnez l’ancien drapeau. La bibliothèque forge-std que forge init installe en était à la v1.17.0 (30 septembre 2026) au 27 octobre 2026.
Côté éditeur, VS Code avec une extension Solidity apporte la coloration syntaxique et le saut vers les définitions. forge init --vscode écrit un fichier .vscode/settings.json et un remappings.txt pour que l’extension retrouve les bibliothèques de lib/ ; d’après le code source de Foundry, ces réglages visent l’extension vscode-solidity de Juan Blanco. D’autres extensions conviennent aussi : choisissez-en une qui est toujours mise à jour.
Apprendre le langage Solidity en écrivant un premier contrat, ligne par ligne
Le contrat est une tirelire. N’importe qui peut y déposer de l’ether, chaque adresse peut retirer son propre solde et rien d’autre, et le propriétaire (le compte qui l’a déployé) peut suspendre les nouveaux dépôts. Il est assez court pour se lire en entier, et chaque ligne enseigne une particularité du langage. Le voici au complet ; enregistrez-le sous src/PiggyBank.sol.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;
/// @notice Each user can deposit ether and withdraw their own balance.
contract PiggyBank {
address public immutable owner;
bool public paused;
mapping(address => uint256) public balanceOf;
event Deposited(address indexed account, uint256 amount);
event Withdrawn(address indexed account, uint256 amount);
error NotOwner();
error IsPaused();
error ZeroAmount();
error InsufficientBalance(uint256 available, uint256 requested);
error TransferFailed();
modifier onlyOwner() {
require(msg.sender == owner, NotOwner());
_;
}
constructor() {
owner = msg.sender;
}
function setPaused(bool newState) external onlyOwner {
paused = newState;
}
function deposit() external payable {
if (paused) revert IsPaused();
if (msg.value == 0) revert ZeroAmount();
balanceOf[msg.sender] += msg.value;
emit Deposited(msg.sender, msg.value);
}
function withdraw(uint256 amount) external {
uint256 available = balanceOf[msg.sender];
if (amount > available) revert InsufficientBalance(available, amount);
// Effets d'abord : on met à jour le solde AVANT d'envoyer l'ether.
balanceOf[msg.sender] = available - amount;
emit Withdrawn(msg.sender, amount);
// Interaction en dernier.
(bool ok, ) = msg.sender.call{value: amount}("");
require(ok, TransferFailed());
}
}
L’en-tête : licence SPDX et pragma
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;
La première ligne est une étiquette de licence lisible par une machine. Le compilateur émet un avertissement quand elle manque ; MIT est un choix courant pour du code ouvert. La seconde ligne est le pragma : ^0.8.28 signifie « toute version du compilateur à partir de 0.8.28, jusqu’à 0.9.0 exclu ». Par défaut (le réglage auto_detect_solc est activé), Foundry lit le pragma, choisit une version du compilateur qui le satisfait et la télécharge si elle n’est pas encore sur votre machine.
Pourquoi 0.8.28 comme plancher ? Le contrat utilise require avec une erreur personnalisée. D’après le journal des modifications de Solidity, la version 0.8.26 (mai 2024) a introduit cette forme pour le seul pipeline via-IR, et la 0.8.27 (septembre 2024) l’a rendue disponible avec le pipeline classique par défaut, celui que la plupart des projets utilisent. Un pragma indique la plus ancienne version du compilateur avec laquelle votre code est connu pour fonctionner : nous avons compilé tous les fichiers de cette leçon avec solc 0.8.28 et 0.8.38, sans erreur ni avertissement. N’écrivez pas un >=0.8.0 sans limite haute sur du code qui détient de l’argent : vous promettriez qu’il compile et se comporte pareil sur des versions que vous n’avez jamais essayées.
Variables d’état, mappings et visibilité
address public immutable owner;
bool public paused;
mapping(address => uint256) public balanceOf;
Ces trois lignes forment le stockage du contrat, conservé sur la chaîne entre deux appels. owner est une adresse marquée immutable : elle est fixée une fois dans le constructeur, puis intégrée au code déployé, ce qui la rend moins chère à lire et impossible à modifier. paused est un simple booléen qui vaut false au départ.
Le mapping est le cœur de la banque. Il fonctionne comme une table de hachage entre une adresse et un nombre, avec une particularité sur laquelle insiste la documentation de Solidity : toute clé possible « existe » déjà avec la valeur zéro, et l’on ne peut pas lister les clés. Ainsi balanceOf[quelqu'un] vaut 0 tant que cette personne n’a rien déposé, et si vous avez besoin de la liste des déposants, c’est à vous de la stocker.
Le mot public fait écrire au compilateur une fonction de lecture gratuite, de sorte que tout le monde peut appeler owner(), paused() ou balanceOf(address). Les fonctions ont quatre niveaux de visibilité : public, external (appelable seulement de l’extérieur), internal (ce contrat et ceux qui en héritent) et private (ce contrat seul). Privé ne veut pas dire secret : tout le stockage d’un contrat peut être lu par quiconque interroge la chaîne.
Événements et erreurs personnalisées
event Deposited(address indexed account, uint256 amount);
event Withdrawn(address indexed account, uint256 amount);
error NotOwner();
error IsPaused();
error ZeroAmount();
error InsufficientBalance(uint256 available, uint256 requested);
error TransferFailed();
Les événements sont des journaux. Un contrat ne peut pas les lire, mais les wallets, les explorateurs de blocs et les applications décentralisées bâties au-dessus d’un contrat les écoutent pour mettre un écran à jour sans relire tout le stockage. Marquer account comme indexed permet à une application de filtrer le journal, par exemple « tous les dépôts de cette adresse ».
Les erreurs personnalisées donnent un nom, et au besoin des données, à un appel qui échoue. InsufficientBalance(available, requested) dit exactement à l’appelant ce qui s’est passé, ce qui aide les utilisateurs comme les tests. Elles sont aussi moins chères que de longs messages de revert, car seuls un sélecteur de 4 octets et les arguments sont renvoyés.
Le modificateur, le constructeur et msg.sender
modifier onlyOwner() {
require(msg.sender == owner, NotOwner());
_;
}
constructor() {
owner = msg.sender;
}
function setPaused(bool newState) external onlyOwner {
paused = newState;
}
msg.sender est l’adresse qui a émis l’appel en cours : un compte qui signe une transaction, ou un autre contrat qui appelle celui-ci. Dans le constructeur, qui s’exécute une fois au déploiement, c’est le déployeur, et cette adresse devient donc le propriétaire.
Un modificateur enveloppe une fonction. onlyOwner exécute sa vérification, puis l’emplacement _; exécute le corps de la fonction qu’il décore. require(condition, NotOwner()) prend d’abord la condition qui doit être vraie : si elle est fausse, l’appel est annulé avec l’erreur personnalisée. C’est l’image miroir de la forme if (...) revert employée plus bas dans deposit, où le if nomme la situation qui ne doit pas se produire. Les deux donnent la même erreur à l’appelant ; prenez celle qui se lit le mieux. Un revert annule tous les changements de l’appel, comme s’il n’avait jamais eu lieu. setPaused est external parce que rien, à l’intérieur du contrat, ne l’appelle.
deposit : fonctions payable et msg.value
function deposit() external payable {
if (paused) revert IsPaused();
if (msg.value == 0) revert ZeroAmount();
balanceOf[msg.sender] += msg.value;
emit Deposited(msg.sender, msg.value);
}
Seule une fonction marquée payable peut recevoir de l’ether ; envoyez de l’ether à n’importe quelle autre fonction et l’appel est annulé. msg.value est le montant envoyé, en wei (1 ether vaut 1018 wei ; le code peut écrire directement 1 ether). La fonction vérifie d’abord, avec la forme if (...) revert Erreur();, qui est l’autre façon de lever une erreur personnalisée et fonctionne sur tous les compilateurs 0.8 depuis la 0.8.4. Elle met ensuite le stockage à jour, puis émet l’événement.
withdraw : pourquoi l’ordre de trois lignes compte
function withdraw(uint256 amount) external {
uint256 available = balanceOf[msg.sender];
if (amount > available) revert InsufficientBalance(available, amount);
// Effets d'abord : on met à jour le solde AVANT d'envoyer l'ether.
balanceOf[msg.sender] = available - amount;
emit Withdrawn(msg.sender, amount);
// Interaction en dernier.
(bool ok, ) = msg.sender.call{value: amount}("");
require(ok, TransferFailed());
}
Lisez l’ordre. D’abord la vérification : l’appelant a-t-il assez ? Ensuite l’effet : le solde baisse dans le stockage. Ce n’est qu’à la fin que vient l’interaction, l’envoi d’ether avec l’instruction de bas niveau .call{value: amount}(""), qui renvoie un indicateur de réussite que le code doit tester : require(ok, TransferFailed()) annule tout le retrait, mise à jour du solde comprise, quand le transfert échoue. Cette séquence a un nom, « checks-effects-interactions » (vérifications, effets, interactions), et la section sur la sécurité montre ce qui arrive quand les deux dernières étapes échangent leur place.
C’est aussi le modèle du retrait à l’initiative de l’utilisateur (« pull ») que recommande la documentation de Solidity : les gens viennent retirer leur propre argent, au lieu que le contrat l’envoie à une liste de destinataires, où un seul destinataire défaillant pourrait bloquer tout le monde.
Compiler avec forge build
Supprimez les fichiers Counter si vous les aviez gardés, enregistrez le contrat et lancez forge build. Si aucun compilateur solc adapté n’est encore installé, Foundry en télécharge un d’abord ; il écrit ensuite les artefacts du contrat dans out/, le dossier de sortie par défaut d’après la référence de configuration. Pour chaque contrat, l’artefact contient l’ABI (la liste des fonctions, événements et erreurs que d’autres programmes utilisent pour l’appeler) et le bytecode qui serait déployé. Une faute de frappe arrête la compilation avec le fichier, la ligne et un message ; les avertissements ne l’arrêtent pas, mais traitez chacun comme un rapport de bogue.

Une précision sur nos propres vérifications : nous avons compilé ce contrat et chaque fichier de test de cette leçon avec le compilateur officiel solc-js, versions 0.8.28 et 0.8.38, contre forge-std v1.17.0 et OpenZeppelin Contracts v5.7.0. Tout a compilé sans avertissement. Nous avons suivi la logique des tests à la main, mais nous n’avons pas lancé forge sur cette machine : nous n’affichons donc pas la sortie de forge. Exécutez les commandes et lisez la vôtre.
Tester un smart contract avec Foundry : forge test, cheatcodes et fuzzing
Un contrat que vous n’avez pas testé est un contrat que vous n’avez pas fini. Dans Foundry, les tests sont des contrats Solidity placés dans le dossier test/, dans des fichiers qui se terminent par .t.sol, comme l’expose le guide des tests de Foundry (en anglais). Enregistrez ce qui suit sous test/PiggyBank.t.sol. La première moitié plante le décor et vérifie les cas normaux.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;
import {Test} from "forge-std/Test.sol";
import {PiggyBank} from "../src/PiggyBank.sol";
contract PiggyBankTest is Test {
PiggyBank bank;
address alice = makeAddr("alice");
address bob = makeAddr("bob");
// Même événement que dans le contrat, pour que le test dise ce qu'il attend.
event Deposited(address indexed account, uint256 amount);
function setUp() public {
bank = new PiggyBank(); // ce contrat de test devient le propriétaire
vm.deal(alice, 10 ether);
vm.deal(bob, 10 ether);
}
function test_DepositUpdatesBalance() public {
vm.prank(alice);
bank.deposit{value: 1 ether}();
assertEq(bank.balanceOf(alice), 1 ether);
assertEq(address(bank).balance, 1 ether);
}
function test_DepositEmitsEvent() public {
vm.expectEmit(true, false, false, true);
emit Deposited(alice, 1 ether);
vm.prank(alice);
bank.deposit{value: 1 ether}();
}
function test_WithdrawReturnsEther() public {
vm.prank(alice);
bank.deposit{value: 1 ether}();
vm.prank(alice);
bank.withdraw(1 ether);
assertEq(alice.balance, 10 ether);
assertEq(bank.balanceOf(alice), 0);
}
// ... les tests de revert et de fuzzing suivent plus bas
}
Comment un test Foundry est construit
Le contrat de test hérite de Test, fourni par forge-std, qui apporte les assertions et les cheatcodes. setUp() s’exécute avant chaque test, de sorte que chacun part d’une banque neuve : les tests ne dépendent jamais les uns des autres. Toute fonction dont le nom commence par test est un test ; la convention est test_ suivi de ce qu’il prouve.
assertEq(a, b) fait échouer le test quand les deux valeurs diffèrent. makeAddr("alice") renvoie une adresse neuve et l’étiquette « alice » dans les traces, pour que vous lisiez des noms plutôt que de l’hexadécimal. Dans setUp, c’est le contrat de test qui déploie la banque : il en est donc lui-même le propriétaire, un détail qui compte pour le test de pause ci-dessous.
Les cheatcodes : vm.prank, vm.deal et vm.expectEmit
Les cheatcodes sont des fonctions spéciales de l’environnement de test, atteintes par l’objet vm. Derrière lui se trouve une adresse fixe, 0x7109709ECfa91a80626fF3989D68f67F5b1DD12D, que Foundry intercepte. Les trois utilisés jusqu’ici :
vm.deal(alice, 10 ether)fixe le solde en ether d’Alice à partir de rien. Cela n’existe que dans l’environnement de test.vm.prank(alice)fait venir d’Alice l’appel suivant. Dans les mots de la référence de Foundry, il « sets msg.sender to the specified address for the next call » (en anglais). Seulement le suivant : c’est pourquoi le test de retrait l’appelle deux fois.vm.expectEmit(true, false, false, true)annonce que l’appel suivant doit émettre un événement. Les quatre booléens disent quelles parties comparer : le premier champ indexé (iciaccount), le deuxième et le troisième (inutilisés), et les données (amount). Vous émettez ensuite l’événement attendu, recopié dans le contrat de test, puis vous faites l’appel.
La seconde moitié du fichier teste ce qui doit échouer, plus un test de fuzzing. Collez-la à la place de la ligne de commentaire.
function test_RevertWhen_DepositIsZero() public {
vm.expectRevert(PiggyBank.ZeroAmount.selector);
vm.prank(alice);
bank.deposit();
}
function test_RevertWhen_WithdrawingTooMuch() public {
vm.prank(alice);
bank.deposit{value: 1 ether}();
vm.expectRevert(
abi.encodeWithSelector(PiggyBank.InsufficientBalance.selector, 1 ether, 2 ether)
);
vm.prank(alice);
bank.withdraw(2 ether);
}
function test_RevertWhen_NotOwnerPauses() public {
vm.expectRevert(PiggyBank.NotOwner.selector);
vm.prank(bob);
bank.setPaused(true);
}
function test_OwnerCanPause() public {
bank.setPaused(true); // msg.sender est ce contrat de test, le propriétaire
vm.expectRevert(PiggyBank.IsPaused.selector);
vm.prank(alice);
bank.deposit{value: 1 ether}();
}
function testFuzz_DepositThenWithdraw(uint256 amount) public {
amount = bound(amount, 1, 100 ether);
vm.deal(alice, amount);
vm.prank(alice);
bank.deposit{value: amount}();
vm.prank(alice);
bank.withdraw(amount);
assertEq(alice.balance, amount);
assertEq(bank.balanceOf(alice), 0);
}
vm.expectRevert dit que l’appel suivant doit être annulé, et avec quelle erreur. Une erreur personnalisée sans argument se reconnaît à son sélecteur, PiggyBank.ZeroAmount.selector ; une erreur avec arguments exige l’erreur complète encodée, que construit abi.encodeWithSelector. La documentation précise que les autres appels de cheatcodes placés entre expectRevert et l’appel qui échoue sont ignorés : le vm.prank intercalé ne pose donc pas de problème. Les tests d’échec comptent autant que les tests de réussite : test_RevertWhen_NotOwnerPauses est celui qui prouve que le contrôle d’accès fonctionne.
Les tests de fuzzing : laisser Foundry inventer les entrées
Une fonction de test avec des paramètres devient un test de fuzzing (on dit aussi test à données aléatoires). Foundry appelle testFuzz_DepositThenWithdraw avec de nombreuses valeurs aléatoires de amount, 256 exécutions par test par défaut d’après la référence de configuration des tests. Si une entrée casse une assertion, le test échoue et forge indique l’entrée responsable.
bound(amount, 1, 100 ether) ramène n’importe quel nombre aléatoire dans l’intervalle qui vous intéresse. L’alternative, vm.assume(condition), écarte les entrées qui ne conviennent pas, et trop de rejets font échouer l’exécution (la limite par défaut est de 65 536) ; les propres guides de Foundry préfèrent bound. Pour lancer plus de cas, ajoutez ceci à foundry.toml :
[fuzz]
runs = 1000
Lancer la suite et lire les échecs
forge test
forge test -vv
forge test --match-test testFuzz_DepositThenWithdraw -vvv
Chaque « v » supplémentaire montre davantage. Le guide des tests liste les niveaux :
| Option | Ce que forge affiche |
|---|---|
| (aucune) | Un résumé : quels tests ont réussi ou échoué |
| -vv | Les journaux émis pendant les tests |
| -vvv | Les traces d’exécution des tests qui échouent |
| -vvvv | Les traces de tous les tests, préparation comprise |
| -vvvvv | Les changements de stockage en plus |
| –match-test, –match-contract, –match-path | N’exécute que les tests dont le nom, le contrat ou le fichier correspond |
Quand un test échoue, relancez-le seul avec --match-test et -vvv : la trace montre chaque appel, qui l’a fait, ce qu’il a renvoyé et où il a été annulé.
L’étape qui suit le fuzzing est le test d’invariant : une fonction nommée invariant_... énonce quelque chose qui doit rester vrai quelle que soit la suite d’appels, par exemple « le solde en ether de la banque n’est jamais inférieur à la somme des soldes des utilisateurs ». Foundry appelle alors les fonctions du contrat dans un ordre aléatoire (256 exécutions de profondeur 500 par défaut) et vérifie l’énoncé après chaque appel. Le guide des tests d’invariant (en anglais) montre le schéma ; essayez-le une fois que les tests unitaires vous seront naturels.
Lancer une chaîne locale avec anvil et l’interroger avec cast
Les tests s’exécutent dans la machine virtuelle en mémoire de forge, donc aucun nœud n’est nécessaire. Quand vous voulez une chaîne qui se comporte comme un vrai réseau, lancez anvil dans un second terminal. D’après sa page de référence, il écoute sur 127.0.0.1 :8545 et crée 10 comptes de développement dotés chacun de 10 000 ether de test. Interrogez-le ensuite avec cast :
anvil
Dans un autre terminal, en remplaçant l’indicateur par l’une des adresses de test qu’affiche anvil :
cast balance <address> --ether --rpc-url http://127.0.0.1:8545
Les clés des comptes de développement d’anvil sont publiques : n’envoyez jamais de fonds réels à ces adresses. Plus généralement, ne collez jamais la clé d’un vrai wallet dans un projet de tutoriel, un fichier .env ou un dépôt Git ; si les clés sont nouvelles pour vous, lisez comment fonctionnent les clés privées et publiques. Le déploiement sur anvil, puis sur un réseau de test public, passe par des scripts forge, que couvre une prochaine leçon.
Sécurité des smart contracts : réentrance, contrôle d’accès, tx.origin, dépassements
Les contrats sont attaqués parce qu’ils détiennent de la valeur et que leur code est public. La plupart des pertes ne viennent pas d’une cryptographie de la chaîne qu’on aurait brisée, mais d’une logique de contrat qui fait ce que son auteur n’avait pas prévu, comme l’explique notre guide pour savoir si une blockchain peut être piratée. La documentation de Solidity consacre une page aux considérations de sécurité (en anglais ; la traduction française existe, vérifiez sa version) que tout débutant devrait lire une fois. Voici les classiques, en commençant par un que vous pouvez exécuter.
La réentrance, démontrée sur votre propre machine
Voici une banque écrite mal exprès, un contrat attaquant et un test qui prouve l’attaque. C’est un exemple de manuel, exécuté seulement sur la chaîne de test de Foundry avec des adresses inventées. Enregistrez-le sous test/Reentrancy.t.sol ; ne déployez ce code nulle part.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;
import {Test} from "forge-std/Test.sol";
// MAUVAIS EXPRÈS : envoie l'ether avant de mettre à jour le solde.
contract VulnerableBank {
mapping(address => uint256) public balanceOf;
function deposit() external payable {
balanceOf[msg.sender] += msg.value;
}
function withdraw() external {
uint256 amount = balanceOf[msg.sender];
require(amount > 0, "nothing to withdraw");
(bool ok, ) = msg.sender.call{value: amount}("");
require(ok, "send failed");
balanceOf[msg.sender] = 0; // trop tard
}
}
contract Attacker {
VulnerableBank public immutable bank;
constructor(VulnerableBank _bank) {
bank = _bank;
}
function attack() external payable {
bank.deposit{value: msg.value}();
bank.withdraw();
}
// S'exécute chaque fois que la banque envoie de l'ether à ce contrat.
receive() external payable {
if (address(bank).balance >= 1 ether) {
bank.withdraw();
}
}
}
contract ReentrancyTest is Test {
function test_AttackerDrainsVulnerableBank() public {
VulnerableBank bank = new VulnerableBank();
address victim = makeAddr("victim");
address thief = makeAddr("thief");
vm.deal(victim, 10 ether);
vm.deal(thief, 1 ether);
vm.prank(victim);
bank.deposit{value: 10 ether}();
Attacker attacker = new Attacker(bank);
vm.prank(thief);
attacker.attack{value: 1 ether}();
assertEq(address(bank).balance, 0);
assertEq(address(attacker).balance, 11 ether);
}
}
Suivez l’ether. La victime dépose 10 ether. Le contrat attaquant en dépose 1, donc la banque en détient 11 et enregistre 1 pour l’attaquant. Il appelle alors withdraw. La banque lit le solde (1), envoie 1 ether, et ce n’est qu’après qu’elle mettrait le solde à zéro.
Or envoyer de l’ether à un contrat exécute la fonction receive de ce contrat, et l’instruction de bas niveau call transmet presque tout le gas restant. La fonction receive de l’attaquant appelle de nouveau withdraw. La banque a toujours l’attaquant en mémoire avec 1 ether, car la ligne qui le remet à zéro n’a pas encore été exécutée, et elle paie donc une seconde fois. Elle paie onze fois en tout, jusqu’à être vide. Ensuite, chaque appel imbriqué se termine et met le solde à zéro, beaucoup trop tard.
Les deux assertions vérifient le résultat : la banque finit à 0 et l’attaquant à 11 ether, soit les 10 de la victime plus son propre 1. La page de sécurité de Solidity donne à la réentrance sa propre section, tout en haut de ses pièges, juste après les informations privées et le hasard, avec un exemple vulnérable proche de celui-ci.

Le remède est l’ordre des lignes que vous avez déjà écrit dans PiggyBank.withdraw : mettre à jour le solde avant l’appel externe. Quand l’attaquant revient, son solde est déjà nul et la vérification l’arrête. Cet ordre est le schéma checks-effects-interactions, que la documentation de Solidity recommande dans sa liste de conseils généraux. Une seconde couche est un garde-fou qui refuse purement et simplement la rentrée. ReentrancyGuard d’OpenZeppelin le fournit sous la forme d’un modificateur nonReentrant :
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;
import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
contract FixedBank is ReentrancyGuard {
mapping(address => uint256) public balanceOf;
function deposit() external payable {
balanceOf[msg.sender] += msg.value;
}
function withdraw() external nonReentrant {
uint256 amount = balanceOf[msg.sender];
require(amount > 0, "nothing to withdraw");
balanceOf[msg.sender] = 0; // effet avant interaction
(bool ok, ) = msg.sender.call{value: amount}("");
require(ok, "send failed");
}
}
Utilisez les deux : le bon ordre est le vrai remède, le garde-fou vous protège le jour où quelqu’un ajoutera une fonction dans le mauvais ordre. OpenZeppelin propose aussi ReentrancyGuardTransient, une variante moins chère qui s’appuie sur le stockage transitoire, pour les chaînes qui le prennent en charge.
Contrôle d’accès : qui peut appeler quoi
PiggyBank vérifie son propriétaire à la main avec un modificateur, ce qui convient pour une seule règle. Pour de vrais projets, utilisez Ownable d’OpenZeppelin. Depuis la version 5, son constructeur prend le propriétaire initial en argument, si bien que vous indiquez toujours qui possède le contrat :
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
contract OwnedSwitch is Ownable {
bool public paused;
constructor() Ownable(msg.sender) {}
function setPaused(bool newState) external onlyOwner {
paused = newState;
}
}
Pour plusieurs rôles (un compte qui suspend, un autre qui crée des jetons), la même bibliothèque propose AccessControl. Quel que soit votre choix, écrivez le test négatif, comme le fait test_RevertWhen_NotOwnerPauses. Une seule fonction sans sa vérification suffit à livrer le contrôle d’un contrat à n’importe qui, et un test du cas normal ne le montrera jamais.
Ne jamais autoriser avec tx.origin
tx.origin est le compte qui a signé la transaction, même quand l’appel a atteint votre contrat à travers d’autres contrats ; msg.sender est l’appelant immédiat. Si le contrat de votre portefeuille vérifie tx.origin == owner, un contrat malveillant que l’on vous pousse à appeler peut à son tour appeler votre portefeuille, et la vérification passe. La documentation de Solidity le résume en cinq mots (en anglais) : « Never use tx.origin for authorization. » Utilisez msg.sender.
Entiers : vérifications de dépassement depuis la 0.8.0, et ce qu’elles ne couvrent pas
Depuis Solidity 0.8.0, les opérations arithmétiques sont annulées en cas de dépassement par le haut ou par le bas, selon la page des changements majeurs de la 0.8.0 (en anglais). Soustraire 2 d’un solde de 1 provoque désormais un revert au lieu de produire un énorme nombre par bouclage, ce qui a supprimé toute une famille de bogues présents dans l’ancien code. Un bloc unchecked { ... } désactive ces vérifications pour économiser du gas, et vous ne devriez en voir que là où le code démontre que l’opération ne peut pas déborder.
Ces vérifications ne vous protègent ni des erreurs de logique (une somme juste des mauvais nombres), ni des arrondis dans les divisions (Solidity n’a pas de décimales, et 7 / 2 vaut 3), ni des conversions explicites vers des types plus petits : convertir un grand uint256 en uint96 ne garde que les bits de poids faible, sans annuler l’appel.
Cinq autres pièges à connaître par leur nom
- Les boucles sur une liste que les utilisateurs peuvent allonger peuvent un jour manquer de gas et bloquer la fonction pour de bon. Évitez les boucles sans borne sur le stockage.
- Tout ce qui est en stockage est lisible par tous, y compris les variables privées. Ne rangez jamais un mot de passe ou une réponse secrète dans un contrat.
- Les données de bloc, comme l’horodatage, ne sont pas une source de hasard : les producteurs de blocs les influencent. Utilisez un service de hasard vérifiable quand de l’argent dépend du hasard.
- Supprimer une structure qui contient un mapping ne vide pas le mapping. La documentation de Solidity le range sous « clearing mappings ».
- Un contrat peut recevoir de l’ether sans qu’aucune de ses fonctions s’exécute : ne supposez donc jamais que le solde en ether du contrat est égal à ce que dit votre propre comptabilité.
Bibliothèques auditées et analyse statique avec Slither
Ne réécrivez pas vous-même les jetons, la gestion des propriétaires ou les garde-fous. OpenZeppelin Contracts est une bibliothèque très répandue de briques auditées ; sa dernière version stable est la v5.7.0, publiée le 29 juillet 2026. Installez une version étiquetée et associez le chemin d’import ; la documentation déconseille d’installer depuis la branche master.
forge install OpenZeppelin/[email protected]
echo "@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/" >> remappings.txt
Lancez ensuite un analyseur statique. Slither, développé en open source au sein de l’organisation crytic sur GitHub, lit votre code sans l’exécuter et signale des schémas de bogues connus, la réentrance comprise. Sa dernière version est la 0.11.6 (28 juillet 2026), et elle demande Python 3.10 ou plus récent. Dans un projet Foundry, vérifiez d’abord que forge build passe, puis :
python3 -m pip install slither-analyzer
slither .
Sa liste de détecteurs en comprend plusieurs pour la réentrance. Lisez chaque résultat, corrigez-le ou notez pourquoi il est sans danger, et attendez-vous à des faux positifs ; l’important est d’examiner chacun d’eux.
Ce que les tests et les outils ne détectent pas
Une suite de tests au vert prouve les cas auxquels vous avez pensé. Les équipes expérimentées ajoutent des audits de sécurité externes, des concours d’audit publics et des primes aux bogues avant que de l’argent réel n’entre ; la leçon précédente de ce parcours explique comment fonctionnent ces concours et pourquoi ils sont un bon endroit pour apprendre. Pour vos propres projets, gardez petite la somme qu’un contrat peut détenir tant que son code n’a pas été lu par quelqu’un d’autre que vous.
Quelle version de Solidity utiliser en octobre 2026
Au 27 octobre 2026, la dernière version de Solidity est la 0.8.38, publiée le 22 octobre 2026, d’après les versions publiées sur GitHub du projet. L’annonce de la version (en anglais) la décrit comme corrigeant un « low-severity bug in the Yul optimizer » (un bogue de faible gravité dans l’optimiseur Yul) : quand une variable servant à calculer une adresse en mémoire ou en stockage était réaffectée, l’optimiseur pouvait conserver une connaissance périmée et remplacer une lecture par une valeur obsolète ou supprimer une écriture nécessaire. Cela concerne le code compilé avec l’optimiseur activé, de la 0.8.19 à la 0.8.37 : avec le pipeline via-IR, n’importe quel code peut être touché ; avec le pipeline par défaut, surtout des fonctions auxiliaires générées par le compilateur et certains blocs d’assembleur en ligne.
| Outil | Dernière version | Date de publication |
|---|---|---|
| Compilateur Solidity | 0.8.38 | 22 octobre 2026 |
| Solidity, version précédente | 0.8.37 | 10 septembre 2026 |
| Foundry (stable) | v1.8.6 | 21 octobre 2026 |
| forge-std | v1.17.0 | 30 septembre 2026 |
| OpenZeppelin Contracts | v5.7.0 | 29 juillet 2026 |
| Slither | 0.11.6 | 28 juillet 2026 |
Versions indiquées sur la page des versions publiées GitHub de chaque projet, consultées le 27 octobre 2026.
En pratique : compilez avec la dernière version 0.8, gardez un intervalle de pragma dont vous avez réellement testé le plancher, et quand vous déployez quelque chose de réel, fixez une version exacte du compilateur dans foundry.toml pour que le bytecode puisse être reproduit et vérifié. Quand un tutoriel utilise une version bien plus ancienne, comme la 0.6 ou la 0.7, la syntaxe peut encore compiler mais les règles de sécurité diffèrent, les vérifications de dépassement en tête ; lisez l’introduction de Solidity pour la 0.8.38 (en anglais) plutôt qu’un vieil article de blog.
Que construire après votre premier contrat
Vous disposez maintenant de la boucle que répète tout développeur Solidity, et la meilleure façon d’apprendre Solidity à partir d’ici est de la relancer sans cesse : écrire, compiler, tester les réussites et les échecs, faire du fuzzing, lire les traces, passer l’analyseur. Exercez-vous sur des variantes avant d’aller plus loin. Ajoutez une limite de retrait par jour. Laissez le propriétaire ne rien retirer mais tout suspendre. Écrivez un test d’invariant selon lequel la banque détient toujours au moins la somme de ses soldes. Chaque modification fera casser un test, et c’est précisément le but.
Deux prochaines leçons de ce cours prennent le relais : l’une rassemble les livres, les documentations et les certifications qui valent votre temps, l’autre relie un contrat à une page web et le déploie sur un réseau de test public. Pour retrouver le plan mois par mois, il figure dans notre guide pour devenir développeur blockchain, et le guide des métiers de la blockchain montre les postes où ces compétences servent.
Une dernière remarque pour les lecteurs en France, en Belgique et en Suisse. Cette leçon parle de code, pas de droit. Un contrat qui collecte l’argent d’utilisateurs ou émet des jetons peut soulever des questions réglementaires, du ressort en France de l’AMF et de l’ACPR et, au niveau européen, du règlement MiCA ; c’est une étude à part, et un projet d’apprentissage sur une chaîne locale n’en soulève aucune.



