Back to Blog

[バタフライ効果] バグ修正が引き起こした複合セキュリティインシデント

Code Auditing
October 10, 2021
8 min read

BlockSecチーム(@BlockSecTeam)より

先週、Compoundプロトコルでは、多数のCOMPトークンが「誤って」ユーザーに送信されるバグが発生しました。このバグ(本ブログのバグ2)の原因は、以前発見された別のバグ(本ブログのバグ1)の修正が不適切だったことにあります。

本ブログでは、最初のバグの根本原因と、その修正が2番目のバグを引き起こした理由を詳しく説明します。

背景

CompoundプロトコルはCompoundホワイトペーパーに基づいています。cTokenコントラクトを通じて、ブロックチェーン上のアカウントは、イーサまたはERC-20トークンをプロトコルに供給してcTokenを受け取るか、他の資産を担保としてプロトコルから資産を借り入れます。Compound cTokenコントラクトは、これらの残高を追跡し、借り手への金利をアルゴリズムで設定します

ユーザーをインセンティブ化するため、Compoundに流動性を提供するユーザー(資本を供給する)は利息を受け取ることができます。具体的には、ユーザーはCompoundに資産(イーサまたは他のERC20トークンなど)を供給し、対応するcTokenを受け取ります。ユーザーがCompoundに借入がない場合、cTokenがCompoundに返却されると、元本資産(イーサまたはERC20トークン)と利息がユーザーに返還されます。例えば、ユーザーが1000イーサを持っている場合、cEth.mint(1000)を通じてCompoundに資産を預け入れることでcTokenを取得できます。

cTokenは、Compoundにロックされた元本資産を表します。ユーザーはさらに、cTokenを担保として他の資産を借り入れることができます。例えば、ユーザーはceth.mint(1000)を通じて1000イーサを預け入れ、その後、取得したcTokenを使用して、75イーサ相当のxDaiを借り入れることができます(過剰担保 - この数値は担保率によって異なります)。cDai.borrow(x)を通じて行われます。

コアロジックは Comptroller コントラクトで実装されています。これは、ユーザーのステータス、例えばユーザーがCompoundにいくらトークンを預け入れたか、いくらトークンを借り入れたか、そしてユーザーがさらにトークンを借り入れられるかどうかを管理します。このプロセスで呼び出される関数には、getHypotheticalAccountLiquidityInternal()borrowAllowed()mintAllowed() などがあります。

Compoundには COMPというガバナンストークンもあります。COMPトークンは提案に投票するために使用できます。さらに、COMPトークンは取引所で取引できます。現在、COMPの価格は約300ドルです。

バグ1

2021年9月31日、Compound DAOに新しい提案(提案62)があり、Comptrollerのバグを修正することを目的としていました。

このバグは、各ブロックでユーザーに配布されるCOMPトークンの数を示す CompSpeed に関連していました。

mint 関数のフロー

以下では、mint 関数を使用してこのバグの原因を説明します。mint 関数の呼び出しチェーンは、mintmintInternalmintFresh です。

mintFresh 関数では、mintAllowed を呼び出し、その後cTokenのユーザー残高を更新します。

mintAllowed 関数では、まずupdateCompSupplyIndex を呼び出し、次に distributeSupplierComp を呼び出して、1) 市場の compSupplyState を更新し、2) COMPトークンをユーザーに配布します。

updateCompSupplyIndex

updateCompSupplyIndex 関数は、各市場のステータス、特に compSupplyState[cToken] を更新します。

CompMarketState 構造体には、この更新のブロック番号 (block) と、ユーザー(cTokenを保有するユーザー)に配布されるべきCOMPトークンの数に影響するボーナスインデックス (index) が記録されています。

各トークンのボーナスインデックス(index)とは何ですか? これは、累積された値です(次の式に示す通り)。

これは、ユーザーに配布されるべきCOMPの数(ユーザーが保有する各cTokenごと)を示しています。

distributeSupplierComp

もう一つの関数 distributeSupplierComp は、ユーザー(サプライヤー)に配布されるべきCOMPトークンの数を compAccrued[supplier] に記録する責任があります。

具体的には、updateCompSupplyIndex 関数でグローバルボーナスインデックスを compSupplyState で更新します。その後、distributeSupplierComp 関数では、supplyIndex が現在のボーナスインデックスを記録し、supplierIndex がユーザー(サプライヤー)の最後のボーナスインデックスを示します。 (supplyIndex - supplierIndex) * ユーザーのcToken残高 という差分値が、ユーザーに配布されるべきCOMPトークンの数を示します。

