Installer et configurer Foundry (forge, cast, anvil)
Créer un projet avec la structure standard
Ecrire des tests en Solidity avec DSTest
Utiliser les cheatcodes pour simuler des scénarios
Appliquer les assertions et le logging
Prerequis
Solidity basics (SC-1 a SC-4)
Terminal/bash basics
Git installe
Duree estimee : 45 minutes
1. Introduction a Foundry
Foundry est une boite a outils de développement Ethereum ecrite en Rust, extremement rapide.
Composants principaux
Outil
Description
Usage principal
forge
Compilateur et testeur
forge build, forge test
cast
Interface RPC
Interagir avec les contrats
anvil
Noeud local
Testnet local
chisel
REPL Solidity
Prototypage interactif
# Installation de Foundry (a executer dans un terminal)# Note: Sur Windows, utiliser Git Bash ou WSLinstall_commands ='''# 1. Installer Foundryup (gestionnaire de version)curl -L https://foundry.paradigm.xyz | bash# 2. Redemarrer le shell ou sourcer le profilesource ~/.bashrc # ou ~/.zshrc sur Mac# 3. Installer Foundryfoundryup# 4. Verifier l'installationforge --versioncast --versionanvil --version'''print("Commandes d'installation de Foundry:")print(install_commands)
Commandes d'installation de Foundry:
# 1. Installer Foundryup (gestionnaire de version)
curl -L https://foundry.paradigm.xyz | bash
# 2. Redemarrer le shell ou sourcer le profile
source ~/.bashrc # ou ~/.zshrc sur Mac
# 3. Installer Foundry
foundryup
# 4. Verifier l'installation
forge --version
cast --version
anvil --version
Vérification de l’installation de Foundry en testant la disponibilité des outils forge, cast et anvil.
# Verification reelle de Foundryimport subprocessdef check_tool(name):try: result = subprocess.run([name, "--version"], capture_output=True, text=True, timeout=10)if result.returncode ==0:returnTrue, result.stdout.strip()returnFalse, result.stderr.strip()exceptFileNotFoundError:returnFalse, "Non installe"exceptExceptionas e:returnFalse, str(e)print("Verification des outils Foundry:")print("-"*50)all_ok =Truefor tool in ["forge", "cast", "anvil"]: ok, info = check_tool(tool) status ="OK"if ok else"MANQUANT"print(f" {tool}: {status}")if ok:print(f" {info}") all_ok = all_ok and okprint("-"*50)if all_ok:print("Tous les outils sont installes !")else:print("Installez Foundry: curl -L https://foundry.paradigm.xyz | bash && foundryup")
Verification des outils Foundry:
--------------------------------------------------
forge: OK
forge Version: 1.5.1-stable
Commit SHA: b0a9dd9ceda36f63e2326ce530c10e6916f4b8a2
Build Timestamp: 2025-12-22T11:40:33.870895500Z (1766403633)
Build Profile: maxperf
cast: OK
cast Version: 1.5.1-stable
Commit SHA: b0a9dd9ceda36f63e2326ce530c10e6916f4b8a2
Build Timestamp: 2025-12-22T11:40:33.870895500Z (1766403633)
Build Profile: maxperf
anvil: OK
anvil Version: 1.5.1-stable
Commit SHA: b0a9dd9ceda36f63e2326ce530c10e6916f4b8a2
Build Timestamp: 2025-12-22T11:40:33.870895500Z (1766403633)
Build Profile: maxperf
--------------------------------------------------
Tous les outils sont installes !
Interpretation : Verification de l’environnement Foundry
Résultat obtenu : la cellule précédente verifie localement la presence de forge, cast et anvil. Si un outil apparait MANQUANT, installer Foundry puis relancer la cellule.
Outil
Rôle dans le workflow
Verification
forge
Compiler, tester, deployer
forge --version
cast
Interagir avec les contrats on-chain
cast --version
anvil
Noeud local de test
anvil --version
Points cles : - Quand Foundry est installe, les trois outils partagent normalement la même version (build unique Rust) ; une incoherence de version signalerait une installation corrompue - Si un outil est MANQUANT, relancer foundryup re-installe les trois - chisel (REPL Solidity) est optionnel et non verifie ici
2. Structure d’un projet Foundry
La commande forge init crée une structure standard.
Pourquoi cette structure est un contrat, pas une suggestion.foundry.toml mappe les noms logiques aux dossiers physiques : src (vos contrats de production), out (les artifacts de compilation ABI + bytecode), libs (où forge cherche les imports). Renommer un dossier sans mettre à jour foundry.toml casse silencieusement la compilation ou la résolution d’imports. Les remappings (forge-std/=lib/forge-std/src/) sont la pièce qui rend les imports Solidity lisibles : au lieu de chemins relatifs fragiles, vos contrats écrivent import 'forge-std/Test.sol' et forge le récrit vers le chemin physique. Cette convention-over-configuration est aussi pourquoi forge init installe lib/forge-std/ par défaut : la librairie standard de test attend ce chemin exact.
# Creation d'un nouveau projet Foundryproject_setup ='''# Creer un nouveau projetmkdir my-foundry-projectcd my-foundry-projectforge init# Structure generee:# my-foundry-project/# |-- src/# | `-- Counter.sol # Contrat exemple# |-- test/# | `-- Counter.t.sol # Test exemple# |-- script/# | `-- Counter.s.sol # Script de deploiement# |-- lib/# | `-- forge-std/ # Librairie standard# `-- foundry.toml # Configuration'''print("Creation d'un projet Foundry:")print(project_setup)
Creation d'un projet Foundry:
# Creer un nouveau projet
mkdir my-foundry-project
cd my-foundry-project
forge init
# Structure generee:
# my-foundry-project/
# |-- src/
# | `-- Counter.sol # Contrat exemple
# |-- test/
# | `-- Counter.t.sol # Test exemple
# |-- script/
# | `-- Counter.s.sol # Script de deploiement
# |-- lib/
# | `-- forge-std/ # Librairie standard
# `-- foundry.toml # Configuration
Configuration du fichier foundry.toml définissant les paramètres du projet Foundry (répertoires, compilateur, profils).
Configuration foundry.toml:
[profile.default]
src = "src"
out = "out"
libs = ["lib"]
solc_version = "0.8.28"
optimizer = true
optimizer_runs = 200
# Configuration de test
verbosity = 2
fuzz_runs = 256
# Remappings pour les imports
remappings = [
"forge-std/=lib/forge-std/src/",
"@openzeppelin/=lib/openzeppelin-contracts/"
]
Interpretation : Structure du projet
Repertoire
Contenu
Usage
src/
Contrats sources
Code de production
test/
Fichiers de test
Tests unitaires et d’integration
script/
Scripts de deploiement
Deploiement et migration
lib/
Dependances
forge-std, OpenZeppelin
out/
Artifacts de compilation
ABI, bytecode (genere)
Convention de nommage : Les fichiers de test ont le suffixe .t.sol
3. Ecrire des tests avec Solidity
Foundry permet d’ecrire des tests en Solidity pur, sans JavaScript.
# Contrat a tester : Counter.solcounter_contract ='''// SPDX-License-Identifier: MITpragma solidity ^0.8.28;contract Counter { uint256 public number; function setNumber(uint256 newNumber) public { number = newNumber; } function increment() public { number++; } function decrement() public { number--; }}'''print("Contrat Counter.sol (src/Counter.sol):")print(counter_contract)
Contrat Counter.sol (src/Counter.sol):
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;
contract Counter {
uint256 public number;
function setNumber(uint256 newNumber) public {
number = newNumber;
}
function increment() public {
number++;
}
function decrement() public {
number--;
}
}
Test basique utilisant le pattern DSTest pour valider le comportement du contrat Counter déployé.
# Test basique avec DSTest patternbasic_test ='''// SPDX-License-Identifier: MITpragma solidity ^0.8.28;import "forge-std/Test.sol";import "../src/Counter.sol";contract CounterTest is Test { Counter public counter; // Execute avant chaque test function setUp() public { counter = new Counter(); } function test_InitialState() public view { assertEq(counter.number(), 0, "Initial number should be 0"); } function test_SetNumber() public { counter.setNumber(42); assertEq(counter.number(), 42, "Number should be 42"); } function test_Increment() public { counter.increment(); assertEq(counter.number(), 1, "Number should be 1 after increment"); } function test_Decrement() public { counter.setNumber(5); counter.decrement(); assertEq(counter.number(), 4, "Number should be 4 after decrement"); }}'''print("Test basique (test/Counter.t.sol):")print(basic_test)
Test basique (test/Counter.t.sol):
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;
import "forge-std/Test.sol";
import "../src/Counter.sol";
contract CounterTest is Test {
Counter public counter;
// Execute avant chaque test
function setUp() public {
counter = new Counter();
}
function test_InitialState() public view {
assertEq(counter.number(), 0, "Initial number should be 0");
}
function test_SetNumber() public {
counter.setNumber(42);
assertEq(counter.number(), 42, "Number should be 42");
}
function test_Increment() public {
counter.increment();
assertEq(counter.number(), 1, "Number should be 1 after increment");
}
function test_Decrement() public {
counter.setNumber(5);
counter.decrement();
assertEq(counter.number(), 4, "Number should be 4 after decrement");
}
}
Commandes Foundry pour compiler le projet et exécuter les tests avec différents niveaux de verbosité.
# Commandes de testtest_commands ='''# Compiler le projetforge build# Executer tous les testsforge test# Executer avec verboseforge test -vvv# Executer un test specifiqueforge test --match-test test_Increment# Executer les tests d'un contrat specifiqueforge test --match-contract CounterTest# Voir les logs (emit log)forge test -vvvv'''print("Commandes forge test:")print(test_commands)
Commandes forge test:
# Compiler le projet
forge build
# Executer tous les tests
forge test
# Executer avec verbose
forge test -vvv
# Executer un test specifique
forge test --match-test test_Increment
# Executer les tests d'un contrat specifique
forge test --match-contract CounterTest
# Voir les logs (emit log)
forge test -vvvv
Sortie attendue de la commande forge test illustrant le résultat d’une exécution réussie des tests unitaires.
# Pour executer forge test, il faut un projet Foundry initialise.# Voici la sortie attendue lorsque les tests passent.try: result = subprocess.run(["forge", "--version"], capture_output=True, text=True, timeout=10)if result.returncode ==0:print(f"forge disponible: {result.stdout.strip()}")print()print("Pour executer les tests :")print(" mkdir test-project && cd test-project")print(" forge init")print(" # Copiez les contrats ci-dessus dans src/ et test/")print(" forge test -vvv")else:print("forge non disponible")exceptFileNotFoundError:print("forge non installe - sortie attendue :")print()print(" [PASS] test_Decrement() (gas: ~28000)")print(" [PASS] test_InitialState() (gas: ~5000)")print(" [PASS] test_Increment() (gas: ~23000)")print(" [PASS] test_SetNumber() (gas: ~24000)")print(" Test result: ok. 4 passed; 0 failed")
forge disponible: forge Version: 1.5.1-stable
Commit SHA: b0a9dd9ceda36f63e2326ce530c10e6916f4b8a2
Build Timestamp: 2025-12-22T11:40:33.870895500Z (1766403633)
Build Profile: maxperf
Pour executer les tests :
mkdir test-project && cd test-project
forge init
# Copiez les contrats ci-dessus dans src/ et test/
forge test -vvv
Interpretation : Résultats des tests
Note de validation : si la cellule précédente affiche forge non installe - sortie attendue, les lignes [PASS] sont un exemple statique fourni par le fallback pedagogique du notebook. Elles ne constituent pas une exécution reelle de forge test. Pour obtenir une preuve d’exécution Foundry, installer Foundry puis relancer la cellule afin qu’elle affiche la version de forge et les commandes a executer.
Indicateurs de succes lors d’une exécution reelle :
Élément
Signification
[PASS]
Test reussi
gas: XXXXX
Consommation de gas
4 passed; 0 failed
Resume d’exécution
Niveaux de verbosite :
Flag
Description
-v
Resume minimal
-vv
Traces des tests
-vvv
Traces avec appels internes
-vvvv
Traces completes + logs
-vvvvv
Tout, y compris le setup
Exercice 1 : Ecrire des tests pour un contrat Calculator
Le contrat Calculator ci-dessous implemente les 4 opérations arithmetiques avec protection contre la division par zero. Ecrivez un fichier de test Foundry couvrant : l’etat initial, l’addition, la soustraction, et la division par zero.
Indices : - assertEq pour verifier les résultats de calcul - vm.expectRevert() pour tester la division par zero - Chaque fonction de test doit etre independante (le setUp reinitialise l’etat)
# Exercice 1 : Tests pour un contrat Calculator# TODO etudiant : ecrire les tests Foundry pour le contrat Calculator# Indice : inspirez-vous du pattern CounterTest (setUp, assertEq, vm.expectRevert)# Etape 1 : implementer test_Addition - verifier que add(10, 20) == 30# Etape 2 : implementer test_Subtraction - verifier que sub(20, 10) == 10# Etape 3 : implementer test_DivideByZero - verifier que div(10, 0) revertCALCULATOR_TEST_SKELETON ='''// SPDX-License-Identifier: MITpragma solidity ^0.8.28;contract Calculator { function add(uint256 a, uint256 b) public pure returns (uint256) { return a + b; } function sub(uint256 a, uint256 b) public pure returns (uint256) { require(a >= b, "Underflow"); return a - b; } function mul(uint256 a, uint256 b) public pure returns (uint256) { return a * b; } function div(uint256 a, uint256 b) public pure returns (uint256) { require(b > 0, "Division by zero"); return a / b; }}// TODO etudiant : ecrire le contrat de test CalculatorTest// Indice : heritez de Test, utilisez assertEq pour les resultats// et vm.expectRevert pour les cas d'erreurcontract CalculatorTest is Test { Calculator public calc; function setUp() public { calc = new Calculator(); } // TODO etudiant : tester add(10, 20) == 30 function test_Addition() public pure { // TODO etudiant pass; } // TODO etudiant : tester sub(20, 10) == 10 function test_Subtraction() public pure { // TODO etudiant pass; } // TODO etudiant : tester que div(10, 0) revert avec "Division by zero" function test_DivideByZero() public { // TODO etudiant : utiliser vm.expectRevert(bytes("Division by zero")) pass; }}'''print("Exercice a completer : Calculator tests avec assertions et expectRevert")
Exercice a completer : Calculator tests avec assertions et expectRevert
4. Cheatcodes (vm)
Les cheatcodes sont des fonctions speciales accessibles via l’objet vm pour manipuler l’etat du test.
Pourquoi les cheatcodes existent (et pourquoi tester en Solidity sans eux serait impossible). Trois valeurs que le code de production lit sont cryptographiquement ou physiquement impossibles à forger depuis un contrat : msg.sender (lié à la signature de la transaction, vous ne pouvez pas signer comme Alice sans sa clé privée), block.timestamp / block.number (fixés par le réseau), et les soldes ETH (comptabilisés par l’EVM). Sans cheatcodes, tester ‘que fait mon contrat si Alice l’appelle dans 30 jours avec 5 ETH’ nécessiterait de générer une signature valide et d’attendre – rendant tout test non-déterministe. vm.prank, vm.deal, vm.warp court-circuitent l’EVM : forge intercepte l’appel au niveau de la VM de test (anvil) et injecte la valeur mockée directement, sans passer par la cryptographie. C’est l’API de test de la VM ; c’est ce qui rend les tests Foundry déterministes et ce qui les distingue des suites JS (Hardhat) où il faut câbler des signers et des helpers.
# Cheatcodes de basecheatcode_intro ='''// L'objet vm est disponible via forge-std/Test.sol// Il permet de modifier l'etat de la blockchain de test// Cheatcodes principaux:// vm.prank(address) - Change msg.sender pour le prochain appel// vm.startPrank(addr) - Change msg.sender jusqu'a stopPrank()// vm.deal(addr, amt) - Donne des ETH a une adresse// vm.warp(timestamp) - Change le block.timestamp// vm.roll(number) - Change le block.number// vm.expectRevert() - Attend un revert au prochain appel// vm.record() - Enregistre les acces storage'''print("Cheatcodes Foundry:")print(cheatcode_intro)
Cheatcodes Foundry:
// L'objet vm est disponible via forge-std/Test.sol
// Il permet de modifier l'etat de la blockchain de test
// Cheatcodes principaux:
// vm.prank(address) - Change msg.sender pour le prochain appel
// vm.startPrank(addr) - Change msg.sender jusqu'a stopPrank()
// vm.deal(addr, amt) - Donne des ETH a une adresse
// vm.warp(timestamp) - Change le block.timestamp
// vm.roll(number) - Change le block.number
// vm.expectRevert() - Attend un revert au prochain appel
// vm.record() - Enregistre les acces storage
Exemple d’utilisation de vm.prank et vm.deal pour simuler des appels depuis une adresse spécifique avec un solde défini.
# Exemple avec vm.prank et vm.dealprank_deal_test ='''contract PrankDealTest is Test { Counter public counter; address public alice = address(0x1); address public bob = address(0x2); function setUp() public { counter = new Counter(); } function test_Prank() public { // Simule un appel depuis l'adresse d'Alice vm.prank(alice); counter.setNumber(100); // msg.sender est maintenant reset a l'adresse originale counter.setNumber(200); // Appele par l'adresse par defaut } function test_Deal() public { // Donne 10 ETH a Bob vm.deal(bob, 10 ether); assertEq(bob.balance, 10 ether, "Bob should have 10 ETH"); } function test_StartStopPrank() public { // Prank persistant pour plusieurs appels vm.startPrank(alice); counter.setNumber(1); counter.increment(); counter.increment(); vm.stopPrank(); assertEq(counter.number(), 3); }}'''print("Test avec vm.prank et vm.deal:")print(prank_deal_test)
Test avec vm.prank et vm.deal:
contract PrankDealTest is Test {
Counter public counter;
address public alice = address(0x1);
address public bob = address(0x2);
function setUp() public {
counter = new Counter();
}
function test_Prank() public {
// Simule un appel depuis l'adresse d'Alice
vm.prank(alice);
counter.setNumber(100);
// msg.sender est maintenant reset a l'adresse originale
counter.setNumber(200); // Appele par l'adresse par defaut
}
function test_Deal() public {
// Donne 10 ETH a Bob
vm.deal(bob, 10 ether);
assertEq(bob.balance, 10 ether, "Bob should have 10 ETH");
}
function test_StartStopPrank() public {
// Prank persistant pour plusieurs appels
vm.startPrank(alice);
counter.setNumber(1);
counter.increment();
counter.increment();
vm.stopPrank();
assertEq(counter.number(), 3);
}
}
Interpretation : Simulation d’identite et de solde
Résultat obtenu : vm.prank et vm.deal sont les deux cheatcodes les plus utilises pour les tests de contrôle d’acces.
Cheatcode
Portee
Effet
vm.prank(addr)
1 appel
msg.sender = addr pour l’appel suivant
vm.startPrank(addr)
Persistant
msg.sender = addr jusqu’a stopPrank
vm.deal(addr, amt)
Immédiat
Force le solde ETH de addr
Points cles : - vm.prank reset après un seul appel ; utiliser startPrank/stopPrank pour les sequences multi-appels - vm.deal peut aussi créer des ETH ex nihilo (utile pour tester les depots sans setup complexe) - Combiner vm.prank(alice) + vm.deal(alice, 1 ether) pour simuler un utilisateur avec des fonds qui appelle une fonction payable
Manipulation du temps de la blockchain avec vm.warp et vm.roll pour tester des fonctions dépendantes du temps.
# Exemple avec vm.warp et vm.rolltime_test ='''contract TimeTest is Test { function test_Warp() public { // Avancer le temps de 1 jour vm.warp(block.timestamp + 1 days); assertGt(block.timestamp, 0, "Timestamp should be advanced"); emit log_named_uint("Current timestamp", block.timestamp); } function test_Roll() public { // Avancer de 100 blocs vm.roll(block.number + 100); assertGt(block.number, 0, "Block number should be advanced"); emit log_named_uint("Current block", block.number); } function test_Skip() public { // skip() est un raccourci pour warp() relatif uint256 before = block.timestamp; skip(3600); // Avance de 1 heure assertEq(block.timestamp, before + 3600); }}'''print("Test avec vm.warp et vm.roll:")print(time_test)
Test avec vm.warp et vm.roll:
contract TimeTest is Test {
function test_Warp() public {
// Avancer le temps de 1 jour
vm.warp(block.timestamp + 1 days);
assertGt(block.timestamp, 0, "Timestamp should be advanced");
emit log_named_uint("Current timestamp", block.timestamp);
}
function test_Roll() public {
// Avancer de 100 blocs
vm.roll(block.number + 100);
assertGt(block.number, 0, "Block number should be advanced");
emit log_named_uint("Current block", block.number);
}
function test_Skip() public {
// skip() est un raccourci pour warp() relatif
uint256 before = block.timestamp;
skip(3600); // Avance de 1 heure
assertEq(block.timestamp, before + 3600);
}
}
Interpretation : Manipulation du temps blockchain
Résultat obtenu : vm.warp, vm.roll et skip permettent de tester des logiques temporelles sans attendre.
Cheatcode
Modifie
Unite
Reset après test
vm.warp(ts)
block.timestamp
Secondes absolues
Oui
vm.roll(n)
block.number
Numéro de bloc
Oui
skip(dt)
block.timestamp
Secondes relatives
Oui
Points cles : - L’etat du blockchain est reinitialise entre chaque test (setUp appele a chaque fois) - skip(3600) est preferable a vm.warp(block.timestamp + 3600) pour la lisibilite - Pour les contrats avec vesting/locking, combiner skip avec des assertions sur les soldes avant/après
Utilisation de vm.expectRevert pour vérifier qu’une transaction échoue avec le bon message d’erreur.
# Exemple avec vm.expectRevertrevert_test ='''contract RevertTest is Test { Counter public counter; function setUp() public { counter = new Counter(); } function test_ExpectRevert() public { // Test qu'un appel revert vm.expectRevert(); counter.someFunctionThatReverts(); } function test_ExpectRevert_WithMessage() public { // Test avec message d'erreur specifique vm.expectRevert(bytes("Unauthorized")); counter.restrictedFunction(); } function test_ExpectRevert_CustomError() public { // Test avec custom error vm.expectRevert(Counter.Unauthorized.selector); counter.restrictedFunction(); }}'''print("Test avec vm.expectRevert:")print(revert_test)
Test avec vm.expectRevert:
contract RevertTest is Test {
Counter public counter;
function setUp() public {
counter = new Counter();
}
function test_ExpectRevert() public {
// Test qu'un appel revert
vm.expectRevert();
counter.someFunctionThatReverts();
}
function test_ExpectRevert_WithMessage() public {
// Test avec message d'erreur specifique
vm.expectRevert(bytes("Unauthorized"));
counter.restrictedFunction();
}
function test_ExpectRevert_CustomError() public {
// Test avec custom error
vm.expectRevert(Counter.Unauthorized.selector);
counter.restrictedFunction();
}
}
Interpretation : Tester les reverts avec expectRevert
Résultat obtenu : trois variantes de vm.expectRevert couvrent tous les patterns de test d’erreur.
Variante
Quand l’utiliser
Exemple
vm.expectRevert()
N’importe quel revert
Protection generique
vm.expectRevert(bytes("msg"))
require("msg")
Verifier le message exact
vm.expectRevert(Selector)
Custom error
Unauthorized.selector
Points cles : - expectRevert ne capture que le prochain appel ; un second revert non attendu fera echouer le test - Pour les custom errors, utiliser .selector pour comparer uniquement le sélecteur 4 bytes - Toujours tester a la fois le cas passant et le cas revertant pour la même fonction
Exercice 2 : Tester le contrôle d’acces d’un OwnableCounter
Le contrat OwnableCounter ci-dessous restreint setNumber au proprietaire. Ecrivez des tests qui verifient : 1. Que le proprietaire peut appeler setNumber avec succes 2. Qu’un non-proprietaire provoque un revert avec vm.expectRevert
Indices : - Utilisez vm.prank(alice) pour simuler un appel depuis Alice - Le proprietaire est l’adresse qui a deploye le contrat (dans setUp, c’est le contrat de test) - Utilisez vm.expectRevert(bytes("Not owner")) pour capturer le revert exact
# Exercice 2 : Tester le controle d'acces d'un OwnableCounter# TODO etudiant : ecrire les tests pour le controle d'acces# Indice : le proprietaire est l'adresse qui deploye le contrat# Etape 1 : deployer le contrat dans setUp# Etape 2 : tester que le proprietaire peut appeler setNumber# Etape 3 : tester qu'un non-proprietaire provoque un revertOWNABLE_COUNTER_TEST ='''// SPDX-License-Identifier: MITpragma solidity ^0.8.28;contract OwnableCounter { address public owner; uint256 public number; constructor() { owner = msg.sender; } function setNumber(uint256 newNumber) public { require(msg.sender == owner, "Not owner"); number = newNumber; } function increment() public { number++; }}// TODO etudiant : ecrire le contrat de testcontract OwnableCounterTest is Test { OwnableCounter public counter; address public alice = address(0x1); function setUp() public { counter = new OwnableCounter(); } // TODO etudiant : verifier que le proprietaire peut setNumber function test_OwnerCanSetNumber() public { // TODO etudiant pass; } // TODO etudiant : verifier qu'un non-proprietaire est reverts function test_NonOwnerCannotSetNumber() public { // TODO etudiant : utiliser vm.prank(alice) + vm.expectRevert pass; }}'''print("Exercice a completer : OwnableCounter access control tests")
Exercice a completer : OwnableCounter access control tests
Resume des Cheatcodes
Cheatcode
Usage
Exemple
vm.prank(addr)
Change msg.sender (1 appel)
vm.prank(alice); counter.setNumber(1);
vm.startPrank(addr)
Change msg.sender (persistent)
vm.startPrank(alice); ... vm.stopPrank();
vm.deal(addr, amt)
Donne des ETH
vm.deal(bob, 10 ether);
vm.warp(ts)
Change le timestamp
vm.warp(1000000000);
vm.roll(num)
Change le numéro de bloc
vm.roll(100);
vm.expectRevert()
Attend un revert
vm.expectRevert(); contract.fails();
5. Assertions et Logging
Foundry fournit un ensemble complet d’assertions et de fonctions de logging.
Pourquoi les assertions sont natives et le logging est votre debugger. Les tests sont écrits en Solidity – donc assertEq(a, b) s’exécute directement dans l’EVM pendant le run de test, sans round-trip RPC vers un harnais JS qui comparerait les valeurs. Zéro latence réseau, et l’égalité se compare au niveau du type on-chain (uint256, address, bytes) avec la sémantique exacte du runtime de production. Le logging (emit log_*) existe parce que l’EVM n’a pas de points d’arrêt : impossible de poser un breakpoint dans une fonction Solidity. Les logs sont votre printf debugging – ils atterrissent dans les traces forge (-vvv), qui reconstruct la stack d’appels et les valeurs aux points de log. C’est pourquoi verbosity dans foundry.toml est un paramètre de premier ordre : il contrôle combien de contexte de trace vous voyez à l’échec.
# Assertions disponiblesassertions_list ='''// Assertions d'egaliteassertEq(a, b) // a == bassertEq(a, b, "message") // avec message d'erreurassertEqDecimal(a, b, decimals) // pour les nombres decimaux// Assertions de comparaisonassertTrue(condition) // condition == trueassertFalse(condition) // condition == falseassertGt(a, b) // a > bassertGe(a, b) // a >= bassertLt(a, b) // a < bassertLe(a, b) // a <= b// Assertions specialesassertEq(address1, address2) // Pour les adressesassertEq(bytes1, bytes2) // Pour les bytesassertEq(string1, string2) // Pour les strings'''print("Assertions Foundry:")print(assertions_list)
Assertions Foundry:
// Assertions d'egalite
assertEq(a, b) // a == b
assertEq(a, b, "message") // avec message d'erreur
assertEqDecimal(a, b, decimals) // pour les nombres decimaux
// Assertions de comparaison
assertTrue(condition) // condition == true
assertFalse(condition) // condition == false
assertGt(a, b) // a > b
assertGe(a, b) // a >= b
assertLt(a, b) // a < b
assertLe(a, b) // a <= b
// Assertions speciales
assertEq(address1, address2) // Pour les adresses
assertEq(bytes1, bytes2) // Pour les bytes
assertEq(string1, string2) // Pour les strings
Exemple complet d’un contrat de test utilisant les assertions Foundry pour valider le comportement du Counter.
# Exemple complet d'assertionsassertions_test ='''contract AssertionsTest is Test { function test_AllAssertions() public { uint256 a = 10; uint256 b = 10; uint256 c = 20; // Egalite assertEq(a, b, "a should equal b"); // Comparaisons assertGt(c, a, "c should be greater than a"); assertGe(c, a, "c should be greater or equal to a"); assertLt(a, c, "a should be less than c"); assertLe(a, c, "a should be less or equal to c"); // Booleens assertTrue(a == b, "a should equal b"); assertFalse(a == c, "a should not equal c"); } function test_AddressAssertions() public { address alice = address(0x1); address bob = address(0x1); address charlie = address(0x2); assertEq(alice, bob, "Alice should equal Bob"); assertTrue(alice != charlie, "Alice should differ from Charlie"); }}'''print("Test complet avec assertions:")print(assertions_test)
Test complet avec assertions:
contract AssertionsTest is Test {
function test_AllAssertions() public {
uint256 a = 10;
uint256 b = 10;
uint256 c = 20;
// Egalite
assertEq(a, b, "a should equal b");
// Comparaisons
assertGt(c, a, "c should be greater than a");
assertGe(c, a, "c should be greater or equal to a");
assertLt(a, c, "a should be less than c");
assertLe(a, c, "a should be less or equal to c");
// Booleens
assertTrue(a == b, "a should equal b");
assertFalse(a == c, "a should not equal c");
}
function test_AddressAssertions() public {
address alice = address(0x1);
address bob = address(0x1);
address charlie = address(0x2);
assertEq(alice, bob, "Alice should equal Bob");
assertTrue(alice != charlie, "Alice should differ from Charlie");
}
}
Fonctions de logging disponibles dans Foundry pour afficher des valeurs pendant l’exécution des tests avec verbosité.
# Fonctions de logginglogging_funcs ='''// Logging de base (affiche avec -vvvv)emit log("Simple message");// Logging nommeemit log_named_uint("Value", 42);emit log_named_int("Signed Value", -42);emit log_named_address("Address", 0x1234...);emit log_named_bytes32("Bytes", bytes32Value);emit log_named_string("String", "Hello");emit log_named_decimal_uint("Amount", 1 ether, 18);// Logging d'adresse decodeemit log_address(0x1234...);// Logging de bytesemit log_bytes(hex"1234");emit log_bytes32(bytes32Value);'''print("Fonctions de logging Foundry:")print(logging_funcs)
Fonctions de logging Foundry:
// Logging de base (affiche avec -vvvv)
emit log("Simple message");
// Logging nomme
emit log_named_uint("Value", 42);
emit log_named_int("Signed Value", -42);
emit log_named_address("Address", 0x1234...);
emit log_named_bytes32("Bytes", bytes32Value);
emit log_named_string("String", "Hello");
emit log_named_decimal_uint("Amount", 1 ether, 18);
// Logging d'adresse decode
emit log_address(0x1234...);
// Logging de bytes
emit log_bytes(hex"1234");
emit log_bytes32(bytes32Value);
Exemple complet intégrant les fonctions de logging pour afficher des informations de débogage pendant les tests.
# Exemple avec logginglogging_test ='''contract LoggingTest is Test { Counter public counter; function setUp() public { counter = new Counter(); } function test_WithLogging() public { emit log("=== Debut du test ==="); emit log_named_uint("Initial value", counter.number()); counter.setNumber(42); emit log_named_uint("After setNumber(42)", counter.number()); counter.increment(); emit log_named_uint("After increment", counter.number()); emit log_named_decimal_uint("Balance in ETH", address(this).balance, 18); emit log("=== Fin du test ==="); assertEq(counter.number(), 43); }}'''print("Test avec logging:")print(logging_test)
Test avec logging:
contract LoggingTest is Test {
Counter public counter;
function setUp() public {
counter = new Counter();
}
function test_WithLogging() public {
emit log("=== Debut du test ===");
emit log_named_uint("Initial value", counter.number());
counter.setNumber(42);
emit log_named_uint("After setNumber(42)", counter.number());
counter.increment();
emit log_named_uint("After increment", counter.number());
emit log_named_decimal_uint("Balance in ETH", address(this).balance, 18);
emit log("=== Fin du test ===");
assertEq(counter.number(), 43);
}
}
Interpretation : Logging et debugging en pratique
Résultat obtenu : le test LoggingTest illustre le cycle debug typique avec les fonctions emit log_*.
Fonction
Usage
Verbosité requise
emit log("msg")
Marqueur de section / debug simple
-vvvv
emit log_named_uint("label", val)
Valeur numérique
-vvvv
emit log_named_decimal_uint("ETH", amt, 18)
Montant ETH décodé
-vvvv
emit log_named_address("User", addr)
Adresse
-vvvv
Points cles : - Les logs ne sont visibles qu’avec -vvvv ou plus ; en CI (-vv), ils sont silencieux - log_named_decimal_uint decode automatiquement les raw wei en ETH lisible - Le pattern “Debut du test / valeurs intermediaires / Fin du test” structure la sortie de debug
Exercice 3 : Verifier les events avec vm.expectEmit
Testez que le contrat SimpleVault (section 6) emet correctement les events Deposit et Withdraw avec les bons paramètres.
Objectif : utiliser vm.expectEmit(true, false, false, true) pour verifier qu’un event est emis avec les bons indexed et non-indexed paramètres lors d’un depot et d’un retrait.
Indices : - vm.expectEmit(true, false, false, true) verifie le 1er paramètre indexed et la donnee non-indexed - Emettez l’event attendu AVANT l’appel a la fonction testee - emit Deposit(alice, 1 ether) définit les valeurs attendues
# Exercice 3 : Verifier les events avec vm.expectEmit# TODO etudiant : ecrire les tests d'event pour SimpleVault# Indice : utilisez vm.expectEmit(true, false, false, true) avant chaque appel# Etape 1 : tester que deposit() emet Deposit(user, amount)# Etape 2 : tester que withdraw() emet Withdraw(user, amount)# Etape 3 : tester qu'un deposit de 0 n'emet PAS d'event (car revert)EVENT_TEST_SKELETON ='''// SPDX-License-Identifier: MITpragma solidity ^0.8.28;import "forge-std/Test.sol";import "../src/SimpleVault.sol";contract EventTest is Test { SimpleVault public vault; address public alice = address(0x1); function setUp() public { vault = new SimpleVault(); vm.deal(alice, 10 ether); } // TODO etudiant : tester que deposit emet Deposit(alice, 1 ether) function test_DepositEmitsEvent() public { // TODO etudiant : utiliser vm.expectEmit puis emit l'event attendu // TODO etudiant : appeler vault.deposit{value: 1 ether}() via vm.prank(alice) pass; // TODO etudiant } // TODO etudiant : tester que withdraw emet Withdraw(alice, 1 ether) function test_WithdrawEmitsEvent() public { // TODO etudiant : d'abord deposer, puis tester l'event de withdraw pass; // TODO etudiant }}'''print("Exercice a completer : Event emission tests pour SimpleVault")
Exercice a completer : Event emission tests pour SimpleVault
6. Exemples guidés
Exemple guide 1 : Test d’un contrat SimpleVault
Créez un test pour un contrat qui permet de deposer et retirer des ETH.
# Contrat SimpleVault a testervault_contract ='''// SPDX-License-Identifier: MITpragma solidity ^0.8.28;contract SimpleVault { mapping(address => uint256) public balances; event Deposit(address indexed user, uint256 amount); event Withdraw(address indexed user, uint256 amount); function deposit() public payable { require(msg.value > 0, "Must deposit more than 0"); balances[msg.sender] += msg.value; emit Deposit(msg.sender, msg.value); } function withdraw(uint256 amount) public { require(balances[msg.sender] >= amount, "Insufficient balance"); balances[msg.sender] -= amount; (bool success, ) = payable(msg.sender).call{value: amount}(""); require(success, "Transfer failed"); emit Withdraw(msg.sender, amount); } function getBalance() public view returns (uint256) { return balances[msg.sender]; }}'''print("Contrat SimpleVault.sol:")print(vault_contract)
Contrat SimpleVault.sol:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;
contract SimpleVault {
mapping(address => uint256) public balances;
event Deposit(address indexed user, uint256 amount);
event Withdraw(address indexed user, uint256 amount);
function deposit() public payable {
require(msg.value > 0, "Must deposit more than 0");
balances[msg.sender] += msg.value;
emit Deposit(msg.sender, msg.value);
}
function withdraw(uint256 amount) public {
require(balances[msg.sender] >= amount, "Insufficient balance");
balances[msg.sender] -= amount;
(bool success, ) = payable(msg.sender).call{value: amount}("");
require(success, "Transfer failed");
emit Withdraw(msg.sender, amount);
}
function getBalance() public view returns (uint256) {
return balances[msg.sender];
}
}
Écriture de la suite de tests pour le contrat SimpleVault en utilisant les cheatcodes Foundry étudiés précédemment. La solution complète ci-dessous sert d’exemple guidé résolu.
# Exercice : Ecrivez les tests pour SimpleVault# Utilisez les cheatcodes de ce notebookVAULT_TEST_SKELETON ="""// SPDX-License-Identifier: MITpragma solidity ^0.8.28;import "forge-std/Test.sol";import "../src/SimpleVault.sol";contract SimpleVaultTest is Test { SimpleVault public vault; address public alice = address(0x1); address public bob = address(0x2); function setUp() public { vault = new SimpleVault(); vm.deal(alice, 10 ether); vm.deal(bob, 5 ether); } function test_Deposit() public { vm.prank(alice); vault.deposit{value: 1 ether}(); assertEq(vault.balances(alice), 1 ether); assertEq(address(vault).balance, 1 ether); assertEq(alice.balance, 9 ether); } function test_DepositFailsWithZero() public { vm.prank(alice); vm.expectRevert(bytes("Must deposit more than 0")); vault.deposit{value: 0}(); } function test_Withdraw() public { vm.prank(alice); vault.deposit{value: 2 ether}(); vm.prank(alice); vault.withdraw(1 ether); assertEq(vault.balances(alice), 1 ether); assertEq(address(vault).balance, 1 ether); assertEq(alice.balance, 9 ether); } function test_WithdrawFailsWithInsufficientBalance() public { vm.prank(bob); vault.deposit{value: 1 ether}(); vm.prank(bob); vm.expectRevert(bytes("Insufficient balance")); vault.withdraw(2 ether); }}"""print("Squelette du test SimpleVault.t.sol :")print(VAULT_TEST_SKELETON)print("Indices :")print(" - vm.deal(alice, 10 ether) pour donner des ETH")print(" - vm.prank(alice) pour simuler un appel depuis Alice")print(" - vault.deposit{value: 1 ether}() pour deposer")print(" - vm.expectRevert(bytes(\"message\")) avant un revert attendu")
Squelette du test SimpleVault.t.sol :
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;
import "forge-std/Test.sol";
import "../src/SimpleVault.sol";
contract SimpleVaultTest is Test {
SimpleVault public vault;
address public alice = address(0x1);
address public bob = address(0x2);
function setUp() public {
vault = new SimpleVault();
vm.deal(alice, 10 ether);
vm.deal(bob, 5 ether);
}
function test_Deposit() public {
vm.prank(alice);
vault.deposit{value: 1 ether}();
assertEq(vault.balances(alice), 1 ether);
assertEq(address(vault).balance, 1 ether);
assertEq(alice.balance, 9 ether);
}
function test_DepositFailsWithZero() public {
vm.prank(alice);
vm.expectRevert(bytes("Must deposit more than 0"));
vault.deposit{value: 0}();
}
function test_Withdraw() public {
vm.prank(alice);
vault.deposit{value: 2 ether}();
vm.prank(alice);
vault.withdraw(1 ether);
assertEq(vault.balances(alice), 1 ether);
assertEq(address(vault).balance, 1 ether);
assertEq(alice.balance, 9 ether);
}
function test_WithdrawFailsWithInsufficientBalance() public {
vm.prank(bob);
vault.deposit{value: 1 ether}();
vm.prank(bob);
vm.expectRevert(bytes("Insufficient balance"));
vault.withdraw(2 ether);
}
}
Indices :
- vm.deal(alice, 10 ether) pour donner des ETH
- vm.prank(alice) pour simuler un appel depuis Alice
- vault.deposit{value: 1 ether}() pour deposer
- vm.expectRevert(bytes("message")) avant un revert attendu
Exemple guide 2 : Test avec avancee temporelle
Créez un test pour un contrat de verrouillage temporel (Timelock).
# Contrat Timelock a testertimelock_contract ='''// SPDX-License-Identifier: MITpragma solidity ^0.8.28;contract Timelock { uint256 public constant LOCK_DURATION = 1 days; mapping(address => uint256) public lockTime; mapping(address => uint256) public lockedAmount; event Locked(address indexed user, uint256 amount, uint256 unlockTime); event Unlocked(address indexed user, uint256 amount); function lock() public payable { require(msg.value > 0, "Must lock more than 0"); lockTime[msg.sender] = block.timestamp + LOCK_DURATION; lockedAmount[msg.sender] += msg.value; emit Locked(msg.sender, msg.value, lockTime[msg.sender]); } function unlock() public { require(lockedAmount[msg.sender] > 0, "No locked funds"); require(block.timestamp >= lockTime[msg.sender], "Still locked"); uint256 amount = lockedAmount[msg.sender]; lockedAmount[msg.sender] = 0; (bool success, ) = payable(msg.sender).call{value: amount}(""); require(success, "Transfer failed"); emit Unlocked(msg.sender, amount); }}'''print("Contrat Timelock.sol:")print(timelock_contract)
Contrat Timelock.sol:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;
contract Timelock {
uint256 public constant LOCK_DURATION = 1 days;
mapping(address => uint256) public lockTime;
mapping(address => uint256) public lockedAmount;
event Locked(address indexed user, uint256 amount, uint256 unlockTime);
event Unlocked(address indexed user, uint256 amount);
function lock() public payable {
require(msg.value > 0, "Must lock more than 0");
lockTime[msg.sender] = block.timestamp + LOCK_DURATION;
lockedAmount[msg.sender] += msg.value;
emit Locked(msg.sender, msg.value, lockTime[msg.sender]);
}
function unlock() public {
require(lockedAmount[msg.sender] > 0, "No locked funds");
require(block.timestamp >= lockTime[msg.sender], "Still locked");
uint256 amount = lockedAmount[msg.sender];
lockedAmount[msg.sender] = 0;
(bool success, ) = payable(msg.sender).call{value: amount}("");
require(success, "Transfer failed");
emit Unlocked(msg.sender, amount);
}
}
Écriture de la suite de tests pour le contrat Timelock en utilisant notamment vm.warp() pour simuler l’écoulement du temps. La solution complète ci-dessous sert d’exemple guidé résolu.
# Exercice : Ecrivez les tests pour Timelock# Vous devrez utiliser vm.warp() ou skip() pour le tempsTIMELOCK_TEST_SKELETON ="""// SPDX-License-Identifier: MITpragma solidity ^0.8.28;import "forge-std/Test.sol";import "../src/Timelock.sol";contract TimelockTest is Test { Timelock public timelock; address public alice = address(0x1); function setUp() public { timelock = new Timelock(); vm.deal(alice, 10 ether); } function test_Lock() public { uint256 startTime = block.timestamp; vm.prank(alice); timelock.lock{value: 2 ether}(); assertEq(timelock.lockedAmount(alice), 2 ether); assertEq(timelock.lockTime(alice), startTime + 1 days); assertEq(address(timelock).balance, 2 ether); } function test_CannotUnlockBeforeTime() public { vm.prank(alice); timelock.lock{value: 1 ether}(); vm.prank(alice); vm.expectRevert(bytes("Still locked")); timelock.unlock(); } function test_CanUnlockAfterTime() public { vm.prank(alice); timelock.lock{value: 3 ether}(); skip(1 days + 1); uint256 balanceBefore = alice.balance; vm.prank(alice); timelock.unlock(); assertEq(timelock.lockedAmount(alice), 0); assertEq(address(timelock).balance, 0); assertEq(alice.balance, balanceBefore + 3 ether); }}"""print("Squelette du test Timelock.t.sol :")print(TIMELOCK_TEST_SKELETON)print("Indices :")print(" - skip(1 days + 1) pour avancer le temps")print(" - vm.warp(timestamp) pour aller a un moment precis")print(" - timelock.lockTime(alice) pour le temps de deblocage")
Squelette du test Timelock.t.sol :
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;
import "forge-std/Test.sol";
import "../src/Timelock.sol";
contract TimelockTest is Test {
Timelock public timelock;
address public alice = address(0x1);
function setUp() public {
timelock = new Timelock();
vm.deal(alice, 10 ether);
}
function test_Lock() public {
uint256 startTime = block.timestamp;
vm.prank(alice);
timelock.lock{value: 2 ether}();
assertEq(timelock.lockedAmount(alice), 2 ether);
assertEq(timelock.lockTime(alice), startTime + 1 days);
assertEq(address(timelock).balance, 2 ether);
}
function test_CannotUnlockBeforeTime() public {
vm.prank(alice);
timelock.lock{value: 1 ether}();
vm.prank(alice);
vm.expectRevert(bytes("Still locked"));
timelock.unlock();
}
function test_CanUnlockAfterTime() public {
vm.prank(alice);
timelock.lock{value: 3 ether}();
skip(1 days + 1);
uint256 balanceBefore = alice.balance;
vm.prank(alice);
timelock.unlock();
assertEq(timelock.lockedAmount(alice), 0);
assertEq(address(timelock).balance, 0);
assertEq(alice.balance, balanceBefore + 3 ether);
}
}
Indices :
- skip(1 days + 1) pour avancer le temps
- vm.warp(timestamp) pour aller a un moment precis
- timelock.lockTime(alice) pour le temps de deblocage
Ce notebook a couvert les fondamentaux du framework Foundry pour le test de smart contracts en Solidity. Nous avons commence par l’installation et la verification de l’environnement (forge, cast, anvil), puis explore la structure standard d’un projet Foundry avec ses repertoires src/, test/, script/ et lib/. L’ecriture de tests en Solidite pur via le pattern DSTest a ete demontree a travers le contrat Counter, illustrant les assertions (assertEq, assertTrue, assertGt) et les fonctions de logging (emit log_named_uint).
Les cheatcodes vm constituent l’apport le plus puissant de Foundry : vm.prank et vm.startPrank pour simuler des identites, vm.deal pour créer des ETH, vm.warp et vm.roll pour manipuler le temps blockchain, et vm.expectRevert pour tester les cas d’erreur. Ces outils permettent de tester des scénarios complexes (contrôle d’acces, verrouillage temporel, soldes) sans infrastructure lourde.
Le notebook suivant, SC-13-Fuzz-Invariants-Python, approfondit ces techniques en introduisant le fuzz testing – la generation automatique d’inputs aleatoires pour decouvrir des bugs aux limites, et les tests d’invariants pour verifier que des proprietes fondamentales restent toujours vraies.