Piège de pool toxique Uniswap : 130 000 $ de frais en 29 heures
BlockbeatsTitre original : « Cotation 0 %, exécution 12,8 % : retour sur le piège des frais dynamiques des pools toxiques d'Uniswap v4 »
Auteur original : Wang Zihao, BitsLab
Du 22 au 23 août 2026, un pool à frais dynamiques USDC/WBNB sur Uniswap v4 sur BNB Chain affichait des frais de 0 % dans les cotations des agrégateurs, mais les utilisateurs se voyaient en réalité facturer des frais de LP de 12,8 %. En environ 29 heures, ce pool a enregistré 21 086 transactions et des frais cumulés de 131 888 dollars, avant d'être retiré. Toutes les transactions victimes ont réussi : le contrat n'a pas été piraté, la sortie était supérieure au minimum fixé par l'utilisateur, et aucun signal anormal n'a été détecté — la différence s'est discrètement transformée en frais.
Cet article prend comme point de départ une transaction échantillon :
(0x7615dec2ce80506ec461a47739eb8533ac2bc7c605ca56c255964481b76363d0 : l'utilisateur a investi 5 926,90 USDT et a reçu 757,43 USDT de moins que prévu), et décompose comment ce pool a réussi à « coter 0 %, exécuter 12,8 % » : les trois niveaux de décision de la fonction de frais, la protection contre le glissement neutralisée, et la liste blanche qui exclut les autres LP. Toutes les conclusions sont basées sur des données on-chain et des vérifications locales contrôlées.
01 Le chemin de la cotation à l'exécution
Lorsqu'un utilisateur initie un échange sur le front-end d'un agrégateur, le chemin complet est le suivant :

Il y a deux faits contextuels sur ce chemin, sur lesquels repose chaque niveau de décision suivant.
Commençons par le côté v4 : il permet au Hook de remplacer les frais transaction par transaction. PoolKey.fee = 0x800000 déclare un pool à frais dynamiques ; le Hook renvoie 0x400000 | fee comme troisième valeur de retour de beforeSwap. Le code dans v4-core qui reçoit cette valeur de retour (Pool.sol:303-305) :

isOverride() vérifie le drapeau de remplacement, removeOverrideFlagAndValidate() retire 0x400000 et vérifie qu'il ne dépasse pas MAX_LP_FEE = 1 000 000 (100 %), puis exécute le swap avec ces frais et écrit l'événement Swap. Autrement dit, ce que le Hook renvoie, l'utilisateur le paie — tant que la sortie reste supérieure à amountOutMin. C'est une fonctionnalité officielle, et ajuster les frais en fonction de la volatilité ou des stocks est un usage légitime.
Regardons maintenant le Hook lui-même : il ne déclare que deux permissions. v4 encode les permissions dans les 14 bits de poids faible de l'adresse du contrat (Hooks.sol:29-47), vérifiées au déploiement et immuables par la suite. Les définitions des drapeaux côté v4-core et le décodage d'adresse correspondent :

Le test on-chain de getHookPermissions() renvoie un résultat conforme au bitmap : seuls ces deux bits sont vrais.
Le Hook n'a pas de code source. Cet article décompile le bytecode pour obtenir la fonction de frais complète — voici l'analyse comportementale de cette logique qui a déjà été exécutée on-chain :

Examinons chaque niveau. Les deux premiers répondent à la question « est-ce une simulation ? », le troisième détermine combien la transaction réelle paiera.
02 Les trois niveaux de décision de la fonction de frais
Décision 1 : distinguer l'appelant
Lorsque la fonction de frais est appelée, elle compare d'abord tx.origin : s'il est égal à l'une des trois adresses constantes, elle renvoie directement le niveau de repli. Le niveau de repli est actuellement configuré à 0 %.
Ces trois adresses ne sont pas choisies au hasard. Lorsque eth_call ne spécifie pas de from, l'origine de la transaction tombe sur l'adresse zéro — les agrégateurs ne la spécifient généralement pas lors des cotations par lots. Vérification directe dans un fork local :

Le premier niveau ne reconnaît que ces trois adresses. Voyons comment le Hook peut encore distinguer après avoir spécifié une adresse ordinaire.
Décision 2 : vérifier le gas
En remplaçant tx.origin par une adresse ordinaire et en réessayant, le retour est toujours de 0 % — le premier niveau a été contourné, mais les frais n'ont pas changé. Cela montre que le premier niveau n'est pas le seul critère, et nous amène au deuxième niveau : gasleft() * 10100 / 10000 >= gate.
Les agrégateurs ont l'habitude de fournir un gas très élevé pour éviter que les routes candidates n'échouent en simulation par manque de gas ; lors d'une transaction réelle, le gas a déjà été consommé par le routeur, les autorisations et les sauts précédents avant d'atteindre le Hook. Même code, trois niveaux de gas :

La ligne de démarcation mesurée se situe entre 16,85 M et 16,9 M de gas. La différence entre 30 M et 2,59 M, c'est la différence entre « cotation » et « exécution ».
Décision 3 : pseudo-aléatoire par transaction
Les transactions réelles qui passent les deux premiers niveaux entrent dans le troisième : hacher l'empreinte de l'environnement du bloc, prendre le modulo 10 000, et renvoyer le niveau de frais correspondant à l'intervalle du résultat. La configuration actuelle a trois niveaux : 8 % / 10 % / 10 %, avec repli à 0 % si aucun ne correspond.
Ce niveau comporte deux caractéristiques de conception notables. L'empreinte mélange trois champs du calldata — le calldata de cotation de l'agrégateur et celui du routage réel sont naturellement différents, donc les empreintes diffèrent aussi. De plus, le pseudo-aléatoire par transaction donne une distribution aux frais, rendant difficile de détecter une régularité sur quelques transactions, et une simple énumération de règles ne suffit pas à les bloquer.
Sur les 21 086 événements on-chain, 19 niveaux de frais historiques apparaissent (6,8 %–28 %), ce qui montre que les niveaux de frais ont été reconfigurés à la demande par une fonction d'administration ; au moment de la transaction échantillon, le niveau effectif était de 12,8 %.
Vue d'ensemble des trois niveaux combinés

Nous pouvons maintenant définir un « pool toxique » — un pool v4 qui satisfait simultanément trois conditions : PoolKey.fee porte le drapeau de frais dynamiques ; la décision de frais lit des signaux de l'environnement d'exécution (gas, origine, environnement de bloc) plutôt que l'état public du marché ; et les frais divergent systématiquement entre la simulation de cotation et l'exécution on-chain, au bénéfice du déployeur du pool toxique. La ligne de démarcation réside dans ce que lisent les frais : renvoyer les mêmes frais à tout appelant relève d'une conception de marché programmable ; renvoyer des frais différents selon « qui demande la cotation » est une tromperie envers le système de routage.
À ce stade, le mécanisme de divergence des frais est complet. Mais une fois les frais prélevés, pourquoi la transaction réussit-elle quand même ? C'est la question suivante.
03 Pourquoi la protection contre le glissement n'a pas fait échouer la transaction
Examinons d'abord le chemin complet et les montants de cette transaction :

L'événement Swap de ce saut enregistre les frais effectifs dans le champ fee : 128000. L'unité de frais de v4 est de 1 000 000 pour 100 %, donc 128000 correspond à 12,8 %. La perte de cette transaction peut être mesurée sous deux angles : du point de vue du pool, les frais sont prélevés sur l'entrée en WBNB ; du point de vue de la route complète, la différence entre l'entrée et la sortie en stablecoins est de 757,43 USDT, soit une perte de 12,7794 %, qui inclut également les frais des sauts précédents et l'écart de parité. Les deux chiffres ne diffèrent que de 0,02 point de pourcentage, ce qui montre que la principale source de perte est bien les frais de LP de ce pool.
amountOutMin ne vérifie que la limite inférieure de la sortie finale, sans regarder combien de frais chaque saut intermédiaire a prélevé :

Les frais de 12,8 % tombent entièrement dans la marge de 20,32 %, la sortie reste supérieure au plancher — la transaction réussit, sans retour en arrière. Le défaut habituel pour les routes de stablecoins est de 0,1 %–1 %, cette route offrait plus de 20 fois cet espace. L'utilisateur pensait qu'une marge large était « plus sûre », mais en réalité, cela remettait chaque contrat du chemin à l'auto-discipline de la contrepartie. Représentons ce calcul :

04 Les opérations du déployeur du pool toxique
Liste blanche : exclure les autres LP
La logique de liste blanche se trouve dans beforeAddLiquidity, voici le résultat de la décompilation :

Pour ajouter de la liquidité, il faut passer trois portes : d'abord la vérification d'appel du PoolManager commune à tous les Hooks v4, puis le passage obligatoire par le PositionManager officiel, et enfin la vérification de la liste blanche des détenteurs de positions. Les adresses qui ne figurent pas sur la liste sont rejetées par le contrat à ce stade, les autres LP ne peuvent pas entrer, les revenus de frais ne sont pas dilués et reviennent entièrement au déployeur. La liste elle-même est maintenue par une fonction d'administration (sélecteur 0xc4452e52) par adresse.
Niveau 0 % et volume artificiel
En examinant les 21 086 transactions ensemble, la distribution révèle un schéma clair : niveau 0 % : 6 946 transactions, volume de 12 947 751 dollars, moyenne de 1 864 dollars par transaction ; niveaux avec frais : 14 140 transactions, volume de 1 120 106 dollars, moyenne de 79 dollars par transaction. Les gros volumes se concentrent sur le niveau 0 %, les frais se concentrent sur les petites transactions. L'explication la plus raisonnable pour les gros volumes à 0 % est que le déployeur a effectué lui-même des transactions avec un gas élevé : le gas élevé correspond au même critère que celui utilisé par les cotations des agrégateurs, et le coût des frais pour les transactions auto-générées est proche de zéro. Le volume artificiel a propulsé le pool dans les classements des plateformes de marché (une capture montre un volume sur 24 h d'environ 10,82 millions de dollars et 16 671 transactions), et aux yeux des agrégateurs, c'est un pool avec une bonne profondeur et des frais bas.
Reconfiguration des niveaux de frais à la demande
La décision 3 mentionnait 19 niveaux de frais historiques, ils proviennent de la fonction de configuration des frais décompilée (sélecteur 0x4d909a45, signature publique non répertoriée) :

