07/06/2006
Uniswap v4 ha llegado para revolucionar el ecosistema de las finanzas descentralizadas (DeFi), introduciendo conceptos innovadores que prometen mayor flexibilidad y eficiencia. Entre estas novedades, los hooks se destacan como la herramienta más poderosa para los desarrolladores, permitiendo personalizar la lógica de los pools de liquidez de maneras que antes eran impensables. Si alguna vez has querido añadir funcionalidades personalizadas a un swap o a la provisión de liquidez, estás en el lugar correcto.

En esta guía completa, te llevaremos de la mano en el proceso de conceptualizar, construir y probar un hook básico desde cero. Crearemos un "Hook de Puntos", un sistema diseñado para incentivar la compra de un token específico y la adición de liquidez a su pool. Al finalizar, no solo tendrás un hook funcional, sino también una comprensión sólida de cómo puedes empezar a construir tus propias soluciones sobre Uniswap v4.
¿Qué Construiremos? Un Hook de Puntos
Imagina que has lanzado un token llamado TOKEN y quieres fomentar su adopción. Una estrategia efectiva es recompensar a los usuarios que interactúan con él. Con los hooks de Uniswap v4, podemos integrar esta lógica de recompensas directamente en el protocolo. Anteriormente, esto requeriría soluciones off-chain o contratos auxiliares complejos, pero ahora podemos hacerlo de forma nativa.
Nuestra lógica será simple pero efectiva:
- Recompensa por Swap: Cuando un usuario intercambia ETH por nuestro TOKEN, recibirá una cantidad de PUNTOS equivalente al ETH utilizado en el swap.
- Recompensa por Liquidez: Cuando un usuario añade liquidez al par ETH/TOKEN, será recompensado con PUNTOS en proporción a la cantidad de ETH que aportó.
Para que estas recompensas sean tangibles y rastreables, crearemos un token ERC20 llamado POINTS que se acuñará (mint) directamente en la billetera del usuario. Esto proporciona una gratificación instantánea y visible.
Diseño y Arquitectura del Hook
El primer paso es decidir qué puntos de anclaje (hooks) utilizaremos. La documentación de Uniswap v4 ofrece una variedad de ellos, cada uno correspondiendo a una etapa diferente en el ciclo de vida de una transacción en el pool. Para nuestro caso de uso, necesitamos actuar después de que un swap o una adición de liquidez se haya completado con éxito. Por lo tanto, los candidatos ideales son afterSwap y afterAddLiquidity.
¿Por qué `after` y no `before`?
La elección entre hooks `before...` (antes de la acción) y `after...` (después de la acción) es fundamental y depende completamente de tu objetivo. Los hooks `before` son ideales para validaciones: comprobar si una dirección está en una lista blanca, aplicar una tarifa previa, o incluso revertir la transacción si no se cumple una condición. Los hooks `after` son para acciones que deben ocurrir como consecuencia de una operación exitosa, como registrar datos, otorgar recompensas o actualizar un oráculo.
En nuestro caso, solo queremos recompensar transacciones exitosas y necesitamos saber exactamente cuánto ETH se utilizó, lo cual solo es posible después de que la operación se complete. El uso del `BalanceDelta` proporcionado en los hooks `after` es clave, ya que refleja el cambio real en el balance, incluso en casos de llenados parciales.
Tabla Comparativa: Hooks `before` vs `after`
| Característica | Hooks `before...` | Hooks `after...` |
|---|---|---|
| Momento de Ejecución | Antes de que la acción principal modifique el estado del pool. | Después de que la acción principal ha modificado el estado. |
| Capacidad de Revertir | Pueden revertir la transacción principal. | No pueden revertir la transacción principal, ya que esta ya ocurrió. |
| Datos Clave Disponibles | Parámetros de entrada de la transacción. | `BalanceDelta`, el cambio neto real en los balances. |
| Caso de Uso Típico | Validación, control de acceso, tarifas previas. | Recompensas, análisis, registro de datos, oráculos. |
Preparando el Entorno de Desarrollo
Para empezar a codificar, utilizaremos Foundry, un conjunto de herramientas de desarrollo para Ethereum rápido, portable y modular escrito en Rust. Uniswap proporciona una plantilla de repositorio perfecta para comenzar.
Primero, clona el repositorio y navega hasta el directorio:
git clone https://github.com/uniswapfoundation/v4-template.git cd v4-templateLuego, instala las dependencias necesarias con Forge, el gestor de dependencias de Foundry:
# Requiere tener Foundry instalado forge installPuedes ejecutar las pruebas iniciales para asegurarte de que todo está configurado correctamente:
forge testConstruyendo el Esqueleto del Hook
Ahora, vamos a la acción. Crea un nuevo archivo llamado `PointsHook.sol` dentro del directorio `src/`.
Comenzaremos con la estructura básica del contrato. Este esqueleto importa las dependencias necesarias, hereda de `BaseHook` y define qué permisos de hook vamos a utilizar.
pragma solidity ^0.8.24; import { BaseHook } from "v4-periphery/src/utils/BaseHook.sol"; import { Hooks } from "v4-core/src/libraries/Hooks.sol"; import { IPoolManager } from "v4-core/src/interfaces/IPoolManager.sol"; import { PoolKey } from "v4-core/src/types/PoolKey.sol"; import { PoolId, PoolIdLibrary } from "v4-core/src/types/PoolId.sol"; import { BalanceDelta } from "v4-core/src/types/BalanceDelta.sol"; contract PointsHook is BaseHook { constructor(IPoolManager _poolManager) BaseHook(_poolManager) {} function getHookPermissions() public pure override returns (Hooks.Permissions memory) { return Hooks.Permissions({ beforeInitialize: false, afterInitialize: false, beforeAddLiquidity: false, afterAddLiquidity: true, // Habilitamos este hook beforeRemoveLiquidity: false, afterRemoveLiquidity: false, beforeSwap: false, afterSwap: true, // Habilitamos este hook beforeDonate: false, afterDonate: false, beforeSwapReturnDelta: false, afterSwapReturnDelta: false, afterAddLiquidityReturnDelta: false, afterRemoveLiquidityReturnDelta: false }); } }La función `getHookPermissions` es crucial. Le dice al PoolManager qué funciones de hook implementa este contrato. Al habilitar `afterAddLiquidity` y `afterSwap` como `true`, el PoolManager sabrá que debe llamar a nuestro contrato después de que ocurran estas acciones en un pool que lo tenga configurado.
Implementando la Lógica del Hook
Con el esqueleto listo, vamos a añadir la funcionalidad. Primero, agreguemos las funciones vacías `_afterSwap` y `_afterAddLiquidity` que nuestro contrato debe implementar.
// ... dentro del contrato PointsHook function _afterSwap( address, PoolKey calldata key, IPoolManager.SwapParams calldata, BalanceDelta delta, bytes calldata ) internal override returns (bytes4, int128) { return (BaseHook.afterSwap.selector, 0); } function _afterAddLiquidity( address sender, PoolKey calldata key, IPoolManager.ModifyLiquidityParams calldata params, BalanceDelta delta, BalanceDelta feesAccrued, bytes calldata hookData ) internal override returns (bytes4, BalanceDelta) { return (BaseHook.afterAddLiquidity.selector, delta); }Por ahora, estas funciones simplemente devuelven el selector de la función base, un patrón que indica al PoolManager que la ejecución del hook fue exitosa pero no se realizó ninguna acción que modifique el estado de forma inesperada.
1. El Token de Puntos (POINTS)
Necesitamos un token para recompensar a los usuarios. Crearemos un contrato simple `PointsToken.sol` en `src/` que hereda de `ERC20` y `Owned` de Solmate, una librería de contratos inteligentes segura y eficiente.
pragma solidity ^0.8.24; import { ERC20 } from "solmate/src/tokens/ERC20.sol"; import { Owned } from "solmate/src/auth/Owned.sol"; contract PointsToken is ERC20, Owned { constructor() ERC20("Points Token", "POINTS", 18) Owned(msg.sender) {} function mint(address to, uint256 amount) external onlyOwner { _mint(to, amount); } }Este contrato permite que solo el propietario (nuestro `PointsHook`) pueda acuñar nuevos tokens.
2. Conectando el Hook con el Token
Ahora, nuestro `PointsHook` necesita poder crear y controlar el `PointsToken`. Lo instanciaremos en el constructor.
// ... dentro del contrato PointsHook import { PointsToken } from "./PointsToken.sol"; contract PointsHook is BaseHook { PointsToken public pointsToken; constructor(IPoolManager _poolManager) BaseHook(_poolManager) { pointsToken = new PointsToken(); } // ... resto del código }3. Otorgando las Recompensas
Crearemos funciones auxiliares para calcular y otorgar los puntos. La lógica es simple: 1 punto por cada unidad de ETH (en wei).
// ... dentro del contrato PointsHook function getPointsForAmount( uint256 amount ) internal pure returns (uint256) { return amount; } function awardPoints(address to, uint256 amount) internal { pointsToken.mint(to, getPointsForAmount(amount)); }4. Obteniendo la Dirección del Usuario
Un desafío interesante es: ¿cómo sabe el hook a quién recompensar? El PoolManager no pasa directamente la dirección del usuario final (`msg.sender` de la transacción original) a los hooks. La solución es usar el parámetro `hookData`, un campo de bytes arbitrario que se puede pasar al invocar la acción de swap o liquidez. Codificaremos la dirección del usuario en este campo.
// ... dentro del contrato PointsHook function getHookData(address user) public pure returns (bytes memory) { return abi.encode(user); } function parseHookData( bytes calldata data ) public pure returns (address user) { return abi.decode(data, (address)); }5. Lógica Final de `_afterSwap`
Ahora unimos todas las piezas en la función `afterSwap`. Verificamos que el swap sea de ETH (currency0) a TOKEN (currency1), extraemos la dirección del usuario y le otorgamos los puntos correspondientes a la cantidad de ETH gastada.
function _afterSwap( address, PoolKey calldata key, IPoolManager.SwapParams calldata swapParams, BalanceDelta delta, bytes calldata hookData ) internal override returns (bytes4, int128) { // Condición 1: Asegurarnos que es un pool con ETH (currency0 es la dirección cero) if (!key.currency0.isAddressZero()) { return (BaseHook.afterSwap.selector, 0); } // Condición 2: Asegurarnos de que el swap es de ETH a TOKEN (zeroForOne = true) if (!swapParams.zeroForOne) { return (BaseHook.afterSwap.selector, 0); } // Obtener la dirección del usuario desde hookData address user = parseHookData(hookData); // Calcular el ETH gastado a partir del delta. delta.amount0() será negativo. uint256 ethSpendAmount = uint256(int256(-delta.amount0())); // Otorgar los puntos awardPoints(user, ethSpendAmount); return (BaseHook.afterSwap.selector, 0); }6. Lógica Final de `_afterAddLiquidity`
La lógica para `_afterAddLiquidity` es muy similar. Verificamos que sea un pool de ETH, obtenemos la dirección del usuario y le recompensamos según el ETH que ha añadido a la liquidez.
function _afterAddLiquidity( address, PoolKey calldata key, IPoolManager.ModifyLiquidityParams calldata, BalanceDelta delta, BalanceDelta, bytes calldata hookData ) internal override returns (bytes4, BalanceDelta) { // Condición: Asegurarnos que es un pool con ETH if (!key.currency0.isAddressZero()) { return (BaseHook.afterAddLiquidity.selector, delta); } // Obtener la dirección del usuario address user = parseHookData(hookData); // Calcular el ETH añadido. delta.amount0() será negativo. uint256 ethSpendAmount = uint256(int256(-delta.amount0())); // Otorgar los puntos awardPoints(user, ethSpendAmount); return (BaseHook.afterAddLiquidity.selector, delta); }¡Y listo! Nuestro hook está completamente implementado.
Probando Nuestro Hook con Foundry
Un contrato inteligente sin pruebas es una receta para el desastre. Afortunadamente, Foundry hace que escribir pruebas en Solidity sea increíblemente sencillo. Crearemos un archivo de prueba `PointsHook.t.sol` en la carpeta `test/`.
El archivo de prueba comenzará con una función `setUp` que despliega todos los contratos necesarios: el PoolManager, los tokens, nuestro hook y crea un pool inicial con algo de liquidez.
Prueba de la Funcionalidad de Swap
Este caso de prueba simula un swap de 1 ETH por TOKEN y verifica que el usuario reciba 1e18 PUNTOS como recompensa.
function test_PointsHook_Swap() public { uint256 startingPoints = pointsToken.balanceOf(address(this)); bool zeroForOne = true; int256 amountSpecified = -1e18; // Swap de 1 ETH swap( key, zeroForOne, amountSpecified, hook.getHookData(address(this)) // Pasamos la dirección del usuario ); uint256 endingPoints = pointsToken.balanceOf(address(this)); assertEq( endingPoints - startingPoints, uint256(-amountSpecified), "Los puntos por swap deben ser 1:1 con el ETH" ); }Prueba de la Funcionalidad de Añadir Liquidez
Este caso de prueba añade liquidez al pool y verifica que los puntos otorgados correspondan a la cantidad de liquidez añadida. Usamos `assertApproxEqAbs` para tener en cuenta pequeñas desviaciones por el redondeo de los ticks.
function test_PointsHook_Liquidity() public { uint256 startingPoints = pointsToken.balanceOf(address(this)); uint128 liqToAdd = 100e18; (uint256 amount0, uint256 amount1) = LiquidityAmounts.getAmountsForLiquidity( SQRT_PRICE_1_1, TickMath.getSqrtPriceAtTick(tickLower), TickMath.getSqrtPriceAtTick(tickUpper), liqToAdd ); posm.mint( key, tickLower, tickUpper, liqToAdd, amount0 + 1, amount1 + 1, address(this), block.timestamp, hook.getHookData(address(this)) ); uint256 endingPoints = pointsToken.balanceOf(address(this)); assertApproxEqAbs(endingPoints - startingPoints, uint256(liqToAdd), 10); }Para ejecutar las pruebas, simplemente corre `forge test` en tu terminal. Si todo está correcto, las pruebas pasarán, validando que nuestro hook funciona como se esperaba.
Preguntas Frecuentes (FAQ)
¿Qué es exactamente un hook en Uniswap v4?
Un hook es un contrato inteligente externo que se "engancha" a un pool de liquidez en puntos específicos de su ejecución (por ejemplo, antes o después de un swap). Permite a los desarrolladores ejecutar código personalizado, modificando o extendiendo el comportamiento estándar del pool.
¿Por qué usamos `hookData` para pasar la dirección del usuario?
Debido a la arquitectura de Uniswap v4, donde las interacciones pueden pasar por múltiples contratos (como un Router), el `msg.sender` original de la transacción no está disponible directamente para el hook. `hookData` es el mecanismo diseñado para pasar datos de contexto, como la dirección del usuario final, desde el punto de entrada de la transacción hasta el hook.
¿Puedo usar este hook para cualquier par de tokens?
El código de este tutorial está diseñado específicamente para un par ETH/TOKEN, donde ETH es `currency0`. Para adaptarlo a otros pares (por ejemplo, USDC/TOKEN), necesitarías modificar las comprobaciones de las direcciones de los tokens dentro de las funciones `_afterSwap` y `_afterAddLiquidity`.
Próximos Pasos
¡Felicidades! Has construido tu primer hook funcional en Uniswap v4. Has aprendido a definir permisos, implementar la lógica post-operación y escribir pruebas robustas con Foundry. Este es solo el comienzo. A partir de aquí, puedes explorar conceptos más avanzados como la gestión de estado dentro del hook, la creación de oráculos, la implementación de tarifas dinámicas o la integración con otros protocolos DeFi. La flexibilidad de los hooks abre un universo de posibilidades para la innovación en finanzas descentralizadas.
Si quieres conocer otros artículos parecidos a Crea tu Primer Hook en Uniswap v4: Guía Práctica puedes visitar la categoría Juegos.
