8 января 2026 года протокол Truebit в сети Ethereum подвергся атаке, в результате которой было потеряно около 26 миллионов долларов [1]. Первопричиной стало целочисленное переполнение (integer overflow) в логике определения цены покупки токенов TRU. Поскольку контракт был скомпилирован с использованием Solidity v0.6.10, где проверки на переполнение не выполняются по умолчанию, большое промежуточное значение при расчете стоимости покупки «зациклилось» (wrapped around) до гораздо меньшего числа. В результате злоумышленник смог приобрести огромное количество TRU за небольшое количество ETH или вовсе бесплатно, а затем немедленно продать полученные TRU обратно контракту за ETH по выгодному курсу, истощив резервы протокола.
0x0 Справочная информация
Truebit предоставляет вычислительные услуги для Ethereum посредством внесетевых вычислений и интерактивной верификации [2]. В рамках протокола токены TRU выступают в качестве основного экономического инструмента для координации стимулов, включая стейкинг и выплаты за выполнение задач.
Протокол предоставляет две публичные функции для покупки и погашения TRU:
-
buyTRU()выполняет покупку TRU. Необходимая стоимость в ETH рассчитывается внутренней функцией ценообразования, которая также используетсяgetPurchasePrice(), поэтомуgetPurchasePrice()отражает точную логику ценообразования в сети, применяемую во время выполнения покупки. -
sellTRU()выполняет продажу (погашение) TRU. Ожидаемую выплату в ETH можно узнать черезgetRetirePrice().
Ключевым аспектом дизайна является асимметрия ценообразования:
- Для покупок используется выпуклая кривая связывания (маржинальная цена растет по мере увеличения предложения).
- Для продаж используется правило линейного погашения (пропорционально резервам).
Поскольку исходный код контракта реализации не является публичным, следующий анализ основан на декомпилированном байт-коде.
Логика покупки
Функция buyTRU() (и функция getPurchasePrice()) делегирует ценообразование приватной функции _getPurchasePrice(), которая вычисляет объем ETH, необходимый для покупки amount токенов TRU.
function buyTRU(uint256 amount) public payable {
require(msg.data.length - 4 >= 32);
v0 = _getPurchasePrice(amount); // получить цену покупки
require(msg.value == v0, Error('Платеж в ETH не соответствует заказу TRU'));
v1 = 0x18ef(100 - _setParameters, msg.value);
v2 = _SafeDiv(100, v1);
v3 = _SafeAdd(v2, _reserve);
_reserve = v3;
require(bool(stor_97_0_19.code.size));
v4 = stor_97_0_19.mint(msg.sender, amount).gas(msg.gas);
require(bool(v4), 0, RETURNDATASIZE()); // проверка статуса вызова, распространение ошибки при сбое
return msg.value;
}
function getPurchasePrice(uint256 amount) public nonPayable {
require(msg.data.length - 4 >= 32);
v0 = _getPurchasePrice(amount); // получить цену покупки
return v0;
}
function _getPurchasePrice(uint256 amount) private {
require(bool(stor_97_0_19.code.size));
v0, /* uint256 */ v1 = stor_97_0_19.totalSupply().gas(msg.gas);
require(bool(v0), 0, RETURNDATASIZE());
require(RETURNDATASIZE() >= 32);
v2 = 0x18ef(v1, v1)
v3 = 0x18ef(_setParameters, v2);
v4 = 0x18ef(v1, v1);
v5 = 0x18ef(100, v4);
v6 = _SafeSub(v3, v5);// знаменатель = 100 * totalSupply**2 - _setParameters * totalSupply**2
v7 = 0x18ef(amount, _reserve);
v8 = 0x18ef(v1, v7);
v9 = 0x18ef(200, v8);// числитель_2 = 200 * totalSupply * amount * _reserve
v10 = 0x18ef(amount, _reserve);
v11 = 0x18ef(amount, v10);
v12 = 0x18ef(100, v11);// числитель_1 = 100 * amount**2 * _reserve
v13 = _SafeDiv(v6, v12 + v9); // ценаПокупки = (числитель_1 + числитель_2) / знаменатель
return v13;
}
Исходя из декомпилированной логики, цена покупки может быть выражена функцией в стиле кривой связывания:
Где:
- amount: количество TRU к покупке
- reserve (_reserve): резервы эфира контракта
- totalSupply: общее предложение TRU
- θ (_setParameters): коэффициент, зафиксированный на уровне 75
Эта кривая призвана сделать крупные покупки значительно дороже (выпуклый рост стоимости), что препятствует спекуляциям и снижает риск манипуляции на стороне покупки.
Логика продажи
Функция sellTRU() (и функция getRetirePrice()) использует приватную функцию _getRetirePrice() для расчета выплаты в ETH при погашении TRU.
function sellTRU(uint256 amount) public nonPayable {
require(msg.data.length - 4 >= 32);
require(bool(stor_97_0_19.code.size));
v0, /* uint256 */ v1 = stor_97_0_19.allowance(msg.sender, address(this)).gas(msg.gas);
require(bool(v0), 0, RETURNDATASIZE());
require(RETURNDATASIZE() >= 32);
require(v1 >= amount, Error('Недостаточный allowance для TRU'));
v2 = _getRetirePrice(amount); // получить цену погашения
v3 = _SafeSub(v2, _reserve);
_reserve = v3;
require(bool(stor_97_0_19.code.size));
v4, /* uint256 */ v5 = stor_97_0_19.transferFrom(msg.sender, address(this), amount).gas(msg.gas);
require(bool(v4), 0, RETURNDATASIZE());
require(RETURNDATASIZE() >= 32);
require(bool(stor_97_0_19.code.size));
v6 = stor_97_0_19.burn(amount).gas(msg.gas);
require(bool(v6), 0, RETURNDATASIZE());
v7 = msg.sender.call().value(v2).gas(!v2 * 2300);
require(bool(v7), 0, RETURNDATASIZE());
return v2;
}
function getRetirePrice(uint256 amount) public nonPayable {
require(msg.data.length - 4 >= 32);
v0 = _getRetirePrice(amount); // получить цену погашения
return v0;
}
function _getRetirePrice(uint256 amount) private {
require(bool(stor_97_0_19.code.size));
v0, /* uint256 */ v1 = stor_97_0_19.totalSupply().gas(msg.gas);
require(bool(v0), 0, RETURNDATASIZE());
require(RETURNDATASIZE() >= 32);
v1 = v2.length;
v3 = v2.data;
v4 = 0x18ef(_reserve, amount);// числитель = _reserve * amount
if (v1 > 0) {
assert(v1);
return v4 / v1;// ценаПогашения = числитель / totalSupply
} else {
// ...
}
Правило погашения является линейным:
Цена погашения пропорциональна доле общего предложения, которая погашается (т.е. amount / totalSupply), умноженной на reserve.
Эта осознанная асимметрия создает широкий спред: покупка является выпуклой (дорого при крупных объемах), в то время как продажа — линейной (погашается лишь пропорциональная доля резервов). В обычных условиях такой спред делает мгновенный арбитраж «покупка→продажа» непривлекательным.
0x1 Анализ уязвимости
Несмотря на задуманный дизайн, при котором крупные покупки являются дорогими, функция _getPurchasePrice() содержит целочисленное переполнение в своих арифметических операциях. Поскольку контракт был скомпилирован с использованием Solidity 0.6.10, арифметические операции над uint256 могут молчаливо переполняться и зацикливаться по модулю 2^256, если они не защищены явно (например, через SafeMath).
function _getPurchasePrice(uint256 amount) private {
// ... (код аналогичен приведенному выше)
v13 = _SafeDiv(v6, v12 + v9); // ценаПокупки = (числитель_1 + числитель_2) / знаменатель
return v13;
}
В _getPurchasePrice() достаточно большое значение amount вызывает переполнение во время сложения двух больших членов в числителе (v12 + v9 в декомпилированном фрагменте). Когда происходит это переполнение, числитель «сворачивается» до малого значения, что приводит к тому, что финальное деление возвращает искусственно заниженную цену покупки, потенциально вплоть до нуля.
Критически важно, что переполнение затрагивает только сторону покупки. Функция продажи остается линейной и ведет себя как задумывалось, поэтому злоумышленник может:
- купить большое количество TRU по заниженной (или нулевой) цене, а затем
- погасить его за ETH через
sellTRU()по гораздо более высокому эффективному курсу.
0x2 Анализ атаки
Злоумышленник выполнил несколько раундов арбитража в рамках одной транзакции [3], повторяя цепочку: getPurchasePrice() -> buyTRU() -> sellTRU().
Первый раунд: бесплатная покупка, затем продажа с прибылью
Предоставив тщательно подобранный объем покупки (240,442,509.453... TRU), злоумышленник вызвал переполнение в _getPurchasePrice(), снизив расчетную цену покупки до 0 ETH и позволив получить ~240 миллионов TRU бесплатно.
Приведенный ниже код на Python иллюстрирует, что числитель превышает 2^256, и после «зацикливания» вычисленная цена покупки становится крошечной дробной величиной, которая при приведении к целому числу становится нулевой.
>>> _reserve = 0x1ceec1aef842e54d9ee
>>> totalSupply = 161753242367424992669183203
>>> amount = 240442509453545333947284131
>>> numerator = int(100 * amount * _reserve * (amount + 2 * totalSupply))
>>> numerator > 2**256
True
>>> denominator = (100 - 75) * totalSupply**2
>>> purchasePrice = (numerator - 2**256) / denominator
>>> purchasePrice
0.00025775798757211426
>>> int(purchasePrice)
0
Затем злоумышленник немедленно вызвал sellTRU(), погасив TRU за 5,105 ETH из резервов протокола.
Последующие раунды: покупки по низкой цене, затем продажа с прибылью
Злоумышленник повторял цикл несколько раз. Более поздние покупки не всегда были строго бесплатными, но переполнение продолжало удерживать цены покупки значительно ниже соответствующих доходов от продажи.
В целом, злоумышленник вывел из резервов Truebit 8,535 ETH.
0x3 Итоги
Инцидент был вызван неконтролируемым целочисленным переполнением в логике ценообразования Truebit на стороне покупки. Хотя асимметричная модель ценообразования (покупка/продажа) была призвана противостоять спекуляциям, компиляция с использованием старой версии Solidity (до 0.8) без систематической защиты от переполнения подорвала этот дизайн и позволила истощить резервы.
Для любого производственного контракта, использующего версии Solidity ниже 0.8, разработчикам следует:
- Применять безопасную арифметику (например,
SafeMathили эквивалентные проверки) для каждой соответствующей операции, или, - предпочтительно, перейти на Solidity 0.8+, чтобы воспользоваться встроенными проверками на переполнение.
Ссылки
[1] https://x.com/Truebitprotocol/status/2009328032813850839
[2] https://docs.truebit.io/v1docs
[3] Транзакция атаки
О компании BlockSec
BlockSec — поставщик услуг в области безопасности блокчейнов и крипто-комплаенса полного цикла. Мы создаем продукты и услуги, которые помогают клиентам проводить аудит кода (включая смарт-контракты, блокчейны и кошельки), предотвращать атаки в режиме реального времени, анализировать инциденты, отслеживать незаконные средства и выполнять обязательства по ПОД/ФТ на протяжении всего жизненного цикла протоколов и платформ.
BlockSec опубликовала множество статей по безопасности блокчейнов на престижных конференциях, сообщила о нескольких атаках «нулевого дня» на DeFi-приложения, заблокировала многочисленные попытки взлома, сохранив более 20 миллионов долларов, и обеспечила безопасность криптовалют на миллиарды долларов.
-
Официальный сайт: https://blocksec.com/
-
Официальный аккаунт в Twitter: https://twitter.com/BlockSecTeam



