Sécurité·10 min de lecture·Par Solingo

Bonnes Pratiques de Sécurité Solidity — Déployez des Smart Contracts Sûrs

La checklist de sécurité essentielle pour chaque développeur Solidity. Du contrôle d'accès aux protections reentrancy.

# Bonnes Pratiques de Sécurité Solidity — Déployez des Smart Contracts Sûrs

La sécurité des smart contracts n'est pas négociable. Contrairement aux logiciels traditionnels, les bugs dans les smart contracts déployés peuvent entraîner des pertes de fonds irréversibles. Ce guide couvre les 12 pratiques de sécurité essentielles que tout développeur Solidity doit suivre.

Plus de 3 milliards de dollars ont été perdus dans des hacks de smart contracts en 2025. Ne laissez pas votre contrat être le prochain. Construisons du code sécurisé dès le premier jour.

1. Utilisez la Dernière Version de Solidity

Règle : Utilisez toujours Solidity 0.8.0 ou supérieur.

Pourquoi : La version 0.8.0 a introduit des vérifications automatiques d'overflow/underflow, éliminant toute une classe de vulnérabilités.

// ❌ MAUVAIS - Vulnérable aux overflow

pragma solidity ^0.7.6;

contract VulnerableToken {

mapping(address => uint256) public balances;

function transfer(address to, uint256 amount) external {

balances[msg.sender] -= amount; // Peut underflow !

balances[to] += amount; // Peut overflow !

}

}

// ✅ BON - Protégé par défaut

pragma solidity ^0.8.26;

contract SafeToken {

mapping(address => uint256) public balances;

function transfer(address to, uint256 amount) external {

balances[msg.sender] -= amount; // Revert sur underflow

balances[to] += amount; // Revert sur overflow

}

}

Bonne pratique : Fixez une version spécifique pour la production (pragma solidity 0.8.26;) pour éviter tout comportement inattendu avec les mises à jour du compilateur.

2. Protégez-vous Contre la Reentrancy

La vulnérabilité : Un attaquant appelle récursivement votre contrat avant que l'état ne soit mis à jour.

Exemple fameux : The DAO hack (2016) — $60M volés.

// ❌ VULNÉRABLE

contract VulnerableBank {

mapping(address => uint256) public balances;

function withdraw() external {

uint256 amount = balances[msg.sender];

// Appel externe AVANT la mise à jour de l'état

(bool success,) = msg.sender.call{value: amount}("");

require(success);

balances[msg.sender] = 0; // TROP TARD

}

}

// Attaque :

contract Attacker {

VulnerableBank bank;

receive() external payable {

if (address(bank).balance > 0) {

bank.withdraw(); // Appel récursif !

}

}

}

Solution 1 : Checks-Effects-Interactions Pattern

// ✅ SÉCURISÉ

function withdraw() external {

uint256 amount = balances[msg.sender];

balances[msg.sender] = 0; // Mise à jour d'état AVANT l'appel externe

(bool success,) = msg.sender.call{value: amount}("");

require(success);

}

Solution 2 : ReentrancyGuard (OpenZeppelin)

import "@openzeppelin/contracts/security/ReentrancyGuard.sol";

contract SafeBank is ReentrancyGuard {

mapping(address => uint256) public balances;

function withdraw() external nonReentrant {

uint256 amount = balances[msg.sender];

(bool success,) = msg.sender.call{value: amount}("");

require(success);

balances[msg.sender] = 0;

}

}

Règle d'or : Toujours mettre à jour l'état AVANT les appels externes.

3. Contrôle d'Accès Strict

Principe : Seules les adresses autorisées doivent pouvoir exécuter des fonctions sensibles.

// ❌ PAS DE PROTECTION

contract UnsafeVault {

function withdrawAll() external {

payable(msg.sender).transfer(address(this).balance); // N'importe qui peut appeler !

}

}

// ✅ OWNER UNIQUEMENT

