Как отмечалось в нашей предыдущей статье, более 30% проектов в репозитории Awesome Uniswap v4 Hooks[1] содержат уязвимости. Стоит отметить, что упомянутые уязвимости специфичны для взаимодействий с Uniswap v4. Соответственно, в данной статье мы рассмотрим безопасную логику взаимодействия хуков со следующих двух точек зрения:
- Ненадлежащий контроль доступа
- Некорректная валидация входных данных
Для каждой категории мы начнём с анализа уязвимости и продемонстрируем возможность её эксплуатации, предоставив соответствующий Proof-of-Concept (PoC). Далее последует обсуждение возможных стратегий смягчения последствий.
Ненадлежащий контроль доступа
Как правило, взаимодействия, связанные с хуками Uniswap v4, можно классифицировать в зависимости от того, выступает ли хук в роли локера, захватывающего блокировку в PoolManager для выполнения операций в пулах. Два основных сценария взаимодействия требуют надлежащего контроля доступа:
- Взаимодействие Hook-PoolManager: Это взаимодействие между официальными функциями обратного вызова и
PoolManager. Функции обратного вызова включают восемь колбэков действий пула (т.е.initialize,modifyPosition,swapиdonate) и колбэк блокировки (т.е.lockAcquired).
- Внутреннее взаимодействие Hook-Internal: Это взаимодействия, происходящие внутри контракта хука (выступающего в роли локера).
Взаимодействия Hook-PoolManager относительно просты. Здесь хук выступает исключительно как хук, принимая восемь колбэков действий пула. Логика хука не влияет на связанные пулы, то есть потоков средств между хуком и пулами не существует. Параметры, предоставляемые функциями обратного вызова, используются для изменения необходимых хранилищ или в качестве важных параметров функций. Ключевым вопросом является возможность манипулирования параметрами колбэков.
Взаимодействия Hook-Internal несколько сложнее. На практике многие прототипы хуков делают больше, чем просто выступают в роли чистых хуков. Некоторые разработчики позволяют хукам предоставлять пользователям функции управления средствами. Эти функции могут быть реализованы не в контрактах хуков, но в данном контексте мы всё равно можем рассматривать их совокупно как хуки. В таких случаях хук принимает пользовательские средства и выполняет операции с пулом, такие как управление ликвидностью или свопы. Это означает, что контракт должен захватить блокировку у PoolManager, превращая хук в локер.
Фонд Uniswap учёл эту ситуацию и интегрировал функцию в свой шаблон хука. В частности, шаблон BaseHook предоставляет функцию lockAcquired в качестве колбэка блокировки следующим образом:
function lockAcquired(bytes calldata data) external virtual poolManagerOnly
returns (bytes memory) {
(bool success, bytes memory returnData) = address(this).call(data);
if (success) return returnData;
if (returnData.length == 0) revert LockFailure();
// if the call failed, bubble up the reason
/// @solidity memory-safe-assembly
assembly {
revert(add(returnData, 32), mload(returnData))
}
}
Для выполнения пользовательской логики lockAcquired принимает байты data и выполняет низкоуровневый вызов к себе, используя эти data. Содержимое data зависит от бизнес-логики хука и может быть изменено пользователями, что потенциально приводит к проблемам безопасности из-за взаимодействий Hook-Internal, инициируемых lockAcquired. Следует отметить, что дизайн хуков настолько гибок, что в данной ситуации невозможно охватить все возможные сценарии. Наш основной фокус здесь — захват блокировки хуком и его последующие внутренние взаимодействия. Углубление в другую потенциальную бизнес-логику сделало бы ситуацию слишком сложной для данного обсуждения.
В обоих сценариях приоритетом является устранение любых недостатков контроля доступа, которые могут привести к эксплуатации, поскольку эти функции имеют чётко определённые субъекты взаимодействия. В последующих подразделах мы последовательно рассмотрим каждый сценарий и обсудим необходимые средства контроля доступа для обеспечения более безопасной логики взаимодействия.
Анализ уязвимости
Контроль доступа служит высокоэффективным и простым решением безопасности для многих проектов. Если функция предназначена для вызова определёнными субъектами, она должна включать контроль доступа. Наиболее известным примером контроля доступа является контракт Ownable библиотеки OpenZeppelin, который требует, чтобы привилегированные функции вызывались только владельцем контракта. Очевидно, что два описанных выше сценария являются подходящими случаями для применения данного типа контроля.
Взаимодействие Hook-PoolManager: Для безопасного взаимодействия с PoolManager хуки должны обеспечивать необходимый контроль доступа к этим функциям обратного вызова. В частности, эти колбэки должны быть вызываемы исключительно PoolManager, а не какими-либо другими аккаунтами. Несоблюдение таких ограничений может оставить эти чувствительные интерфейсы уязвимыми для потенциальной эксплуатации злоумышленниками.
Помимо восьми колбэков действий пула, колбэк блокировки (т.е. lockAcquired), выполняющий пользовательскую логику после получения блокировки от PoolManager, также должен решать эту проблему.
Внутреннее взаимодействие Hook-Internal: Функции, задействованные во внутренних взаимодействиях хука, также предназначены для вызова конкретными субъектами. Как было сказано ранее, этот сценарий содержит два этапа. Во-первых, функция lockAcquired локера вызывается PoolManager, что означает, что функция должна требовать, чтобы msg.sender был PoolManager. Во-вторых, хук соответствующим образом диспетчеризует вызов функции. Согласно дизайну BaseHook, это реализуется посредством низкоуровневых вызовов к самому хуку. Это означает, что эти функции должны быть объявлены как external и ограничивать вызывающую сторону адресом самого хука.
Возьмём один из примеров, перечисленных в репозитории Awesome Uniswap v4 Hooks, а именно Stop Loss Order, в качестве примера[2]:
Интегрированные непосредственно в пулы Uniswap V4, стоп-лосс ордера публикуются в блокчейне и исполняются через хук afterSwap(). Для гарантии исполнения не требуются внешние боты или участники.
Рассмотрим функцию колбэка afterSwap:

Очевидно, что приведённая функция предназначена для выполнения чувствительных операций. Однако из-за ненадлежащего контроля доступа она может быть использована злоумышленниками, манипулирующими аргументами (например, key и params), что приведёт к непредвиденному поведению. Например, колбэк afterSwap может работать, исходя из предположения, что своп уже произошёл в PoolManager. После этого он может инициировать действия по записи важной информации о состоянии, такой как текущая цена или собранные комиссии за своп. Однако если afterSwap не ограничивает свои вызовы строго от PoolManager, злоумышленники могут фальсифицировать параметр params, что приведёт к искажению записанных состояний.
Эксплойт и PoC
Для простоты мы будем использовать базовый PoC для иллюстрации данной проблемы контроля доступа. Как правило, функция beforeInitialize хука принимает параметр типа PoolKey, который должен содержать адрес данного хука в поле hooks (поскольку PoolManager будет использовать это поле для определения адреса вызываемого хука).
На скриншоте представлен PoC, демонстрирующий эксплуатацию хука с ненадлежащим контролем доступа, как показано в DiamondHookPoC [3].
При отсутствии ограничений доступа к функции колбэка beforeInitialize злоумышленники могут передавать произвольный poolKey в эту функцию. Хук не проверяет, соответствует ли хук этого poolKey адресу текущего хука.

