Tras el lanzamiento en mainnet de Uniswap v4, el mecanismo Hook se ha convertido en una de las innovaciones más destacadas en DeFi. La plataforma de lanzamiento de memecoins en la cadena Base, Flaunch, utiliza Hook para implementar un precio fijo de preventa y un mecanismo de liquidación automática en el lanzamiento; el protocolo de liquidez Bunni v2 emplea Hook para construir modelos de liquidez programable y de remortgaje; este año, tokens como SATO, uPEG (Unipeg) y Slonks, centrados en el uso de Hook, también lograron aumentos de decenas de veces en un corto período.
En el otro lado del auge del ecosistema Hook, los ataques aprovechando defectos en la implementación de Hook también están aumentando significativamente. Este artículo comenzará con el mecanismo Hook de Uniswap v4 y analizará paso a paso su pila de llamadas principal, ayudando a los proyectos a comprender las posibles vulnerabilidades que contiene.
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 de Hook: permite a los desarrolladores adjuntar contratos personalizados a los eventos del ciclo de vida del pool de liquidez, inyectando lógica arbitraria en puntos como swap, agregar o eliminar liquidez e inicialización.
Los cambios clave de la v4 son los siguientes:
Patrón Singleton: El estado de todos los pools es gestionado centralmente por un solo contrato PoolManager, ya no se despliega un contrato independiente por cada pool.
- Contabilidad flash: Los cambios de saldo intermedios durante el proceso de operación solo se registran en el almacenamiento transitorio y se liquidan de forma unificada al finalizar la transacción.
Mecanismo Hook: Cada pool puede vincularse a un contrato Hook, y PoolManager llamará a este contrato en puntos clave (beforeInitialize, beforeSwap, afterAddLiquidity, etc.).
- El Hook no se puede reemplazar: una vez que se completa la inicialización del 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 mismo es actualizable depende de su implementación).

Durante la fase v3, los desarrolladores solo necesitaban confiar en el protocolo Uniswap; en la fase v4, la seguridad de cada pool depende del Hook al que está vinculado. Los Hooks transforman el AMM de un primitivo financiero fijo en una infraestructura financiera programable, pero el modelo de seguridad se fragmentó desde el “nivel de protocolo” al “nivel de pool”.
2. Arquitectura Hook
2.1 PoolManager y el modelo unlock/callback
El contrato principal de la versión v4 es el PoolManager singleton. Cualquier operación que modifique el estado del pool (intercambio, agregar o eliminar liquidez) debe llamar primero a PoolManager.unlock() para obtener un permiso de devolución de llamada único, y luego completar la acción específica dentro de unlockCallback(). Al finalizar todo el proceso, PoolManager verifica que el libro mayor esté equilibrado:

Cuando NonzeroDeltaCount != 0, se revertirá directamente; esta es la restricción fundamental del flash accounting v4. Cualquier Hook puede temporalmente desequilibrar la contabilidad durante su ejecución, pero debe saldarlo por sí mismo antes de que finalice la transacción; de lo contrario, toda la transacción se revertirá.
Cada pool está identificado de forma única por la estructura PoolKey, que incluye el campo hooks:

PoolId se calcula mediante keccak256(PoolKey), por lo que direcciones de hooks diferentes generan pools distintos. Esto también significa que PoolManager no verifica si una dirección de hook ha sido utilizada previamente para otro pool; el mismo contrato de hook puede estar vinculado simultáneamente a múltiples pools.

2.2 Codificación de bits de permiso Hook en la dirección
Un diseño contraintuitivo de la v4 es que los permisos del Hook no los determina una variable interna del contrato, sino la dirección de despliegue del contrato Hook.
PoolManager verifica los 14 bits menos significativos de la dirección Hook para determinar si se debe llamar a ese Hook en un punto específico del ciclo de vida:

