深入分析:Truebit 事件

深入分析:Truebit 事件

2026年1月8日,以太坊上的Truebit协议遭到攻击,导致约2600万美元的损失 [1]。根本原因是TRU购买定价逻辑中的整数溢出。由于合约使用Solidity v0.6.10编译,该版本默认不强制执行溢出检查,导致购买成本计算中的一个中间大值环绕成一个非常小的数字。因此,攻击者能够以极低的甚至零ETH购买大量的TRU,然后立即以有利的汇率将获得的TRU卖回给合约换取ETH,从而耗尽协议储备。

0x0 背景

Truebit通过链下计算和交互式验证为以太坊提供计算服务 [2]。该协议使用原生代币TRU,并提供两个公开的交易函数:

  • buyTRU():执行TRU购买。所需的ETH成本由内部定价函数计算,该函数也用于getPurchasePrice(),因此getPurchasePrice()反映了购买执行期间适用的确切链上定价逻辑。

  • sellTRU():执行TRU销售(赎回)。预期的ETH支付可以通过getRetirePrice()查询。

关键的设计方面是定价不对称:

  • 购买使用凸形定价曲线(边际价格随供应量增加而增加)。
  • 销售使用线性赎回规则(与储备金成比例)。

由于合约源代码不公开,以下分析基于反编译的字节码。

购买逻辑

buyTRU()函数(以及getPurchasePrice()函数)将定价委托给内部函数_getPurchasePrice(),该函数计算购买amount TRU所需的ETH。

function buyTRU(uint256 amount) public payable {
    require(msg.data.length - 4 >= 32);
    v0 = _getPurchasePrice(amount); // 获取购买价格
    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()); // 检查调用状态,错误时传播错误数据
    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); // purchasePrice = (numerator_1 + numerator_2) / denominator
    return v13;
}

从反编译的逻辑来看,购买价格可以表示为关于以下内容的定价曲线风格函数:

其中,

  • amount:要购买的TRU数量
  • reserve_reserve):合约的以太坊储备金
  • totalSupply:TRU的总供应量
  • θ_setParameters):一个系数,固定为75

这条曲线旨在使大额购买成本越来越高(成本增长呈凸形),以阻止投机并减少即时买入方面的操纵。

销售逻辑

sellTRU()函数(以及getRetirePrice()函数)使用内部函数_getRetirePrice()来计算赎回TRU时支付的ETH。

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('Insufficient TRU allowance'));
    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;// retirePrice = numerator / totalSupply
    } else {
    // ...
}

赎回规则是线性的:

赎回价格与被赎回的总供应量分数(即amount / totalSupply)乘以reserve成正比。

这种故意的不对称性造成了巨大的价差:购买是凸形的(大规模时昂贵),而销售是线性的(只赎回储备金的比例部分)。在正常情况下,这种价差使得即时买入→卖出套利不具吸引力。

0x1 漏洞分析

尽管有“大额购买昂贵”的设计意图,但_getPurchasePrice()在其算术运算中包含了一个整数溢出。由于合约使用Solidity 0.6.10编译,uint256上的算术运算会静默溢出并以2^256取模环绕,除非有明确的保护(例如通过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()); // 检查调用状态,错误时传播错误数据
    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); // purchasePrice = (numerator_1 + numerator_2) / denominator
    return v13;
}

_getPurchasePrice()中,足够大的amount会在两个大分子项(反编译片段中的v12 + v9)的加法过程中触发溢出。当发生此溢出时,分子环绕成一个很小的值,导致最终除法返回一个人为偏低的购买价格,可能为零。

至关重要的是,溢出仅影响买入方定价。卖出方函数保持线性并按预期运行,因此攻击者可以:

  • 以低于成本价(或零成本)购买大量TRU,然后
  • 通过sellTRU()以高得多的有效汇率将其赎回为ETH。

0x2 攻击分析

攻击者在一次交易中执行了多轮套利 [3],重复了以下过程:

                               `getPurchasePrice()` -> `buyTRU()` -> `sellTRU()`

第一轮:零成本购买,然后获利卖出

通过提供一个精心选择的购买金额(240,442,509.453,545,333,947,284,131),攻击者触发了_getPurchasePrice()中的溢出,将计算出的购买价格降低到0 ETH,并允许以零成本获得约2.4亿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版之前)进行编译,破坏了设计并导致了储备金耗尽。

对于任何仍在使用0.8版以下Solidity的生产合约,开发者应:

  • 对每个相关操作应用防溢出算术(例如,SafeMath或等效检查),或
  • 最好迁移到Solidity 0.8+以利用默认的溢出检查。

参考

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

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

[3] 攻击交易

Sign up for the latest updates
Weekly Web3 Security Incident Roundup | Feb 9 – Feb 15, 2026

Weekly Web3 Security Incident Roundup | Feb 9 – Feb 15, 2026

During the week of February 9 to February 15, 2026, three blockchain security incidents were reported with total losses of ~$657K. All incidents occurred on the BNB Smart Chain and involved flawed business logic in DeFi token contracts. The primary causes included an unchecked balance withdrawal from an intermediary contract that allowed donation-based inflation of a liquidity addition targeted by a sandwich attack, a post-swap deflationary clawback that returned sold tokens to the caller while draining pool reserves to create a repeatable price-manipulation primitive, and a token transfer override that burned tokens directly from a Uniswap V2 pair's balance and force-synced reserves within the same transaction to artificially inflate the token price.

Top 10 "Awesome" Security Incidents in 2025

Top 10 "Awesome" Security Incidents in 2025

To help the community learn from what happened, BlockSec selected ten incidents that stood out most this year. These cases were chosen not only for the scale of loss, but also for the distinct techniques involved, the unexpected twists in execution, and the new or underexplored attack surfaces they revealed.

#10 Panoptic Incident: XOR Linearity Breaks the Position Fingerprint Scheme

#10 Panoptic Incident: XOR Linearity Breaks the Position Fingerprint Scheme

On August 29, 2025, Panoptic disclosed a Cantina bounty finding and confirmed that, with support from Cantina and Seal911, it executed a rescue operation on August 25 to secure roughly $400K in funds. The issue stemmed from a flaw in Panoptic’s position fingerprint calculation algorithm, which could have enabled incorrect position identification and downstream fund risk.