Uniswap v4 за допомогою Hook реалізує програмову ліквідність, але через недоліки, такі як кодування дозволів та контроль доступу, стає місцем частих атак; потрібно бути обережним щодо ризиків неправильного використання Async Hook та бухгалтерської логіки.Автор статті, джерело: Beosin
Після запуску Uniswap v4 у головній мережі, механізм Hook став одним із найбільш обговорюваних інновацій у DeFi. Платформа запуску мемкоінів Flaunch на ланцюзі Base використовує Hook для забезпечення фіксованої ціни передпродажу та автоматичного механізму виходу на ринок; протокол ліквідності Bunni v2 застосовує Hook для створення програмованої ліквідності та моделі повторного залучення; протягом цього року токени, такі як SATO, uPEG (Unipeg) та Slonks, які грають з механізмом Hook, також досягли зростання в ціні в десятки разів за короткий термін.
На тлі процвітання екосистеми Hook атаки, спрямовані на вади реалізації Hook, значно зростають. У цій статті ми розглянемо механізм Hook Uniswap v4, поступово аналізуючи його основний стек викликів, щоб допомогти проектам зрозуміти можливі вразливості.
Безпека Uniswap v4 Hook
1. Вступ
Найбільш значущою зміною архітектури Uniswap v4 порівняно з v3 є введення механізму Hook (гачків): він дозволяє розробникам підключати користувацькі контракти до подій життєвого циклу ліквідності, впроваджуючи довільну логіку в точках обміну, додавання/зняття ліквідності, ініціалізації тощо.
Нижче наведено кілька ключових змін у v4:
Шаблон Singleton: стан усіх пулув керується єдиним контрактом PoolManager, замість розгортання окремих контрактів для кожного пулу
- Миттєве облікове оброблення: проміжні зміни балансу під час транзакції враховуються лише у тимчасовому сховищі, а підсумкове звітність здійснюється лише наприкінці транзакції
- Механізм Hook: кожен пул може бути прив’язаний до контракту Hook, і PoolManager викликає цей контракт у ключових точках (beforeInitialize, beforeSwap, afterAddLiquidity тощо)
- Hook не можна змінити: після ініціалізації пулу прив’язаний адреса Hook фіксована назавжди (адреса Hook, прив’язана до пулу, не може бути змінена, але чи може сам контракт Hook оновлюватися, залежить від його реалізації)
У період v3 розробникам потрібно було довіряти лише самому протоколу Uniswap; у період v4 безпека кожного пулу залежить від прив’язаного до нього Hook. Hook перетворив AMM з фіксованого фінансового примітива на програмову фінансову інфраструктуру, але модель безпеки розбилася з «рівня протоколу» на «рівень пулу».
2. Архітектура Hook 2.1 PoolManager та модель unlock/callback
Ядром v4 є одиночний PoolManager. Будь-яка операція зі зміною стану пулу (обмін, додавання/зняття ліквідності) повинна спочатку викликати PoolManager.unlock(), отримати одноразовий доступ до зворотного виклику, а потім виконати конкретні дії в unlockCallback(). Після завершення всього процесу PoolManager перевіряє баланс бухгалтерського обліку:
При NonzeroDeltaCount != 0 виконується негайне revert — це ключове обмеження v4 flash accounting. Будь-який Hook може тимчасово порушити баланс обліку, але повинен самостійно вирівняти його до завершення угоди, інакше вся угода відміняється.
Кожен пул унікально ідентифікується структурою PoolKey, яка містить поле hooks:
PoolId обчислюється за допомогою keccak256(PoolKey), тому різні адреси hooks створюють різні пули. Це також означає, що PoolManager не перевіряє, чи використовувалася певна адреса hooks раніше для іншого пулу; один і той самий контракт hooks може бути прив’язаний до кількох пулив одночасно.
2.2 Кодування прав доступу Hook у адресі
Однією з неінтуїтивних особливостей v4 є те, що дозволи Hook визначаються не змінною всередині контракту, а адресою розгортання контракту Hook.
PoolManager перевіряє 14 найменш значущих бітів адреси Hook, щоб визначити, чи потрібно викликати цей Hook у певній точці життєвого циклу:
Наприклад, BEFORE_SWAP_FLAG = 1 << 7. Якщо 7-й біт адреси Hook дорівнює 1, PoolManager викликає beforeSwap() цього Hook перед обміном; інакше, навіть якщо контракт Hook реалізує beforeSwap(), він ніколи не буде викликаний PoolManager.
Це означає, що під час розгортання Hook адресу необхідно обчислити за допомогою CREATE2 + salt, щоб сформувати адресу з нижніми бітами, які повністю збігаються з цільовими правами. Офіційний Uniswap надає інструмент HookMiner для цієї мети:
При невідповідності бітів дозволів реалізації функцій виникають два типи проблем:
(1) Реалізовано функцію hook, але адреса не закодована відповідними бітами дозволів — PoolManager ніколи не викликатиме цю функцію, логіка є еквівалентом відсутньої
(2) Адреса кодує певний біт дозволу, але hook не реалізує відповідну функцію — під час зворотного виклику PoolManager може відмовитися, що призведе до DOS або невдачі перевірки значення повернення, заважаючи виконанню відповідних операцій.
Це також є природною перешкодою для оновлення Hook: якщо Hook може бути оновлений через проксі, адреса розгортання залишається незмінною під час оновлення, тому після оновлення можна змінювати лише реалізацію існуючих функцій Hook, але не можна додавати нові типи Hook. Щоб забезпечити можливість майбутнього розширення, необхідно на початковому етапі розгортання заздалегідь виділити всі можливі біти дозволів.
2.3 BaseHook та одна з загалом не помічених ловушок контролю доступу
У старій версії периферії Uniswap v4 абстрактний контракт BaseHook, який розробники можуть успадковувати для реалізації власних Hook. Однією з ключових функцій BaseHook є надання модифікатора onlyPoolManager для функції unlockCallback():
Але — тут існує дуже легко знехтуваний дизайн-пастка — ранні версії BaseHook додавали onlyPoolManager лише для unlockCallback, не надаючи жодної захисту для інших callback-функцій hook (beforeSwap, afterSwap, beforeAddLiquidity тощо). Контроль доступу до цих функцій повинен бути явно доданий розробником hook.
3. Перегляд коду життєвого циклу Hook
Наведемо повний стек викликів від моменту, коли користувач ініціює обмін з точним введенням, до його розрахунку.
3.1 Ініціалізація пулу та прив’язка Hook
Будь-хто може викликати PoolManager.initialize() для створення нового пулу:
isValidHookAddress перевіряє лише сумісність бітів прав доступу адреси та поля fee, але не перевіряє, чи вже використовується Hook в іншому пулі, нічого не перевіряє щодо того, чи «згоден» цей Hook приймати цей PoolKey. Якщо під час розробки Hook не було додано логіки білого списку або прив’язки до одного пулі в beforeInitialize, будь-хто може створити новий пул з тим самим Hook, але довільною парою токенів і запустити всі наступні зворотні виклики Hook.
3.2 beforeSwap та BeforeSwapDelta
Вхід у процес обміну — це PoolManager.swap(), який перед виконанням основної логіки обміну викликає Hooks.beforeSwap():
Повернене значення beforeSwap — це кортеж (bytes4, BeforeSwapDelta, uint24):
- bytes4: має дорівнювати IHooks.beforeSwap.selector, інакше PoolManager негайно відміняє
- BeforeSwapDelta: Hook здійснює дельта-корекцію для вказаного та невказаного токенів у цій операції обміну
- uint24: значення динамічної ставки LP (діє лише при увімкненій динамічній ставці для пулу)
BeforeSwapDelta — це псевдонім int256, старші 128 біт — це дельта вказаного токена (тобто токен, кількість якого вказав користувач), молодші 128 біт — це дельта невказаного токена:
Варто звернути увагу, що семантика BeforeSwapDelta полягає в тому, що Hook, який стягує комісію, повинен повертати позитивне значення, а Hook, який повертає токени, — негативне. Розробники легко можуть переплутати знаки; крім того, відповідність між specified та unspecified залежить від знаків params.zeroForOne та amountSpecified, і навіть невелика помилка у написанні може призвести до неправильного розташування токенів.
PoolManager додає specifiedDelta, повернений beforeSwap, безпосередньо до amountToSwap:
Цей рядок містить ключовий зміст: Hook може перехоплювати суму swap. Коли hookDeltaSpecified дорівнює -params.amountSpecified, amountToSwap безпосередньо стає нульовим, що означає, що Hook повністю бере на себе цей swap — це так званий Async Hook або Custom Curve Hook.
Async Hook — це найбільш ризикованний шаблон проектування у v4: він суттєво замінює логіку обміну Uniswap власною логікою Hook. Якщо Hook містить вразливості або є зловмисним, кошти користувачів більше не захищені вихідною ціноутворюючою логікою Uniswap, а залежать виключно від правильності реалізації самого Hook.
3.3 Дельта-розрахунок та NonzeroDeltaCount
delta, що повертаються beforeSwap і afterSwap, не спричиняють негайного переказу, а замість цього записуються до внутрішнього обліку PoolManager:
Коли накопичена дельта будь-якого токена змінюється з нуля на ненульове значення, NonzeroDeltaCount збільшується; коли вона знову стає нулем — зменшується. Як зазначено у розділі 2.1, якщо після завершення unlock() значення NonzeroDeltaCount != 0, вся транзакція відміняється.
Hook балансує свою дельту за допомогою двох дій: settle() (переказ до PoolManager) та take() (вилучення з PoolManager):
Механізм, що забезпечує чітку безпеку: усі повинні в кінцевому підсумку закрити свої позиції. Але він гарантує лише «збереження балансу», а не «коректність балансу». Якщо Hook у beforeSwap повертає зловмисно сформований delta, PoolManager вірно зараховує цей delta, і якщо в кінці він було вирівняний, угоду вважається успішною — навіть якщо це означає, що Hook може шляхом підробки бізнес-стану змусити систему помилково вважати, що атакуючий має певні активи, а PoolManager не може виявити такі помилки на рівні бізнес-логіки.
Раніше подія безпеки Cork Protocol виникла через вразливість у його Hook, і до атаки вона вже пройшла аудит чотирма аудиторськими компаніями. Після інциденту ми виявили:
З чотирьох аудитів три не включають контракт CorkHook
- Єдина аудиторська фірма, яка перевірила CorkHook, виявила частину проблем у коді та надала пропозиції щодо покращення, але не охопила питання контролю доступу
- Інший аудитор у своєму звіті чітко рекомендував: «цікавим наступним завданням було б довести інваріанти для функцій CorkHook, які викликаються різними компонентами, перевіреними в межах цього завдання». Ця рекомендація з позиції аналізу після події має високу цілеспрямованість.
Це виявляє нову аудиторську сліпу зону в епоху v4 Hook: експоненційний ріст складності протоколу робить саме визначення обсягу аудиту важливою безпековою рішенням. Ланцюжок взаємодій Hook з іншими контрактами протоколу дуже довгий — окремий аудит контракту Hook не дозволяє виявити комбінаційні проблеми між контрактами; навпаки, аудит сусідніх контрактів із виключенням Hook зі сфери аудиту призводить до пропуску найбільшої поверхні атаки епохи v4.
5. Рефлексія
Порівнюючи механізм протоколу та атаку Cork, можна виділити кілька ключових аспектів безпеки v4 Hook:
(1) Якщо функція зворотного виклику Hook залежить від контексту виклику, наданого PoolManager, слід явно обмежити її виклик лише PoolManager. BaseHook не зробить цього за розробника — це найбільш поширена помилка проектування, яка суперечить досвіду аудиту контрактів у версії 4.
(2) Прив’язка Hook до пулів не обмежується PoolManager. Розробники повинні самостійно реалізувати білий список пулів або прив’язку до одного пулу у beforeInitialize.
(3) Дозволи адреси Hook повинні точно відповідати реалізації функції. Розрахована адреса повинна містити всі можливі дозволи, які можуть бути додані в майбутньому.
(4) Async / Custom Curve Hook це повністю кастомна реалізація swap. Він не має жодних захистів на рівні протоколу Uniswap і повинен бути аудитовано як повністю автономний фінансовий контракт.
(5) Дельта-облік «збереження» не означає «правильність». NonzeroDeltaCount == 0 забезпечує лише кінцеве рівновагу обліку, але не гарантує, що вміст обліку не був зловживно змінений
(6) Замішування типів токенів між ринками — це нова поверхня атаки в епоху v4. Коли протокол дозволяє користувачам створювати ринки, перевірка семантики токенів є обов’язковою і не може залежати лише від перевірки інтерфейсу.
Кожен Hook є окремою областю довіри, а безпека кожного пулу визначається його прив’язаним Hook. Тому складність аудиту безпеки Hook більше не полягає у «перевірці одного коду», а в «перевірці повного підпротоколу» — ця зміна означає методологічне удосконалення як для проектів, так і для аудиторів.
Джерела
(1) Cork Protocol. «Post-mortem експлуатації від 28 травня 2025 року». 2025-06-04. https://www.cork.tech/blog/post-mortem
(2) OWASP Smart Contract Security Top 10 2026, SC01: Вразливості контролю доступу. https://scs.owasp.org/sctop10/SC01-AccessControlVulnerabilities/
(3) Біла книга Uniswap v4. https://app.uniswap.org/whitepaper-v4.pdf
(4) Uniswap v4-core. https://github.com/Uniswap/v4-core
(5) Uniswap v4-periphery. https://github.com/Uniswap/v4-periphery
Beosin — одна з перших у світі блокчейн-безпекових компаній, що спеціалізується на формальній верифікації, пропонує повний екосистемний підхід «безпека + відповідність». Компанія має філії в понад 10 країнах та регіонах, а її послуги охоплюють аудит безпеки коду до запуску проекту, моніторинг та блокування ризиків під час роботи проекту, відновлення вкрадених коштів, антигрошове прання (AML) віртуальних активів та оцінка відповідності місцевим регуляторним вимогам — усі ці «однохвилинні» послуги з відповідності та безпеки блокчейну. Зв’яжіться з нами, залишивши повідомлення у нашому каналі.