contract SafeVault {

address public immutable owner;

constructor() {

owner = msg.sender;

}

modifier onlyOwner() {

require(msg.sender == owner, "Not owner");

_;

}

function withdrawAll() external onlyOwner {

payable(owner).transfer(address(this).balance);

}

}

Mieux : Utilisez OpenZeppelin Ownable

import "@openzeppelin/contracts/access/Ownable.sol";

contract ManagedVault is Ownable {

function withdrawAll() external onlyOwner {

payable(owner()).transfer(address(this).balance);

}

// Bonus : transfert de propriété sécurisé inclus

}

Contrôle d'accès avancé : Role-Based Access Control (RBAC)

import "@openzeppelin/contracts/access/AccessControl.sol";

contract AdvancedVault is AccessControl {

bytes32 public constant MANAGER_ROLE = keccak256("MANAGER_ROLE");

bytes32 public constant TREASURER_ROLE = keccak256("TREASURER_ROLE");

constructor() {

_grantRole(DEFAULT_ADMIN_ROLE, msg.sender);

}

function deposit() external payable onlyRole(MANAGER_ROLE) {}

function withdraw(uint amount) external onlyRole(TREASURER_ROLE) {}

}

4. Validez Tous les Inputs

Règle : Ne faites jamais confiance aux données externes.

// ❌ PAS DE VALIDATION

function setDiscount(uint256 discount) external {

// Et si discount = 100% ? Ou 1000% ?

userDiscount[msg.sender] = discount;

}

// ✅ VALIDATION STRICTE

function setDiscount(uint256 discount) external {

require(discount <= 50, "Discount too high"); // Max 50%

require(discount > 0, "Discount must be positive");

userDiscount[msg.sender] = discount;

}

Validation d'adresses :

// ❌ DANGEREUX

function setTreasury(address newTreasury) external onlyOwner {

treasury = newTreasury; // Et si newTreasury = address(0) ?

}

// ✅ SÉCURISÉ

function setTreasury(address newTreasury) external onlyOwner {

require(newTreasury != address(0), "Zero address");

treasury = newTreasury;

}

5. Gérez les Appels Externes avec Précaution

Problème : Les appels externes peuvent échouer ou être malveillants.

// ❌ UTILISE transfer() (peut échouer silencieusement sur certains cas)

function sendReward(address user, uint amount) external {

payable(user).transfer(amount); // Reverts si échec, mais 2300 gas seulement

}

// ✅ UTILISE call() avec vérification

function sendReward(address user, uint amount) external {

(bool success,) = payable(user).call{value: amount}("");

require(success, "Transfer failed");

}

Pourquoi call() > transfer() ?

  • transfer() : limite de 2300 gas (peut échouer avec des contrats receveurs complexes)
  • call() : forward tout le gas restant (mais nécessite une vérification explicite)

Pattern "Pull over Push" pour les paiements :

// Au lieu de pousser les paiements (risque si un destinataire reverts)

contract PushPayments {

function distributeRewards(address[] calldata users, uint[] calldata amounts) external {

for (uint i = 0; i < users.length; i++) {

(bool success,) = payable(users[i]).call{value: amounts[i]}("");

// Si un user revert, TOUS les suivants échouent !

}

}

}

// ✅ Laissez les users retirer (pull pattern)

contract PullPayments {

mapping(address => uint) public pendingRewards;

function claimReward() external {

uint amount = pendingRewards[msg.sender];

pendingRewards[msg.sender] = 0; // Checks-effects-interactions

(bool success,) = payable(msg.sender).call{value: amount}("");

require(success);

}

}

6. Utilisez des Custom Errors (Gas-Efficient)

Depuis Solidity 0.8.4 : Les custom errors économisent du gas vs require() avec strings.

// ❌ COÛTEUX (string stockée on-chain)

require(msg.sender == owner, "Caller is not the owner");

// ✅ GAS-EFFICIENT

error Unauthorized();

if (msg.sender != owner) revert Unauthorized();

Avec paramètres :

