Back to Blog

Глубокий анализ: инцидент с Truebit

Code Auditing
14 января 2026 г.
8 min read

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 из резервов протокола.

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

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

В этих раундах злоумышленник вывел значительное количество 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 миллионов долларов, и обеспечила безопасность криптовалют на миллиарды долларов.

Sign up for the latest updates
За пределами смарт-контракта: операционная безопасность доменов и DNS в Web3
Security Insights

За пределами смарт-контракта: операционная безопасность доменов и DNS в Web3

Аудит контрактов не проверяет сам контракт. Мы провели 800 SEAL-проверок DNS и регистраторов для 100 доменов из TVL Top 100 DefiLlama — лишь один домен прошёл все. Каких четырёх мер защиты не хватает большинству и почему это критично для пользователя.

Поверхности атаки Web3: обзор тестирования на проникновение

Поверхности атаки Web3: обзор тестирования на проникновение

Крипто-организации сохраняют все традиционные уязвимости и добавляют цепочку работы с деньгами. Статья описывает систему через четыре компонента: приложение, авторизацию и подпись, взаимодействие с блокчейном, инфраструктуру — с их зонами атак, а также пять специфичных для web3 областей: от эксплуатации и подписания до вывода средств и контрактов.

Потеряно ~23 млн $: эксплойты Cosmos EVM и Moonwell | BlockSec Weekly
Security Insights

Потеряно ~23 млн $: эксплойты Cosmos EVM и Moonwell | BlockSec Weekly

За период (22.08.2026–30.08.2026): 5 инцидентов, ~$22,7 млн убытков; Tectonic — $74–119,5 млн, откачены на Cronos. Взлом Cosmos EVM (~$5,7 млн) на TAC Chain, Moonwell, Ajna, Rain Card Exploit (Ed25519) на Solana.

Best Security Auditor for Web3

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

BlockSec Audit
Глубокий анализ: инцидент с Truebit