BlockSecチーム(@BlockSecTeam)より

先週、Compoundプロトコルでは、ユーザーに大量のCOMPトークンを「誤って」送信してしまうバグが発生しました。このバグ(本ブログのバグ2)の原因は、以前発見された別のバグ(本ブログのバグ1)の修正が不適切だったことにあります。
本ブログでは、最初のバグの根本原因と、最初のバグの修正がなぜ2番目のバグを引き起こしたのかを詳しく説明します。
背景
CompoundプロトコルはCompoundホワイトペーパーに基づいています。cTokenコントラクトを介して、ブロックチェーン上のアカウントは資産(EtherまたはERC-20トークン)をプロトコルに供給してcTokenを受け取るか、他の資産を担保としてプロトコルから資産を借り入れます。CompoundのcTokenコントラクトはこれらの残高を追跡し、借り手に対する金利をアルゴリズムで設定します。
ユーザーをインセンティブ化するために、Compoundに流動性を提供するユーザー(資本を供給する)は利息を受け取ることができます。具体的には、ユーザーはCompoundに資産(Etherまたは他のERC20トークンなど)を供給し、対応するcTokenを受け取ります。ユーザーがCompoundに債務を一切負っていない場合、cTokenをCompoundに返却すると、元本資産(EtherまたはERC20トークン)と利息がユーザーに返還されます。例えば、ユーザーが1000 Etherを保有している場合、cEth.mint(1000)を通じて資産をCompoundに預け入れ、cTokenを取得できます。

cTokenは、Compoundにロックされている元本資産を表します。ユーザーはさらに、cTokenを担保として他の資産を借り入れることができます。例えば、ユーザーはceth.mint(1000)を通じて1000 Etherを預け入れ、取得したcTokenを使用して、75 Ether相当の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トークンの配布を再度有効にしているため、compSupplyState[address(cToken)].blockがゼロではないため、1090行目と1091行目のステートメントは実行されず、indexと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)は、このトランザクションで4,466.542459954989867175 COMPトークンを受け取りました(0x6416ed016c39ffa23694a70d8a386c613f005be18aa0048ded8094f6165e7308)。

トランザクションのさらなるデバッグにより、バグ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
![[バタフライ効果] バグ修正が引き起こした複合セキュリティインシデント](/_next/image?url=https%3A%2F%2Fassets.blocksec.com%2Ffrontend%2Fblocksec-strapi-online%2FThe_Butterfly_Effect_The_Compound_Security_Incident_Caused_by_a_Bugfix_4e7b2d04f3.png&w=1920&q=75)


