Back to Blog

In-Depth Analysis: The Truebit Incident

Code Auditing
January 14, 2026

On January 8, 2026, the Truebit Protocol on Ethereum was exploited, resulting in approximately $26 million in losses [1]. The root cause was an integer overflow in the TRU token purchase pricing logic. Because the contract was compiled with Solidity v0.6.10, which does not enforce overflow checks by default, a large intermediate value in the purchase-cost computation wrapped around to a much smaller number. As a result, an attacker could purchase a very large amount of TRU for little or even zero ETH, then immediately sell the acquired TRU back to the contract for ETH at a favorable rate, draining protocol reserves.

0x0 Background

Truebit provides computation services for Ethereum via off-chain computation and interactive verification [2]. Within the protocol, TRU tokens serve as the core economic instrument for coordinating incentives, including staking and task-related payments.

The protocol exposes two public functions for buying and redeeming TRU:

  • buyTRU() executes TRU purchases. The required ETH cost is computed by an internal pricing function that is also used by getPurchasePrice(), so getPurchasePrice() reflects the exact on-chain pricing logic applied during purchase execution.

  • sellTRU() executes TRU sales (redemptions). The expected ETH payout can be queried via getRetirePrice().

A key design aspect is pricing asymmetry:

  • Purchases use a convex bonding curve (marginal price increases as supply increases).
  • Sales use a linear redemption rule (proportional to reserves).

Because the source code of the implementation contract is not public, the following analysis is based on decompiled bytecode.

Buy logic

The buyTRU() function (and the getPurchasePrice() function) delegates pricing to a private function _getPurchasePrice() that calculates the ETH requried to purchase amount TRU.

function buyTRU(uint256 amount) public payable {
    require(msg.data.length - 4 >= 32);
    v0 = _getPurchasePrice(amount); // get the purchase price
    require(msg.value == v0, Error('ETH payment does not match TRU order'));
    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()); // checks call status, propagates error data on error
    return msg.value;
}

function getPurchasePrice(uint256 amount) public nonPayable {
    require(msg.data.length - 4 >= 32);
    v0 = _getPurchasePrice(amount); // get the purchase price
    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()); // checks call status, propagates error data on error
    require(RETURNDATASIZE() >= 32);
    v2 = 0x18ef(v1, v1)
    v3 = 0x18ef(_setParameters, v2);
    v4 = 0x18ef(v1, v1);
    v5 = 0x18ef(100, v4);
    v6 = _SafeSub(v3, v5);// denominator = 100 * totalSupply**2 - _setParameters * totalSupply**2 
    v7 = 0x18ef(amount, _reserve);
    v8 = 0x18ef(v1, v7);
    v9 = 0x18ef(200, v8);// numerator_2 = 200 * totalSupply * amount * _reserve
    v10 = 0x18ef(amount, _reserve);
    v11 = 0x18ef(amount, v10);
    v12 = 0x18ef(100, v11);// numerator_1 = 100 * amount**2 * _reserve
    v13 = _SafeDiv(v6, v12 + v9); // purchasePrice = (numerator_1 + numerator_2) / denominator
    return v13;
}

From the decompiled logic, the purchase price can be expressed as a bonding-curve style function of:

Where,

  • amount: TRU to be purchased
  • reserve (_reserve): the contract's Ether reserves
  • totalSupply: the total supply of TRU
  • θ (_setParameters): a coefficient, fixed at 75

This curve is intended to make large purchases increasingly expensive (convex cost growth), discouraging speculation and reducing immediate buy-side manipulation.

Sell logic

The sellTRU() function (and the getRetirePrice() function) utilizes private function _getRetirePrice() to calculate the ETH paid out when redeeming 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()); // checks call status, propagates error data on error
    require(RETURNDATASIZE() >= 32);
    require(v1 >= amount, Error('Insufficient TRU allowance'));
    v2 = _getRetirePrice(amount); // get the retire price
    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()); // checks call status, propagates error data on error
    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()); // checks call status, propagates error data on error
    v7 = msg.sender.call().value(v2).gas(!v2 * 2300);
    require(bool(v7), 0, RETURNDATASIZE()); // checks call status, propagates error data on error
    return v2;
}