Les quatre niveaux de frais et les seuils sont regroupés et écrits dans l'emplacement de stockage keccak(poolId, 2). La lecture on-chain de cet emplacement donne 0x0138800186a00186a0, ce qui correspond segment par segment au format d'empaquetage de la fonction. Les niveaux de frais sont ici des paramètres ajustables à tout moment, et non figés au déploiement.
Cycle de vie
Ce pool a été créé le 22/08 à 06:57, le premier swap est apparu à 07:13, et la transaction échantillon de cet article a eu lieu à 21:39 ; la dernière transaction a eu lieu le 23/08 à 12:25, puis le pool a été retiré. La période active totale est d'environ 29 heures, avec un volume total de 14 067 857 dollars et des revenus de frais de 131 888 dollars (sans déduire les coûts du déployeur). Lors de la vérification, le drapeau d'enregistrement était revenu à l'état actif — le pool est toujours là et peut être réactivé à tout moment.
05 Chemin complet
Relions toute la chaîne d'attaque :

Toute la causalité de la chaîne est dans le schéma ci-dessus, les trois conditions sont indispensables : la simulation atteint les deux premiers niveaux, la transaction réelle tombe dans le troisième niveau, et la marge est supérieure aux frais. Si l'une d'elles n'est pas remplie, cette méthode échoue.
06 Corrections et recommandations
Le premier point concerne la méthode de cotation des agrégateurs. La cause profonde est que la simulation et l'exécution ne suivent pas le même chemin : la cotation utilise une requête idéalisée au niveau du pool, avec l'adresse zéro et un gas élevé ; la transaction réelle porte une identité réelle et un gas déjà consommé. Le Hook peut justement lire ces différences, d'où la divergence des frais. La solution est de rapprocher la simulation de l'exécution — utiliser le calldata réel du routeur qui sera envoyé, les vrais from/to, et un gas proche de la transaction on-chain pour la simulation ; après la transaction, décoder l'événement Swap pour vérifier les frais, et rétrograder ou retirer le pool si la cotation ne correspond pas.
Le deuxième point concerne le réglage du glissement par l'utilisateur. La cause profonde est un min_out trop lâche : la marge de 20,32 % absorbe entièrement les frais de 12,8 %, et la transaction réussit normalement. Pour les routes de stablecoins courantes, 0,1 %–1 % suffit pour un usage quotidien, il faut vérifier ce chiffre avant d'initier une transaction ; les portefeuilles et les front-ends devraient resserrer les valeurs par défaut pour bloquer la plupart des risques pour les utilisateurs.
Le troisième point concerne l'admission au routage. La cause profonde est que tout pool à frais dynamiques peut participer directement à la compétition de cotation : un Hook sans code source ni audit peut entrer dans les routes recommandées grâce à un volume artificiel. Pour les pools à frais dynamiques sans profil fiable, ne pas les inclure par défaut dans le routage ou les rétrograder fortement est la solution la plus sûre.
Données et notes
· Transaction échantillon :
https://bscscan.com/tx/0x7615dec2ce80506ec461a47739eb8533ac2bc7c605ca56c255964481b76363d0 (bloc 117497524)
· Transaction d'initialisation du pool :
https://bscscan.com/tx/0x8fa72ef72d77b61f715dc9ce90548249ab045c6d78716078cd5ab425bbe69a05
· PoolManager :
0x28e2ea090877bf75740558f6bfb36a5ffee9e9df
· ID du pool :
0x36e5540e9dedc02229fe8a82aa5b10c0bf07d1fa74e4f2ffe0efd00fa1a36aea
· Hook :
0xd111b3ddd92e627f1864520c770e913ec04e0880 (sans code source, le pseudo-code est une décompilation indépendante réalisée pour cet article ; administrateur 0x08b03e1a5444d469f4dc954e74d3f662c94a6b13)
· Volume et frais : cumul transaction par transaction des 21 086 événements Swap de ce pool (eth_getLogs), WBNB converti au prix d'exécution de chaque transaction ; les revenus sont bruts, sans déduire les coûts du déployeur
Ce contenu est fourni à titre informatif et éducatif uniquement et ne constitue pas un conseil en investissement lié à BTCC. BTCC s’efforce de garantir la véracité, l’exactitude et l’originalité du contenu ci-dessus, sans pouvoir toutefois les garantir.