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 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]. The protocol uses a native token, TRU, and exposes two public trading functions:
-
buyTRU()executes TRU purchases. The required ETH cost is computed by an internal pricing function that is also used bygetPurchasePrice(), sogetPurchasePrice()reflects the exact on-chain pricing logic applied during purchase execution. -
sellTRU()executes TRU sales (redemptions). The expected ETH payout can be queried viagetRetirePrice().
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 contract source code is not public, the following analysis is based on decompiled bytecode.
Buy logic
The buyTRU() function (and the getPurchasePrice() function) delegates pricing to an internal 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 internal 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.
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.,
SafeMathor 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
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.
-
Official website: https://blocksec.com/
-
Official Twitter account: https://twitter.com/BlockSecTeam


