# 8 Design Patterns Solidity Que Tout Développeur Devrait Connaître
Les design patterns sont des solutions éprouvées à des problèmes récurrents. En Solidity, ils préviennent les bugs, économisent le gas, et rendent le code maintenable.
Contrairement au développement web, les erreurs de smart contract sont permanentes. Un seul bug de reentrancy peut drainer des millions. Ces 8 patterns feront de vous un développeur plus sûr et professionnel.
1. Checks-Effects-Interactions (CEI)
Problème : Attaques de reentrancy—appels externes déclenchant une ré-entrée inattendue.
Solution : Suivre cet ordre dans chaque fonction :
require)Code Vulnérable :
function withdraw(uint amount) public {
require(balances[msg.sender] >= amount);
// INTERACTION avant EFFECT (vulnérable !)
(bool success,) = msg.sender.call{value: amount}("");
require(success);
balances[msg.sender] -= amount; // Mis à jour APRÈS l'appel externe
}
Attaque :
La fonction fallback de l'attaquant rappelle withdraw avant que balances soit mis à jour, vidant le contrat.
Code Sécurisé (CEI) :
function withdraw(uint amount) public {
// 1. CHECKS
require(balances[msg.sender] >= amount, "Insufficient balance");
// 2. EFFECTS
balances[msg.sender] -= amount;
// 3. INTERACTIONS
(bool success,) = msg.sender.call{value: amount}("");
require(success, "Transfer failed");
}
Maintenant, même si l'attaquant rentre à nouveau, sa balance est déjà à zéro.
Quand utiliser : Toujours, surtout lors d'appels externes.
---
2. Pull Over Push (Pattern de Retrait)
Problème : Pousser des paiements vers plusieurs adresses peut échouer si l'une revert, bloquant tout le monde.
Solution : Laisser les utilisateurs tirer leurs fonds au lieu de pousser.
Mauvais (Push) :
function distributeRewards(address[] memory recipients) public {
for (uint i = 0; i < recipients.length; i++) {
// Si un transfert échoue, toute la fonction revert
(bool success,) = recipients[i].call{value: 1 ether}("");
require(success);
}
}
Bon (Pull) :
mapping(address => uint) public pendingWithdrawals;
function distributeRewards(address[] memory recipients) public {
for (uint i = 0; i < recipients.length; i++) {
pendingWithdrawals[recipients[i]] += 1 ether;
}
}
function withdraw() public {
uint amount = pendingWithdrawals[msg.sender];
require(amount > 0, "No funds");
pendingWithdrawals[msg.sender] = 0; // Pattern CEI
(bool success,) = msg.sender.call{value: amount}("");
require(success);
}
Avantages :
- Un utilisateur ne peut pas bloquer les autres
- Efficace en gas (pas de boucles dans le chemin critique)
- L'utilisateur contrôle quand retirer
Quand utiliser : Airdrops, dividendes, distribution de récompenses.
---
3. Guard Check (Pattern de Modifier)
Problème : La logique de validation répétée encombre les fonctions.
Solution : Extraire les vérifications dans des fonctions modifier réutilisables.
Sans Modifiers :
function adminFunction() public {
require(msg.sender == owner, "Not owner");
require(!paused, "Contract paused");
// ... logique
}
function anotherAdminFunction() public {
require(msg.sender == owner, "Not owner");
require(!paused, "Contract paused");
// ... logique
}
Avec Modifiers :
modifier onlyOwner() {
require(msg.sender == owner, "Not owner");
_;
}
modifier whenNotPaused() {
require(!paused, "Contract paused");
_;
}
function adminFunction() public onlyOwner whenNotPaused {
// Logique propre et lisible
}
Modifiers Courants :
modifier nonReentrant() {
require(!locked, "Reentrant call");
locked = true;
_;
locked = false;
}
modifier validAddress(address addr) {
require(addr != address(0), "Zero address");
_;
}
modifier withinLimit(uint amount) {
require(amount <= MAX_AMOUNT, "Exceeds limit");
_;
}
Quand utiliser : Toute logique de validation répétée.
---
4. Factory Pattern
Problème : Déployer plusieurs instances du même contrat.
Solution : Un contrat factory qui crée et suit les contrats enfants.
// Contrat enfant
contract Token {
string public name;
address public owner;
constructor(string memory _name, address _owner) {
name = _name;
owner = _owner;
}
}
// Contrat factory
contract TokenFactory {
Token[] public tokens;
mapping(address => Token[]) public userTokens;
event TokenCreated(address indexed owner, address tokenAddress);
function createToken(string memory name) public returns (address) {
Token newToken = new Token(name, msg.sender);
tokens.push(newToken);
userTokens[msg.sender].push(newToken);
emit TokenCreated(msg.sender, address(newToken));
return address(newToken);
}
function getTokenCount() public view returns (uint) {
return tokens.length;
}
function getUserTokens(address user) public view returns (Token[] memory) {
return userTokens[user];
}
}
Avantages :
- Registre centralisé de toutes les instances
- Déploiement convivial (une transaction)
- Suivi de la propriété/métriques
Avancé : Minimal Proxy (EIP-1167)
Pour des clones efficaces en gas :
import "@openzeppelin/contracts/proxy/Clones.sol";
contract MinimalProxyFactory {
address public implementation;
constructor(address _implementation) {
implementation = _implementation;
}
function createClone() public returns (address) {
// Coûte ~10x moins de gas que 'new'
return Clones.clone(implementation);
}
}
Quand utiliser : Factories de tokens, collections NFT, création de DAO.
---
5. Proxy Pattern (Contrats Évolutifs)
Problème : Les smart contracts sont immuables—vous ne pouvez pas corriger de bugs ou ajouter de fonctionnalités.
Solution : Séparer la logique (évolutive) du stockage (permanent).
Transparent Proxy (OpenZeppelin) :
// Contrat d'implémentation (logique)
contract BoxV1 {
uint256 public value;
function store(uint256 newValue) public {
value = newValue;
}
}
// Version améliorée
contract BoxV2 {
uint256 public value;
function store(uint256 newValue) public {
value = newValue;
}
function increment() public {
value += 1;
}
}
Déploiement avec Foundry :
import "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol";
contract DeployProxy is Script {
function run() external {
BoxV1 implementation = new BoxV1();
TransparentUpgradeableProxy proxy = new TransparentUpgradeableProxy(
address(implementation),
msg.sender, // admin
"" // pas de données d'initialisation
);
// Utilise le proxy comme BoxV1
BoxV1(address(proxy)).store(42);
}
}
Mise à niveau :
function upgrade() external {
BoxV2 newImplementation = new BoxV2();
ProxyAdmin(proxyAdmin).upgrade(proxy, address(newImplementation));
}
Règles Critiques :
Quand utiliser : Contrats de haute valeur (DAOs, trésoreries), protocoles évolutifs.
---
6. Access Control (Basé sur les Rôles)
Problème : Un simple onlyOwner ne passe pas à l'échelle. Vous avez besoin de rôles (admin, minter, burner).
Solution : Pattern AccessControl d'OpenZeppelin.
import "@openzeppelin/contracts/access/AccessControl.sol";
contract RoleBasedToken is AccessControl {
bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
bytes32 public constant BURNER_ROLE = keccak256("BURNER_ROLE");
mapping(address => uint) public balances;
constructor() {
// Le déployeur est admin par défaut
_grantRole(DEFAULT_ADMIN_ROLE, msg.sender);
_grantRole(MINTER_ROLE, msg.sender);
}
function mint(address to, uint amount) public onlyRole(MINTER_ROLE) {
balances[to] += amount;
}
function burn(address from, uint amount) public onlyRole(BURNER_ROLE) {
balances[from] -= amount;
}
// L'admin peut accorder/révoquer des rôles
function addMinter(address account) public onlyRole(DEFAULT_ADMIN_ROLE) {
grantRole(MINTER_ROLE, account);
}
}
Rôles Hiérarchiques :
bytes32 public constant ADMIN_ROLE = keccak256("ADMIN_ROLE");
bytes32 public constant MODERATOR_ROLE = keccak256("MODERATOR_ROLE");
constructor() {
_setRoleAdmin(MODERATOR_ROLE, ADMIN_ROLE); // Les admins gèrent les modérateurs
}
Quand utiliser : DAOs, systèmes multi-admin, protocoles DeFi.
---
7. Oracle Pattern (Source de Données de Confiance)
Problème : Les smart contracts ne peuvent pas accéder aux données off-chain (prix, météo, scores sportifs).
Solution : Pattern oracle avec des fournisseurs de données de confiance.
Oracle Simple (Centralisé) :
contract PriceOracle {
address public oracle;
mapping(string => uint) public prices; // symbole => prix en USD (8 décimales)
event PriceUpdated(string symbol, uint price, uint timestamp);
modifier onlyOracle() {
require(msg.sender == oracle, "Not oracle");
_;
}
constructor(address _oracle) {
oracle = _oracle;
}
function updatePrice(string memory symbol, uint price) public onlyOracle {
prices[symbol] = price;
emit PriceUpdated(symbol, price, block.timestamp);
}
function getPrice(string memory symbol) public view returns (uint) {
uint price = prices[symbol];
require(price > 0, "Price not set");
return price;
}
}
// Contrat consommateur
contract LendingProtocol {
PriceOracle oracle;
function calculateCollateral(uint ethAmount) public view returns (uint usdValue) {
uint ethPrice = oracle.getPrice("ETH"); // ex: 2000_00000000 ($2000)
usdValue = (ethAmount * ethPrice) / 1e18;
}
}
Production : Utiliser Chainlink
import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol";
contract ChainlinkConsumer {
AggregatorV3Interface internal priceFeed;
constructor() {
// ETH/USD sur Ethereum mainnet
priceFeed = AggregatorV3Interface(0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419);
}
function getLatestPrice() public view returns (int) {
(, int price,,,) = priceFeed.latestRoundData();
return price; // 8 décimales
}
}
Quand utiliser : DeFi (flux de prix), gaming (aléatoire), assurance (événements du monde réel).
---
8. Emergency Stop (Circuit Breaker)
Problème : Un bug critique est découvert—vous devez mettre en pause le contrat.
Solution : Pattern pausable.
import "@openzeppelin/contracts/security/Pausable.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
contract EmergencyStop is Pausable, Ownable {
mapping(address => uint) public balances;
constructor() Ownable(msg.sender) {}
function deposit() public payable whenNotPaused {
balances[msg.sender] += msg.value;
}
function withdraw(uint amount) public whenNotPaused {
require(balances[msg.sender] >= amount);
balances[msg.sender] -= amount;
payable(msg.sender).transfer(amount);
}
// Fonctions d'urgence (admin seulement)
function pause() public onlyOwner {
_pause();
}
function unpause() public onlyOwner {
_unpause();
}
}
Avancé : Pause avec Verrou Temporel
uint public pausedUntil;
function emergencyPause(uint duration) public onlyOwner {
pausedUntil = block.timestamp + duration;
}
modifier whenNotPaused() {
require(block.timestamp > pausedUntil, "Contract paused");
_;
}
Quand utiliser : Protocoles de haute valeur, lancements publics, sécurité incertaine.
---
Combiner les Patterns
Les contrats du monde réel utilisent plusieurs patterns :
contract SecureVault is Ownable, Pausable, ReentrancyGuard {
mapping(address => uint) public balances;
// Guard Check + Emergency Stop
function deposit() public payable whenNotPaused {
balances[msg.sender] += msg.value;
}
// CEI + Pull Pattern + Reentrancy Guard
function withdraw(uint amount) public whenNotPaused nonReentrant {
// CHECKS
require(balances[msg.sender] >= amount, "Insufficient balance");
// EFFECTS
balances[msg.sender] -= amount;
// INTERACTIONS
(bool success,) = msg.sender.call{value: amount}("");
require(success, "Transfer failed");
}
// Access Control
function pause() public onlyOwner {
_pause();
}
}
---
Guide de Sélection de Patterns
| Cas d'Usage | Patterns |
|----------|----------|
| Contrat token | CEI, Guard Check, Access Control |
| Marketplace NFT | CEI, Pull Pattern, Emergency Stop |
| DAO | Factory, Proxy, Access Control |
| Protocole DeFi | CEI, Oracle, Emergency Stop, Pull Pattern |
| Système évolutif | Proxy, Access Control |
---
Anti-Patterns à Éviter
1. Tx.origin pour l'Authentification
// VULNÉRABLE
require(tx.origin == owner);
// SÉCURISÉ
require(msg.sender == owner);
2. Pragma Flottant
// RISQUÉ
pragma solidity ^0.8.0;
// SÛR
pragma solidity 0.8.20;
3. Variables de Bloc pour l'Aléatoire
// PRÉVISIBLE (les mineurs peuvent manipuler)
uint random = uint(keccak256(abi.encodePacked(block.timestamp, block.difficulty)));
// SÉCURISÉ
// Utiliser Chainlink VRF pour un vrai aléatoire
---
Conclusion
Ces 8 patterns sont la fondation du développement Solidity professionnel :
Maîtrisez-les, et vous écrirez du code qui est :
- Sécurisé (résistant aux attaques)
- Efficace (optimisé en gas)
- Maintenable (clair, modulaire)
- Professionnel (suit les standards de l'industrie)