2026 年 1 月 8 日,以太坊上的 Truebit 協議遭到攻擊,導致約 2,600 萬美元的損失 [1]。根本原因是 TRU 代幣購買定價邏輯中存在整數溢位。由於該合約是使用 Solidity v0.6.10 編譯的,此版本預設不會強制執行溢位檢查,因此購買成本計算中的一大中間數值繞回(wrapped around)為一個極小的數字。結果,攻擊者能夠以極低的成本(甚至零 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);// 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;
}
從反編譯的邏輯來看,購買價格可以表示為以下債券曲線樣式的函數:
其中,
- 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);// 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(),將 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] 攻擊交易
關於 BlockSec
BlockSec 是一家全棧區塊鏈安全與加密合規服務供應商。我們構建產品和服務,協助客戶在協議和平台的完整生命周期內進行代碼審計(包括智能合約、區塊鏈和錢包)、即時攔截攻擊、分析事件、追蹤非法資金並履行 AML/CFT 合規義務。
BlockSec 已在頂級學術會議上發表多篇區塊鏈安全論文,披露了多個 DeFi 應用的零日漏洞,成功攔截多次攻擊並挽救了超過 2,000 萬美元的資產,保護了數十億美元的加密貨幣。
-
官方網站: https://blocksec.com/
-
官方 Twitter: https://twitter.com/BlockSecTeam
-
🔗 BlockSec 審計服務 : 提交請求



