Back to Blog

Смертельная интеграция: уязвимости в хуках из-за рискованных взаимодействий

Code Auditing
November 20, 2023
8 min read

Как отмечалось в нашей предыдущей статье, более 30% проектов в репозитории Awesome Uniswap v4 Hooks[1] содержат уязвимости. Стоит отметить, что уязвимости, о которых мы здесь говорим, связаны именно с взаимодействиями в Uniswap v4. Соответственно, в этой статье мы подробно рассмотрим логику безопасного взаимодействия хуков с двух следующих точек зрения:

  • Некорректный контроль доступа
  • Ненадлежащая проверка входных данных

Для каждой категории мы начнем с анализа уязвимости и продемонстрируем её потенциальную эксплуатацию, предоставив соответствующее доказательство концепции (PoC). Затем последует обсуждение потенциальных стратегий по снижению рисков.

Некорректный контроль доступа

Как правило, взаимодействия, связанные с хуками Uniswap v4, можно классифицировать в зависимости от того, выступает ли хук в роли «блокировщика» (locker), получающего блокировку в PoolManager для выполнения операций в пулах. Два основных сценария взаимодействия требуют надлежащего контроля доступа:

  • Взаимодействие Hook-PoolManager: включает взаимодействие между официальными callback-функциями и PoolManager. Callback-функции включают восемь回调 функций действий с пулом (т.е. initialize, modifyPosition, swap и donate) и callback-функцию блокировки (т.е. lockAcquired).
  • Взаимодействие Hook-Internal: относится к взаимодействиям, происходящим внутри самого контракта хука (действующего как блокировщик).

Взаимодействия типа Hook-PoolManager относительно просты. Здесь хук выступает исключительно в своей роли, принимая восемь callback-функций пула. Логика в хуке не затрагивает связанные пулы, что означает отсутствие потоков денежных средств между хуком и пулами. Параметры, предоставляемые callback-функциями, используются для изменения необходимых хранилищ или в качестве важных параметров функций. Ключевым моментом является возможность манипулирования параметрами callback-функций.

Взаимодействия типа Hook-Internal несколько сложнее. На практике многие прототипы хуков делают больше, чем просто выступают в роли чистых хуков. Некоторые разработчики позволяют хукам предоставлять функции управления средствами для своих пользователей. Эти функции могут быть реализованы не только в контрактах хуков, но мы все равно можем рассматривать их коллективно как хуки в данном контексте. В этих случаях хук принимает средства пользователей и выполняет операции в пуле, такие как управление ликвидностью или свопы. Это означает, что контракт должен получить блокировку от PoolManager, превращая хук в «блокировщик». Uniswap Foundation учли эту ситуацию и интегрировали функцию в свой шаблон хука. В частности, шаблон BaseHook предоставляет функцию lockAcquired в качестве callback-функции блокировки:

    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();
        // если вызов не удался, поднимаем ошибку выше
        /// @solidity memory-safe-assembly
        assembly {
            revert(add(returnData, 32), mload(returnData))
        }
    }

Для выполнения пользовательской логики lockAcquired принимает data в байтах и осуществляет низкоуровневый вызов самой себя, используя эти данные. Данные data зависят от бизнес-логики хука и могут быть изменены пользователями, что потенциально может привести к проблемам безопасности из-за взаимодействий Hook-Internal, инициируемых через lockAcquired. Обратите внимание, что дизайн хуков настолько гибкий, что мы не можем охватить все возможные сценарии в этой ситуации. Наш основной фокус здесь — получение хуком блокировки и последующие внутренние взаимодействия. Погружение в иную бизнес-логику сделало бы ситуацию слишком сложной для данного обсуждения.

В обоих сценариях приоритетной задачей является устранение любых ошибок контроля доступа, которые могут привести к эксплуатации, учитывая, что эти функции имеют четко определенные субъекты взаимодействия. В следующих подразделах мы последовательно рассмотрим каждый сценарий и обсудим необходимый контроль доступа для обеспечения более безопасной логики взаимодействия.