Por ejemplo, BEFORE_SWAP_FLAG = 1 << 7. Si el bit 7 de la dirección del Hook es 1, PoolManager llamará a beforeSwap() del Hook antes del intercambio; de lo contrario, incluso si el contrato del Hook implementa beforeSwap(), nunca será llamado por PoolManager.
Esto significa que, al implementar Hook, se debe calcular la dirección mediante CREATE2 + salt para construir 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, se generan dos tipos de problemas:
(1) Se implementó una función hook, pero la dirección no está codificada con los bits de permiso correspondientes: PoolManager nunca llamará a esta función, por lo que la lógica es equivalente a inexistente.
(2) La dirección codifica un bit de permiso, pero el hook no implementa la función correspondiente: PoolManager puede revertir durante la devolución de llamada, lo que provoca un DOS o falla en la validación del valor devuelto, impidiendo la ejecución de la operación relacionada.
Esto también constituye una barrera natural para la actualización de Hook: si Hook se puede actualizar a través de un proxy, la dirección de despliegue no cambia durante la actualización, por lo que solo se puede modificar la implementación de las funciones Hook existentes, pero no se pueden agregar nuevos tipos de Hook. Para reservar capacidad de expansión futura, es necesario predefinir todos los bits de permiso posibles en el momento del despliegue inicial.
2.3 BaseHook y una trampa de control de acceso ampliamente ignorada
El contrato abstracto BaseHook proporcionado por la periferia de Uniswap v4 permite a los desarrolladores heredarlo para implementar Hooks personalizados. Una función importante de BaseHook es proporcionar el modificador onlyPoolManager a la función unlockCallback():

Pero—aquí existe una trampa de diseño muy fácil de pasar por alto—la versión inicial de BaseHook solo agregó onlyPoolManager a unlockCallback, sin ninguna protección para otras funciones de retorno de hook (beforeSwap, afterSwap, beforeAddLiquidity, etc.). El control de acceso a estas funciones debe ser añadido explícitamente por el desarrollador del hook.
3. Lectura del código del ciclo de vida del gancho
Por ejemplo, con un intercambio de entrada exacta, a continuación se analiza la pila de llamadas completa 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 permisos de la dirección y el campo fee, pero no verifica si el Hook ya se está utilizando en otro pool ni si el Hook "acepta" este PoolKey. Si el Hook no incluye lógica de lista blanca o vinculación a un solo pool en beforeInitialize, cualquier persona puede construir un nuevo pool que utilice el mismo Hook pero con cualquier par de tokens, y desencadenar todos los callbacks posteriores del Hook.
3.2 beforeSwap y BeforeSwapDelta
La entrada del proceso de swap es PoolManager.swap(), que llama a Hooks.beforeSwap() antes de ejecutar la lógica central de swap:

El valor devuelto de beforeSwap es una tupla de tres elementos (bytes4, BeforeSwapDelta, uint24):
- bytes4: debe ser igual a IHooks.beforeSwap.selector, de lo contrario, PoolManager revertirá directamente
- BeforeSwapDelta: El hook ajusta el delta para el token especificado y el token no especificado en este swap
- uint24: valor de cobertura de la tarifa LP dinámica (solo aplica cuando el pool tiene activada la tarifa dinámica)
BeforeSwapDelta es un alias de int256, donde los 128 bits superiores son el delta del token especificado (es decir, el token cuya cantidad el usuario especificó) y los 128 bits inferiores son el delta del token no especificado:

Tenga en cuenta que la semántica de BeforeSwapDelta es que el Hook debe devolver un valor positivo al cobrar tarifas y un valor negativo al reembolsar tokens. Los desarrolladores fácilmente pueden invertir el signo; además, la correspondencia entre specified y unspecified depende del signo de params.zeroForOne y amountSpecified; un pequeño error en la implementación puede provocar un desplazamiento incorrecto de tokens.
PoolManager sumará directamente el specifiedDelta devuelto por beforeSwap a amountToSwap:

Esta línea implica un significado clave: el Hook puede interceptar la cantidad de swap. Cuando hookDeltaSpecified es igual a -params.amountSpecified, amountToSwap se establece directamente en cero, lo que equivale a que el Hook asume completamente este swap: esto se conoce como Async Hook o Custom Curve Hook.
Async Hook es uno de los patrones de diseño más riesgosos en la versión 4: en esencia, reemplaza la lógica de intercambio de Uniswap con su propia lógica de Hook. Si el Hook tiene vulnerabilidades o es malicioso por naturaleza, los fondos del usuario ya no estarán protegidos por la lógica de fijación de precios nativa de Uniswap, sino que dependerán principalmente de la corrección de la implementación del Hook.
3.3 Liquidación de delta y NonzeroDeltaCount
El delta devuelto por beforeSwap y afterSwap no activa inmediatamente una transferencia, sino que se registra en el libro mayor 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 menciona en 2.1, al finalizar unlock(), si NonzeroDeltaCount != 0, toda la transacción se revertirá.
Hook equilibra su delta mediante dos acciones: settle() (transferencia a PoolManager) y take() (retiro de PoolManager):

