Back to Blog

詳細分析:Truebit事件

Code Auditing
January 14, 2026
11 min read

2026年1月8日、Ethereum上のTruebit Protocolが約2600万ドルの損失をもたらす脆弱性を突かれました[1]。根本原因は、TRUトークン購入価格計算ロジックにおける整数オーバーフローでした。コントラクトはデフォルトでオーバーフローチェックを強制しないSolidity v0.6.10でコンパイルされていたため、購入コスト計算中の大きな中間値がラップアラウンドして非常に小さな数値になりました。その結果、攻撃者は非常に大量のTRUをほとんど、あるいは全くETHなしで購入し、すぐに獲得したTRUを有利なレートでコントラクトにETHと交換して売却し、プロトコルの準備金を枯渇させることができました。

0x0 背景

Truebitは、オフチェーン計算とインタラクティブな検証を通じてEthereumに計算サービスを提供します[2]。プロトコル内では、TRUトークンがステーキングやタスク関連の支払いなど、インセンティブの調整のための主要な経済手段として機能します。

プロトコルは、TRUの購入と償還のために2つの公開関数を公開しています。

  • buyTRU()はTRUの購入を実行します。必要なETHコストは、getPurchasePrice()によっても使用される内部価格設定関数によって計算されるため、getPurchasePrice()は購入実行中に適用される正確なオンチェーン価格設定ロジックを反映します。

  • sellTRU()はTRUの売却(償還)を実行します。期待されるETHの支払いは、getRetirePrice()を通じて照会できます。

主要な設計上の側面は、価格設定の非対称性です。

  • 購入は、供給が増加するにつれて限界価格が増加する、凸状のバンディングカーブを使用します。
  • 売却は、準備金に比例する線形償還ルールを使用します。

実装コントラクトのソースコードは公開されていないため、以下の分析は逆コンパイルされたバイトコードに基づいています。

購入ロジック

buyTRU()関数(およびgetPurchasePrice()関数)は、amount TRUの購入に必要なETHを計算するプライベート関数_getPurchasePrice()に価格設定を委任します。

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):コントラクトのEther準備金
  • 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 moduloでラップします。

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が2つの大きな分子項(逆コンパイルされたスニペットのv12 + v9)の加算中にオーバーフローを引き起こします。このオーバーフローが発生すると、分子は小さな値にラップされ、最終的な除算が人工的に低い購入価格(ゼロの可能性あり)を返す原因となります。

特に、オーバーフローは購入側価格設定にのみ影響します。売却側関数は線形であり、意図したとおりに機能するため、攻撃者は以下のことが可能です。

  • 過小評価された(またはゼロの)コストで大量のTRUを購入し、
  • sellTRU()を通じて、はるかに高い実効レートでそれをETHに償還します。

0x2 攻撃分析

攻撃者は単一のトランザクション内で複数のアービトラージラウンドを実行し[3]、getPurchasePrice() -> buyTRU() -> sellTRU()を繰り返しました。

第一ラウンド:ゼロコスト購入、その後利益を上げて売却

慎重に選択された購入額(240,442,509.453,545,333,947,284,131)を供給することで、攻撃者は_getPurchasePrice()のオーバーフローを引き起こし、計算された購入価格を0 ETHに削減し、約2億4000万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未満)でコンパイルしたことは、設計を損ない、準備金の枯渇を可能にしました。

Solidityバージョン0.8未満を使用している本番環境のコントラクトでは、開発者は以下のことを行うべきです。

  • すべての関連する演算にオーバーフローセーフな算術(例: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万ドル以上を救済し、数十億ドルの暗号通貨を保護してきました。

Sign up for the latest updates
The Decentralization Dilemma: Cascading Risk and Emergency Power in the KelpDAO Crisis
Security Insights

The Decentralization Dilemma: Cascading Risk and Emergency Power in the KelpDAO Crisis

This BlockSec deep-dive analyzes the KelpDAO $290M rsETH cross-chain bridge exploit (April 18, 2026), attributed to the Lazarus Group, tracing a causal chain across three layers: how a single-point DVN dependency enabled the attack, how DeFi composability cascaded the damage through Aave V3 lending markets to freeze WETH liquidity exceeding $6.7B across Ethereum, Arbitrum, Base, Mantle, and Linea, and how the crisis forced decentralized governance to exercise centralized emergency powers. The article examines three parameters that shaped the cascade's severity (LTV, pool depth, and cross-chain deployment count) and provides an exclusive technical breakdown of Arbitrum Security Council's forced state transition, an atomic contract upgrade that moved 30,766 ETH without the holder's signature.

Weekly Web3 Security Incident Roundup | Apr 13 – Apr 19, 2026
Security Insights

Weekly Web3 Security Incident Roundup | Apr 13 – Apr 19, 2026

This BlockSec weekly security report covers four attack incidents detected between April 13 and April 19, 2026, across multiple chains such as Ethereum, Unichain, Arbitrum, and NEAR, with total estimated losses of approximately $310M. The highlighted incident is the $290M KelpDAO rsETH bridge exploit, where an attacker poisoned the RPC infrastructure of the sole LayerZero DVN to fabricate a cross-chain message, triggering a cascading WETH freeze across five chains and an Arbitrum Security Council forced state transition that raises questions about the actual trust boundaries of decentralized systems. Other incidents include a $242K MMR proof forgery on Hyperbridge, a $1.5M signed integer abuse on Dango, and an $18.4M circular swap path exploit on Rhea Finance's Burrowland protocol.

Weekly Web3 Security Incident Roundup | Apr 6 – Apr 12, 2026
Security Insights

Weekly Web3 Security Incident Roundup | Apr 6 – Apr 12, 2026

This BlockSec weekly security report covers four DeFi attack incidents detected between April 6 and April 12, 2026, across Linea, BNB Chain, Arbitrum, Optimism, Avalanche, and Base, with total estimated losses of approximately $928.6K. Notable incidents include a $517K approval-related exploit where a user mistakenly approved a permissionless SquidMulticall contract enabling arbitrary external calls, a $193K business logic flaw in the HB token's reward-settlement logic that allowed direct AMM reserve manipulation, a $165.6K exploit in Denaria's perpetual DEX caused by a rounding asymmetry compounded with an unsafe cast, and a $53K access control issue in XBITVault caused by an initialization-dependent check that failed open. The report provides detailed vulnerability analysis and attack transaction breakdowns for each incident.

Best Security Auditor for Web3

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

BlockSec Audit