Сжигание токенов, вызванное продажей, почти полностью опустошило резерв TENDRIL в пуле ликвидности BNB Chain 4 ноября 2026 года, подготовив почву для свопа, в результате которого из пула вывели почти 4,42 млн USDT. Чистый отток средств из пула в рамках транзакции был меньше — 555,670.538752928908475139 USDT, поскольку ранее в ходе той же транзакции в пул поступили значительные суммы USDT.
Главные выводы
Контракт TENDRIL дважды списал из пула сумму, вдвое превышающую первоначальную сумму продажи.
До получения оставшихся токенов продавца в пуле почти не было TENDRIL.
Отказ от владения не лишил контроллер права сжигать токены.
Итоговый объём USDT, полученный при обмене, и чистый отток средств в рамках транзакции — это разные величины.
Как продажа активировала сжигание токенов
При продаже контроллер был вызван до того, как оставшиеся у продавца токены TENDRIL поступили в пул. В 16:02:45 UTC вызов router-контракта transferFrom инициировал продажу 39,053,180,124.060957132414461269 TENDRIL, активировав налог токена и логику контроллера.
При переводах в основной пул взимался налог с продажи в размере 30%, за исключением переводов с одного из двух настроенных адресов: _poolControl и _markControl. Исполнявший транзакцию контракт не подпадал ни под одно из исключений. Поэтому токен сначала отправил 11,715,954,037.218287139724338380 TENDRIL на _poolControl.
Затем токен вызвал функцию sellToken контроллера, передав ей исходную сумму продажи, а не только сумму налога. Этот вызов завершился до того, как токен зачислил в пул оставшиеся 70%.
Историческая трассировка вызовов показывает последовательность: transferFrom, sellToken, controlburn и sync, а затем итоговый обмен. Трассировка опкодов контроллера подтверждает, что он умножил исходную сумму продажи на два, получив точную сумму, впоследствии изъятую из пула.
Функция controlburn изъяла токены пула, не уменьшая общее предложение
Функция controlburn перевела TENDRIL с баланса пула на мёртвый адрес. Она не уменьшила учтённое общее предложение токенов и не сожгла токены поставщиков ликвидности.
Для проверки разрешения требовалось, чтобы адрес вызывающего совпадал с _poolControl. После авторизации функция использовала _basicTransfer, чтобы списать токены с баланса пула и зачислить их на мёртвый адрес. Разрешение пула на расходование токенов или вывод ликвидности не использовались: записи о собственном балансе контролировал сам контракт токена.
В журнале 28 зафиксировано изъятие 78,106,360,248.121914264828922538 TENDRIL. В пуле осталось 6,678 минимальных единиц. Поскольку у TENDRIL 18 десятичных знаков, остаток составлял всего 0.000000000000006678 TENDRIL.
При этом сжигание не привело к одновременному изъятию USDT. Почти весь баланс TENDRIL исчез, тогда как USDT в пуле оставались доступными для следующего обмена.
Отказ от владения не заблокировал этот механизм. На блоке 131653426, непосредственно перед инцидентом, вызов owner() возвращал нулевой адрес. Однако функция controlburn проверяла настроенный адрес контроллера, а не owner(), поэтому это отдельное разрешение оставалось действующим. Функция не была общедоступной: произвольный вызывающий не прошёл бы проверку авторизации.
Сброс резервов позволил почти полностью вывести USDT
Сразу после сжигания контроллер вызвал sync(), зафиксировав истощённый баланс токенов до поступления в пул оставшихся токенов от продажи. В журнале 29 резервы были установлены на уровне 4,421,023.357469283419586501 USDT и 0.000000000000006678 TENDRIL.
Лишь после этого в пул поступили 27,337,226,086.842669992690122889 TENDRIL. При итоговом обмене из пула было переведено 4,421,023.357469283419586499 USDT, оставив в нём 0.000000000000000002 USDT.
Масштаб этого объёма объясняется арифметикой постоянного произведения. Если для примера не учитывать комиссии и округление, объём USDT на выходе рассчитывается так: резерв USDT умножается на объём поступивших токенов, а результат делится на сумму резерва токенов и объёма поступивших токенов. При почти нулевом учтённом резерве токенов объём поступивших токенов преобладает в знаменателе, и объём USDT на выходе приближается к полному резерву USDT.
Однако этот итоговый вывод не равен чистой сумме, изъятой из пула за всю транзакцию. Общий отток USDT составил 5,117,517.824288279504509510, а приток — 4,561,847.285535350596034371. Разница составляет 555,670.538752928908475139 USDT чистого оттока.
Код контракта и исторические трассировки подтверждают последовательность событий
Сжигание токенов, вызванное продажей, было реконструировано с использованием проверенного в обозревателе исходного кода, исторических вызовов, журналов событий и трассировки опкодов. Повторная компиляция токена с помощью Solidity v0.8.24+commit.e11b9ed9 при отключённой оптимизации совпала с исполняемыми байтами рантайма на блоке 131653426, если не учитывать метаданные компилятора.
Декодированный аргумент сжигания точно соответствовал переводу из пула на мёртвый адрес. Целочисленные вычисления также подтвердили размеры налога, оставшихся после продажи токенов и резервов. Общее предложение не изменилось между блоками непосредственно до и после транзакции.
Успешная транзакция, связанная с инцидентом, появилась в блоке 131653427. Проверки квитанции через две публичные конечные точки RPC BNB Chain совпали, а потоки USDT на уровне адресов при повторной проверке 6 ноября 2026 года сошлись точно.
Статья подготовлена при содействии искусственного интеллекта и проверена редакционной командой.
