Trampa de pool tóxico en Uniswap: 131.888 dólares en 29 horas
BlockbeatsTítulo original: «Cotización 0%, ejecución 12,8%: análisis de la trampa de tarifa dinámica del "pool tóxico" de Uniswap v4»
Autor original: Wang Zihao, BitsLab
Entre el 22 y el 23 de agosto de 2026, un pool de tarifa dinámica USDC/WBNB de Uniswap v4 en BNB Chain mostró una tarifa del 0% en las cotizaciones de los agregadores, pero a los usuarios se les cobró en realidad una comisión de LP del 12,8%. En aproximadamente 29 horas, este pool procesó 21.086 transacciones y acumuló comisiones por 131.888 dólares, tras lo cual fue retirado. Todas las transacciones afectadas se ejecutaron con éxito: el contrato no fue vulnerado, la salida superó el límite mínimo establecido por el usuario y no hubo señales anómalas; la diferencia se convirtió silenciosamente en comisiones.
Este artículo toma como punto de partida una transacción de muestra:
(0x7615dec2ce80506ec461a47739eb8533ac2bc7c605ca56c255964481b76363d0: el usuario aportó 5.926,90 USDT y recibió 757,43 USDT menos de lo esperado), y desglosa cómo este pool logró «cotizar 0% y ejecutar 12,8%»: la triple capa de decisión de la función de tarifa, la protección de deslizamiento anulada y la lista blanca que dejó fuera a otros LP. Todas las conclusiones se basan en datos on-chain y verificación controlada local.
01 La cadena desde la cotización hasta la ejecución
Cuando un usuario inicia un intercambio en el frontend del agregador, toda la cadena funciona así:

Hay dos hechos contextuales en esta cadena sobre los que se construye cada capa de decisión posterior.
Primero, el lado de v4: permite que el Hook anule la tarifa en cada transacción. PoolKey.fee = 0x800000 declara un pool de tarifa dinámica; el Hook devuelve 0x400000 | fee como tercer valor de retorno en beforeSwap. El código en v4-core que recibe este valor de retorno (Pool.sol:303-305):

isOverride() comprueba la bandera de anulación, removeOverrideFlagAndValidate() elimina 0x400000 y valida que no supere MAX_LP_FEE = 1.000.000 (100%), y luego ejecuta el swap con esa tarifa y lo registra en el evento Swap. Es decir, el usuario paga lo que devuelva el Hook, siempre que la salida siga siendo superior a amountOutMin. Esta es una función oficial; ajustar la tarifa según la volatilidad o el inventario son usos legítimos.
Ahora veamos el Hook en sí: solo declaró dos permisos. v4 codifica los permisos en los 14 bits inferiores de la dirección del contrato (Hooks.sol:29-47), se validan en el despliegue y no se pueden modificar después. La definición de banderas en v4-core y la decodificación de la dirección coinciden:

La prueba on-chain de getHookPermissions() devuelve un resultado consistente con el mapa de bits: solo estos dos bits son verdaderos.
El Hook no tiene código fuente. Este artículo descompila la función de tarifa completa a partir del bytecode; a continuación se analiza el comportamiento de esta lógica que ya se ha ejecutado on-chain:

A continuación, capa por capa. Las dos primeras responden a «¿es esto una simulación?»; la tercera decide cuánto paga la transacción real.
02 La triple capa de decisión de la función de tarifa
Decisión 1: distinguir al llamador
Cuando se llama a la función de tarifa, primero compara tx.origin: si es igual a cualquiera de las tres direcciones constantes, devuelve directamente la tarifa de respaldo. La tarifa de respaldo está configurada actualmente en 0%.
Las tres direcciones no son aleatorias. Cuando eth_call no especifica from, el origin de la transacción cae en la dirección cero; los agregadores normalmente no lo especifican en las consultas por lotes. Verificación directa en un fork local:

La primera capa solo reconoce estas tres direcciones. La siguiente capa muestra cómo el Hook puede distinguir cuando se especifica una dirección normal.
Decisión 2: comprobar el gas
Al cambiar tx.origin a una dirección normal y reintentar, el retorno sigue siendo 0%: la primera capa se ha eludido, pero la tarifa no cambia. Esto indica que la primera capa no es la única decisión; localizamos la segunda capa: gasleft() * 10100 / 10000 >= gate.
Los agregadores suelen dar un gas muy alto en las cotizaciones para evitar que las rutas candidatas fallen en la simulación por falta de gas; en la transacción real, el gas ya ha sido consumido por el Router, las autorizaciones y los saltos previos antes de entrar en el Hook. El mismo código, tres niveles de gas:

La línea divisoria medida se encuentra entre 16,85M y 16,9M de gas. La diferencia entre 30M y 2,59M es la diferencia entre «cotización» y «ejecución».
Decisión 3: pseudoaleatoriedad por transacción
Las transacciones reales que superan las dos primeras capas entran en la tercera: se calcula un hash de la huella del entorno del bloque, se toma módulo 10.000 y, según el intervalo resultante, se devuelve la tarifa correspondiente. La configuración actual tiene tres niveles: 8% / 10% / 10%; si no cae en ninguno, se devuelve 0%.
Esta capa tiene dos detalles de diseño notables. La huella mezcla tres campos del calldata: el calldata de la cotización del agregador y el de la ruta real son diferentes, por lo que las huellas también lo son. Además, la pseudoaleatoriedad por transacción distribuye las tarifas, lo que dificulta detectar un patrón con solo unas pocas transacciones; una enumeración simple de reglas no lo detiene.
En los 21.086 eventos on-chain aparecen 19 tarifas históricas (6,8%–28%), lo que indica que los niveles de tarifa se reconfiguran mediante una función de administrador según sea necesario; en la transacción de muestra, la tarifa vigente era del 12,8%.
Panorama de las tres capas combinadas

Con esto ya podemos definir un «pool tóxico»: un pool v4 que cumple tres condiciones simultáneamente: PoolKey.fee lleva la bandera de tarifa dinámica; la decisión de tarifa lee señales del entorno de ejecución (gas, origin, entorno del bloque) en lugar del estado público del mercado; y la tarifa de la simulación de cotización y la de la ejecución on-chain divergen sistemáticamente, beneficiando al desplegador del pool tóxico. La línea divisoria está en qué lee la tarifa: si devuelve la misma tarifa a cualquier llamador, es un diseño de mercado programable; si devuelve tarifas diferentes según «quién pregunta», es un engaño al sistema de enrutamiento.
Hasta aquí, el mecanismo de divergencia de tarifas está completo. Pero después de cobrar la tarifa, ¿por qué la transacción sigue teniendo éxito? Esa es la siguiente pregunta.
03 Por qué la protección de deslizamiento no revirtió
Primero veamos la ruta completa y los importes de esta transacción:

El evento Swap de ese salto registra la tarifa vigente en el campo fee: 128000. En v4, la unidad de tarifa es 1.000.000 = 100%, por lo que 128000 es 12,8%. La pérdida de esta transacción se puede medir desde dos ángulos: desde el pool, la tarifa se cobra sobre la entrada de WBNB; desde la ruta completa, la diferencia entre la entrada y la salida de stablecoins es de 757,43 USDT, una pérdida del 12,7794%, que incluye comisiones de saltos previos y desviación del peg. Los dos números difieren solo 0,02 puntos porcentuales, lo que indica que la principal fuente de pérdida es la comisión de LP de este pool.
amountOutMin solo valida el límite inferior de la salida final, sin mirar cuánta comisión se cobró en cada salto intermedio:

La tarifa del 12,8% cae completamente dentro del colchón del 20,32%; la salida sigue estando por encima del mínimo, por lo que la transacción tiene éxito y no revierte. El valor predeterminado habitual en rutas de stablecoins es 0,1%–1%; esta ruta dio un margen más de 20 veces mayor. El usuario cree que un colchón amplio es «más seguro», pero en realidad deja cada contrato de la ruta a la autodisciplina de la contraparte. Representemos esta cuenta en un gráfico:

04 Operaciones del desplegador del pool tóxico
Lista blanca: dejar fuera a otros LP
La lógica de la lista blanca está en beforeAddLiquidity; el resultado de la descompilación es el siguiente:

Para añadir liquidez hay que pasar tres puertas: primero, la comprobación de llamada de PoolManager que tiene cualquier Hook de v4; después, debe pasar por el PositionManager oficial; y por último, se verifica la lista blanca de titulares de posiciones. Las direcciones que no están en la lista son revertidas por el contrato aquí; otros LP no pueden entrar, los ingresos por comisiones no se diluyen y van íntegramente al desplegador. La lista en sí se mantiene mediante una función de administrador (selector 0xc4452e52) por dirección.
Nivel 0% y volumen falso
Al observar las 21.086 transacciones en conjunto, la distribución muestra un patrón claro: nivel 0%: 6.946 transacciones, volumen de 12.947.751 dólares, promedio de 1.864 dólares por transacción; niveles con tarifa: 14.140 transacciones, volumen de 1.120.106 dólares, promedio de 79 dólares por transacción. Las transacciones grandes se concentran en el nivel 0%, y las comisiones se concentran en transacciones pequeñas. La explicación más razonable para las grandes transacciones al 0% es que el propio desplegador realizó auto-transacciones con gas alto: el gas alto coincide con el mismo criterio que la cotización del agregador, por lo que el coste de comisión de las auto-transacciones es casi cero. El volumen falso empujó al pool a los rankings de los sitios de mercado (una captura muestra un volumen de 24h de unos 10,82 millones de dólares y 16.671 transacciones); a los ojos del agregador, este es un pool con buena profundidad y baja tarifa.
Reconfiguración de niveles de tarifa según demanda
En la decisión 3 se mencionaron 19 tarifas históricas; provienen de la función de configuración de tarifas descompilada (selector 0x4d909a45, firma pública no registrada):

