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 — это ключевое ограничение flash-учета v4. Любой Hook может временно нарушить баланс счетов в процессе выполнения, но должен самостоятельно закрыть разницу до завершения транзакции, иначе вся транзакция откатывается.
Каждый пул уникально идентифицируется структурой PoolKey, которая содержит поле hooks:
PoolId вычисляется с использованием keccak256(PoolKey), поэтому разные адреса hooks создают разные пулы. Это также означает, что PoolManager не проверяет, использовался ли какой-либо адрес hook ранее для других пулов; один и тот же контракт hook может быть одновременно привязан к нескольким пулам.
2.2 Кодировка прав доступа Hook в адресе
Одним из неинтуитивных решений в v4 является то, что права Hook определяются не переменной внутри контракта, а адресом развертывания контракта Hook.
PoolManager проверяет младшие 14 бит адреса Hook, чтобы определить, должен ли этот Hook быть вызван на определённом этапе жизненного цикла:
Например, BEFORE_SWAP_FLAG = 1 << 7. Если седьмой бит адреса Hook установлен в 1, PoolManager вызовет beforeSwap() этого Hook до обмена; в противном случае, даже если контракт Hook реализует beforeSwap(), он никогда не будет вызван PoolManager.
Это означает, что при развертывании Hook адрес должен быть вычислен с помощью CREATE2 + salt таким образом, чтобы получить адрес с младшими битами, полностью совпадающими с целевыми правами. Официальный инструмент Uniswap для этой цели — HookMiner:
Несоответствие битов разрешений и реализации функций приводит к двум типам проблем:
(1) Реализована функция hook, но адрес не закодирован с соответствующими битами разрешений — PoolManager никогда не вызовет эту функцию, логика бессмысленна
(2) Адрес кодирует определенный бит разрешения, но хук не реализует соответствующую функцию — при обратном вызове PoolManager может произойти revert, что приведет к DOS или сбою проверки возвращаемого значения, из-за чего соответствующая операция не сможет быть выполнена.
Это также является естественным препятствием для обновления Hook: если Hook можно обновить через прокси, адрес развертывания остается неизменным во время обновления, поэтому после обновления можно изменять реализацию существующих функций Hook, но не добавлять новые типы Hook. Для обеспечения возможности расширения в будущем необходимо заранее выделить все возможные биты разрешений при первоначальном развертывании.
2.3 BaseHook и одна часто игнорируемая ловушка управления доступом
В устаревшей версии периферии Uniswap v4 предоставлялся абстрактный контракт BaseHook, который разработчики могли наследовать для реализации пользовательских хуков. Одной из ключевых функций BaseHook является предоставление модификатора onlyPoolManager для функции unlockCallback():
Однако — здесь существует очень легко упускаемая ловушка проектирования — в ранних версиях BaseHook для unlockCallback был добавлен только onlyPoolManager, а для других функций обратного вызова hook (beforeSwap, afterSwap, beforeAddLiquidity и т. д.) никакой защиты не предусмотрено. Контроль доступа к этим функциям должен быть явно добавлен разработчиком hook самостоятельно.
3. Чтение кода жизненного цикла Hook
На примере exact-input swap ниже приведён полный стек вызовов от инициации пользователем сделки до её завершения.
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 заключается в том, что хук, взимающий комиссию, должен возвращать положительное значение, а хук, возвращающий токены, — отрицательное. Разработчики легко могут перепутать знаки; кроме того, соответствие между specified и unspecified зависит от знаков params.zeroForOne и amountSpecified, и небольшая ошибка в написании может привести к смещению токенов.
PoolManager напрямую добавляет specifiedDelta, возвращаемый beforeSwap, к amountToSwap:
Эта строка содержит ключевой смысл: Hook может перехватывать сумму свопа. Когда hookDeltaSpecified равно -params.amountSpecified, amountToSwap сразу становится нулевым, что означает, что Hook полностью берет на себя этот своп — это так называемый 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. «Постмортем эксплуатации от 28 мая 2025 года». 2025-06-04. https://www.cork.tech/blog/post-mortem
(2) OWASP 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) для виртуальных активов, а также оценку соответствия местным регуляторным требованиям — все это в рамках «одного окна» для блокчейн-соответствия и безопасности. Свяжитесь с нами через поле для сообщений в нашем канале.

