Розробка кастомних хуків для Uniswap v4
Uniswap v4 змінив архітектуру радикально: один singleton-контракт PoolManager управляє всіма пулами, а розширення логіки — через хуки. Це не просто callback-и. Хуки отримують контроль над критичними точками життєвого циклу пулу: до і після ініціалізації, до і після свапу, до і після додавання/видалення ліквідності. Правильно написаний хук дозволяє вбудувати limit orders, динамічне ціноутворення, fee rebate, MEV-capture — без форку протоколу. Неправильний — заблокує пул або стане вектором атаки.
Архітектура хуків: що важливо розуміти до написання коду
Флаги та дозволи
Адреса хука кодує дозволи в бітах. Біти 0-7 визначають, які callback-и активні: BEFORE_SWAP_FLAG, AFTER_SWAP_FLAG, BEFORE_ADD_LIQUIDITY_FLAG тощо. Якщо хук оголошує getHookPermissions() з флагом afterSwap: true, але адреса деплоюу не містить відповідний біт — PoolManager ревертне при ініціалізації пулу.
Це означає: адреса контракту хука не довільна. Потрібен CREATE2-деплой з підбором salt до збігу потрібних бітів в адресі. Для складного хука з 4-5 флагів підбір salt — окрема задача, яку розв'язують off-chain скриптом.
PoolKey та ізоляція пулів
Кожен пул у v4 ідентифікується PoolKey: {currency0, currency1, fee, tickSpacing, hooks}. Адреса хука — частина ідентифікатора пулу. Два пули з однаковими токенами та fee, але різними хуками — різні пули з різними liquidity positions. Це означає: ліквідність неможливо «мігрувати» між хуками без повного виведення та повторного входу.
Transient storage та EIP-1153
V4 активно використовує EIP-1153 transient storage — хранилище, яке очищується в кінці транзакції. Це дешевше від SSTORE/SLOAD і ідеально для тимчасового стану усередину транзакції (наприклад, флаг «свап уже йде»). Хуки можуть використовувати transient storage для reentrancy-захисту без постійного storage overhead.
Типічні кейси хуків та їхні складності
Dynamic fee хук
Найпопулярніший запит: fee, яка змінюється залежно від волатильності. Логіка afterSwap: рахуємо відхилення від TWAP, якщо >threshold — піднімаємо fee для наступного свапу через poolManager.updateDynamicLPFee().
Проблема: TWAP потрібно зберігати in-hook. Якщо використовуємо Uniswap v3 TWAP oracle як reference — це зовнішній виклик з afterSwap, що додає 3-5k газу до кожного свапу. Альтернатива: власний rolling TWAP в hook storage, оновлюється в afterSwap. Дешевше, але потребує bootstrap періоду та обробки edge case при перших свапах.
Другий нюанс: updateDynamicLPFee можна викликати тільки якщо пул ініціалізований з FEE_DYNAMIC_FLAG. Цей флаг повинен бути встановлений в fee полі PoolKey при створенні пулу. Пропустити — контракт задеплоєн, пул створений, хук не працює. Переіграти неможливо.
Limit order хук
beforeSwap перевіряє, чи є pending limit orders в діапазоні поточного тика. Якщо так — виконує їх як частину свапу. Реалізація: маппінг tick => orders[], обхід при перетині тика.
Головний ризик: unbounded loop по orders на тику. Якщо на одному тику накопилося 500 ордерів, один свап через цей тик витратить >1M газу і упрется в block gas limit. Захист: ограничення на кількість ордерів на тик + батчинг виконання через окрему keeper-функцію для накопичених ордерів.
MEV capture через afterSwap fee redistribution
Ідея: якщо свап викликав значне руху ціни (підозра на MEV), перенаправити частину fee у окремий пул компенсацій для LP. afterSwap рахує price impact, якщо вище порога — відправляє додатковий платіж у vault.
Технічна складність: afterSwap отримує delta — зміну балансів. Потрібно розрахувати price impact на основі delta та початкового стану пулу. Захопити початковий стан у beforeSwap і зберегти в transient storage — щоб у afterSwap можна було порівняти. Це класичний паттерн для пар beforeX/afterX хуків.
Інструменти розробки
Foundry — єдиний нормальний вибір для v4 хуків. Репозиторій v4-core написаний під Foundry, тести — теж. forge test --fork-url <mainnet> дозволяє тестувати хук проти реального стану PoolManager.
v4-template від Uniswap — стартова точка. Містить правильний setup HookMiner для CREATE2 деплоюу, базовий BaseHook з абстракціями, приклади тестів.
Slither з кастомними детекторами для v4 — перевіряємо правильність флагів, відсутність storage collision з PoolManager slots.
Частісні помилки при розробці хуків
| Помилка | Наслідок | Рішення |
|---|---|---|
| Неправильні біти в адресі | Пул не ініціалізується | CREATE2 + HookMiner до деплоюу |
Зовнішній виклик в beforeSwap без reentrancy guard |
Можливий reentrancy через хук | nonReentrant + transient storage lock |
| Unbounded loop в order book | DoS через gas limit | Ограничення ордерів на тик + keeper |
Використання SSTORE в гарячому шляху |
+20k газу на кожен свап | Transient storage (EIP-1153) |
Мутація PoolKey в хуці |
Неможливо — PoolKey immutable | Проектувати логіку без зміни key |
Процес розробки
Специфікація (2-3 дні). Формалізуємо поведінку хука в кожній точці життєвого циклу. Які інваріанти мають дотримуватися? «Сума fee завжди ≥ base fee», «limit order ніколи не виконується за цною гірше заявленої».
Розробка (5-7 днів). Foundry + v4-template. CREATE2 deployment script з HookMiner. Property-based тесты через Echidna на ключові інваріанти.
Fork-тестування (2-3 дні). Тесты проти реального mainnet стану: ініціалізація пулу, серія свапів, граничні випадки (порожній пул, single-sided liquidity, великий price impact).
Аудит та газовий профіль. Slither + ручний review. Gas snapshot через forge snapshot — порівнюємо gas cost свапу з хуком та без. Допустимий overhead для більшості кейсів: <10k газу на свап.
Орієнтири по срокам
Простий хук (dynamic fee або whitelist) — 1 тиждень включаючи тесты. Хук середної складності (limit orders, MEV capture) — 2-3 тижні. Комплексна система з кількома взаємодіючими хуками — від 4 тижнів.
Вартість розраховується індивідуально після обговорення потрібної механіки.







