该文章深入分析了2026年1月8日发生在以太坊上的 Truebit 协议被攻击事件,攻击者利用TRU购买定价逻辑中的整数溢出漏洞,以极低的成本购买大量TRU,然后以有利的价格卖回合约换取ETH,最终从协议储备中抽取了8535个ETH。文章详细解析了漏洞原理、攻击过程以及修复建议。

2026年1月8日,以太坊上的 Truebit 协议遭到攻击,导致约 2600 万美元的损失 [1]。根本原因是 TRU 购买定价逻辑中的整数溢出。由于该合约是用 Solidity v0.6.10 编译的,该版本默认不强制执行溢出检查,因此购买成本计算中的一个较大的中间值回绕成一个很小的数字。因此,攻击者可以用很少甚至零 ETH 购买大量 TRU,然后立即将获得的 TRU 以优惠的价格卖回给合约以换取 ETH,从而耗尽协议储备。
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);// 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;
}
从反编译的逻辑来看,购买价格可以表示为以下 bonding-curve 样式的函数:

其中,
该曲线旨在使大量购买变得越来越昂贵(凸性成本增长),从而阻止投机并减少直接的买方操纵。
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);// numerator = _reserve * amount
if (v1 > 0) {
assert(v1);
return v4 / v1;// retirePrice = numerator / totalSupply
} else {
// ...
}
赎回规则是线性的:

赎回价格与赎回的总供应量的比例(即 amount / totalSupply)乘以 reserve 成正比。
这种故意的非对称性造成了很大的价差:购买是凸性的(大规模购买很昂贵),而出售是线性的(仅赎回储备金的比例份额)。在正常情况下,这种价差使得立即进行 buy→sell 套利变得不具吸引力。
尽管目的是 大量购买很昂贵 的设计,但 _getPurchasePrice() 在其算术运算中包含整数溢出。 由于合约是用 Solidity 0.6.10 编译的,因此除非明确保护(例如,通过 SafeMath),否则 uint256 上的算术运算可能会静默溢出并以 2^256 为模进行回绕。
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);// 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;
}
在 _getPurchasePrice() 中,足够大的 amount 会触发两个大的分子项相加期间的溢出(反编译代码段中的 v12 + v9)。 发生此溢出时,分子回绕为一个很小的值,从而导致最终除法返回人为的低购买价格,可能为零。
至关重要的是,溢出仅影响买方定价。 卖方函数保持线性并按预期运行,因此攻击者可以:
sellTRU() 以更高的有效利率将其赎回为 ETH。攻击者在单个交易中执行了多轮套利 [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(),从协议储备中以 5,105 ETH 的价格赎回 TRU。

攻击者多次重复该周期。 后来的购买并不总是严格的零成本,但溢出仍然使购买价格远低于相应的出售回报。
在这些轮次中,攻击者提取了大量的 ETH,我们的调查表明,在第一轮之后可能仍然可以进行额外的零成本购买,但攻击者选择一些非零成本轮次的原因尚不清楚。
总的来说,攻击者从 Truebit 的储备中耗尽了 8,535 ETH。
此事件最终是由 Truebit 的买方定价逻辑中的未检查的整数溢出引起的。 尽管该协议的不对称买/卖定价模型旨在抵抗投机,但在没有系统溢出保护的情况下使用较旧的 Solidity 版本(pre-0.8)进行编译会破坏设计并导致储备金耗尽。
对于任何仍在使用低于 0.8 的 Solidity 版本的生产合约,开发人员应:
SafeMath 或等效检查),或者[1] https://x.com/Truebitprotocol/status/2009328032813850839
[2] https://docs.truebit.io/v1docs
[3] 攻击交易
BlockSec 是一家全栈区块链安全和加密合规提供商。我们构建产品和服务,帮助客户执行代码审计(包括智能合约、区块链和钱包),实时拦截攻击,分析事件,追踪非法资金,并在协议和平台的整个生命周期中满足 AML/CFT 义务。
BlockSec 已在著名会议上发表了多篇区块链安全论文,报告了多个 DeFi 应用程序的零日攻击,阻止了多起黑客攻击,挽救了超过 2000 万美元,确保了数十亿美元的加密货币安全。
官方 Twitter 账号:https://twitter.com/BlockSecTeam
- 原文链接: blocksec.com/blog/in-dep...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!