# Attaques sandwich et comment se proteger du MEV
Quand vous echangez des tokens sur un automated market maker (AMM), votre transaction attend dans le mempool public avant d'etre minee. Quiconque observe ce mempool peut lire votre intention : le montant, la paire de tokens et, surtout, votre tolerance au slippage. Une attaque sandwich exploite precisement cette visibilite. C'est l'une des formes les plus courantes de MEV (Maximal Extractable Value), et contrairement a un bug de contrat, elle ne requiert aucune faille dans le code que vous appelez. Il suffit que vous diffusiez un trade rentable en clair.
Cet article explique le mecanisme, pourquoi une borne de slippage trop large offre discretement votre argent a un searcher, et les defenses pratiques que vous pouvez appliquer en tant qu'utilisateur comme en tant que concepteur de protocole.
Comment fonctionne une attaque sandwich
Un AMM de type pool a produit constant price un swap le long de la courbe x * y = k. Plus votre trade est grand par rapport aux reserves, plus le prix bouge en votre defaveur. Ce mouvement est le price impact, et il est deterministe pour des reserves donnees.
Un sandwich, ce sont trois transactions ordonnees autour de la votre :
L'attaquant profite du prix qu'il a lui-meme deplace, et c'est vous qui le payez via une execution degradee. Il n'a pas besoin de predire l'avenir. Il lui faut seulement votre transaction en attente et la capacite d'ordonner les siennes autour.
Un exemple concret
Supposons un pool contenant 1 000 ETH et 2 000 000 USDC. Vous voulez acheter de l'ETH avec 100 000 USDC. Sur la courbe brute, vous recevriez environ 47,6 ETH. Si l'attaquant depense d'abord 200 000 USDC pour acheter de l'ETH, le pool se decale, et vos memes 100 000 USDC achetent maintenant sensiblement moins. L'attaquant revend ensuite son ETH dans le prix gonfle. L'ecart entre ce que vous auriez du recevoir et ce que vous avez recu est sa marge, moins le gas.
Pourquoi une borne de slippage trop large echoue
Toute fonction de swap serieuse prend un argument de sortie minimale. Dans les routeurs de style Uniswap V2, cela ressemble a ceci :
function swapExactTokensForTokens(
uint256 amountIn,
uint256 amountOutMin,
address[] calldata path,
address to,
uint256 deadline
) external returns (uint256[] memory amounts);
Le amountOutMin est votre protection : la transaction reverte si vous deviez recevoir moins. Le probleme, c'est la marge que vous lui laissez. Beaucoup d'interfaces appliquent par defaut un tampon confortable, parfois 0,5 pour cent, 1 pour cent, ou pire, jusqu'a 49 pour cent dans les cas extremes. Ce tampon n'est pas une marge de securite pour vous. C'est le profit maximal que vous avez autorise l'attaquant a extraire.
Voyez les choses ainsi : un sandwich n'est rentable que jusqu'au point ou votre trade passe encore votre amountOutMin. Une borne plus serree retrecit cette fenetre. Fixez un nombre qui reflete les conditions reelles du marche plus une volatilite reelle, pas un chiffre rond paresseux. Si vous autorisez 3 pour cent de slippage sur un pool calme, vous invitez un searcher a en prendre la majeure partie.
// Calculer amountOutMin a partir du quote courant avec une tolerance serree.
// Exemple : 30 points de base (0,30 %).
uint256 quoted = router.getAmountsOut(amountIn, path)[path.length - 1];
uint256 amountOutMin = quoted * 9970 / 10000;
Le deadline compte aussi. Un deadline perime laisse une transaction trainer et s'executer a un prix futur, manipule. Gardez-le court, de l'ordre de quelques minutes.
Protections pour les utilisateurs
Resserrer amountOutMin reduit la taille du gain mais ne cache pas votre trade. Le correctif le plus fort consiste a cesser de diffuser en clair.
- Order flow prive / Flashbots Protect : au lieu du mempool public, vous soumettez via un relais prive qui envoie la transaction directement aux block builders. Les searchers ne la voient pas avant son inclusion, donc il n'y a rien a sandwicher. C'est la protection la plus efficace pour un trader individuel.
- Slippage serre plus deadline court : defense en profondeur meme quand vous utilisez une route privee, car toutes les routes ne sont pas totalement privees.
- Fractionner les gros trades : decouper un gros swap en morceaux plus petits abaisse le price impact par transaction, mais augmente le gas total. A manier avec soin.
- Trader sur des pools a forte liquidite : le meme trade nominal deplace moins le prix quand les reserves sont importantes, laissant une marge plus mince a l'attaquant.
Protections pour les concepteurs de protocole
Si vous construisez un contrat qui swap pour le compte des utilisateurs (un vault, un routeur, un rebalanceur), la responsabilite vous revient. N'appelez jamais un swap avec amountOutMin = 0. Cette seule ligne est une invitation permanente a vider la valeur a chaque interaction.
Transmettre de vrais minimums, ne pas coder zero en dur
// Anti-pattern : l'appelant n'a aucune protection.
router.swapExactTokensForTokens(amountIn, 0, path, address(this), block.timestamp);
// Mieux : exiger que l'appelant specifie, et imposer une fenetre de fraicheur.
function rebalance(uint256 amountIn, uint256 minOut, uint256 deadline) external {
require(deadline >= block.timestamp, "expired");
require(minOut > 0, "minOut required");
router.swapExactTokensForTokens(amountIn, minOut, path, address(this), deadline);
}
Etre TWAP-aware, pas naif sur le prix spot
Si votre contrat lit un prix pour decider (valorisation de collateral, controle de juste valeur, borne de bon sens sur un swap), ne faites jamais confiance au prix spot instantane d'un AMM. Le prix spot est exactement ce qu'un sandwich manipule. Utilisez un prix moyen pondere par le temps (TWAP) sur une fenetre assez longue pour que le deplacer pendant cette duree coute plus cher que ce que rapporte l'attaque.
Uniswap V3 expose des donnees de tick cumulees que vous pouvez lire pour un TWAP :
uint32[] memory secondsAgos = new uint32[](2);
secondsAgos[0] = 600; // il y a 10 minutes
secondsAgos[1] = 0; // maintenant
(int56[] memory tickCumulatives, ) = pool.observe(secondsAgos);
int56 tickDelta = tickCumulatives[1] - tickCumulatives[0];
int24 avgTick = int24(tickDelta / int56(uint56(600)));
// Convertir avgTick en prix avec la tick math standard, puis l'utiliser.
Un TWAP eleve le cout de la manipulation, car l'attaquant doit maintenir le prix loin de la juste valeur sur de nombreux blocs, pas un seul. Ce n'est pas magique. Une fenetre courte ou un pool fin peuvent encore etre deplaces, donc dimensionnez la fenetre selon la valeur en jeu.
Commit-reveal pour l'intention d'ordre
Quand le trade lui-meme doit etre public sur une seule chaine, vous pouvez cacher l'intention dans le temps. Dans un schema commit-reveal, l'utilisateur soumet d'abord un hash de son ordre (le commit) puis seulement plus tard le texte en clair (le reveal). Un searcher qui voit le commit ne peut pas front-runner un trade dont la direction et la taille sont inconnues.
mapping(address => bytes32) public commitments;
function commit(bytes32 orderHash) external {
commitments[msg.sender] = orderHash;
}
function reveal(uint256 amountIn, uint256 minOut, uint256 nonce) external {
bytes32 expected = keccak256(abi.encodePacked(amountIn, minOut, nonce, msg.sender));
require(commitments[msg.sender] == expected, "bad reveal");
delete commitments[msg.sender];
// executer le swap avec les parametres reveles
}
Le commit-reveal ajoute une transaction et de la latence, et il ne resout pas a lui seul le back-running, donc il convient mieux aux encheres par lots et aux systemes d'ordres qu'a un simple swap retail. Considerez-le comme une brique, pas comme un correctif cle en main.
Une courte checklist mentale
- Mon
amountOutMinest-il derive d'un quote frais avec une tolerance serree et realiste ?
- Mon
deadlineest-il court ?
- Est-ce que je diffuse vers le mempool public, ou via un relais prive ?
- Si un contrat lit un prix pour decider, est-ce un TWAP et non une lecture spot ?
- Un chemin de swap quelque part dans mon systeme passe-t-il
0comme sortie minimale ?
A pratiquer concretement
La facon la plus rapide d'integrer tout cela est de construire l'attaque puis de s'en defendre. Sur app.solingo-blockchain.xyz, vous pouvez parcourir le fonctionnement des AMM, calculer le price impact sur une courbe a produit constant, et ecrire une logique de swap qui fixe un vrai amountOutMin et lit un TWAP plutot que le spot. Voir votre propre slippage trop large se faire sandwicher dans un exercice tend a graver la lecon.