Ядовитый пул Uniswap: $131 888 комиссий за 29 часов
BlockbeatsОригинальное название: «Котировка 0%, исполнение 12,8%: разбор ловушки динамической комиссии „ядовитого пула“ Uniswap v4»
Автор оригинала: Ван Цзыхао, BitsLab
С 22 по 23 августа 2026 года динамический пул USDC/WBNB на Uniswap v4 в сети BNB Chain показывал в котировках агрегаторов комиссию 0%, но при фактическом исполнении с пользователей взималась комиссия LP в размере 12,8%. Примерно за 29 часов через этот пул прошла 21 086 сделок, а совокупные комиссии составили 131 888 долларов, после чего регистрация пула была отменена. Все пострадавшие сделки были успешными: контракт не был взломан, выходная сумма превышала установленный пользователем минимум, никаких аномальных сигналов не было — разница тихо превратилась в комиссию.
В этой статье мы рассмотрим одну из таких сделок в качестве примера:
(0x7615dec2ce80506ec461a47739eb8533ac2bc7c605ca56c255964481b76363d0: пользователь предоставил 5 926,90 USDT, а получил на 757,43 USDT меньше ожидаемого) и разберём, как этому пулу удалось добиться «котировки 0%, исполнения 12,8%»: трёхуровневая логика функции комиссии, обойдённая защита от проскальзывания и белый список, не пускающий других поставщиков ликвидности. Все выводы основаны на ончейн-данных и локальной контролируемой проверке.
01 Цепочка от котировки до исполнения
После того как пользователь инициирует обмен через интерфейс агрегатора, вся цепочка выглядит следующим образом:

В этой цепочке есть два фоновых факта, на которых строятся все последующие уровни проверки.
Сначала посмотрим на сторону v4: она позволяет хуку переопределять комиссию для каждой сделки. PoolKey.fee = 0x800000 объявляет пул с динамической комиссией; хук в третьем возвращаемом значении beforeSwap передаёт 0x400000 | fee. Код в v4-core, принимающий это возвращаемое значение (Pool.sol:303-305):

isOverride() проверяет флаг переопределения, removeOverrideFlagAndValidate() снимает 0x400000 и проверяет, что комиссия не превышает MAX_LP_FEE = 1 000 000 (100%), затем выполняет текущий своп с этой комиссией и записывает событие Swap. То есть хук возвращает то, что платит пользователь, — при условии, что выходная сумма всё ещё выше amountOutMin. Это официальная функция, и корректировка комиссии в зависимости от волатильности или ликвидности является законным применением.
Теперь посмотрим на сам хук: он объявляет только два разрешения. v4 кодирует разрешения в младших 14 битах адреса контракта (Hooks.sol:29-47), проверяет их при развёртывании, и впоследствии они не могут быть изменены. Определения флагов на стороне v4-core и декодирование адреса:

Ончейн-вызов getHookPermissions() возвращает значение, совпадающее с битовой маской: только эти два разрешения активны.
У хука нет исходного кода. В этой статье мы декомпилировали полный текст функции комиссии из байт-кода — ниже представлен поведенческий разбор этой логики, которая уже работала в сети:

Далее рассмотрим по уровням. Первые два уровня отвечают на вопрос «является ли это симуляцией», третий определяет, сколько заплатит реальная сделка.
02 Трёхуровневая логика функции комиссии
Проверка 1: различение вызывающего
После вызова функции комиссии сначала сравнивается tx.origin: если он равен любому из трёх константных адресов, сразу возвращается резервное значение. В настоящее время резервное значение настроено на 0%.
Эти три адреса выбраны не случайно. Если eth_call не указывает from, origin транзакции попадает на нулевой адрес — агрегаторы при массовых котировках обычно не указывают его. Проверка в локальном форке:

Первый уровень распознаёт только эти три адреса. Посмотрим, как хук может различать вызовы после указания обычного адреса.
Проверка 2: проверка газа
Если заменить tx.origin на обычный адрес и повторить попытку, возвращается всё равно 0% — первый уровень обойдён, но комиссия не изменилась. Это означает, что первый уровень не единственный; находим второй уровень: gasleft() * 10100 / 10000 >= gate.
Агрегаторы обычно задают очень высокий лимит газа при котировках, чтобы кандидатные маршруты не проваливались в симуляции из-за нехватки газа; к моменту реальной сделки газ уже израсходован роутером, авторизацией и предыдущими переходами. Один и тот же код, три значения газа:

Граница на практике находится между 16,85M и 16,9M газа. Разница между 30M и 2,59M — это и есть разница между «котировкой» и «исполнением».
Проверка 3: псевдослучайный выбор для каждой сделки
Реальные сделки, прошедшие первые два уровня, попадают на третий: хэш отпечатка окружения блока берётся по модулю 10 000, и в зависимости от полученного интервала возвращается соответствующий тариф комиссии. Текущая конфигурация — три тарифа 8% / 10% / 10%, при промахе возвращается 0%.
В этом уровне есть две примечательные особенности. В отпечаток подмешиваются три поля calldata — calldata котировки агрегатора и calldata реального маршрута изначально различаются, поэтому и отпечатки различаются. Кроме того, псевдослучайный выбор для каждой сделки придаёт комиссии распределение: по нескольким сделкам трудно заметить закономерность, и простое перечисление правил не помогает.
Во всех 21 086 ончейн-событиях встречается 19 исторических тарифов (6,8%–28%), что говорит о том, что тарифы постоянно перенастраивались функцией администратора по мере необходимости; на момент исполнения примера действовал тариф 12,8%.
Общая картина трёх уровней

Теперь можно дать определение «ядовитого пула» — это пул v4, одновременно удовлетворяющий трём условиям: PoolKey.fee несёт флаг динамической комиссии; решение о комиссии читает сигналы среды исполнения (gas, origin, окружение блока), а не публичное состояние рынка; комиссия при симуляции котировки и при ончейн-исполнении систематически расходится, и выгоду получает развернувший пул. Граница проходит по тому, что читает комиссия: если она возвращает одинаковую комиссию любому вызывающему — это программируемый рыночный дизайн; если она возвращает разную комиссию в зависимости от того, «кто спрашивает цену», — это введение в заблуждение системы маршрутизации.
На этом механизм расхождения комиссии полностью описан. Но почему сделка всё равно оказывается успешной после списания комиссии — это следующий вопрос.
03 Почему защита от проскальзывания не сработала
Сначала посмотрим на полный путь и суммы этой сделки:

Событие Swap этого перехода записывает действующую комиссию в поле fee: 128000. В v4 единица комиссии равна 1 000 000 за 100%, поэтому 128000 — это 12,8%. Потерю по этой сделке можно оценить с двух сторон: со стороны пула комиссия взимается с входящего WBNB; со стороны всего маршрута разница между входящими и исходящими стейблкоинами составляет 757,43 USDT, то есть потеря 12,7794%, включая комиссии предыдущих переходов и отклонение от привязки. Разница между двумя цифрами всего 0,02 процентного пункта, что подтверждает: основной источник потери — комиссия LP этого пула.
amountOutMin проверяет только нижнюю границу конечного выхода и не смотрит, сколько комиссии было взято на каждом промежуточном переходе:

Комиссия 12,8% целиком укладывается в буфер 20,32%, выход всё ещё выше нижней границы — сделка успешна, транзакция не откатывается. Обычное значение по умолчанию для маршрутов основных стейблкоинов — 0,1%–1%, а этот маршрут давал более чем в 20 раз больше пространства. Пользователь думал, что широкий буфер — это «надёжнее», а на деле доверял каждый контракт на пути самодисциплине контрагента. Изобразим этот расчёт на графике:

04 Действия развернувшего «ядовитый пул»
Белый список: не пускать других поставщиков ликвидности
Логика белого списка находится в beforeAddLiquidity, результат декомпиляции:

Для добавления ликвидности нужно пройти три барьера: сначала проверка вызова PoolManager, присущая любому хуку v4, затем обязательное прохождение через официальный PositionManager, и наконец сверка белого списка держателей позиций. Адреса, не входящие в список, отклоняются контрактом, другие поставщики ликвидности не могут войти, доход от комиссий не размывается и полностью достаётся развернувшему пул. Сам список ведётся функцией администратора (селектор 0xc4452e52) по адресам.
Тариф 0% и накрутка объёмов
Если рассмотреть все 21 086 сделок вместе, распределение показывает чёткую картину: тариф 0% — 6 946 сделок, объём 12 947 751 доллар, в среднем 1 864 доллара на сделку; тарифы с комиссией — 14 140 сделок, объём 1 120 106 долларов, в среднем 79 долларов на сделку. Крупные сделки сосредоточены на тарифе 0%, комиссии собираются с мелких сделок. Самое разумное объяснение крупных сделок с 0% — развернувший пул сам совершал фиктивные сделки с высоким газом: высокий газ попадает в ту же проверку, что и котировки агрегатора, поэтому комиссия для собственных сделок близка к нулю. Накрученный объём выводил пул в рейтинги агрегаторов (снимок показывает около 10,82 млн долларов за 24 часа, 16 671 сделка), и в глазах агрегатора это выглядело как пул с хорошей глубиной и низкой комиссией.
Перенастройка тарифов по мере необходимости
В проверке 3 упоминались 19 исторических тарифов; они берутся из декомпилированной функции конфигурации тарифов (селектор 0x4d909a45, публичная сигнатура не зарегистрирована):

