Desde que Uniswap v4 se lanzó en mainnet, el mecanismo Hook se ha convertido en una de las innovaciones más seguidas en DeFi. La plataforma de lanzamiento de memecoin Flaunch en Base Chain utiliza Hook para implementar un precio de preventa fijo y un mecanismo de liquidación y lanzamiento automático; el protocolo de liquidez Bunni v2 utiliza Hook para construir modelos de liquidez programable y reestaking; tokens como SATO, uPEG (Unipeg) y Slonks, centrados en la mecánica de Hook, también han registrado aumentos de decenas de veces en cortos períodos este año.
En el otro lado de la prosperidad del ecosistema Hook, los ataques dirigidos a defectos en la implementación de Hook también han aumentado significativamente. Este artículo comenzará con el mecanismo Hook de Uniswap v4, analizando gradualmente su stack de llamadas central, para ayudar a los desarrolladores de proyectos a comprender las posibles vulnerabilidades presentes.
Seguridad del Hook de Uniswap v4
1. Introducción
El cambio arquitectónico más significativo de Uniswap v4 respecto a v3 es la introducción del mecanismo Hook (gancho): permite a los desarrolladores adjuntar contratos personalizados a eventos del ciclo de vida del pool de liquidez, inyectando lógica arbitraria en nodos como swap, adición/eliminación de liquidez e inicialización.
Los cambios clave de v4 son los siguientes:
- Modo Singleton: El estado de todos los pools es gestionado centralmente por un único contrato PoolManager, ya no se despliega un contrato independiente para cada pool.
- Flash accounting (contabilidad flash): Los cambios de saldo intermedios durante una transacción solo se registran en transient storage, y se liquidan de manera unificada solo al final de la transacción.
- Mecanismo Hook: Cada pool puede vincularse a un contrato Hook, y el PoolManager llamará a este contrato en puntos clave (beforeInitialize, beforeSwap, afterAddLiquidity, etc.).
- Hook no intercambiable: Una vez que se inicializa el pool, la dirección del Hook vinculada se fija permanentemente (la dirección del Hook vinculada al pool no se puede modificar, pero si el contrato Hook en sí es actualizable depende de su implementación).
En la era de v3, los desarrolladores solo necesitaban confiar en el propio protocolo Uniswap; en la era de v4, la seguridad de cada pool depende del Hook al que está vinculado. Hook convierte a la AMM de un primitivo financiero fijo en una infraestructura financiera programable, pero el modelo de seguridad también se fragmenta de "nivel de protocolo" a "nivel de pool".
2. Arquitectura del Hook
2.1 Modelo PoolManager y unlock/callback
El contrato central de v4 es el PoolManager singleton. Cualquier operación de cambio de estado en un pool (swap, adición/eliminación de liquidez) debe llamar primero a PoolManager.unlock(), obtener permisos de callback únicos y luego completar la acción específica en unlockCallback(). Al final del proceso, el PoolManager verificará si el libro de contabilidad está equilibrado:
Si NonzeroDeltaCount != 0, se revierte directamente. Esta es la restricción central del flash accounting de v4. Cualquier Hook puede desequilibrar temporalmente las cuentas durante su ejecución, pero debe resolverlo antes de que termine la transacción; de lo contrario, toda la transacción se revierte.
Cada pool se identifica de manera única por la estructura PoolKey, que incluye el campo hooks:
PoolId se calcula mediante keccak256(PoolKey), por lo tanto, direcciones de Hook diferentes producirán pools diferentes. Esto también significa que PoolManager no verificará si una dirección Hook ha sido usada previamente en otros pools; el mismo contrato Hook puede estar vinculado a múltiples pools simultáneamente.
2.2 Permisos del Hook codificados en la dirección
Un diseño contraintuitivo de v4 es: Los permisos del Hook no están determinados por una variable interna del contrato, sino por la dirección de despliegue del contrato Hook.
PoolManager verifica los 14 bits más bajos de la dirección Hook para determinar si este Hook necesita ser llamado en un punto específico del ciclo de vida:
Por ejemplo, BEFORE_SWAP_FLAG = 1 << 7. Si el bit 7 de la dirección Hook es 1, PoolManager llamará al beforeSwap() de este Hook antes de un swap; de lo contrario, incluso si el contrato Hook implementa beforeSwap(), nunca será llamado por PoolManager.
Esto significa que al desplegar el Hook se debe calcular la dirección mediante CREATE2 + salt, construyendo una dirección cuyos bits bajos coincidan exactamente con los permisos objetivo. Uniswap proporciona oficialmente la herramienta HookMiner para este propósito:
Cuando los bits de permiso no coinciden con la implementación de la función, surgen dos tipos de problemas:
(1) Se implementa una función hook, pero la dirección no tiene codificado el bit de permiso correspondiente: PoolManager nunca llamará a esa función, la lógica es inefectiva.
(2) La dirección tiene codificado un bit de permiso, pero el hook no implementa la función correspondiente: PoolManager puede revertir al realizar la callback, provocando un DOS o fallo en la validación del valor de retorno, lo que impide la ejecución de la operación relacionada.
Esto también es una barrera natural para la actualización de Hook: si el Hook es actualizable a través de un proxy, la dirección de despliegue no cambia durante la actualización, por lo que solo se pueden modificar las implementaciones de funciones hook existentes, no agregar nuevos tipos de hook. Para reservar capacidad de expansión futura, es necesario "minar" previamente todos los bits de permiso que podrían usarse en el despliegue inicial.
2.3 BaseHook y una trampa de control de acceso comúnmente pasada por alto
El contrato abstracto BaseHook proporcionado por versiones anteriores de la periferia de Uniswap v4, del cual los desarrolladores pueden heredar para implementar Hooks personalizados. Una función importante de BaseHook es proporcionar el modificador onlyPoolManager para la función unlockCallback():
Sin embargo, aquí hay una trampa de diseño muy fácil de pasar por alto: las versiones anteriores de BaseHook solo agregaron onlyPoolManager a unlockCallback, y no brindaron ninguna protección a otras funciones de callback del hook (beforeSwap, afterSwap, beforeAddLiquidity, etc.). El control de acceso para estas funciones debe ser agregado explícitamente por el desarrollador del Hook.
3. Recorrido del Código del Ciclo de Vida del Hook
Tomando un swap exact-input como ejemplo, analizamos a continuación el stack completo de llamadas desde que el usuario inicia la transacción hasta su liquidación.
3.1 Inicialización del pool y vinculación del Hook
Cualquiera puede llamar a PoolManager.initialize() para crear un nuevo pool:
isValidHookAddress solo verifica la compatibilidad entre los bits de permiso de la dirección y el campo fee, no verifica si el Hook ya ha sido usado en otros pools, ni si este Hook está "dispuesto" a aceptar este PoolKey. Si el Hook no implementa lógica de lista blanca o vinculación a un solo pool en beforeInitialize durante su diseño, cualquiera puede construir un nuevo pool usando el mismo Hook pero con cualquier par de tokens, y desencadenar todas las callbacks posteriores del Hook.
3.2 beforeSwap y BeforeSwapDelta
La entrada al flujo de swap es PoolManager.swap(), que antes de ejecutar la lógica central del swap llamará a Hooks.beforeSwap():
El valor de retorno de beforeSwap es una tripleta (bytes4, BeforeSwapDelta, uint24):
- bytes4: Debe ser igual a IHooks.beforeSwap.selector, de lo contrario PoolManager revierte directamente.
- BeforeSwapDelta: El ajuste del delta que el Hook realiza para el token especificado (specified token) y el no especificado (unspecified token) en este swap.
- uint24: Valor de sobrescritura de tarifa dinámica de LP (solo efectivo cuando el pool tiene tarifas dinámicas habilitadas).
BeforeSwapDelta es un alias de int256, donde los 128 bits superiores son el delta del token especificado (el token cuya cantidad especifica el usuario) y los 128 bits inferiores son el delta del token no especificado:
Es importante notar que la semántica de BeforeSwapDelta es: el Hook debe devolver un valor positivo si cobra una tarifa, y un valor negativo si devuelve tokens. Los desarrolladores pueden confundir fácilmente el signo; además, la correspondencia entre specified y unspecified depende de params.zeroForOne y del signo de amountSpecified, y un pequeño descuido en la escritura puede llevar a una confusión de tokens.
PoolManager sumará directamente el specifiedDelta devuelto por beforeSwap a amountToSwap:
Esta línea implica una semántica clave: el Hook puede retener parte del monto del swap. Cuando hookDeltaSpecified es igual a -params.amountSpecified, amountToSwap se vuelve cero directamente, lo que equivale a que el Hook tome el control completo de este swap; esto es lo que se conoce como Async Hook o Custom Curve Hook.
Async Hook es uno de los patrones de diseño de mayor riesgo en v4: esencialmente reemplaza la lógica de swap de Uniswap con la lógica propia del Hook. Si el Hook tiene vulnerabilidades o es malicioso, los fondos de los usuarios ya no estarán sujetos a la lógica de precios nativa de Uniswap, sino que dependerán principalmente de la corrección de la implementación del Hook en sí.
3.3 Liquidación del Delta y NonzeroDeltaCount
Los deltas devueltos por beforeSwap y afterSwap no activan transferencias inmediatamente, sino que se registran en el libro de contabilidad interno de PoolManager:
Cada vez que el delta acumulado de un token cambia de cero a distinto de cero, NonzeroDeltaCount se incrementa; cuando vuelve a cero, se decrementa. Como se mencionó en 2.1, si al final de unlock() NonzeroDeltaCount != 0, toda la transacción se revierte.
El Hook equilibra su delta mediante dos acciones: settle() (transferir a PoolManager) y take() (tomar de PoolManager):
La semántica de seguridad que introduce este mecanismo es clara: al final, todos deben saldar sus cuentas. Pero solo garantiza la "conservación de la contabilidad", no la "corrección de la contabilidad". Si el Hook devuelve un delta maliciosamente construido en beforeSwap, PoolManager registrará fielmente este delta, y mientras finalmente se salde, la transacción será exitosa, incluso si esto significa que el Hook puede, falsificando el estado del negocio, hacer que el sistema crea erróneamente que el atacante tiene ciertos derechos sobre activos, y PoolManager no puede identificar este error a nivel de negocio.
El incidente de seguridad anterior del Protocolo Cork se debió precisamente a que su Hook tenía una vulnerabilidad, y antes del ataque ya había sido auditado por cuatro empresas. En el análisis posterior descubrimos:
- Tres de las cuatro auditorías no incluían el contrato CorkHook en su alcance.
- La única que auditó CorkHook identificó algunos problemas de código y envió sugerencias de mejora, pero no cubrió completamente el problema de control de acceso.
- Otra empresa auditora indicó explícitamente en su informe: "an interesting follow-up engagement would be to prove the invariants for the CorkHook functions that are being invoked by different components verified within the scope of this engagement". Esta sugerencia, en retrospectiva, era muy relevante.
Esto expone un nuevo punto ciego de auditoría en la era de los Hooks de v4: el crecimiento explosivo en la complejidad del protocolo hace que la definición del alcance en sí sea una decisión de seguridad. La cadena de interacción entre el Hook y otros contratos del protocolo es muy larga, auditar solo el contrato Hook es insuficiente para descubrir problemas de combinación entre contratos; a la inversa, auditar contratos periféricos excluyendo al Hook del alcance, deja fuera la mayor superficie de ataque de la era v4.
4. Reflexión
Mirando juntos los mecanismos del protocolo y el análisis del ataque a Cork, se pueden resumir varios puntos clave del modelo de seguridad del Hook de v4:
(1) Si las funciones de callback del Hook dependen del contexto de llamada proporcionado por PoolManager, se debe restringir explícitamente su llamada solo a PoolManager. BaseHook no hace esto por los desarrolladores; esta es la trampa de diseño que más fácilmente entra en conflicto con la experiencia de auditoría de contratos en general en v4.
(2) La relación de vinculación entre el Hook y el pool no está restringida por PoolManager. Los desarrolladores deben implementar listas blancas de pools o vinculación a un solo pool en beforeInitialize.
(3) Los bits de permiso de la dirección del Hook deben coincidir estrictamente con la implementación de las funciones. La dirección calculada debe incluir previamente todos los bits de permiso que podrían necesitarse en el futuro.
(4) Async / Custom Curve Hook es esencialmente una implementación de swap completamente personalizada. No tiene ninguna protección a nivel de protocolo de Uniswap y debe ser auditada bajo el estándar de "contrato financiero completamente autónomo".
(5) La "conservación" de la contabilidad delta no equivale a "corrección". NonzeroDeltaCount == 0 solo garantiza que el libro mayor esté equilibrado al final, no que su contenido no haya sido manipulado maliciosamente.
(6) La confusión de tipos de tokens entre mercados es una nueva superficie de ataque en la era v4. Cuando un protocolo permite a los usuarios crear mercados, la verificación semántica de los tokens es obligatoria, no se puede confiar solo en la verificación de interfaz.
Cada Hook es un dominio de confianza independiente, y la seguridad de cada pool está determinada por el Hook al que está vinculado. Por lo tanto, la complejidad de la auditoría de seguridad de Hook ya no es "auditar un código", sino "auditar un subprotocolo completo". Este cambio implica una actualización metodológica tanto para los desarrolladores de proyectos como para los auditores.
Ver artículo original



















