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

Sign up for the latest updates

Best Security Auditor for Web3

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

BlockSec Audit