С момента запуска основной сети Uniswap v4 механизм Hook стал одной из самых обсуждаемых инноваций в DeFi. Платформа для запуска memecoin Flaunch в сети Base использует Hook для реализации фиксированной цены предпродажи и механизма автоматического листинга с ликвидацией; протокол ликвидности Bunni v2 использует Hook для создания моделей программируемой ликвидности и рестейкинга; в этом году токены, такие как SATO, uPEG (Unipeg) и Slonks, также продемонстрировали рост в десятки раз за короткий период, основываясь на механиках Hook.
На фоне процветания экосистемы Hook количество атак, направленных на уязвимости в реализации Hook, также значительно возросло. В этой статье мы начнем с механизма Hook в Uniswap v4 и поэтапно проанализируем его основный стек вызовов, чтобы помочь проектам понять возможные уязвимости.
Безопасность Hook в Uniswap v4
1. Введение
Наиболее значительное архитектурное изменение Uniswap v4 по сравнению с v3 — это введение механизма Hook (крючков): разработчикам разрешено подключать пользовательские контракты к событиям жизненного цикла пулов ликвидности, внедряя произвольную логику на таких этапах, как swap, добавление/удаление ликвидности, инициализация и т.д.
Ключевые изменения в v4 следующие:
- Singleton-режим: Состояние всех пулов централизованно управляется единым контрактом PoolManager, больше не требуется развертывать отдельный контракт для каждого пула.
- Flash accounting: Промежуточные изменения баланса во время транзакции учитываются только в transient storage, окончательный расчет происходит единовременно только в конце транзакции.
- Механизм 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. Любая операция изменения состояния пула (swap, добавление/удаление ликвидности) должна сначала вызвать PoolManager.unlock(), чтобы получить разовый доступ на обратный вызов, а затем выполнить конкретные действия в unlockCallback(). В конце всего процесса PoolManager проверит, сбалансирована ли бухгалтерская книга:
Если NonzeroDeltaCount != 0, транзакция сразу же откатывается (revert). Это ключевое ограничение flash accounting в 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. Если 7-й бит адреса Hook равен 1, PoolManager вызовет beforeSwap() этого Hook перед swap; в противном случае, даже если контракт Hook реализует beforeSwap(), PoolManager никогда его не вызовет.
Это означает, что при развертывании Hook необходимо через CREATE2 + salt вычислить адрес, сформировав адрес, у которого младшие биты полностью соответствуют целевым разрешениям. Uniswap официально предоставляет инструмент HookMiner для этой цели:
Несоответствие между битами разрешений и реализацией функций может привести к двум типам проблем:
(1) Функция hook реализована, но в адресе не закодирован соответствующий бит разрешения — PoolManager никогда не вызовет эту функцию, логика становится фиктивной.
(2) В адресе закодирован бит разрешения, но hook не реализует соответствующую функцию — При обратном вызове PoolManager может произойти revert (что приведет к DOS) или проверка возвращаемого значения завершится неудачей, что сделает невозможным выполнение соответствующей операции.
Это также является естественным препятствием для обновления Hook: если Hook является обновляемым через прокси, адрес развертывания не меняется при обновлении, поэтому после обновления можно изменить только реализацию существующих функций hook, но нельзя добавить новые типы hook. Чтобы зарезервировать возможности для будущего расширения, необходимо предварительно «выкопать» все потенциально используемые биты разрешений при первоначальном развертывании.
2.3 BaseHook и широко игнорируемая ловушка контроля доступа
Абстрактный контракт BaseHook, предоставляемый ранними версиями периферии Uniswap v4, позволяет разработчикам наследовать его для реализации пользовательских Hook. Одна из важных функций BaseHook — предоставление модификатора onlyPoolManager для функции unlockCallback():
Однако — здесь есть очень легко упускаемая из виду ловушка дизайна — ранние версии BaseHook добавляли onlyPoolManager только для unlockCallback, не предоставляя никакой защиты для других функций обратного вызова 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
Точка входа в процесс swap — это PoolManager.swap(), которая перед выполнением основной логики swap вызывает Hooks.beforeSwap():
Возвращаемое значение beforeSwap представляет собой тройку (bytes4, BeforeSwapDelta, uint24):
- bytes4: должно равняться IHooks.beforeSwap.selector, иначе PoolManager сразу выполнит revert.
- BeforeSwapDelta: Корректировка дельты (delta) для specified token и unspecified token, которую Hook вносит в этот swap.
- uint24: Значение переопределения динамической комиссии LP (действует только если в пуле включена динамическая комиссия).
BeforeSwapDelta — это псевдоним для int256, где старшие 128 бит — это delta для specified token (тот токен, количество которого указал пользователь), а младшие 128 бит — delta для unspecified token:
Важно отметить, что семантика BeforeSwapDelta следующая: 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: по сути, он заменяет логику swap Uniswap на собственную логику Hook. Если в Hook есть уязвимость или он изначально злонамеренный, средства пользователей больше не будут защищены нативной логикой ценообразования Uniswap и будут в основном зависеть от корректности реализации самого Hook.
3.3 Урегулирование Delta и NonzeroDeltaCount
Дельта, возвращаемая beforeSwap и afterSwap, не вызывает немедленный перевод средств, а записывается во внутреннюю бухгалтерскую книгу PoolManager:
Каждый раз, когда накопленная дельта для токена изменяется с нуля на ненулевое значение, NonzeroDeltaCount увеличивается на единицу; при возвращении к нулю — уменьшается на единицу. Как упоминалось в разделе 2.1, если в конце unlock() NonzeroDeltaCount != 0, вся транзакция откатывается (revert).
Hook балансирует свою дельту с помощью двух действий: settle() (перевод в PoolManager) и take() (изъятие из PoolManager):
Эта механизм обеспечивает четкую семантику безопасности: в конечном итоге все должны сбалансировать счета. Но он гарантирует только «сохранение баланса счетов», а не «правильность счетов». Если Hook возвращает злонамеренно сконструированную дельту в beforeSwap, PoolManager будет честно учитывать эту дельту, и если в итоге она будет урегулирована, транзакция будет успешной — даже если это означает, что Hook может сфальсифицировать состояние бизнес-логики, заставив систему ошибочно считать, что у злоумышленника есть определенные имущественные права, а PoolManager не сможет распознать эту ошибку на уровне бизнес-логики.
Предыдущий инцидент безопасности с Cork Protocol произошел из-за уязвимости в его Hook, и до атаки он уже прошел аудит в четырех аудиторских компаниях. При последующем анализе мы обнаружили:
- В трех из четырех аудитов объем проверки (scope) не включал контракт CorkHook.
- Единственная компания, которая аудировала CorkHook, выявила некоторые проблемы в коде и предложила улучшения, но не полностью охватила проблемы контроля доступа.
- Другая аудиторская компания в своем отчете четко рекомендовала: «an interesting follow-up engagement would be to prove the invariants for the CorkHook functions that are being invoked by different components verified within the scope of this engagement». С точки зрения последующего анализа, эта рекомендация была весьма актуальной.
Это выявило новую слепую зону аудита в эпоху Hook v4: экспоненциальный рост сложности протоколов привел к тому, что само определение объема аудита стало вопросом безопасности. Цепочки взаимодействия Hook с другими контрактами протокола очень длинные, и отдельный аудит контракта Hook недостаточен для обнаружения комбинированных проблем между контрактами; и наоборот, аудит периферийных контрактов с исключением Hook из объема проверки приведет к пропуску самой большой поверхности атаки в эпоху v4.
4. Размышления
Сопоставляя механизмы протокола и анализ атаки на Cork, можно выделить несколько ключевых моментов модели безопасности Hook в v4:
(1) Если функции обратного вызова Hook зависят от контекста вызова, предоставляемого PoolManager, следует явно ограничить их вызов только со стороны PoolManager. BaseHook не сделает этого за разработчика — это ловушка дизайна в v4, которая наиболее легко конфликтует с опытом аудита обычных контрактов.
(2) Отношение привязки Hook к пулу не ограничивается PoolManager. Разработчик должен самостоятельно реализовать белый список пулов или логику привязки к одному пулу в beforeInitialize.
(3) Биты разрешений адреса Hook должны строго соответствовать реализации функций. Рассчитанный адрес должен заранее включать все биты разрешений, которые могут понадобиться в будущем.
(4) Async / Custom Curve Hook по сути являются полностью пользовательскими реализациями swap. Они не имеют никакой защиты на уровне протокола Uniswap и должны аудироваться по стандартам «полностью автономных финансовых контрактов».
(5) «Сохранение» в бухгалтерском учете дельты не равно «правильности». NonzeroDeltaCount == 0 гарантирует только конечный баланс счетов, но не гарантирует, что содержимое счетов не было злонамеренно изменено.
(6) Путаница типов токенов между рынками — это новая поверхность атаки в эпоху v4. Когда протокол позволяет пользователям создавать рынки, семантическая проверка токенов обязательна, нельзя полагаться только на проверку интерфейса.
Каждый Hook представляет собой независимую доменную зону доверия, и безопасность каждого пула определяется привязанным к нему Hook. Таким образом, сложность аудита безопасности Hook больше не сводится к «проверке одного кода», а к «проверке целого суб-протокола» — это изменение требует методологического обновления как для команд проектов, так и для аудиторов.
Смотреть оригинал



