Хотя важно отметить, что эксплойт в данном сценарии может не привести к финансовым потерям для хука, он тем не менее наглядно демонстрирует, как состояние хука может быть изменено через незащищённые функции обратного вызова.
Как снизить риски
Для обеспечения безопасности взаимодействий Hook-PoolManager как колбэки хука, так и колбэк блокировки должны ограничивать свою доступность исключительно PoolManager.
К счастью, Uniswap v4 предоставляет лучшие практики через BaseHook в своём репозитории v4-periphery[4].
BaseHook предоставляет модификатор poolManagerOnly для ограничения вызовов строго от PoolManager:
/// @dev Only the pool manager may call this function
modifier poolManagerOnly() {
if (msg.sender != address(poolManager)) revert NotPoolManager();
_;
}
Этот модификатор можно эффективно применять для обеспечения надлежащего контроля доступа к чувствительным колбэкам хука и блокировки.
С другой стороны, наличие взаимодействий Hook-Internal требует, чтобы любые значимые функции изменения состояния, вызываемые через колбэк lockAcquired согласно спецификации BaseHook, не могли вызываться произвольно.
Для выполнения этого требования BaseHook предлагает модификатор selfOnly. Этот модификатор ограничивает доступность объявленной функции только самим хуком, запрещая внешним контрактам напрямую вызывать эти чувствительные функции в злонамеренных целях.
/// @dev Only this address may call this function
modifier selfOnly() {
if (msg.sender != address(this)) revert NotSelf();
_;
}
Таким образом, наследуясь от BaseHook, пользовательские хуки могут использовать встроенные модификаторы контроля доступа и колбэки для обеспечения надлежащего контроля доступа.
Некорректная валидация входных данных
BaseHook в v4-periphery[4] предлагает решение для более безопасной логики взаимодействия, которым могут воспользоваться разработчики хуков. Тем не менее мы продолжаем наблюдать случаи ненадлежащего использования, открывающие новые векторы атак в существующих хуках.
По умолчанию хуки позволяют любому пулу регистрироваться через функцию initialize в PoolManager. Однако если хук не валидирует базовые активы регистрирующегося пула, злоумышленники могут зарегистрировать пул с фиктивными токенами, что позволит им повторно войти в хук через функцию transfer токенов.
Эта уязвимость неочевидна, поскольку сам хук может не выполнять вредоносную логику. Однако когда хук вызывает PoolManager, взаимодействия между PoolManager и базовыми активами вредоносного пула могут потенциально передать поток управления злоумышленнику через функцию take в PoolManager.
/// @inheritdoc IPoolManager
function take(Currency currency, address to, uint256 amount) external override
noDelegateCall onlyByLocker {
_accountDelta(currency, amount.toInt128());
reservesOf[currency] -= amount;
currency.transfer(to, amount);
}
По существу, уязвимость возникает из-за ненадлежащей валидации зарегистрированного пула, с которым планируют взаимодействовать пользователи хука. Мы рассмотрим эту уязвимость на конкретном примере и обсудим потенциальные стратегии её смягчения.
Анализ уязвимости
Take Profits Hook[5] — это хук, перечисленный в Awesome Uniswap v4 Hooks:
В этом примере мы создаём хук, позволяющий пользователям размещать позиции 'тейк-профит'. Например, в пуле ETH/DAI, если в данный момент 1 ETH = 1500 DAI, можно разместить ордер тейк-профит типа "продать весь мой ETH, когда 1 ETH = 2000 DAI", который будет исполнен автоматически.
Рассмотрим функцию _handleSwap в этом хуке. Эта функция выполняет своп для исполнения ордеров тейк-профит после получения блокировки.
![Рисунок 3: Функция _handleSwap контракта Take Profits Hook[5]](https://assets.blocksec.com/frontend/blocksec-strapi-online/take_profit_handle_Swap_36133dc1fe.jpg)
Можно заметить, что эта функция не защищена никаким модификатором контроля доступа. Однако строка 250 эффективно ограничивает доступ таким образом, что эта функция может быть вызвана только после получения блокировки от PoolManager. В противном случае poolManager.swap завершится с ошибкой, поскольку оператор не будет самым последним локером. Иными словами, _handleSwap должен вызываться в определённом порядке при условии, что зарегистрированные пулы прошли валидацию. К сожалению, хук не реализует такую валидацию.
Из-за этой некорректной реализации хук уязвим к атаке повторного входа. Эта уязвимость может позволить злоумышленникам принудительно выполнять произвольные свопы, используя средства, депонированные пользователями.
Эксплойт и PoC
В частности, атака может быть осуществлена следующим образом:
- Злоумышленник регистрирует вредоносный пул с фиктивными токенами, указывая Take Profits Hook в качестве хука пула.
- Злоумышленник размещает стоп-профит ордер в вредоносном пуле через хук.
- Злоумышленник выполняет своп в вредоносном пуле, инициируя вызов
fillOrderв колбэкеafterSwapдля исполнения стоп-профит ордера злоумышленника. - Хук вызывает функцию
lockPoolManagerдля запроса блокировки и вызывает функцию_handleSwapв колбэкеlockAcquired. - В функции
_handleSwapпереводы токенов инициируют вредоносную логику в контракте фиктивного токена, который повторно входит в функцию_handleSwap. Это возможно, поскольку_handleSwapявляется внешней функцией без каких-либо ограничений доступности. Поскольку блокировка уже была получена, злоумышленник может принудить хук выполнять произвольные свопы в любом пуле, при условии что хук располагает достаточными базовыми активами. Злоумышленник может затем применить сэндвич-атаку на свопы для извлечения прибыли за счёт других пользователей.
Следующая подробная схема иллюстрирует последовательность атаки.

Как уже упоминалось, сам хук не вызывает вредоносную логику. Единственная ошибка заключается в том, что хук не предотвращает регистрацию ненадёжных токенных пулов в контракте PoolManager. Косвенно вредоносная логика контракта фиктивного токена вызывается через операции переноса токенов, что также является разновидностью ненадёжного внешнего вызова.
Как снизить риски
Существует три возможных подхода для снижения рисков потенциальных атак, связанных с некорректной валидацией входных данных:
-
Надлежащий контроль доступа. Используя строительные блоки из
BaseHook, хук может строго управлять доступностью функций. Это предотвращает вызов чувствительных функций произвольными аккаунтами. -
Блокировка повторного входа. В описанном выше сценарии атаки этот подход, несомненно, предотвращает повторный вход вредоносной логики токена в чувствительные функции. Однако в некоторых случаях дизайн хука требует, чтобы сам хук допускал повторный вход. В частности, когда хуку необходимо выполнить некоторые действия с пулом, он должен разрешать
PoolManagerповторно входить в его колбэки для завершения этих действий. Блокировка повторного входа может нарушить эту предусмотренную функциональность. -
Подход белого списка. Это потребует от привилегированного администратора внесения одобренных пулов в белый список хуков. Администратор обеспечивает, что пулы из белого списка не создают потенциальных рисков. Однако ограничение состоит в том, что пользователи хука смогут выполнять операции только с ограниченным числом одобренных администратором пулов через хук. Несмотря на то что подход белого списка повышает безопасность, он существенно ограничивает функциональность хука.
Найти идеальное решение, балансирующее между безопасностью и удобством использования для хуков, непросто. Хотя мы обсуждаем несколько подходов к снижению рисков, разработчикам необходимо вдумчиво взвешивать компромиссы при проектировании своих хуков. Цель должна состоять в максимально возможном снижении потенциальных рисков при сохранении предусмотренной функциональности. Кроме того, наше обсуждение охватывает лишь уязвимости, которые могут возникать во взаимодействиях, специфичных для функций Uniswap v4. Практические приложения, несомненно, будут более комплексными. Всегда убеждайтесь, что понимаете каждую строку своих контрактов, и оставайтесь в безопасности!
Заключение
В данной статье мы исследуем уязвимости, возникающие в логике взаимодействия хуков, концентрируясь на двух сценариях: ненадлежащий контроль доступа и некорректная валидация входных данных. Мы представляем подробный анализ уязвимостей, иллюстрируем возможные варианты эксплуатации вместе с их PoC и обсуждаем потенциальные стратегии снижения рисков. Мы полагаем, что эти наблюдения могут способствовать безопасной разработке и использованию хуков, а также направлять будущие усилия по обнаружению уязвимостей.
Список литературы
[2] Stop Loss Order
[3] DiamondHookPoC
[4] v4-periphery
[5] Take Profits