バグ1の原因

市場の supplySpeedcompSpeeds[address[cToken]])を調整するための setCompSpeed という別の関数があります。

これは、市場の CompSpeed をゼロに設定すると、その市場ではCOMPトークンがユーザーに配布されないことを意味するためです。したがって、まずCOMPの配布を無効にしてから再度有効にしたい場合は、次の手順に従うことができます。

  • ステップI: CompSpeed[cToken] をゼロに設定して、COMPトークンの配布を無効にします。
  • ステップII: setCompSpeed 関数を呼び出して、CompSpeed[cToken] をゼロ以外の値に設定します。

ステップI: ステップIでCOMPトークンの配布が無効にされた市場(supplySpeed == 0)では、ブロックはゼロではありません。なぜなら、updateCompSupplyIndex (else if (deltaBlocks > 0)) でブロックは継続的に更新されるためです。

ステップII: ステップIIの操作を実行すると、setCompSpeedInternal 関数は else if (compSpeed != 0) ステートメント(1083行目)を通過します。次に、1088行目から1093行目では、if (compSupplyState[address(cToken)].index == 0 && compSupplyState[address(cToken)].block == 0) というチェックがあり、これは新しい市場indexblock を初期化します。しかし、既存の市場(新しい市場ではない)でCOMPトークンの配布を再有効化しているため、1090行目と1091行目のステートメントは実行されず、indexblock は初期化されません(compSupplyState[address(cToken)].block はゼロではないため)。

要約すると、現在無効になっている市場では、index はゼロですが、block はゼロではありません。これは、CompSpeed[cToken] をゼロ以外の値に設定するために setCompSpeed を呼び出して無効になった市場を再有効化すると、index 値が CompInitialIndex (1e36) に再初期化されない(1090行目と1091行目は実行されない)ことを意味します。

バグ1の影響

次に、COMPトークンの配布を担当する distributeSupplierComp 関数をさらに詳しく見ていきます。

supplierIndexcompInitialIndex です。しかし、バグのために supplyIndex はゼロのままです。これにより、 Double memory deltaIndex = sub_(supplyIndex=0, supplierIndex=1e36) のアンダーフローが発生します。

バグ2:バグ1の修正によって導入されたもの

バグを修正するために、プロジェクトオーナーはコードロジックを変更しました。具体的には、新しい市場を初期化する際に index を直ちに compInitialIndex に初期化します。

グローバルボーナスインデックス (index) が compInitialIndex に初期化されたため、ユーザーボーナスインデックスもこの値に初期化されるべきです。 distributeSupplierComp 関数を見てみましょう。

1234行目のif条件は、supplierIndex == 0 の場合でも満たされません。なぜなら、supplyIndexcompInitialIndex (1e36) と等しい(それより大きくない)からです。これにより、 supplierIndexcompInitialIndex に正しく初期化されず(値は0のまま)、 deltaIndex (supplyIndex - supplierIndex) はゼロではなく compInitialIndex になります。ユーザーのcToken残高がゼロでない場合、 supplierTokens は大きな値になります。

要約すると、バグ1の修正前にユーザーが偶然 mint 操作を実行した場合、そのユーザーはcTokenを保有しており、COMPトークンは配布されているため supplierIndex はゼロになります。その後、バグ1の修正(バグ2を導入)が行われた後、ユーザーが再度 mint 関数を呼び出すと、大量のCOMPトークン(1e36 * ctoken.balanceOf(user))を受け取ることができます。

実例

影響を受けた市場を以下に示します。

0xF5DCe57282A584D2746FaF1593d3121Fcac444dC: cSAI
0x12392F67bdf24faE0AF363c24aC620a2f67DAd86: cTUSD
0x95b4eF2869eBD94BEb4eEE400a99824BF5DC325b: cMKR
0x4B0181102A0112A2ef11AbEE5563bb4a3176c9d7: cSUSHI
0xe65cdB6479BaC1e22340E4E755fAE7E509EcD06c: cAAVE
0x80a2AE356fc9ef4305676f7a3E2Ed04e12C33946: cYFI

ユーザー(0xa7b95d2a2d10028cc4450e453151181cbcac74fc)は、このトランザクション(0x6416ed016c39ffa23694a70d8a386c613f005be18aa0048ded8094f6165e7308)で 4,466.542459954989867175 COMPトークンを受け取りました。