Los cuatro niveles de tarifa y los umbrales se empaquetan juntos y se escriben en la ranura de almacenamiento keccak(poolId, 2). La lectura on-chain de esa ranura es 0x0138800186a00186a0, que coincide segmento a segmento con el formato de empaquetado de la función. Los niveles de tarifa son parámetros ajustables en cualquier momento, no fijados en el despliegue.
Ciclo de vida
Este pool se creó el 22 de agosto a las 06:57, la primera transacción de swap apareció a las 07:13, y a las 21:39 se produjo la transacción de muestra de este artículo; la última transacción fue el 23 de agosto a las 12:25, y después se retiró. Todo el período activo fue de aproximadamente 29 horas, con un volumen total de 14.067.857 dólares e ingresos por comisiones de 131.888 dólares (sin descontar los costes del desplegador). Al revisar, la bandera de registro ha vuelto a estar activa: el pool sigue ahí y puede reactivarse en cualquier momento.
05 Ruta completa
Conectemos toda la cadena de ataque:

Toda la causalidad de la cadena está en el gráfico anterior; las tres condiciones son indispensables: la simulación acierta en las dos primeras capas, la transacción real cae en la tercera capa y el colchón es mayor que la tarifa. Si cualquiera de ellas no se cumple, este método falla.
06 Correcciones y recomendaciones
La primera está en el método de cotización del agregador. La causa raíz es que la simulación y la ejecución no siguen el mismo camino: la cotización utiliza una consulta idealizada a nivel de pool, con dirección cero y gas alto; la transacción real lleva identidad real y gas ya consumido. El Hook puede leer precisamente estas diferencias, y la tarifa diverge. La solución es acercar la simulación a la ejecución: simular con el calldata real del Router que se va a enviar, el from/to real y un gas cercano al de la transacción on-chain; después de la ejecución, decodificar el evento Swap para verificar la tarifa y, si no coincide con la cotización, reducir su peso o retirarla.
La segunda está en la configuración de deslizamiento del usuario. La causa raíz es que min_out se da demasiado holgado: el colchón del 20,32% absorbe por completo la tarifa del 12,8% y la transacción se ejecuta con normalidad. En rutas de stablecoins, 0,1%–1% es suficiente para el uso diario; conviene mirar ese número antes de lanzar la transacción; que las carteras y los frontends/backends ajusten los valores predeterminados puede evitar la mayor parte del riesgo para el usuario.
La tercera está en la admisión de rutas. La causa raíz es que cualquier pool de tarifa dinámica puede participar directamente en la competencia de cotizaciones: un Hook sin código abierto y sin historial de auditoría puede entrar en las rutas recomendadas gracias al volumen falso. Para los pools de tarifa dinámica sin un perfil confiable, la opción más segura es no incluirlos en el enrutamiento por defecto o reducir significativamente su peso.
Datos y notas
· Transacción de muestra:
https://bscscan.com/tx/0x7615dec2ce80506ec461a47739eb8533ac2bc7c605ca56c255964481b76363d0 (bloque 117497524)
· Transacción de inicialización del pool:
https://bscscan.com/tx/0x8fa72ef72d77b61f715dc9ce90548249ab045c6d78716078cd5ab425bbe69a05
· PoolManager:
0x28e2ea090877bf75740558f6bfb36a5ffee9e9df
· Pool ID:
0x36e5540e9dedc02229fe8a82aa5b10c0bf07d1fa74e4f2ffe0efd00fa1a36aea
· Hook:
0xd111b3ddd92e627f1864520c770e913ec04e0880 (sin código fuente; el pseudocódigo es una descompilación independiente de este artículo; administrador 0x08b03e1a5444d469f4dc954e74d3f662c94a6b13)
· Volumen y comisiones: acumulación transacción a transacción de los 21.086 eventos Swap del pool (eth_getLogs), WBNB convertido al precio de ejecución de cada transacción; los ingresos son brutos, sin descontar los costes del desplegador
Este contenido se proporciona únicamente con fines informativos y educativos y no constituye asesoramiento de inversión relacionado con BTCC. BTCC realiza todos los esfuerzos posibles, pero no puede garantizar la veracidad, exactitud u originalidad del contenido anterior.