error InsufficientBalance(uint256 requested, uint256 available);

function withdraw(uint256 amount) external {

if (balances[msg.sender] < amount) {

revert InsufficientBalance(amount, balances[msg.sender]);

}

// ...

}

Économie de gas : ~50% sur les revert comparé à require avec strings.

7. Protégez les Fonctions Payable

Règle : Toute fonction recevant de l'ETH doit être payable.

// ❌ FONCTION PAYABLE SANS LOGIQUE

function updateSettings() external payable {

// Pourquoi payable si on n'utilise pas msg.value ?

}

// ✅ PAYABLE UNIQUEMENT SI NÉCESSAIRE

function deposit() external payable {

balances[msg.sender] += msg.value;

}

function updateSettings() external {

// Pas payable = revert si ETH envoyé

}

Vérifiez msg.value :

function buyToken(uint amount) external payable {

uint cost = amount * TOKEN_PRICE;

require(msg.value == cost, "Incorrect payment"); // Pas >=, exactement

// ...

}

8. Évitez les Délégations Dangereuses

delegatecall : Exécute du code dans le contexte du contrat appelant (même storage).

// ❌ DANGEREUX - L'implémentation peut modifier notre storage

contract Proxy {

address public implementation;

fallback() external payable {

// Délègue TOUT à l'implémentation

(bool success,) = implementation.delegatecall(msg.data);

require(success);

}

}

// Un attaquant pourrait déployer une implémentation malveillante qui modifie implementation

✅ Sécurisé avec contrôle d'accès :

contract SafeProxy is Ownable {

address public implementation;

function setImplementation(address newImpl) external onlyOwner {

implementation = newImpl;

}

fallback() external payable {

(bool success,) = implementation.delegatecall(msg.data);

require(success);

}

}

Ou utilisez des proxies audités (OpenZeppelin UUPS, Transparent Proxy).

9. Randomness Sécurisée

Problème : block.timestamp, block.number, blockhash sont prévisibles.

// ❌ MAUVAISE RANDOM (prévisible par les mineurs)

function rollDice() external returns (uint) {

uint randomNumber = uint(keccak256(abi.encodePacked(block.timestamp, msg.sender))) % 6;

// Un mineur peut manipuler block.timestamp

return randomNumber;

}

✅ Utilisez Chainlink VRF (Verifiable Random Function) :

import "@chainlink/contracts/src/v0.8/VRFConsumerBase.sol";

contract FairDice is VRFConsumerBase {

bytes32 internal keyHash;

uint256 internal fee;

function rollDice() external returns (bytes32 requestId) {

require(LINK.balanceOf(address(this)) >= fee, "Not enough LINK");

return requestRandomness(keyHash, fee);

}

function fulfillRandomness(bytes32 requestId, uint256 randomness) internal override {

uint result = (randomness % 6) + 1;

// Utilisez result (provably random)

}

}

Alternative : Commit-reveal scheme pour des besoins simples.

10. Gérez les Tokens ERC-20 Proprement

Problème : Certains tokens ERC-20 non-standard peuvent causer des problèmes.

// ❌ SUPPOSE QUE transfer() RETOURNE bool

function depositToken(address token, uint amount) external {

IERC20(token).transfer(address(this), amount); // USDT ne retourne pas bool !

}

// ✅ UTILISEZ SafeERC20 (OpenZeppelin)

import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";

function depositToken(address token, uint amount) external {

SafeERC20.safeTransferFrom(IERC20(token), msg.sender, address(this), amount);

}

SafeERC20 gère :

  • Tokens sans return value (USDT)
  • Tokens retournant false au lieu de revert
  • Approbations résiduelles

11. Audit et Tests Exhaustifs

Règle d'or : 1 ligne de Solidity = 3 lignes de tests minimum.

Tests à écrire :

// Hardhat test example