トランザクションのさらなるデバッグにより、バグ2により deltaIndex が1e36になり、ユーザーがその時に偶然cTokenを保有していたことが判明しました。

バグ2の修正

バグ2の修正は簡単です。 distributeSupplierComp 関数内のif条件を変更しました。

学び

  • これは別のバグの修正によって引き起こされたバグです。影響力の大きいプロジェクトのコード変更を徹底的にレビューする方法は、依然として未解決の問題です。
  • DAOは中央集権化のリスクを排除できます。しかし、セキュリティインシデントへの対応プロセスを遅くする可能性もあります。
  • 影響力の大きいDeFiプロジェクトは、従来のプログラムにおける優れたセキュリティプラクティスを採用できます。例えば、継続的なテストプロセスを備えた効率的なファジングシステムを導入することなどです。

BlockSecについて

BlockSecは、2021年に世界的に著名なセキュリティ専門家グループによって設立された、先駆的なブロックチェーンセキュリティ企業です。同社は、新興のWeb3世界におけるセキュリティとユーザビリティの向上に尽力しており、その大量採用を促進しています。この目的のため、BlockSecはスマートコントラクトとEVMチェーンのセキュリティ監査サービス、セキュリティ開発と脅威のプロアクティブなブロックのためのPhalconプラットフォーム、資金追跡と調査のためのMetaSleuthプラットフォーム、そしてWeb3ビルダーが仮想通貨の世界を効率的にサーフィンするためのMetaSuites拡張機能を提供しています。

現在までに、同社はMetaMask、Uniswap Foundation、Compound、Forta、PancakeSwapなど300社以上の著名なクライアントにサービスを提供しており、Matrix Partners、Vitalbridge Capital、Fenbushi Capitalなどの著名な投資家から2回の資金調達ラウンドで数千万米ドルを獲得しています。

公式ウェブサイト: https://blocksec.com/

公式Twitterアカウント: https://twitter.com/BlockSecTeam

Sign up for the latest updates
HarmonyのクロスシャードONEミント + 約4700万ドルの鍵漏洩による損失 | BlockSec
Security Insights

HarmonyのクロスシャードONEミント + 約4700万ドルの鍵漏洩による損失 | BlockSec

2026年8月10日〜16日の週、注目すべきセキュリティ事件5件を紹介、損失総額は約4700万ドル。詳細分析はHarmony Layer-1の連鎖実装欠陥に注目。Harmonyでは、クロスシャードのレシート再生により、未署名のMerkleProofのShardIDとBlockNumから支出マーカーを導出し、ONEが不正発行された。約3.01兆ONEが偽造されたが、名目価値は市場総額を大幅に超え、実現・確定損失ではないため総額から除外。約4700万ドルは秘密鍵漏洩(Unknown Whale Wallet約2500万ドル、Kite約1400万ドル、Coinsbuy約790万ドル)とFoxのロジック欠陥(約11.7万ドル)による。

約160万ドルの損失:MokeトークンとLpdFiへの攻撃 | BlockSec週次レポート
Security Insights

約160万ドルの損失:MokeトークンとLpdFiへの攻撃 | BlockSec週次レポート

2026年8月3〜9日の週、BNBチェーンで2件の重大なセキュリティインシデントが発生し、いずれも価格操作により総損失は約160万ドルに上った。LpdFi事件(約69.7万ドル)では、注文評価と利息償還の両方に同じ操作可能なPancakeSwapペアのリザーブが使用され、攻撃者がポジションの元本を水増しし過大な利息を請求した。Mokeトークン(約90.6万ドル)では、操作可能なスポット価格とLP配当の重複計上を組み合わせ、MOKEを水増し請求してBNB配当を複数回取得した。

COLDCARDの事件:ウォレットの「ランダム」シードがランダムではなかった時
Security Insights

COLDCARDの事件:ウォレットの「ランダム」シードがランダムではなかった時

COLDCARDファームウェアのビルド・統合バグにより、Bitcoin種生成がソフトウェアRNGフォールバックに誘導され、脆弱な乱数によりウォレットの秘密鍵がオフラインで復元可能となった。欠陥は種自体にあるためファームウェア更新では修正不可。2026年8月7日までに確認された被害は1,405 BTC(約91億円)、非公式推定では最大2,055 BTCに上る。

Best Security Auditor for Web3

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

BlockSec Audit
[バタフライ効果] バグ修正が引き起こした複合セキュリティインシデント