SC-12-Foundry-Testing-Python - Tests avec Foundry

<< Précédent : LLM Assisted | Retour au sommaire | Suivant : Fuzz & Invariants >>


Objectifs d’apprentissage

A la fin de ce notebook, vous saurez :

  1. Installer et configurer Foundry (forge, cast, anvil)
  2. Créer un projet avec la structure standard
  3. Ecrire des tests en Solidity avec DSTest
  4. Utiliser les cheatcodes pour simuler des scénarios
  5. 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 WSL

install_commands = '''
# 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
'''

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 Foundry
import subprocess

def check_tool(name):
    try:
        result = subprocess.run([name, "--version"], capture_output=True, text=True, timeout=10)
        if result.returncode == 0:
            return True, result.stdout.strip()
        return False, result.stderr.strip()
    except FileNotFoundError:
        return False, "Non installe"
    except Exception as e:
        return False, str(e)

print("Verification des outils Foundry:")
print("-" * 50)
all_ok = True
for 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 ok
print("-" * 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 Foundry
project_setup = '''
# 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
'''

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).

# Fichier foundry.toml - 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/"
]
'''

print("Configuration foundry.toml:")
print(foundry_toml)
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.sol
counter_contract = '''
// 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--;
    }
}
'''

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 pattern
basic_test = '''
// 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");
    }
}
'''

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 test
test_commands = '''
# 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
'''

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")
except FileNotFoundError:
    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) revert

CALCULATOR_TEST_SKELETON = '''
// SPDX-License-Identifier: MIT
pragma 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'erreur
contract 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 base
cheatcode_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.deal
prank_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.roll
time_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.expectRevert
revert_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 revert

OWNABLE_COUNTER_TEST = '''
// SPDX-License-Identifier: MIT
pragma 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 test
contract 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 disponibles
assertions_list = '''
// 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
'''

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'assertions
assertions_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 logging
logging_funcs = '''
// 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);
'''

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 logging
logging_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: MIT
pragma 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 tester
vault_contract = '''
// 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];
    }
}
'''

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 notebook

VAULT_TEST_SKELETON = """
// 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);
    }
}
"""

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 tester
timelock_contract = '''
// 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);
    }
}
'''

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 temps

TIMELOCK_TEST_SKELETON = """
// 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);
    }
}
"""

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

7. Resume

Commandes Foundry essentielles

Commande Description
forge init Créer un nouveau projet
forge build Compiler les contrats
forge test Executer les tests
forge test -vvvv Tests avec logs detailles
forge test --match-test <name> Executer un test spécifique
forge install <lib> Installer une dépendance
forge fmt Formater le code

Cheatcodes principaux

Cheatcode Effet
vm.prank(addr) Change msg.sender (1 appel)
vm.startPrank(addr) Change msg.sender (persistent)
vm.deal(addr, amt) Donne des ETH
vm.warp(ts) Change le timestamp
vm.roll(num) Change le numéro de bloc
vm.expectRevert() Attend un revert
vm.expectEmit() Verifie un event

Assertions

Assertion Verifie
assertEq(a, b) Egalite
assertTrue(x) x est vrai
assertFalse(x) x est faux
assertGt(a, b) a > b
assertLt(a, b) a < b

Notebook suivant : SC-13-Fuzz-Invariants-Python - Fuzz Testing et invariants

Resume et perspectives

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.


<< Précédent : LLM Assisted | Retour au sommaire | Suivant : Fuzz & Invariants >>

Retour au sommet