Flux de tokens, NFT et contrats intelligents · Leçon 4 sur 6
Échanges sur les DEX et flux de liquidité
9 min de lecture
Les plateformes d’échange décentralisées sont l’endroit où les jetons volés sont convertis en ETH ou en stablecoins, où les rug pulls du jour du lancement dépouillent les acheteurs, et où les bots extraient de la valeur des traders ordinaires. Chaque échange laisse des preuves précises : l’événement propre au pool, les transferts de jetons qui l’entourent et les réserves du pool avant et après. Cette leçon explique comment lire ces preuves, reconstituer ce qu’un trader a réellement fait via un agrégateur, mesurer l’impact sur le prix et reconnaître les schémas de MEV.
Pools, token0 et token1
Une paire de type Uniswap v2 est un contrat qui détient deux jetons. Le jeton dont l’adresse est la plus basse est token0, et l’autre est token1 ; cet ordre est fixé à la création et n’a rien à voir avec le jeton qui représente « le prix ». Chaque événement rapporte les montants dans cet ordre. Un décodeur doit donc rechercher token0, token1 et les décimales des deux jetons avant toute conversion.
L’événement Swap de v2 est Swap(address indexed sender, uint256 amount0In, uint256 amount1In, uint256 amount0Out, uint256 amount1Out, address indexed to), topic0 0xd78ad95f…d822. Après chaque modification des réserves, la paire émet également Sync(uint112 reserve0, uint112 reserve1) avec les nouveaux soldes. Voici un échange décodé dans une paire où token0 est USDC (6 décimales) et token1 est WETH (18 décimales) :
{
"event": "Swap",
"pair": "0x3f8a…e2d1",
"sender": "0x7a2c…48d0",
"to": "0xa1b2c3d4…9f0e",
"amount0In": "0",
"amount1In": "5000000000000000000",
"amount0Out": "14880819116",
"amount1Out": "0",
"next_log": {"event": "Sync", "reserve0": "2985119180884", "reserve1": "1005000000000000000000"}
}
Interprétation : 5 WETH sont entrés et 14 880,819116 USDC sont sortis, soit un prix d’exécution de 14 880,82 ÷ 5 = 2 976,16 USDC par WETH. Le sender est généralement un contrat de routeur, et non le trader ; to indique où la sortie a été envoyée. Le trader est la personne qui a finalement envoyé l’entrée et reçu la sortie, ce que vous pouvez confirmer à partir des transferts de jetons de la même transaction.
Pools à liquidité concentrée et autres conceptions
Les pools Uniswap v3 émettent Swap(address indexed sender, address indexed recipient, int256 amount0, int256 amount1, uint160 sqrtPriceX96, uint128 liquidity, int24 tick), topic0 0xc42079f9…ca67. Les montants sont signés du point de vue du pool : une valeur positive signifie que le jeton est entré dans le pool, une valeur négative qu’il en est sorti. Un échange de 2 WETH contre de l’USDC dans un pool USDC/WETH affiche amount0 = -5994120000 (5 994,12 USDC sortis) et amount1 = 2000000000000000000 (2 WETH entrés).
L’événement donne également le prix du pool après l’échange, encodé sous forme de racine carrée en virgule fixe :
price_raw = (sqrtPriceX96 / 2^96)^2 -- unités de base de token1 par unité de base de token0
price = price_raw * 10^(decimals0 - decimals1)
example : sqrtPriceX96 = 1446501726624926496477173928747177
price_raw = 333,333,333.33 -> price = 0,000333333 WETH par USDC
inverse = 3,000 USDC par WETH (tick ≈ 196,256, since price_raw = 1.0001^tick)
La valeur mise au carré est exprimée en unités de base ; l’ajustement des décimales (6 − 18 = −12) est donc essentiel. Si vous l’omettez, le prix sera erroné d’un facteur de 10^12.
Les autres conceptions nécessitent leurs propres décodeurs. Uniswap v4 (2025) conserve tous les pools dans un seul contrat PoolManager singleton ; l’address du journal n’identifie donc plus le pool : c’est l’id du pool indexé qui le fait. Son événement Swap, Swap(bytes32 indexed id, address indexed sender, int128 amount0, int128 amount1, uint160 sqrtPriceX96, uint128 liquidity, int24 tick, uint24 fee) (topic0 0x40e9cecb…112f), signe les montants du point de vue de l’utilisateur, à l’inverse de v3, comme le précise le guide de données de la Uniswap Foundation. Les pools Curve émettent TokenExchange avec des index de jetons au lieu d’adresses de jetons. Utilisez un décodeur par protocole et par version, et testez chacun sur une transaction dont vous connaissez le résultat.
Routage par des agrégateurs
Un agrégateur répartit un ordre entre plusieurs pools et chaînes de pools au sein d’une seule transaction. Les événements des pools montrent chaque étape ; les transferts de jetons montrent le résultat net pour le trader. Voici un itinéraire décodé, où le routeur détient les fonds entre les étapes :
| logIndex | Jeton | De | Vers | Montant |
|---|---|---|---|---|
| 10 | WETH | Trader | Routeur | 10 |
| 11 | WETH | Routeur | Pool A (WETH/USDC) | 6 |
| 12 | USDC | Pool A | Routeur | 17 880 |
| 13 | WETH | Routeur | Pool B (WETH/DAI) | 4 |
| 14 | DAI | Pool B | Pool C (DAI/USDC) | 11 930 |
| 15 | USDC | Pool C | Routeur | 11 925 |
| 16 | USDC | Routeur | Trader | 29 805 |
En faisant le solde des entrées et sorties de chaque adresse, on obtient une seule opération : le trader a vendu 10 WETH contre 29 805 USDC, soit une moyenne de 2 980,50 USDC par WETH. Le solde net du routeur et des pools intermédiaires est nul, ou correspond aux modifications propres à leurs réserves.
-- Variation nette de jetons par adresse au sein d’une transaction
SELECT address, token_contract, SUM(delta) AS net_raw
FROM (
SELECT to_address AS address, token_contract, value_raw AS delta
FROM token_transfers WHERE tx_hash = '0x9e1d…b7a4'
UNION ALL
SELECT from_address, token_contract, -value_raw
FROM token_transfers WHERE tx_hash = '0x9e1d…b7a4'
) t
GROUP BY address, token_contract
HAVING SUM(delta) <> 0;
La requête additionne chaque transfert entrant et soustrait chaque transfert sortant pour chaque adresse et chaque jeton, puis ne conserve que les résultats non nuls. Les étapes impliquant de l’ETH natif n’apparaissent pas dans les transferts de jetons et doivent être ajoutées à partir des traces. Dans les systèmes fondés sur les intentions, la transaction est envoyée par un solveur, et non par l’utilisateur ; tx.from identifie donc le solveur. L’utilisateur est identifié à partir des événements de transaction du contrat de règlement et des transferts vers et depuis son adresse.
Ajouts et retraits de liquidité
Les événements de liquidité révèlent qui finance un marché et qui en retire les fonds.
| Protocole | Ajout | Retrait | Enregistrement de propriété |
|---|---|---|---|
| Uniswap v2 | Mint(sender, amount0, amount1) et transfert de jetons LP depuis l’adresse zéro | Jetons LP envoyés à la paire, Burn(sender, amount0, amount1, to), transfert de jetons LP vers l’adresse zéro | Jeton LP fongible (le contrat de la paire) |
| Uniswap v3 | Mint du pool avec plage de ticks ; IncreaseLiquidity sur le gestionnaire de positions | Burn du pool (crédite les jetons), puis Collect (les verse) ; DecreaseLiquidity | NFT de position (ERC-721) |
| Uniswap v4 | ModifyLiquidity dans le PoolManager | Même événement, variation négative de liquidité | Enregistrements de positions, souvent un NFT |
Deux pièges. Dans v3, le Mint du pool désigne le contrat du gestionnaire de positions comme propriétaire ; le véritable fournisseur de liquidité est le détenteur du NFT de position : suivez le NFT. De plus, un Burn v3 ne déplace aucun jeton ; les fonds sortent lors de Collect. Un schéma classique de rug pull est celui d’un déployeur qui crée un jeton, ajoute de la liquidité, attend les acheteurs, puis retire presque toute la liquidité en une transaction ; les montants de Burn ou de DecreaseLiquidity mesurent ce qui a été retiré.
Calculer l’impact sur le prix
L’impact sur le prix compare le prix d’exécution au prix au comptant du pool juste avant l’échange. Pour une paire v2, les réserves avant l’échange sont égales aux réserves de son événement Sync, moins les entrées et plus les sorties : ici, 2 985 119,18 + 14 880,82 = 3 000 000 USDC et 1 005 − 5 = 1 000 WETH, soit un prix au comptant de 3 000.
| Mesure | Valeur |
|---|---|
| Prix au comptant avant l’échange | 3 000,00 USDC par WETH |
| Prix d’exécution | 2 976,16 (−0,79 %) |
| Sortie sans les frais de 0,30 % | 14 925,37, prix de 2 985,07 (−0,50 %) |
| Prix au comptant après l’échange (d’après Sync) | 2 970,27 (−0,99 %) |
Ainsi, sur l’écart de 0,79 %, environ 0,30 % correspond aux frais et environ 0,50 % à l’impact pur sur le prix. Le prix au comptant après l’échange a varié deux fois plus que le prix moyen payé, ce qui est normal pour des pools à produit constant. Un impact important dans un pool peu profond signale une vente forcée, par exemple lorsqu’un attaquant déverse un jeton volé.
Identifier les schémas de MEV et de sandwich
L’activité de valeur extractible maximale (MEV) présente des formes reconnaissables dans les données de blocs :
- Sandwich : dans un même bloc et un même pool, l’acteur X achète le jeton T, une victime achète ensuite T, puis X revend T juste après. Les trois transactions sont généralement adjacentes par leur index ; la quantité revendue par X est approximativement égale à sa quantité achetée, et X paie souvent directement le constructeur du bloc (un transfert d’ETH au bénéficiaire des frais du bloc, visible dans les traces).
- Arbitrage atomique : une transaction échange des jetons au travers d’un cycle de pools (WETH → USDC → DAI → WETH) et se termine avec davantage d’unités du jeton de départ.
- Liquidité juste à temps (JIT) : un Mint v3 juste avant un échange important, puis un Burn et un Collect juste après, effectués par la même adresse.
- Liquidations : abordées dans la leçon Prêts, bridges et comptabilité des protocoles.
-- Candidats au sandwich : même bloc et même pool, ouverture et clôture par un seul acteur
SELECT f.block_number, f.pool, f.actor, v.tx_hash AS victim_tx
FROM dex_swaps f
JOIN dex_swaps v
ON v.block_number = f.block_number AND v.pool = f.pool
AND v.tx_index > f.tx_index AND v.token_bought = f.token_bought
JOIN dex_swaps b
ON b.block_number = f.block_number AND b.pool = f.pool
AND b.tx_index > v.tx_index AND b.actor = f.actor
AND b.token_sold = f.token_bought
WHERE v.actor <> f.actor;
La requête recherche une opération en amont (f), une victime (v) qui achète le même jeton plus tard dans le bloc, puis une opération en aval (b) où le même acteur le revend. Définissez actor comme le contrat du bot qui a reçu le résultat de l’échange plutôt que comme l’expéditeur de la transaction, car les bots sandwich changent souvent d’adresse d’envoi. Considérez les résultats comme des candidats : vérifiez les quantités, le bénéfice et l’absence d’une explication innocente, par exemple deux arbitragistes sans lien entre eux.
Points clés pour l’examen : ordre de token0/token1 et décimales ; décodage des événements Swap (amountIn/amountOut) et Sync d’Uniswap v2 ; montants signés de v3 (positifs vers le pool) et conversion de sqrtPriceX96 en prix avec ajustement des décimales ; PoolManager singleton de v4, id du pool et convention de signe inversée ; solde des transferts pour trouver le résultat d’un trader via un agrégateur, et pourquoi tx.from peut être un routeur ou un solveur ; événements de liquidité v2 et v3, NFT de position et schéma Burn puis Collect ; calcul de l’impact sur le prix à partir des réserves ; signatures de sandwich, d’arbitrage atomique et de liquidité JIT, ainsi que leurs faux positifs.
Questions d’entraînement
Vérifiez votre compréhension. Ces questions ne comptent pas pour l’examen.
Connectez-vous pour enregistrer votre progression et reprendre là où vous vous êtes arrêté. C’est gratuit. Se connecter