describe("SafeVault", function() {

it("Should revert if non-owner tries to withdraw", async function() {

const [owner, attacker] = await ethers.getSigners();

const vault = await SafeVault.deploy();

await expect(

vault.connect(attacker).withdrawAll()

).to.be.revertedWith("Not owner");

});

it("Should handle reentrancy attack", async function() {

// Déployer un contrat attaquant

const attacker = await ReentrancyAttacker.deploy(vault.address);

await expect(

attacker.attack()

).to.be.reverted; // Le guard doit bloquer

});

});

Outils d'analyse statique :

  • Slither : détecte 70+ vulnérabilités
  • Mythril : analyse symbolique
  • Echidna : fuzzing

Audits professionnels :

  • Audit avant tout déploiement mainnet de grande valeur
  • Budget : $10k-$100k+ selon la complexité
  • Auditeurs réputés : Trail of Bits, OpenZeppelin, Consensys Diligence

12. Utilisez des Bibliothèques Auditées

Ne réinventez pas la roue. Utilisez du code audité et testé en production.

OpenZeppelin Contracts :

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";

import "@openzeppelin/contracts/access/Ownable.sol";

import "@openzeppelin/contracts/security/Pausable.sol";

contract MyToken is ERC20, Ownable, Pausable {

constructor() ERC20("MyToken", "MTK") {

_mint(msg.sender, 1000000 * 10**18);

}

function pause() external onlyOwner {

_pause();

}

function unpause() external onlyOwner {

_unpause();

}

// Héritage de tout le code audité d'OZ

}

Autres bibliothèques :

  • Solmate : versions gas-optimisées (mais moins de sécurité prouvée qu'OZ)
  • PRBMath : mathématiques à précision fixe

Checklist de Sécurité Pré-Déploiement

Avant de déployer sur mainnet, vérifiez :

  • [ ] Solidity 0.8.0+ utilisé
  • [ ] Toutes les fonctions payable/external ont des guards
  • [ ] Checks-effects-interactions pattern respecté partout
  • [ ] Pas de tx.origin (utilisez msg.sender)
  • [ ] Contrôle d'accès sur toutes les fonctions sensibles
  • [ ] Custom errors utilisés (gas efficiency)
  • [ ] Bibliothèques OpenZeppelin à jour
  • [ ] Tests couvrant 100% des branches critiques
  • [ ] Slither/Mythril passés sans warnings critiques
  • [ ] Audit professionnel effectué (pour TVL > $1M)
  • [ ] Testé sur testnet pendant 1+ semaines
  • [ ] Plan de réponse aux incidents documenté
  • [ ] Mécanisme de pause/upgrade si nécessaire

Cas Réels de Hacks et Leçons

The DAO (2016) - $60M

Vulnérabilité : Reentrancy

Leçon : Checks-effects-interactions toujours

Parity Wallet (2017) - $150M

Vulnérabilité : Fonction d'initialisation non protégée

Leçon : Protégez les constructeurs/initializers

Ronin Bridge (2022) - $625M

Vulnérabilité : Clés privées compromises

Leçon : Multi-sig, hardware wallets, key management

Nomad Bridge (2022) - $190M

Vulnérabilité : Validation de message défaillante

Leçon : Testez TOUS les edge cases

Conclusion : Sécurité = Responsabilité

Un smart contract sécurisé nécessite :

  • Code défensif (supposez que tout peut échouer)
  • Tests exhaustifs (unit, integration, fuzzing)
  • Audits (interne + externe)
  • Monitoring (détection d'anomalies post-déploiement)
  • Plan d'urgence (circuit breakers, pause, upgrade)
  • La sécurité n'est pas une feature, c'est un mindset.

    Le parcours Sécurité Solidity de Solingo vous forme aux 50+ vulnérabilités connues, avec des labs pratiques de hacking éthique, des audits de code réels, et des exercices CTF (Capture The Flag). Construisez des contrats inviolables — commencez votre formation aujourd'hui.

    Prêt à mettre en pratique ?

    Applique ces concepts avec des exercices interactifs sur Solingo.

    Commencer gratuitement