Анализ уязвимости

Контроль доступа служит высокоэффективным и простым решением по безопасности для многих проектов. Если функция предназначена для вызова определенными сущностями, она должна включать контроль доступа. Самый известный пример контроля доступа — это контракт Ownable библиотеки OpenZeppelin, который требует, чтобы привилегированные функции вызывались только владельцем контракта. Очевидно, что два сценария, которые мы обсудили выше, являются подходящими случаями для такого типа контроля.

Взаимодействие Hook-PoolManager: Для безопасного взаимодействия с PoolManager хуки должны обеспечивать необходимый контроль доступа к этим callback-функциям. В частности, эти callbacks должны быть доступны только для вызова со стороны PoolManager, а не любыми другими аккаунтами. Отсутствие таких проверок может оставить эти чувствительные интерфейсы открытыми для потенциальной эксплуатации злоумышленниками.

Помимо восьми callback-функций действий пула, callback-функция блокировки (т.е. lockAcquired), которая выполняет пользовательскую логику после получения блокировки от PoolManager, также должна решать эту проблему.

Взаимодействие Hook-Internal: Функции, участвующие во внутренних взаимодействиях хука, также разработаны для вызова конкретными субъектами. Как мы уже говорили, этот сценарий состоит из двух этапов. Во-первых, функция lockAcquired блокировщика вызывается самим PoolManager, что означает, что функция должна проверять, что msg.sender — это PoolManager. Во-вторых, хук распределяет вызов функции соответствующим образом. Согласно дизайну BaseHook, это реализовано через низкоуровневые вызовы самого хука. Это означает, что такие функции должны иметь модификатор external и ограничивать возможность вызова только адресом самого хука.

Возьмем в качестве примера один из примеров, перечисленных в репозитории Awesome Uniswap v4 Hooks, а именно Stop Loss Order[2]:

Интегрированные непосредственно в пулы Uniswap V4, стоп-лосс ордера размещаются в сети и исполняются через хук afterSwap(). Никакие внешние боты или участники не требуются для обеспечения исполнения.

Давайте рассмотрим его callback-функцию afterSwap:

Рисунок 1: Функция afterSwap для Stop Loss Order[2]
Рисунок 1: Функция afterSwap для Stop Loss Order[2]

Очевидно, что приведенная выше функция предназначена для выполнения чувствительных операций. Однако из-за некорректного контроля доступа она может быть использована злоумышленниками путем манипулирования аргументами (например, key и params), что приведет к неожиданному поведению. Например, callback-функция afterSwap может работать исходя из предположения, что своп уже произошел в PoolManager. После этого она может инициировать действия по записи важной информации о состоянии, такой как текущая цена или собранные торговые комиссии. Однако, если afterSwap не ограничивает свои вызовы строго запросами от PoolManager, злоумышленники могут сфальсифицировать параметр params, что приведет к искажению записанных данных.

Эксплуатация и PoC

Для простоты мы воспользуемся базовым PoC, чтобы проиллюстрировать эту проблему контроля доступа. Как правило, beforeInitialize хука принимает параметр типа PoolKey, который должен содержать адрес этого хука в поле hooks (поскольку PoolManager будет использовать это поле, чтобы определить адрес хука для вызова).

На скриншоте представлен PoC, демонстрирующий использование хука с некорректным контролем доступа, как это было в DiamondHookPoC [3]. При отсутствии ограничений доступа к функции callback-функции beforeInitialize злоумышленники могут передать произвольный poolKey в эту функцию. Хук не проверяет, соответствует ли хук этого poolKey текущему адресу хука.

Рисунок 2: PoolKey.hooks может быть установлен на нулевой адрес
Рисунок 2: PoolKey.hooks может быть установлен на нулевой адрес

Хотя важно отметить, что эксплойт в этом сценарии может не привести к финансовым потерям для хука, он, тем не менее, наглядно показывает, как состояние хука может быть изменено через незащищенные callback-функции.

