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
Drift Protocol Incident: Multisig Governance Compromise via Durable Nonce Exploitation
Security Insights

Drift Protocol Incident: Multisig Governance Compromise via Durable Nonce Exploitation

On April 1, 2026 (UTC), Drift Protocol on Solana suffered a $285.3M loss after an attacker exploited Solana's durable nonce mechanism to delay the execution of phished multisig approvals, ultimately transferring administrative control of the protocol's 2-of-5 Squads governance with zero timelock. With full admin privileges, the attacker created a malicious collateral market (CVT), inflated its oracle price, relaxed withdrawal protections, and drained USDC, JLP, SOL, cbBTC, and other assets through 31 rapid withdrawals in approximately 12 minutes. This incident highlights how durable nonce-based delayed execution can decouple signer intent from on-chain execution, bypassing the temporal assumptions that multisig security implicitly relies on.

Weekly Web3 Security Incident Roundup | Mar 23 – Mar 29, 2026
Security Insights

Weekly Web3 Security Incident Roundup | Mar 23 – Mar 29, 2026

This BlockSec weekly security report covers eight DeFi attack incidents detected between March 23 and March 29, 2026, across Ethereum and BNB Chain, with total estimated losses of approximately $1.53M. Incidents include a $679K flawed burn mechanism exploit on the BCE token, a $512K spot-price manipulation attack on Cyrus Finance's PancakeSwap V3 liquidity withdrawal, a $133.5K flash-loan-driven referral reward manipulation on a TUR staking contract, and multiple integer overflow, reentrancy, and accounting error vulnerabilities in DeFi protocols. The report provides detailed vulnerability analysis and attack transaction breakdowns for each incident.

Newsletter -  March 2026
Security Insights

Newsletter - March 2026

In March 2026, the DeFi ecosystem experienced three major security incidents. Resolv Protocol lost ~$80M due to compromised privileged infrastructure keys, BitcoinReserveOffering suffered ~$2.7M from a double-minting logic flaw, and Venus Protocol incurred ~$2.15M following a donation attack combined with market manipulation.

Best Security Auditor for Web3

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

BlockSec Audit