Tutoriel·12 min de lecture·Par Solingo

8 Design Patterns Solidity Que Tout Développeur Devrait Connaître

De Checks-Effects-Interactions aux patterns Factory et Proxy. Écrivez des smart contracts plus propres, sûrs et maintenables.

# 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 :

  • Checks : Valider les conditions (require)
  • Effects : Mettre à jour les variables d'état
  • Interactions : Faire les appels externes
  • 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 :

  • Ne jamais changer la disposition du storage (ajouter des variables à la fin uniquement)
  • Utiliser des initializers, pas de constructeurs (les constructeurs s'exécutent dans l'implémentation, pas le proxy)
  • Tester les mises à niveau extensivement sur testnet
  • 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 :

  • Checks-Effects-Interactions : Prévenir la reentrancy
  • Pull Over Push : Éviter les attaques DOS
  • Guard Check : Validation propre et réutilisable
  • Factory : Gérer plusieurs instances
  • Proxy : Contrats évolutifs
  • Access Control : Permissions basées sur les rôles
  • Oracle : Pont on-chain/off-chain
  • Emergency Stop : Atténuation des risques
  • 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)

    Prêt à mettre en pratique ?

    Applique ces concepts avec des exercices interactifs sur Solingo.

    Commencer gratuitement