Как снизить риски

Чтобы обеспечить безопасность взаимодействий Hook-PoolManager, как callback-функции хука, так и callback-функция блокировки должны ограничивать свою доступность только для PoolManager.

К счастью, Uniswap v4 предоставляет лучшие практики через BaseHook в своем репозитории v4-periphery[4]. BaseHook предоставляет модификатор poolManagerOnly для ограничения вызовов строго со стороны PoolManager:

    /// @dev Только pool manager может вызывать эту функцию
    modifier poolManagerOnly() {
        if (msg.sender != address(poolManager)) revert NotPoolManager();
        _;
    }

Этот модификатор можно эффективно использовать для обеспечения надлежащего контроля доступа к чувствительным callback-функциям хука и блокировки.

С другой стороны, наличие взаимодействий Hook-Internal требует, чтобы любые важные функции, изменяющие состояние и вызываемые через callback lockAcquired (как определено в BaseHook), не могли быть вызваны произвольно.

Для выполнения этого требования BaseHook предлагает модификатор selfOnly. Этот модификатор ограничивает доступность объявленной функции только самим хуком, запрещая внешним контрактам напрямую вызывать эти чувствительные функции в злонамеренных целях.

    /// @dev Только этот адрес может вызывать эту функцию
    modifier selfOnly() {
        if (msg.sender != address(this)) revert NotSelf();
        _;
    }

Таким образом, наследуясь от BaseHook, пользовательские хуки могут использовать эти встроенные модификаторы контроля доступа и callback-функции для принудительного обеспечения надлежащей безопасности.

Ненадлежащая проверка входных данных

BaseHook в v4-periphery[4] предлагает решение для более безопасной логики взаимодействия, которым могут воспользоваться разработчики хуков. Однако мы продолжаем наблюдать случаи неправильного использования, которые открывают новые возможности для векторов атак в существующих хуках.

По умолчанию хуки разрешают любому пулу регистрироваться через функцию initialize в PoolManager. Однако если хук не проверяет базовые активы в регистрируемом пуле, злоумышленники могут зарегистрировать пул, содержащий поддельные токены, что позволит им повторно войти (reenter) в хук через функцию 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:

В этом примере мы создаем хук, который позволяет пользователям размещать позиции «take-profit». Например, в пуле ETH/DAI, если в текущий момент 1 ETH = 1500 DAI, вы можете разместить ордер take-profit как «продать весь мой ETH, когда 1 ETH = 2000 DAI», который будет исполнен автоматически.

Давайте взглянем на функцию _handleSwap в этом хуке. Эта функция выполняет своп для заполнения ордеров take-profit после получения блокировки.

Рисунок 3: Функция _handleSwap в Take Profits Hook[5]
Рисунок 3: Функция _handleSwap в Take Profits Hook[5]

Вы можете заметить, что эта функция не защищена никаким модификатором контроля доступа. Однако строка 250 эффективно ограничивает доступ таким образом, что эту функцию можно вызвать только после получения блокировки от PoolManager. В противном случае poolManager.swap завершился бы с ошибкой, так как оператор не был бы последним «блокировщиком». Другими словами, _handleSwap должен вызываться в определенном порядке, при условии, что зарегистрированные пулы проверены. К сожалению, хук не реализует такую проверку.

Из-за этой ошибочной реализации хук подвержен атаке через повторный вход (reentrancy). Эта уязвимость может позволить злоумышленникам совершать произвольные свопы, используя средства, депонированные пользователями.

Эксплуатация и PoC