La semántica de seguridad que ofrece este mecanismo es clara: todos deben finalmente cerrar sus cuentas. Sin embargo, solo garantiza la “conservación de cuentas”, no la “corrección de cuentas”. Si Hook devuelve un delta maliciosamente construido en beforeSwap, PoolManager registrará fielmente ese delta; siempre que finalmente se salde, la transacción se considerará exitosa, incluso si esto implica que Hook puede falsificar el estado del negocio para hacer que el sistema crea erróneamente que el atacante posee ciertos derechos sobre activos, y PoolManager no puede detectar este tipo de errores a nivel de negocio.
Anteriormente, el incidente de seguridad de Cork Protocol se debió a una vulnerabilidad en su Hook, y antes del ataque ya había sido auditado por cuatro empresas de auditoría. Tras el incidente, al realizar un análisis posterior, descubrimos:
Tres de las cuatro auditorías no incluyen el contrato CorkHook en su alcance.
Solo una auditoría de CorkHook identificó algunos problemas en el código y presentó sugerencias de mejora, pero no abarcó completamente los problemas de control de acceso.
Otra firma de auditoría recomendó explícitamente en su informe: "un seguimiento interesante sería demostrar los invariantes para las funciones CorkHook que son invocadas por diferentes componentes verificados dentro del alcance de este compromiso". Esta recomendación, desde una perspectiva de revisión posterior, tiene un enfoque muy específico.
Esto revela una nueva ceguera en la auditoría en la era de los v4 Hooks: el crecimiento exponencial de la complejidad del protocolo hace que la definición del alcance sea en sí misma una decisión de seguridad. La cadena de interacciones entre el Hook y otros contratos del protocolo es muy larga; auditar solo el contrato del Hook no es suficiente para detectar problemas de composición entre contratos; por otro lado, auditar los contratos circundantes mientras se excluye el Hook del alcance deja sin detectar la mayor superficie de ataque de la era v4.
4. Reflexión
Al comparar el mecanismo del protocolo y el ataque Cork, se pueden identificar varios puntos clave del modelo de seguridad v4 Hook:
(1) Si la función de retorno de Hook depende del contexto de llamada proporcionado por PoolManager, debe limitarse explícitamente para que solo pueda ser llamada por PoolManager. BaseHook no realizará esto por el desarrollador; este es el mayor riesgo de diseño que entra en conflicto con la experiencia general de auditoría de contratos en la versión 4.
(2) La relación de vinculación entre Hook y el pool no está restringida por PoolManager. Los desarrolladores deben implementar manualmente una lista blanca de pools o una vinculación de un solo pool en beforeInitialize.
(3) Los bits de permiso de la dirección Hook deben coincidir estrictamente con la implementación de la función. La dirección calculada debe incluir anticipadamente todos los bits de permiso que podrían ampliarse en el futuro.
(4) El hook Async / Custom Curve es esencialmente una implementación completamente personalizada de swap. No tiene ninguna protección a nivel del protocolo Uniswap y debe ser auditada según el estándar de “contrato financiero completamente autónomo”.
(5) La "conservación" en la contabilidad de delta no equivale a "correcta". 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 los protocolos permiten a los usuarios crear mercados, es obligatorio realizar una validación semántica de los tokens, no solo confiar en la verificación de la interfaz.
Cada Hook es un dominio de confianza independiente, y la seguridad de cada pool está determinada por el Hook asociado. Por lo tanto, la complejidad de la auditoría de seguridad de los Hooks ya no es “auditar un solo código”, sino “auditar un subprotocolo completo”: este cambio implica una actualización metodológica tanto para los equipos de proyecto como para los auditores.