function getRetirePrice(uint256 amount) public nonPayable {
    require(msg.data.length - 4 >= 32);
    v0 = _getRetirePrice(amount); // get the retire price
    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()); // checks call status, propagates error data on error
    require(RETURNDATASIZE() >= 32);
    v1 = v2.length;
    v3 = v2.data;
    v4 = 0x18ef(_reserve, amount);// numerator = _reserve * amount
    if (v1 > 0) {
        assert(v1);
        return v4 / v1;// retirePrice = numerator / totalSupply
    } else {
    // ...
}

The redemption rule is linear:

The retire price is proportional to the fraction of total supply being redeemed (i.e., amount / totalSupply) times the reserve.

This deliberate asymmetry creates a wide spread: buying is convex (expensive at scale), while selling is linear (redeems only a proportional share of reserves). Under normal conditions, that spread makes immediate buy→sell arbitrage unattractive.

0x1 Vulnerability Analysis

Despite the intended large buys are expensive design, _getPurchasePrice() contains an integer overflow in its arithmetic. Because the contract was compiled with Solidity 0.6.10, arithmetic operations on uint256 can silently overflow and wrap modulo 2^256 unless explicitly protected (e.g., via SafeMath).

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()); // checks call status, propagates error data on error
    require(RETURNDATASIZE() >= 32);
    v2 = 0x18ef(v1, v1)
    v3 = 0x18ef(_setParameters, v2);
    v4 = 0x18ef(v1, v1);
    v5 = 0x18ef(100, v4);
    v6 = _SafeSub(v3, v5);// denominator = 100 * totalSupply**2 - _setParameters * totalSupply**2 
    v7 = 0x18ef(amount, _reserve);
    v8 = 0x18ef(v1, v7);
    v9 = 0x18ef(200, v8);// numerator_2 = 200 * totalSupply * amount * _reserve
    v10 = 0x18ef(amount, _reserve);
    v11 = 0x18ef(amount, v10);
    v12 = 0x18ef(100, v11);// numerator_1 = 100 * amount**2 * _reserve
    v13 = _SafeDiv(v6, v12 + v9); // purchasePrice = (numerator_1 + numerator_2) / denominator
    return v13;
}

In _getPurchasePrice(), a sufficiently large amount triggers an overflow during the addition of two large numerator terms (v12 + v9 in the decompiled snippet). When this overflow occurs, the numerator wraps to a small value, which causes the final division to return an artificially low purchase price, potentially zero.

Crucially, the overflow affects only the buy-side pricing. The sell-side function remains linear and behaves as intended, so an attacker can:

  • buy a large amount of TRU at an underpriced (or zero) cost, then
  • redeem it for ETH via sellTRU() at a much higher effective rate.

0x2 Attack Analysis

The attacker performed multiple rounds of arbitrage within a single transaction [3], repeating: getPurchasePrice() -> buyTRU() -> sellTRU()

First round: zero-cost purchase, then sell for profit

By supplying a carefully chosen purchase amount (240,442,509.453,545,333,947,284,131), the attacker triggered an overflow in _getPurchasePrice(), reducing the computed purchase price to 0 ETH and allowing acquisition of ~240 million TRU at no cost.

The below python code check illustrates that the numerator exceeds 2^256, and after wrapping, the computed purchase price becomes a tiny fractional value that truncates to zero when cast to an integer.

>>> _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

The attacker then immediately called sellTRU(), redeeming the TRU for 5,105 ETH from protocol reserves.

Subsequent rounds: low-cost purchases, then sell for profit

The attacker repeated the cycle multiple times. Later purchases were not always strictly zero-cost, but overflow continued to keep purchase prices far below the corresponding sell returns.

