Back to Blog

深入分析:Truebit 事件

Code Auditing
January 14, 2026
11 min read

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 萬美元的資產,保護了數十億美元的加密貨幣。

Best Security Auditor for Web3

Validate design, code, and business logic before launch. Aligned with the highest industry security standards.

BlockSec Audit