CertiK:HopeLendが貸付攻撃を受けた事件の分析
著者:CertiK
2023年10月18日19:48:59(北京時間)、Hope.moneyの貸出プールがフラッシュローンを利用した攻撃を受けました。
Hope.moneyには、貸出プラットフォームHopeLend、分散型取引所HopeSwap、ステーブルコインHOPE、ガバナンストークンLTが含まれており、ユーザーに分散型金融のフルスタックサービスを提供しています。
今回の攻撃に関与したプロトコルはHopeLendで、これは分散型貸出プラットフォームであり、ユーザーはプロトコルに流動性を提供したり、過剰担保貸出を行って利益を得ることができます。
事件の経緯
HopeLendのコード実装において、貸出プールには悪用可能な脆弱性が存在していました。預金証明書を破棄する際に、整数除算の誤りが発生し、小数点以下の部分が切り捨てられ、予想よりも少ない証明書の数量が破棄され、期待通りの価値のトークンを得ることができました。
攻撃者はこの欠陥を利用して、Hope.money上に存在する複数の貸出プールの資金を掏り出しました。
その中のhEthWbtc貸出プールは73日前に展開されましたが、資金は存在しなかったため、ハッカーはその貸出プールに大量の資金を注入し、ディスカウント率を劇的に引き上げ、1つのブロック内で他のすべての貸出プールの資金を迅速に掏り出すことに成功しました。
さらに劇的なのは、利用を実行したハッカーは脆弱性を利用した資金を得られず、彼の攻撃取引はフロントランナーに発見され、フロントランナーはその攻撃行動を模倣し、攻撃による利益資金(527 ETH)をすべて奪うことに成功しました。最終的に攻撃利益資金の50%(263 ETH)がフロントランナーによってブロックをパッキングするマイナーへの賄賂(ペイロード)として使用されました。
脆弱性を発見した初期のハッカーは、ブロック18377039で攻撃契約を作成し、ブロック18377042でその攻撃契約を呼び出しました。この時、フロントランナーはメモリプール内の取引を監視し、その攻撃契約を模擬し、フロントラン契約の入力として使用し、同じ18377042ブロックで利用しましたが、初期のハッカーの18377042ブロックの取引はフロントランナーの後に並べられたため、実行に失敗しました。
資金の行方
フロントランナーは利益を得た後の1時間以内に、資金を次のアドレスに移転しました:0x9a9122Ef3C4B33cAe7902EDFCD5F5a486792Bc3A
10月20日13:30:23に、疑わしい公式チームがこのアドレスに連絡し、フロントランナーに26 ETH(10%の利益)を報酬として残すことを許可し、フロントランナーからの返答を得ました。
最終的に資金は、コミュニケーションから1時間後にGnosis Safeのマルチシグ金庫に移転されました。
以下に、実際の脆弱性とハッカーが利用した詳細を示します。
前提情報
HopeLendの貸出プロトコルはAaveからフォークされているため、脆弱性に関連するコアビジネスロジックはAaveのホワイトペーパーを参考にしています。
0x00 預金と貸出
Aaveは純粋なDeFiであり、貸出業務は流動性プールを通じて実現されます。ユーザーはAaveに預金を提供する際、貸出から得られる利益を期待します。
貸出利益は完全にはユーザーに分配されず、一部の利息収入はリスク準備金に計上されます。この部分の割合は少なく、大部分の貸出利益は流動性を提供するユーザーに分配されます。
Aaveで預金を行う際、Aaveはディスカウントの方法を用いて、異なる時間点の預金数量を流動性プールの初期時間点の預金数量の割合に変換します。したがって、各数量の割合に対応する元本と利息の合計は、amount(割合) * index(ディスカウント率)で直接計算でき、計算と理解が大いに便利になります。
これは、ファンドを購入するプロセスに似ていると理解できます。ファンドの初期純資産は1で、ユーザーが100ドルを投入して100の割合を得ると仮定します。一定の時間が経過して利益を得た場合、純資産は1.03に変わります。この時、ユーザーが再度100ドルを投入すると、得られる割合は97となり、ユーザーの総割合は197になります。
これは実際には、その資産をindex(純資産)に基づいてディスカウント処理していることになります。このように処理する理由は、ユーザーの実際の元本と利息の合計がbalanceに現在のindexを掛けたものであるからです。2回目の預金の際、ユーザーの正しい元本と利息の合計は100 * 1.03 + 100 = 203であり、ディスカウント処理を行わなければ、2回目にユーザーが100を預けた後の元本と利息の合計は(100+100) * 1.03 = 206となり、これは誤りです。ディスカウントを行った場合、元本と利息の合計は(100 + 100 / 1.03) * 1.03 = 103 + 100 = 203となり、203の結果は正しいです。
攻撃プロセス
0x25126……403907(hETHWBTCプール)
0x5a63e……844e74(攻撃契約 - 現金化)
初期フラッシュローン資金を借りて、質押
攻撃者はまずAaveからフラッシュローンで2300 WBTCを借り、そのうち2000枚のWBTCをHopeLendに質押(deposit)します。資金はHopeLendのhEthWbtc契約(0x251…907)に移転され、同時に相応の2000枚のhETHWBTCを取得します。
空の貸出プールを利用して初期ディスカウント率(liquidityIndex)を操作
HopeLendからフラッシュローンで2000枚のWBTCを借ります。
現在の価値は1 hETHWBTC = 1 WBTCです。
通常のETHWBTCをWBTCに戻す操作では、交換比率に影響を与えません(利息を得た場合のみ交換比率に影響を与え、1 hETHWBTCはより多くのWBTCを得ることができます)。
この時、ハッカーは一連の複雑な操作を通じてディスカウント率を操作し始めます:
- ハッカーは直接得た2000枚のWBTCをHopeLendのhEthWbtc契約(0x251…907)に直接送金(transfer)します。このステップは返済ではありません。
- ハッカーはその後、前のステップ1で質押(deposit)したほとんどのWBTC(1999.999…)を引き出します。したがって、前のステップでプール内の資産を補充するためにWBTCを戻す必要があります。
- 最後に、ハッカーは最小単位(1e-8)のhEthWbtcのみを保持します。ここでは完全に引き出すことはできません。なぜなら、ディスカウント率(liquidityIndex)を計算する際に、既存のものに新たなものを加える必要があるからです。ゼロにすると、ディスカウント率(liquidityIndex)が0になり、プール内の比率が不均衡になることができません。
- 前のステップで破棄したほとんどのhEthWbtcをWBTCに戻し、以前のフラッシュローンの残りのWBTCを加え、HopeLendプールに借りたフラッシュローンを返済します。合計で2001.8枚のWBTCを支払います(その中には利息1.8枚のWBTCが含まれています)。
- 上記のプロセスでほとんどのhEthWbtcが破棄され、ハッカーのアカウントには最小単位(1e-8)のhEthWbtcのみが残ります。これにより、hETHWBTCの総量が減少し、貸出プールには2001.8枚のWBTCが存在します。この時のディスカウント率(liquidityIndex)は驚異的な126,000,000に達します。
ここで知識が関与します。預金ユーザーの利息は基本的にプール内の流動性の増加から来ており、貸出プールは預金率と使用率に基づいて、貸出と預金の利率を動的に調整します。
ここで、プールがフラッシュローン利息(1.8 WBTC)から追加の流動性を得ると、70%(126,000,000)がliquidityIndexに計上されます。この数値は、各単位の預金(hEthWbt)のディスカウント価値を計算するために使用されます。
プールがハッカーの操作前に空であったため、返済後のtotalLiquidityは1のみで、amountは126000000、初期liquidityIndexは1で、結果は126000001となります。
ディスカウント率をさらに拡大
ハッカーは引き続きHopeLendからフラッシュローンで2000枚のWBTCを借り、毎回追加の1.8枚のWBTCを返済し、毎回LiquidityIndexを126,000,000ずつ累積させます。
ハッカーはこのプロセスを60回繰り返し、最終的にliquidityIndexは7,560,000,001に達し、攻撃者が保有する1つの最小単位のhEthWBTCのディスカウント価値は75.6 WBTC(約214万ドル)に達しました。
これにより、ハッカーはhEthWBTCを操作し、その価値を歪めました。
他の資金が存在する貸出プールを掏り出し、利益を形成
攻撃者は続いて1つの最小単位のhEthWBTCを担保に、HopeLendの他の5つのトークンプールから大量の資産を借り出しました。
含まれるもの:
- 175.4 - WETH
- 145,522.220985 - USDT
- 123,406.134999 - USDC
- 844,282.284002229528476039 - HOPE
- 220,617.821736563540747967 - stHOPE
これらのトークンは利益としてUniswapでWBTCとWETHに交換され、各種手数料を差し引いた後、最終的にハッカーは約263枚のWETHを得ました(賄賂payloadの263.9枚のWETHを除く)。
なぜハッカーは他のプールから大量の資金を借りることができたのか:
借入または預金を引き出す際、貸出契約はユーザーの担保資産の状況を検証し、借出が担保を超えないことを確認します。
以前のディスカウント率がハッカーによって操作され、ディスカウント率がnormalizedIncomeの乗数で担保価値計算に計上されるため、彼の手元の1単位のhEthWBTCの担保価値は75.6 WBTCに達しました。
他のプールから借入するたびに、ハッカーは担保資産の検証を簡単に通過しました。
この時、攻撃者はHopeLendに2000+1.8*60枚のWBTCを投入してliquidityIndexを操作し、1単位のhEtthWBTCのみを残しました。
重要な脆弱性ポイント(整数除法の誤り)を利用して現金化
以前の投入したwBTCを引き出すために、攻撃者は別の攻撃契約を展開しました:0x5a63e……844e74、そしてその中のwithdrawAllBtc()メソッドを呼び出しました:
脆弱性のプロセスは次の通りです:
- まず151.20000002枚のwBTCを預け、現在のliquidityIndex(1最小単位hEthWBTC=75.6wBTC)に基づいて、攻撃者は2つの最小単位のhEthWBTCを得ます。
- 113.4枚のwBTCを引き出し、その対応するhEthWBTCの割合を逆算し、hEthWBTCをburn操作します。
- 113.4枚のwBTCを破棄するには1.9999999998最小単位のhEthWBTCが必要ですが、div関数の精度の問題により、1つの最小単位のhEthWBTCのみが破棄され、したがって利用可能な脆弱性となり、ハッカーは1つの最小単位のhEthWBTCを保持することができます。
重要な脆弱性
hEthWBTCのburnメソッドは高精度の除法rayDivを呼び出しました。
ここで:
a=11340000000(引き出す予定のWBTC)
b=7560000001000000000000000009655610336(ディスカウント率)
(a*1e27+b/2)/b = 1.9999999998ですが、solidityに組み込まれているdivメソッドは切り捨てて1を返します。これは、11340000000 / 7560000001の除法後に小数点以下が切り捨てられたことを意味します。
0x5a63(攻撃契約 - 現金化)は引き続き75.60000001 WBTCを預け、ちょうど1つの最小単位のhEthWBTCを得ることで、2つの最小単位のhEthWBTCを保持し続けます。
このように、113.40000000 WBTCを引き出し、75.60000001 WBTCを預ける操作を繰り返すことで、攻撃者は毎回37.8枚のWBTCを無から得ることができます。
58回のループの後、攻撃者は以前に投入したすべてのwBTCを引き出し、Aaveのフラッシュローンを無事に返済しました。
結論
hEthWBTC貸出プールが初期化されていなかったため、攻撃者はliquidityIndexを容易に操作し、極大に増加させました。引き出し率が除数として大きく増幅され、整数除法の切り捨て誤差により、以前の投入を1つのブロック内でより容易に引き出すことができました。
運転が良好な貸出プールでは、プール内にすでに流動性が存在するため、少量の貸出利息の増加によってディスカウント率が大きく増加することは難しいです。