2026年1月8日,以太坊上的Truebit Protocol遭到利用,导致约2600万美元的损失 [1]。根本原因在于TRU代币购买定价逻辑中的整数溢出。由于该合约使用Solidity v0.6.10编译,该版本默认不强制执行溢出检查,导致在购买成本计算中的一个中间大值回绕成一个非常小的数字。因此,攻击者能够以极低的ETH价格甚至零ETH购买大量TRU,然后立即以有利的汇率将获得的TRU卖回给合约换取ETH,从而耗尽协议储备。
0x0 背景
Truebit通过链下计算和交互式验证为以太坊提供计算服务 [2]。在该协议中,TRU代币作为协调激励(包括质押和与任务相关的支付)的核心经济工具。
该协议公开了两个用于购买和赎回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);// 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;
}
从反编译的逻辑来看,购买价格可以表示为以下变量的曲线函数:
其中:
- 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成正比。
这种故意的非对称性产生了巨大的价差:购买是凸形的(大规模时昂贵),而销售是线性的(只赎回储备金的比例份额)。在正常情况下,这种价差使得即时买入→卖出套利变得不具吸引力。
0x1 漏洞分析
尽管设计意图是“大额购买昂贵”,但_getPurchasePrice()在算术运算中存在整数溢出。由于该合约是用Solidity 0.6.10编译的,uint256上的算术运算可能会在没有显式保护(例如通过SafeMath)的情况下静默溢出并回绕(模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);// 分母 = 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()以高得多的有效汇率赎回TRU。
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。
后续轮次:低成本购买,然后卖出获利
攻击者多次重复了这个循环。后来的购买不总是严格零成本,但溢出继续使购买价格远低于相应的卖出收益。
总而言之,攻击者耗尽了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] 攻击交易
关于BlockSec
BlockSec是全栈区块链安全和加密合规提供商。我们构建产品和服务,帮助客户在协议和平台的全生命周期中进行代码审计(包括智能合约、区块链和钱包)、实时拦截攻击、分析事件、追踪非法资金,并满足AML/CFT要求。
BlockSec在知名会议上发表了多篇区块链安全论文,报告了多个DeFi应用的零日攻击,阻止了多次黑客攻击挽救了超过2000万美元,并保障了数十亿美元的加密货币。
-
官方Twitter账号:https://twitter.com/BlockSecTeam
-
🔗 BlockSec审计服务 : 提交请求
-
🔗 Phalcon安全APP: 预约演示



