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 関数の呼び出しチェーンは、mint → mintInternal → mintFresh です。

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の原因
市場の supplySpeed (compSpeeds[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) というチェックがあり、これは新しい市場の index と block を初期化します。しかし、既存の市場(新しい市場ではない)でCOMPトークンの配布を再有効化しているため、1090行目と1091行目のステートメントは実行されず、index と block は初期化されません(compSupplyState[address(cToken)].block はゼロではないため)。
要約すると、現在無効になっている市場では、index はゼロですが、block はゼロではありません。これは、CompSpeed[cToken] をゼロ以外の値に設定するために setCompSpeed を呼び出して無効になった市場を再有効化すると、index 値が CompInitialIndex (1e36) に再初期化されない(1090行目と1091行目は実行されない)ことを意味します。
バグ1の影響
次に、COMPトークンの配布を担当する distributeSupplierComp 関数をさらに詳しく見ていきます。

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

バグ2:バグ1の修正によって導入されたもの
バグを修正するために、プロジェクトオーナーはコードロジックを変更しました。具体的には、新しい市場を初期化する際に index を直ちに compInitialIndex に初期化します。

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

1234行目のif条件は、supplierIndex == 0 の場合でも満たされません。なぜなら、supplyIndex が compInitialIndex (1e36) と等しい(それより大きくない)からです。これにより、 supplierIndex が compInitialIndex に正しく初期化されず(値は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



