# Tester sur un fork du mainnet avec Foundry
Les tests unitaires prouvent que votre logique fonctionne de maniere isolee. Les tests sur fork prouvent qu'elle fonctionne face a l'etat reel, parfois desordonne, d'une chaine en production : les vraies pools Uniswap, la liquidite reelle d'Aave, le storage exact d'un token que vous ne controlez pas. Foundry rend cela presque gratuit. Vous pointez un test vers une URL RPC, et l'EVM lit l'etat en direct a la demande pendant que vos transactions restent locales. Ce guide couvre les cheatcodes de fork, l'usurpation de baleines, le voyage dans le temps via les numeros de bloc, la configuration RPC et une mise en place CI propre.
Pourquoi les tests sur fork comptent
Mocker les protocoles externes est rapide mais vous ment. Un router Uniswap mocke ne revert jamais sur le slippage comme le vrai, et un ERC20 mocke peut ne pas reproduire un comportement fee-on-transfer ou une blacklist gelee. Le fork supprime les approximations :
- Vous integrez face au vrai bytecode des protocoles deployes.
- Vous lisez le storage en direct : soldes, reserves, prix d'oracle, etat de gouvernance.
- Vous attrapez des bugs d'integration que les mocks ne peuvent structurellement pas reproduire.
Le cout est un aller-retour reseau par slot de storage froid. Foundry met ces lectures en cache, donc la deuxieme execution est rapide.
Configuration RPC
Ne codez jamais une cle API en dur dans un test. Foundry lit des endpoints nommes depuis foundry.toml et resout les variables d'environnement a l'execution.
[rpc_endpoints]
mainnet = "${MAINNET_RPC_URL}"
arbitrum = "${ARBITRUM_RPC_URL}"
[etherscan]
mainnet = { key = "${ETHERSCAN_API_KEY}" }
Placez les secrets dans un fichier .env (et ajoutez-le au .gitignore) :
MAINNET_RPC_URL=https://eth-mainnet.example.com/v3/votre-cle
ARBITRUM_RPC_URL=https://arb-mainnet.example.com/v3/votre-cle
Desormais vm.createFork("mainnet") resout l'alias vers l'URL. Vous pouvez aussi passer une URL brute, mais l'alias garde les secrets hors de votre arbre de sources.
createFork vs createSelectFork
Foundry traite les forks comme des objets que vous gardez par identifiant et entre lesquels vous basculez.
vm.createFork(alias)cree un fork et renvoie son iduint256sans l'activer.
vm.createSelectFork(alias)cree un fork et en fait immediatement l'EVM active.
vm.selectFork(id)bascule le fork actif vers un fork deja cree.
Le schema en deux temps brille quand un seul test touche plus d'une chaine, par exemple un pont cross-chain :
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {Test} from "forge-std/Test.sol";
interface IERC20 {
function balanceOf(address) external view returns (uint256);
function transfer(address to, uint256 amount) external returns (bool);
}
contract MultiForkTest is Test {
uint256 internal mainnetFork;
uint256 internal arbitrumFork;
address constant USDC = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48;
function setUp() public {
mainnetFork = vm.createFork("mainnet");
arbitrumFork = vm.createFork("arbitrum");
}
function test_activeForkSwitches() public {
vm.selectFork(mainnetFork);
assertEq(vm.activeFork(), mainnetFork);
// L'offre de USDC sur le mainnet en direct est non nulle.
assertGt(IERC20(USDC).balanceOf(USDC), 0);
vm.selectFork(arbitrumFork);
assertEq(vm.activeFork(), arbitrumFork);
}
}
Chaque fork garde son propre etat, donc basculer d'avant en arriere ne fait pas fuiter de soldes ni de storage entre les chaines.
Epingler un bloc avec rollFork
Un fork epingle sur un bloc "latest" mouvant rend les tests instables : la liquidite, les prix et les soldes derivent d'une execution a l'autre. Epinglez un numero de bloc precis pour que chaque execution voie un etat identique.
Vous pouvez epingler a la creation, vm.createSelectFork("mainnet", 20_000_000), ou deplacer un fork existant avec vm.rollFork :
function test_pinAndRoll() public {
uint256 fork = vm.createSelectFork("mainnet", 20_000_000);
assertEq(block.number, 20_000_000);
// Avancer le fork vers un bloc ulterieur pour observer les changements d'etat.
vm.rollFork(20_100_000);
assertEq(block.number, 20_100_000);
}
Il existe aussi vm.rollFork(forkId, blockNumber) pour faire avancer un fork non actif, et une variante qui avance jusqu'au bloc d'un hash de transaction precis, utile pour reproduire un incident exactement comme il s'est produit on chain.
L'epinglage est aussi un gain de performance. Un bloc epingle est mis en cache sur disque apres la premiere execution, donc les executions suivantes evitent completement le reseau.
Usurper des comptes
Pour tester un depot, il vous faut des tokens. Sur un fork vous n'avez pas les cles privees d'une baleine, mais vous n'en avez pas besoin. vm.prank et vm.startPrank font apparaitre le prochain appel (ou chaque appel jusqu'a stopPrank) comme provenant de n'importe quelle adresse.
function test_impersonateWhale() public {
vm.selectFork(mainnetFork);
address whale = 0x55FE002aefF02F77364de339a1292923A15844B8; // un detenteur de USDC
address recipient = address(0xBEEF);
uint256 amount = 1_000e6; // USDC a 6 decimales
uint256 avant = IERC20(USDC).balanceOf(recipient);
vm.startPrank(whale);
IERC20(USDC).transfer(recipient, amount);
vm.stopPrank();
assertEq(IERC20(USDC).balanceOf(recipient), avant + amount);
}
Si aucun detenteur pratique n'existe, vm.deal ecrase directement un solde. Pour l'ETH natif, vm.deal(addr, 100 ether). Pour les ERC20, la forme a trois arguments calcule et ecrit le slot de storage pour vous :
// Donner 10 000 USDC a recipient en ecrivant directement le slot du solde.
deal(USDC, recipient, 10_000e6);
Notez que deal pour les tokens fonctionne en devinant le slot du mapping des soldes. Il gere les layouts standards, mais des tokens exotiques (proxies au storage inhabituel, tokens rebasing) peuvent necessiter un vm.store manuel.
Contrats persistants entre forks
Quand vous appelez selectFork, les contrats que vous avez deployes dans le test sont effaces car chaque fork a son propre etat. Si vous avez besoin qu'un contrat utilitaire survive a un changement de fork, marquez-le comme persistant :
function test_persistentHelper() public {
MyHelper helper = new MyHelper();
vm.makePersistent(address(helper));
vm.selectFork(mainnetFork);
// helper a toujours du code ici.
vm.selectFork(arbitrumFork);
// helper a toujours du code ici aussi.
}
Le contrat de test lui-meme et l'adresse du cheatcode sont persistants par defaut, ce qui explique pourquoi vos assertions continuent de fonctionner apres un changement.
Un test d'integration realiste
En assemblant le tout, voici la forme d'un test qui depose de vrais USDC dans un vault face a l'etat en direct :
function test_depositRealUsdc() public {
vm.createSelectFork("mainnet", 20_000_000);
address user = makeAddr("user");
deal(USDC, user, 5_000e6);
Vault vault = new Vault(USDC);
vm.startPrank(user);
IERC20(USDC).approve(address(vault), 5_000e6);
uint256 shares = vault.deposit(5_000e6);
vm.stopPrank();
assertGt(shares, 0);
assertEq(IERC20(USDC).balanceOf(address(vault)), 5_000e6);
}
Ce test atteint le vrai contrat USDC, les vraies decimales, la vraie semantique de transfert. Si USDC blackliste un jour user ou change de comportement, ce test l'attraperait.
Execution et CI
Lancez la suite avec une verbosite qui fait remonter les traces en cas d'echec :
forge test --match-contract MultiForkTest -vvv
En CI, definissez l'URL RPC comme secret et laissez le cache de fork accelerer les executions repetees. Une etape GitHub Actions minimale :
# Dans l'environnement du job
export MAINNET_RPC_URL="${{ secrets.MAINNET_RPC_URL }}"
forge test --fork-block-number 20000000
Deux conseils pratiques pour la CI :
~/.foundry/cache. Foundry y stocke l'etat RPC recupere, reduisant a la fois le temps d'execution et la consommation du fournisseur sur les executions suivantes.Un job de tests sur fork dedie qui tourne selon un planning (plutot qu'a chaque push) garde votre suite unitaire rapide au vert tout en exercant les integrations en direct.
A pratiquer concretement
Lire sur les tests sur fork est une chose ; en ecrire un qui attrape un vrai bug d'integration en est une autre. Sur app.solingo-blockchain.xyz vous pouvez lancer des exercices de style Foundry dans le navigateur, usurper des comptes et verifier vos assertions etape par etape. Ouvrez le parcours de tests, forkez face a l'etat reel d'un protocole et livrez des contrats auxquels vous faites vraiment confiance.