深入分析:Truebit 事件博客

  • blocksec
  • 发布于 2026-01-09 17:43
  • 阅读 41

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

深入分析: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() 查询。

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

  • 购买使用凸性 bonding curve(边际价格随着供应增加而增加)。
  • 出售使用线性赎回规则(与储备金成比例)。

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

购买逻辑

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 样式的函数:

其中,

  • 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);// numerator = _reserve * amount
    if (v1 > 0) {
        assert(v1);
        return v4 / v1;// retirePrice = numerator / totalSupply
    } else {
    // ...
}

赎回规则是线性的:

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

这种故意的非对称性造成了很大的价差:购买是凸性的(大规模购买很昂贵),而出售是线性的(仅赎回储备金的比例份额)。在正常情况下,这种价差使得立即进行 buy→sell 套利变得不具吸引力。

0x1 漏洞分析

尽管目的是 大量购买很昂贵 的设计,但 _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)。 发生此溢出时,分子回绕为一个很小的值,从而导致最终除法返回人为的低购买价格,可能为零。

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

  • 以低估的(或零)成本购买大量 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(),从协议储备中以 5,105 ETH 的价格赎回 TRU。

后续轮次:低成本购买,然后出售获利

攻击者多次重复该周期。 后来的购买并不总是严格的零成本,但溢出仍然使购买价格远低于相应的出售回报。

在这些轮次中,攻击者提取了大量的 ETH,我们的调查表明,在第一轮之后可能仍然可以进行额外的零成本购买,但攻击者选择一些非零成本轮次的原因尚不清楚。

总的来说,攻击者从 Truebit 的储备中耗尽了 8,535 ETH。

0x3 总结

此事件最终是由 Truebit 的买方定价逻辑中的未检查的整数溢出引起的。 尽管该协议的不对称买/卖定价模型旨在抵抗投机,但在没有系统溢出保护的情况下使用较旧的 Solidity 版本(pre-0.8)进行编译会破坏设计并导致储备金耗尽。

对于任何仍在使用低于 0.8 的 Solidity 版本的生产合约,开发人员应:

  • 对每个相关的运算应用溢出安全算术(例如,SafeMath 或等效检查),或者
  • 最好迁移到 Solidity 0.8+ 以从默认溢出检查中受益。

参考

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

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

[3] 攻击交易

关于 BlockSec

BlockSec 是一家全栈区块链安全和加密合规提供商。我们构建产品和服务,帮助客户执行代码审计(包括智能合约、区块链和钱包),实时拦截攻击,分析事件,追踪非法资金,并在协议和平台的整个生命周期中满足 AML/CFT 义务。

BlockSec 已在著名会议上发表了多篇区块链安全论文,报告了多个 DeFi 应用程序的零日攻击,阻止了多起黑客攻击,挽救了超过 2000 万美元,确保了数十亿美元的加密货币安全。

  • 原文链接: blocksec.com/blog/in-dep...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
blocksec
blocksec
江湖只有他的大名,没有他的介绍。