Четыре тарифа и пороги упаковываются вместе и записываются в слот хранения keccak(poolId, 2). Ончейн-чтение этого слота даёт 0x0138800186a00186a0, что по сегментам соответствует формату упаковки функции. Тарифы здесь — параметры, которые можно менять в любой момент, а не фиксированные при развёртывании.
Жизненный цикл
Пул был создан 22 августа в 06:57, первая сделка прошла в 07:13, в 21:39 — сделка-пример из этой статьи; последняя сделка — 23 августа в 12:25, после чего пул был снят с регистрации. Весь активный период составил около 29 часов, общий объём — 14 067 857 долларов, доход от комиссий — 131 888 долларов (без вычета затрат развернувшего). При повторной проверке флаг регистрации снова находится в активном состоянии — пул всё ещё существует и может быть снова запущен в любой момент.
05 Полный путь
Соединим всю цепочку атаки:

Вся причинно-следственная связь цепи показана на рисунке выше, и все три условия необходимы: симуляция попадает на первые два уровня, реальная сделка — на третий, буфер больше комиссии. Если хотя бы одно не выполняется, эта схема не сработает.
06 Исправления и рекомендации
Первое — способ котировки агрегаторов. Корневая причина в том, что симуляция и исполнение идут разными путями: котировка использует идеализированный запрос на уровне пула с нулевым адресом и высоким газом; реальная сделка приходит с реальной личностью и уже израсходованным газом. Хук как раз умеет считывать эти различия, из-за чего комиссия расходится. Решение — приблизить симуляцию к исполнению: использовать реальный calldata роутера, реальные from/to и газ, близкий к ончейн-сделке; после исполнения декодировать событие Swap и сверять комиссию, при расхождении с котировкой понижать вес или отключать маршрут.
Второе — настройки проскальзывания у пользователей. Корневая причина в слишком свободном min_out: буфер 20,32% полностью поглощает комиссию 12,8%, и сделка проходит как обычно. Для маршрутов основных стейблкоинов 0,1%–1% достаточно для повседневного использования; перед отправкой сделки стоит взглянуть на эту цифру; кошельки и интерфейсы, уменьшив значение по умолчанию, могут отсечь большую часть рисков для пользователей.
Третье — допуск маршрутов. Корневая причина в том, что любой пул с динамической комиссией может напрямую участвовать в конкуренции котировок: хук без открытого исходного кода и без аудита точно так же может попасть в рекомендуемые маршруты за счёт накрученного объёма. Для пулов с динамической комиссией без доверенного профиля по умолчанию не включать их в маршрутизацию или значительно понижать вес — это самый надёжный подход.
Данные и пояснения
· Пример сделки:
https://bscscan.com/tx/0x7615dec2ce80506ec461a47739eb8533ac2bc7c605ca56c255964481b76363d0 (блок 117497524)
· Транзакция инициализации пула:
https://bscscan.com/tx/0x8fa72ef72d77b61f715dc9ce90548249ab045c6d78716078cd5ab425bbe69a05
· PoolManager:
0x28e2ea090877bf75740558f6bfb36a5ffee9e9df
· Pool ID:
0x36e5540e9dedc02229fe8a82aa5b10c0bf07d1fa74e4f2ffe0efd00fa1a36aea
· Хук:
0xd111b3ddd92e627f1864520c770e913ec04e0880 (без исходного кода, псевдокод получен в результате независимой декомпиляции, выполненной в рамках этой статьи; администратор 0x08b03e1a5444d469f4dc954e74d3f662c94a6b13)
· Сделки и комиссии: все 21 086 событий Swap этого пула посчитаны по отдельности (eth_getLogs), WBNB пересчитан по цене исполнения той же сделки; доход указан как валовой, без вычета затрат развернувшего
Данный контент предназначен исключительно для информационных и образовательных целей и не является инвестиционным советом, связанным с BTCC. BTCC прилагает все усилия, но не может гарантировать правдивость, точность или оригинальность вышеприведенного контента.