# Post-Mortem — Les Plus Gros Hacks DeFi du T1 2026
Le T1 2026 a vu ~$180M volés dans la DeFi. Nouveaux vecteurs d'attaque, vieux patterns qui reviennent, et quelques leçons douloureuses.
Voici les 5 hacks majeurs, ce qui s'est passé, et ce qu'il faut retenir.
1. Bridge Exploit via Malléabilité de Signature (~$45M)
Protocole : CrossChain Bridge (anonymisé)
Date : 12 janvier 2026
Pertes : $45M
Ce qui s'est passé
Un bridge cross-chain utilisait des signatures ECDSA pour autoriser les retraits. L'attaquant a exploité la malléabilité des signatures ECDSA.
// Contrat vulnérable (simplifié)
contract Bridge {
mapping(bytes32 => bool) public processedWithdrawals;
function withdraw(
address to,
uint256 amount,
bytes memory signature
) external {
bytes32 messageHash = keccak256(abi.encodePacked(to, amount));
address signer = recoverSigner(messageHash, signature);
require(signer == validator, "Invalid signature");
// ❌ Bug : pas de nonce, seulement le hash du message
require(!processedWithdrawals[messageHash], "Already processed");
processedWithdrawals[messageHash] = true;
token.transfer(to, amount);
}
function recoverSigner(bytes32 hash, bytes memory sig) internal pure returns (address) {
(uint8 v, bytes32 r, bytes32 s) = splitSignature(sig);
return ecrecover(hash, v, r, s);
}
}
Exploit : les signatures ECDSA ont deux formes valides (malléabilité) :
signature (r, s, v) → valide
signature (r, -s mod n, v') → aussi valide (même signer, même message)
L'attaquant a :
(r, -s mod n, v')3. Soumis les deux signatures → deux retraits pour le même message
Cause racine
- Pas de nonce dans le message signé
- Vérification basée surmessageHash`
seul (pas sur la signature)
- Pas de protection contre la malléabilité ECDSA
Leçon
Toujours inclure un nonce dans les messages signés :
// Fix
mapping(uint256 => bool) public usedNonces;
function withdraw(
address to,
uint256 amount,
uint256 nonce, // ← Nonce unique
bytes memory signature
) external {
bytes32 messageHash = keccak256(abi.encodePacked(to, amount, nonce));
address signer = recoverSigner(messageHash, signature);
require(signer == validator, "Invalid signature");
require(!usedNonces[nonce], "Nonce used");
usedNonces[nonce] = true;
token.transfer(to, amount);
}
Ou utiliser OpenZeppelin ECDSA qui normalise `s` pour empêcher la malléabilité.
2. Manipulation d'Oracle via Prêt Flash (~$32M)
Protocole : YieldFarm Protocol (anonymisé)
Date : 28 janvier 2026
Pertes : $32M
Ce qui s'est passé
Un protocole de lending utilisait un spot price oracle basé sur une pool Uniswap V3.
// Oracle vulnérable
contract PriceOracle {
IUniswapV3Pool public pool;
function getPrice() external view returns (uint256) {
(uint160 sqrtPriceX96,,,,,,) = pool.slot0();
// ❌ Bug : utilise le prix instantané (manipulable)
return uint256(sqrtPriceX96) ** 2 >> 192;
}
}
// Protocole de lending
contract LendingPool {
function borrow(uint256 collateralAmount) external {
uint256 collateralValue = oracle.getPrice() * collateralAmount;
uint256 maxBorrow = collateralValue * 80 / 100; // 80% LTV
// ...
}
}
Exploit :
Cause racine
- Oracle basé sur `
slot0()`(prix spot instantané)
- Pas de TWAP (time-weighted average price)
- Pool de liquidité insuffisante (manipulation facile)
Leçon
Ne JAMAIS utiliser le prix spot pour des décisions financières critiques.
// Fix : TWAP sur 30 minutes
contract TWAPOracle {
IUniswapV3Pool public pool;
uint32 public constant TWAP_PERIOD = 1800; // 30 min
function getPrice() external view returns (uint256) {
uint32[] memory secondsAgos = new uint32[](2);
secondsAgos[0] = TWAP_PERIOD;
secondsAgos[1] = 0;
(int56[] memory tickCumulatives,) = pool.observe(secondsAgos);
int56 tickCumulativeDelta = tickCumulatives[1] - tickCumulatives[0];
int24 arithmeticMeanTick = int24(tickCumulativeDelta / int56(uint56(TWAP_PERIOD)));
return getQuoteAtTick(arithmeticMeanTick);
}
}
Mieux : utiliser Chainlink ou Pyth pour les prix critiques.
3. Griefing de Restaking Slashing (~$28M)
Protocole : EigenLayer-like Restaking Protocol
Date : 15 février 2026
Pertes : $28M (slashing)
Ce qui s'est passé
Un protocole de restaking permettait de slasher des validators malveillants. L'attaquant a exploité une race condition dans le processus de slashing.
// Contrat de restaking (simplifié)
contract RestakingPool {
mapping(address => uint256) public stakes;
mapping(bytes32 => bool) public slashingProofs;
function slash(
address validator,
bytes memory proof
) external {
bytes32 proofHash = keccak256(proof);
require(!slashingProofs[proofHash], "Already slashed");
require(verifyMisbehavior(proof), "Invalid proof");
// ❌ Bug : TOCTOU (time-of-check/time-of-use)
slashingProofs[proofHash] = true;
uint256 slashAmount = stakes[validator] / 2;
stakes[validator] -= slashAmount;
// ...
}
}
Exploit : l'attaquant a soumis la même preuve de slashing dans deux transactions dans le même block.
slash(validator, proof)` → slash 50%slash(validator, proof)` → re-slash 50% du resteCause racine
- Pas de reentrancy guard
- Vérification `
slashingProofs`après validation (TOCTOU)
- Pas de limite sur le slashing total
Leçon
Protéger contre les race conditions et double-exécution :
// Fix
function slash(
address validator,
bytes memory proof
) external nonReentrant { // ← Reentrancy guard
bytes32 proofHash = keccak256(proof);
require(!slashingProofs[proofHash], "Already slashed");
slashingProofs[proofHash] = true; // ← Marquer AVANT logique
require(verifyMisbehavior(proof), "Invalid proof");
uint256 slashAmount = stakes[validator] / 2;
stakes[validator] -= slashAmount;
// ...
}
Ou utiliser commit-reveal pour éviter la manipulation MEV.
4. Cascade de Depeg LST (~$40M)
Protocole : Multi-protocol Depeg
Date : 8 mars 2026
Pertes : $40M (liquidations en cascade)
Ce qui s'est passé
Pas vraiment un "hack", mais une cascade de liquidations suite au depeg d'un Liquid Staking Token (LST).
// Protocole de lending (simplifié)
contract LendingPool {
uint256 public constant LIQUIDATION_THRESHOLD = 120; // 120%
function liquidate(address borrower) external {
uint256 collateralValue = oracle.getPrice(lstToken) * collateral[borrower];
uint256 debtValue = debt[borrower];
// Si collateral < 120% de la dette → liquidation
require(collateralValue * 100 < debtValue * LIQUIDATION_THRESHOLD, "Healthy");
// ❌ Problème : vente massive → prix chute → plus de liquidations
uint256 seizeAmount = debt[borrower] * 108 / 100; // 8% bonus
lstToken.transferFrom(borrower, msg.sender, seizeAmount);
}
}
Cause racine
- Collateral non-diversifié (concentration sur 1-2 LSTs)
- Oracles lents (pas de circuit breaker sur depeg rapide)
- Liquidation bonus trop élevé (incentive à liquider massivement)
Leçon
Diversifier le collateral et ajouter des circuit breakers :
// Fix : circuit breaker
contract SafeLendingPool {
mapping(address => uint256) public lastPrice;
uint256 public constant MAX_PRICE_CHANGE = 10; // 10% max par heure
function liquidate(address borrower) external {
uint256 currentPrice = oracle.getPrice(lstToken);
uint256 priceChange = abs(currentPrice - lastPrice[lstToken]) * 100 / lastPrice[lstToken];
// Circuit breaker : si prix change > 10% → pause liquidations
require(priceChange <= MAX_PRICE_CHANGE, "Circuit breaker triggered");
// ...
}
}
Ou réduire le liquidation bonus pour éviter les incentives pervers.
5. Frontrun d'Intent Cross-Chain (~$15M)
Protocole : Intent-based DEX Aggregator
Date : 22 mars 2026
Pertes : $15M
Ce qui s'est passé
Un nouveau protocole d'intent-based trading cross-chain permettait aux utilisateurs de signer des "intents" (ordres off-chain). Les fillers exécutaient ces intents on-chain.
// Protocole d'intent (simplifié)
struct Intent {
address user;
address tokenIn;
address tokenOut;
uint256 amountIn;
uint256 minAmountOut;
uint256 deadline;
bytes signature;
}
function fillIntent(Intent memory intent) external {
require(block.timestamp <= intent.deadline, "Expired");
require(verifySignature(intent), "Invalid sig");
// ❌ Bug : pas de slippage protection cross-chain
uint256 amountOut = swap(intent.tokenIn, intent.tokenOut, intent.amountIn);
require(amountOut >= intent.minAmountOut, "Slippage");
tokenOut.transfer(intent.user, amountOut);
}
Exploit : l'attaquant a frontrun les intents en :
minAmountOut``)Cause racine
- Intents publics dans le mempool (pas de privacy)
- Deadline trop longue (plusieurs minutes → temps de manipuler)
- minAmountOut trop laxiste (pas de protection réelle)
Leçon
Utiliser des enchères privées (private mempools, MEV-Boost+, etc.) ou commit-reveal :
// Fix : commit-reveal pour intents
mapping(bytes32 => bool) public commits;
function commitIntent(bytes32 intentHash) external {
commits[intentHash] = true;
}
function revealIntent(Intent memory intent) external {
bytes32 intentHash = keccak256(abi.encode(intent));
require(commits[intentHash], "Not committed");
require(block.timestamp <= intent.deadline, "Expired");
// ...
}
Ou router via private RPC (Flashbots Protect, MEV Blocker).
Patterns Récurrents
Les 5 hacks partagent des patterns communs :
Ce qu'il faut retenir
Pour les devs :
- ✅ Nonces dans toutes les signatures
- ✅ TWAP, jamais spot price
- ✅ Reentrancy guards partout
- ✅ Circuit breakers sur prix/liquidations
- ✅ Private mempools pour les ordres sensibles
Pour les utilisateurs :
- Diversifiez votre collateral (pas tout sur 1 LST)
- Surveillez les health factors de près (surtout en période volatile)
- Utilisez des protocols audités (multi-audits > single audit)
Pour l'écosystème :
- Bug bounties > post-mortem
- Audits continus, pas one-off
- Formal verification pour les protocoles critiques
Le T1 2026 a été coûteux. Espérons que le T2 sera plus safe. 🛡️