Across these rounds, the attacker extracted substantial ETH, and our investigation suggests that additional zero-cost buys may still have been possible after the first round, though the reason the attacker opted for some non-zero-cost rounds is unclear.

Overall, the attacker drained 8,535 ETH from Truebit's reserves.

0x3 Summary

This incident was ultimately caused by an unchecked integer overflow in Truebit's buy-side pricing logic. Although the protocol's asymmetric buy/sell pricing model was intended to resist speculation, compiling with an older Solidity version (pre-0.8) without systematic overflow protection undermined the design and enabled reserve draining.

For any production contract still using Solidity versions below 0.8, developers should:

  • Apply overflow-safe arithmetic (e.g., SafeMath or equivalent checks) to every relevant operation, or
  • Preferably migrate to Solidity 0.8+ to benefit from default overflow checks.

Reference

[1] https://x.com/Truebitprotocol/status/2009328032813850839

[2] https://docs.truebit.io/v1docs

[3] Attack transasction

About BlockSec

BlockSec is a full-stack blockchain security and crypto compliance provider. We build products and services that help customers to perform code audit (including smart contracts, blockchain and wallets), intercept attacks in real time, analyze incidents, trace illicit funds, and meet AML/CFT obligations, across the full lifecycle of protocols and platforms.

BlockSec has published multiple blockchain security papers in prestigious conferences, reported several zero-day attacks of DeFi applications, blocked multiple hacks to rescue more than 20 million dollars, and secured billions of cryptocurrencies.

Sign up for the latest updates
~$88M Lost: COLDCARD & LULA Exploits | BlockSec Weekly
Security Insights

~$88M Lost: COLDCARD & LULA Exploits | BlockSec Weekly

During the week of July 27 to August 2, 2026, two notable security incidents caused roughly $88M in losses across Bitcoin and BNB Chain. The highlighted COLDCARD incident was a hardware-wallet firmware entropy failure: a build guard that checked whether an RNG configuration macro existed rather than whether it was enabled routed seed generation to a deterministic software fallback, enabling an attacker to recover affected seeds and sweep at least 1,370 BTC (~$88M) across a series of on-chain waves. The LULA token on BNB Chain lost ~$578K to a business-logic flaw where an attacker-reachable path could trigger its privileged `recycle()` function, pulling LULA out of a PancakeSwap V2 pair, resyncing its reserves to the manipulated balance, and draining its liquidity.

Newsletter - July 2026
Security Insights

Newsletter - July 2026

July 2026's three largest DeFi incidents totaled approximately $67.9M in losses across Arbitrum and Solana. AFX Trade lost ~$24.15M after a supply chain attack compromised validator signing authority. Ostium's OLP vault was drained of ~$23.75M through compromised oracle infrastructure that submitted attacker-controlled prices. BonkDAO lost ~$20M when an attacker spent $4.4M to acquire enough voting power to pass a malicious treasury transfer with no timelock. All three incidents demonstrate that a protocol's security boundary extends far beyond smart contract code.

~$39.5M Lost: Allbridge, Wanchain & More | BlockSec Weekly
Security Audits

~$39.5M Lost: Allbridge, Wanchain & More | BlockSec Weekly

During the week of July 20-26, 2026, 8 notable security incidents resulted in approximately $39.5M in total losses across Solana, Ethereum, BNB Chain, Arbitrum, Zilliqa, and Cardano. The highlighted Allbridge Core incident (~$1.65M) exposed a Solana input validation flaw where the same Pool account was accepted in both swap roles, with analysis reconstructed entirely from the deployed program binary. Other analyzed incidents include Wanchain (~$500K, flawed message encoding in a Cardano bridge validator), Zilliqa (~$400K, flawed nonce generation in a Ledger app since 2019), and Lien Finance (~$542K, flawed validation logic in bond exchange).

Best Security Auditor for Web3

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

BlockSec Audit