Атака может быть осуществлена следующими шагами:

  1. Злоумышленник регистрирует вредоносный пул с поддельными токенами, указывая Take Profits Hook в качестве хука этого пула.
  2. Злоумышленник размещает ордер stop-profit в вредоносном пуле через этот хук.
  3. Злоумышленник совершает своп в вредоносном пуле, инициируя fillOrder в callback-функции afterSwap для заполнения ордера злоумышленника.
  4. Хук вызывает функцию lock контракта PoolManager для запроса блокировки и вызывает функцию _handleSwap в callback-функции lockAcquired.
  5. В функции _handleSwap переводы токенов инициируют вредоносную логику в контракте поддельного токена, который повторно вызывает функцию _handleSwap. Это возможно, так как _handleSwap является внешней функцией без каких-либо ограничений доступа. Поскольку блокировка уже получена, злоумышленник может заставить хук выполнять произвольные свопы в любом пуле, при условии, что хук содержит достаточно базовых активов. Затем злоумышленник может провести «сэндвич-атаку» свопов, чтобы получить прибыль за счет других пользователей.

На следующей схеме подробно показан ход атаки.

Рисунок 4: Ход атаки
Рисунок 4: Ход атаки

Как упоминалось ранее, сам хук не вызывает вредоносную логику. Единственная ошибка заключается в том, что хук не предотвращает регистрацию пулов с ненадежными токенами в контракте PoolManager. Косвенно вредоносная логика в контракте поддельного токена вызывается через операции перевода токенов, что также является разновидностью недоверенного внешнего вызова.

Как снизить риски

Существует три возможных подхода к снижению потенциальных атак, вызванных ненадлежащей проверкой входных данных:

  • Надлежащий контроль доступа. Используя строительные блоки из BaseHook, хук может строго управлять доступностью функций. Это предотвращает вызов чувствительных функций произвольными аккаунтами.

  • Блокировка повторного входа (Reentrancy Lock). В вышеуказанном сценарии атаки этот подход может несомненно предотвратить повторный вход вредоносной логики токена в чувствительные функции. Однако в некоторых случаях дизайн хука требует, чтобы сам хук поддерживал повторный вход. В частности, когда хуку необходимо выполнить некоторые действия в пуле, он должен разрешить PoolManager повторно входить в свои callback-функции для завершения этих действий. Модификатор реентрантности может нарушить эту запланированную функциональность.

  • Подход с использованием белого списка (Whitelisting). Это потребовало бы от привилегированного администратора добавления одобренных пулов в белый список хуков. Администратор гарантирует, что одобренные пулы не представляют потенциальных рисков. Однако ограничение заключается в том, что пользователи хука смогут выполнять операции только с ограниченным количеством одобренных администратором пулов через этот хук.

Хотя подход с белым списком повышает безопасность, он сильно ограничивает функциональность хука. Найти идеальное решение, балансирующее между безопасностью и удобством использования для хуков, сложно. Хотя мы讨论 (обсудили) несколько подходов к снижению рисков, разработчикам необходимо тщательно обдумать компромиссы в дизайне своих хуков. Цель должна состоять в том, чтобы максимально снизить потенциальные риски, сохраняя при этом запланированную функциональность. Кроме того, наше обсуждение охватывает только уязвимости, которые могут скрываться во взаимодействиях, специфичных для функций Uniswap v4. Практические применения, несомненно, будут более комплексными. Всегда проверяйте каждую строку своих контрактов и оставайтесь SAFU!

Заключение

В этой статье мы рассмотрели уязвимости, возникающие в логике взаимодействия хуков, сосредоточившись на двух сценариях: некорректный контроль доступа и ненадлежащая проверка входных данных. Мы представили подробный анализ уязвимостей, проиллюстрировали потенциальные способы эксплуатации вместе с их PoC и обсудили потенциальные стратегии снижения рисков. Мы верим, что эти идеи могут способствовать безопасной разработке и использованию хуков, а также направлять будущие усилия в области обнаружения уязвимостей.

Список литературы

[1] Awesome Uniswap v4 Hooks

[2] Stop Loss Order

[3] DiamondHookPoC

[4] v4-periphery

[5] Take Profits

Читайте другие статьи этой серии

Best Security Auditor for Web3

Validate design, code, and business logic before launch. Aligned with the highest industry security standards.

BlockSec Audit