Introduction
En 2026, Ethereum reste la référence pour les smart contracts, avec ses mises à niveau comme Prague boostant la scalabilité et la sécurité. Développer un token ERC-20 est un rite de passage pour tout dev blockchain expert : c'est la base des DeFi, NFT et DAOs. Ce tutoriel vous guide pas à pas avec Foundry, l'outil Rust-based ultra-rapide qui domine le dev Solidity (tests 100x plus vite que Hardhat).
Nous créons un token MyToken (MTK) : mint initial, pause/relaunch via owner, et blacklisting avancé. Vous apprendrez à initier un projet, coder le contrat OpenZeppelin-based, tester exhaustivement (fuzzing inclus), et déployer sur Sepolia (testnet Ethereum).
Pourquoi Foundry ? Pas de Node.js, builds parallèles, scripting natif en Solidity. Résultat : un workflow pro, prêt pour mainnet ou L2 comme Base/Optimism. À la fin, vous bookmarquerez ce guide pour vos audits et launches réels. Temps estimé : 30 min.
Prérequis
- Foundry installé (voir ci-dessous si pas fait)
- Git
- Clef privée Ethereum (pour déploiement, via env
PRIVATE_KEY) - RPC Sepolia gratuit (Alchemy/Infura)
- Connaissances Solidity ^0.8, OpenZeppelin, tests Foundry
- Terminal Unix-like (WSL sur Windows)
Installation de Foundry
curl -L https://foundry.paradigm.xyz | bash
foundryup
forge --version
cast --version
anvil --versionCe script one-liner installe Foundry (foundry/anvil/cast/forge) via leur binaire officiel. foundryup met à jour vers la dernière version stable (v0.2.1+ en 2026). Vérifiez avec --version : attendez Forge 0.2.x pour EVM Prague support. Piège : sur macOS M1, ajoutez arch -arm64 si curl foire.
Initialisation du projet
Créez un repo dédié. Foundry init avec template inclut OpenZeppelin libs remotes, prêt pour ERC-20. Cela génère src/, test/, script/, foundry.toml basique. Clonez sur GitHub pour CI/CD.
Initialiser le projet Foundry
forge init erc20-token --template https://github.com/foundry-rs/forge-template
cd erc20-token
git init
git add .
git commit -m "Initial Foundry ERC-20 project"
forge installInit avec template forge-rs ajoute OpenZeppelin v5.x comme lib git submodule. forge install résout les deps (oz, ds-test). Structure : src/ contracts, test/ unit/fuzz, script/ deploy. Piège : oubliez pas cd avant edits, et push sur Git pour forge snapshot CI.
Configuration foundry.toml
[profile.default]
src = "src"
out = "out"
libs = ["lib"]
remappings = [
"@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/",
]
[profile.default.rpc_storage_caching]
chains = { sepolia = { mode = "local" } }
[profile.default.fuzz]
runs = 10000
seed = 123456789
[profile.default.wallets]
from_key = "${PRIVATE_KEY}"Config pro : remappings pour OZ imports clean, RPC caching local pour Sepolia (gain x10 vitesse). Fuzzing à 10k runs avec seed repro. Wallet auto via env PRIVATE_KEY. Piège : sans remappings, imports échouent ; testez avec forge build --verbose.
Développement du smart contract
Remplacez src/Counter.sol par notre ERC-20 avancé. Héritage OZ pour security audits-ready. Features : mint owner-only, pausable, blacklist (burn-like pour compliance).
Smart contract ERC-20 MyToken.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Pausable.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
contract MyToken is ERC20, ERC20Pausable, Ownable, ReentrancyGuard {
mapping(address => bool) public blacklist;
event Blacklisted(address indexed account);
event Unblacklisted(address indexed account);
constructor() ERC20("MyToken", "MTK") Ownable(msg.sender) {
_mint(msg.sender, 1_000_000 * 10**decimals());
}
function pause() public onlyOwner {
_pause();
}
function unpause() public onlyOwner {
_unpause();
}
function blacklistAddress(address account) public onlyOwner {
blacklist[account] = true;
emit Blacklisted(account);
}
function unblacklistAddress(address account) public onlyOwner {
blacklist[account] = false;
emit Unblacklisted(account);
}
function _beforeTokenTransfer(address from, address to, uint256 amount)
internal
whenNotPaused
override(ERC20, ERC20Pausable)
{
require(!blacklist[from] && !blacklist[to], "Blacklisted address");
super._beforeTokenTransfer(from, to, amount);
}
function mint(address to, uint256 amount) public onlyOwner {
_mint(to, amount);
}
function burn(uint256 amount) public {
_burn(msg.sender, amount);
}
}Contrat sécurisé : OZ base + Pausable/Ownable/ReentrancyGuard. Hooks _beforeTokenTransfer bloque blacklist/pause. Mint owner-only, burn public. Constructor mint 1M supply. Piège : override correct sinon infinite recursion ; compilez forge build pour checks.
Tests unitaires et fuzzing
Les tests Foundry sont votre bouclier : invariants, fuzz, traces. Couvrez 100% : deploy, mint, pause, blacklist, reentrancy.
Tests complets Token.t.sol
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.24;
import {Test, console} from "forge-std/Test.sol";
import {MyToken} from "../src/MyToken.sol";
contract MyTokenTest is Test {
MyToken token;
address owner = makeAddr("owner");
address user = makeAddr("user");
address blackUser = makeAddr("blackUser");
function setUp() public {
vm.prank(owner);
token = new MyToken();
}
function testInitialSupply() public {
assertEq(token.totalSupply(), 1_000_000 * 10**token.decimals());
assertEq(token.balanceOf(owner), 1_000_000 * 10**token.decimals());
}
function testMint() public {
uint256 mintAmount = 100 * 10**token.decimals();
vm.prank(owner);
token.mint(user, mintAmount);
assertEq(token.balanceOf(user), mintAmount);
}
function testPause() public {
vm.prank(owner);
token.pause();
vm.expectRevert("Pausable: paused");
token.transfer(user, 1);
vm.prank(owner);
token.unpause();
}
function testBlacklist() public {
vm.startPrank(owner);
token.blacklistAddress(blackUser);
vm.stopPrank();
vm.expectRevert("Blacklisted address");
vm.prank(blackUser);
token.transfer(user, 1);
}
function testFuzzBurn(uint256 amount) public {
vm.assume(amount <= token.balanceOf(owner));
vm.prank(owner);
token.burn(amount);
assertEq(token.balanceOf(owner), 1_000_000 * 10**token.decimals() - amount);
}
function testInvariantTotalSupply() public {
uint256 initial = token.totalSupply();
// Mint/burn/pause ne change pas l'invariant
assertEq(token.totalSupply(), initial);
}
}Suite exhaustive : setUp fork-like, vm.prank simule EOAs, expectRevert checks erreurs. Fuzz testFuzzBurn assume bounds pour gas. Invariant test pour audits. Piège : sans vm.assume, fuzz OOM ; run forge test -vvv pour traces.
Compilation et tests
forge build
forge test --gas-report
forge test --fuzz-runs 10000
forge snapshotBuild vérifie syntaxe/gas. --gas-report optimise (cible <200k deploy). Fuzz 10k iters. snapshot baseline gas pour regressions. Piège : gas spike ? Profilez avec --gas-report et remap deps.
Script de déploiement
Forge scripts en Solidity : broadcast tx, verify Etherscan auto. Set PRIVATE_KEY et SEPOLIA_RPC_URL en .env.
Script Deploy.s.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {Script, console} from "forge-std/Script.sol";
import {MyToken} from "../src/MyToken.sol";
contract Deploy is Script {
function run() external {
uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");
vm.startBroadcast(deployerPrivateKey);
MyToken token = new MyToken();
console.log("MyToken deployed to:", address(token));
console.log("Owner:", token.owner());
console.log("Total Supply:", token.totalSupply());
vm.stopBroadcast();
}
}Script broadcast : vm.envUint lit PRIVATE_KEY sécurisé, new deploy, logs addr. Auto-verify sur Etherscan via chain config. Piège : sans SEPOLIA_RPC_URL env, fallback mainnet ! Set export SEPOLIA_RPC_URL=https://sepolia.infura.io/v3/YOUR_KEY.
Déploiement sur Sepolia
export PRIVATE_KEY=0xYourPrivateKeyHere
export SEPOLIA_RPC_URL=https://rpc.sepolia.org # ou Alchemy
forge script script/Deploy.s.sol:Deploy --rpc-url $SEPOLIA_RPC_URL --broadcast --verify --etherscan-api-key $ETHERSCAN_API_KEY --legacyBroadcast tx live sur Sepolia, --verify source auto Etherscan. --legacy pour create2 compat old chains. Logs addr/console en tx receipt. Piège : nonce collision ? Ajoutez --sender ; check forge verify-contract post-deploy.
Bonnes pratiques
- Sécurité first : Toujours heriter OZ audited contracts, run Slither/Forge invariants.
- Gas optimisation : Profilez
--gas-report, utilisezimmutablepour storage, batch ops. - Tests coverage : Visez 100%+ fuzz/invariants ; intégrez CI GitHub Actions avec
forge test -vv. - Env management :
.env+dotenvpour keys ; multi-sig deploy via Gnosis. - Upgrades : Préparez proxy UUPS dès v1 pour token upgradable.
Erreurs courantes à éviter
- Oubli remappings : Imports OZ fail →
forge remappings --check. - Fuzz unbounded : Gas OOM →
vm.assume(amount < type(uint128).max). - Private key leak : Jamais commit
.env; utilisezsethou wallets HD. - Testnet gas : Sepolia saturé ? Switch Holesky ou L2 testnet comme OP Sepolia.
Pour aller plus loin
Maîtrisez les audits pro avec Slither et Echidna. Déployez sur L2 (Base, Arbitrum) via foundry.toml chains.
Découvrez nos formations Learni Blockchain : Solidity avancé, DeFi fullstack, ZK proofs. Rejoignez la communauté Discord pour reviews contrats.