Uniswap v4 フックのセキュリティ分析:アーキテクチャ、脆弱性、ベストプラクティス

icon MarsBit
共有
AI summary icon概要

Uniswap v4 のメインネット稼働以降、Hook メカニズムは DeFi で最も注目されている革新の一つとなった。Base チェーン上の memecoin エミッションプラットフォーム Flaunch は Hook を使用して、固定プレセール価格と自動清算上場メカニズムを実現している。流動性プロトコル Bunni v2 は Hook を活用して、プログラマブルな流動性と再抵当モデルを構築している。今年、SATO、uPEG(Unipeg)、Slonks などの Hook を活用したトークンは短期間で数十倍の価格上昇を記録した。

Hookエコシステムの繁栄の一方で、Hookの実装欠陥を狙った攻撃が顕著に増加しています。本記事では、Uniswap v4のHookメカニズムを出発点とし、そのコアなコールスタックを段階的に分析することで、プロジェクト側が潜在的な脆弱性を理解するお手伝いをします。

Uniswap v4 フック セキュリティ

1. イントロダクション

Uniswap v4 における v3 との最も顕著なアーキテクチャの変更は、Hook(フック)メカニズムの導入です。これにより、開発者はカスタムコントラクトを流動性プールのライフサイクルイベントにアタッチし、スワップ、流動性の追加・削除、初期化などのポイントで任意のロジックを挿入できます。

v4の主な変更点は以下の通りです:

シングルトンパターン:すべてのプールの状態は、個々のプールごとに独立した契約をデプロイするのではなく、単一のPoolManager契約によって集中して管理されます。

- フラッシュ会計:取引プロセス中の中间残高の変動は一時的なストレージにのみ記録され、取引終了時に一括で決済されます。

- Hookメカニズム:各プールはHookコントラクトをバインドでき、PoolManagerは关键なノード(beforeInitialize、beforeSwap、afterAddLiquidityなど)でそのコントラクトをコールバックします。

- フックは変更不可:プールの初期化が完了すると、バインドされたフックアドレスは永久に固定されます(プールにバインドされたフックアドレスは変更できませんが、フックコントラクト自体のアップグレード可能性はその実装方式に依存します)。

画像

v3 時代には、開発者は Uniswap プロトコル自体を信頼すればよかったが、v4 時代では、各プールのセキュリティはそのプールにバインドされた Hook に依存する。Hook により、AMM は固定された金融プリミティブからプログラマブルな金融インフラへと拡張されたが、セキュリティモデルは「プロトコルレベル」から「プールレベル」へと分散化された。

2. フックアーキテクチャ

2.1 PoolManager と unlock/callback モデル

v4のコアコントラクトはシングルトンのPoolManagerです。プールの状態変更操作(スワップ、流動性の追加または削除)は、すべてまずPoolManager.unlock()を呼び出して一回限りのコールバック権限を取得し、その後unlockCallback()内で具体的な処理を完了しなければなりません。処理全体が終了したとき、PoolManagerは帳簿のバランスが保たれているか検証します:

画像

NonzeroDeltaCount != 0 の場合、直ちに revert されます。これは v4 フラッシュ会計の核心的な制約です。どのフックも実行中に一時的に勘定を不均衡にすることが可能ですが、取引終了前に自ら決済しなければなりません。そうでない場合、その取引全体がロールバックされます。

各プールは、hooksフィールドを含むPoolKey構造によって一意に識別されます。

画像

PoolId は keccak256(PoolKey) によって計算されるため、hooks アドレスが異なると異なるプールが生成されます。これはまた、PoolManager が特定の Hook アドレスが以前他のプールで使用されていたかどうかを検証しないことを意味し、同じ Hook コントラクトを複数のプールが同時にバインドできるということです。

画像

2.2 ホーク権限ビットがアドレスにエンコードされています

v4の直感に反する設計の1つは、フックの権限がコントラクト内部の変数によって決定されるのではなく、フックコントラクトのデプロイアドレスによって決定されることである。

PoolManagerは、Hookアドレスの下位14ビットをチェックして、そのHookがライフサイクルの特定のポイントで呼び出される必要があるかどうかを判断します。

画像

たとえば、BEFORE_SWAP_FLAG = 1 << 7です。Hookアドレスの7ビットが1の場合、PoolManagerはswap前にそのHookのbeforeSwap()を呼び出します。それ以外の場合は、HookコントラクトがbeforeSwap()を実装していても、PoolManagerは決して呼び出しません。

これにより、Hookをデプロイする際、CREATE2 + saltを用いてアドレスを計算し、ターゲット権限と完全に一致する下位ビットを持つアドレスを構築する必要があります。Uniswap公式では、この目的のためにHookMinerツールを提供しています:

画像

権限ビットと関数実装が一致しない場合、2つの問題が発生します:

(1) 特定なhook関数を実装したが、アドレスが対応する権限ビットにエンコードされていない——PoolManagerはこの関数を決して呼び出さず、ロジックは実質的に無効である

(2) アドレスに特定の権限ビットがエンコードされていますが、フックが対応する関数を実装していません——PoolManager がコールバックを実行する際にリバートが発生し、DOSを実現したり、戻り値の検証に失敗したりして、関連する操作が実行できなくなります。

これはまたHookのアップグレードにおける自然な制約でもあります:Hookがプロキシを通じてアップグレード可能である場合、デプロイアドレスはアップグレード中に変更されないため、アップグレード後は既存のHook関数の実装を変更するのみで、新しいHookタイプを追加することはできません。将来の拡張性を確保するには、初期デプロイ時に使用する可能性のあるすべての権限ビットを事前に確保しておく必要があります。

2.3 BaseHook と一般的に見過ごされがちなアクセス制御の罠

Uniswap v4 パーリフェリの旧バージョンで提供されるBaseHook抽象コントラクトは、開発者がカスタムフックを実装するために継承できます。BaseHookの重要な役割の一つは、unlockCallback()関数にonlyPoolManager修飾子を提供することです。

画像

しかし——ここには非常に見過ごされやすい設計上のトラップがあります——早期バージョンのBaseHookは、unlockCallbackにのみonlyPoolManagerを適用し、その他のフックコールバック関数(beforeSwap、afterSwap、beforeAddLiquidityなど)には一切の保護を施していません。これらの関数のアクセス制御は、フック開発者が明示的に追加する必要があります。

3. Hook のライフサイクルコードレビュー

exact-input swap の例を用いて、ユーザーが取引を開始してから決済に至るまでの完全なコールスタックを分析します。

3.1 プールの初期化とフックのバインド

誰でもPoolManager.initialize()を呼び出して新しいプールを作成できます。

画像

isValidHookAddress は、アドレスの権限ビットと fee フィールドの互換性のみを検証し、Hook が他のプールで既に使用されているかどうかや、その Hook がこの PoolKey を受け入れる「意思」があるかどうかは検証しません。Hook の設計時に beforeInitialize にホワイトリストや単一プールバインドのロジックが含まれていない場合、誰でも同じ Hook を使用し、トークンペアを任意に設定した新しいプールを構築して、Hook の後続のすべてのコールバックをトリガーできます。

3.2 beforeSwap と BeforeSwapDelta

スワップのフローのエントリポイントはPoolManager.swap()であり、核心的なスワップロジックを実行する前にHooks.beforeSwap()を呼び出します:

画像

beforeSwap の返値は3つの要素からなるタプル(bytes4、BeforeSwapDelta、uint24)です:

- bytes4:IHooks.beforeSwap.selectorと等しくなければ、PoolManagerは直ちにrevertする

- BeforeSwapDelta:このスワップにおいて、指定されたトークンと指定されていないトークンに対するデルタ調整

- uint24:動的LP手数料率のカバー値(プールが動的手数料を有効にしている場合にのみ適用)

BeforeSwapDelta は int256 のエイリアスであり、上位 128 ビットは指定されたトークンのデルタ(ユーザーが指定した数量のトークン)であり、下位 128 ビットは未指定のトークンのデルタである:

画像

注意:BeforeSwapDelta の意味は、フックが手数料を徴収する場合は正の値を返し、フックがトークンを返却する場合は負の値を返すことです。開発者は符号を間違えやすいです。また、specified と unspecified の対応関係は、params.zeroForOne と amountSpecified の符号に依存するため、書き方を少しでも間違えるとトークンが誤って配置されます。

PoolManager は、beforeSwap が返す specifiedDelta を amountToSwap に直接加算します。

画像

この行には重要な意味が含まれています:Hookはスワップ金額をフリーズできます。hookDeltaSpecifiedが-params.amountSpecifiedと等しい場合、amountToSwapは直接ゼロになり、Hookがこのスワップを完全に制御することを意味します。これがいわゆるAsync HookまたはCustom Curve Hookです。

Async Hook は v4 におけるリスクが最も高い設計パターンです。これは本質的に、Hook 自身のロジックを用いて Uniswap のスワップロジックを置き換えるものです。Hook に脆弱性がある、または悪意のあるものである場合、ユーザーの資金は Uniswap のネイティブな価格決定ロジックによって保護されず、主に Hook 自体の実装の正確性に依存することになります。

3.3 デルタ決済とNonzeroDeltaCount

beforeSwap と afterSwap が返すデルタは、即座に送金をトリガーするのではなく、PoolManager の内部帳簿に記録されます。

画像

トークンの累計デルタがゼロから非ゼロに変化すると、NonzeroDeltaCountは増加し、ゼロに戻ると減少します。2.1で述べたように、unlock()が終了した時点でNonzeroDeltaCount != 0である場合、トランザクション全体はリバートされます。

Hook は、settle()(PoolManager への送金)と take()(PoolManager からの引き出し)の二つのアクションを通じて自身のデルタをバランスさせます:

画像

このメカニズムがもたらすセキュリティの意味は明確です:すべてのユーザーは最終的にポジションを清算しなければなりません。しかし、これは「帳簿の恒常性」を保証するだけで、「帳簿の正確性」を保証しません。HookがbeforeSwap内で悪意を持って構築されたデルタを返した場合、PoolManagerはそのデルタを忠実に記録し、最終的に清算されれば取引は成功します——たとえHookが業務状態を偽造することで、システムが攻撃者が特定の資産権益を保有していると誤認してしまう場合でも、PoolManagerはこのような業務レベルのエラーを検出できません。

以前、Cork Protocolのセキュリティイベントは、そのHookに脆弱性があったことが原因でした。しかし、攻撃を受けた前には、すでに4社の監査会社による監査を受けていました。事後の検証で私たちは発見しました:

四つの監査のうち、三つはCorkHook契約を範囲に含んでいません

CorkHookを監査した唯一の企業は、一部のコード問題を特定し改善提案を提出しましたが、アクセス制御の問題までは網羅していません。

別の監査機関がその報告書で明確に提案している:「この監査範囲内で検証された異なるコンポーネントによって呼び出されるCorkHook関数の不変条件を証明することを、興味深いフォローアップ業務とすることが可能である」。この提案は、事後的な振り返りの観点から非常に的確である。

これはv4 Hook時代における新たな監査の盲点を露呈している:プロトコルの複雑さの爆発的増加により、スコープの設定自体がセキュリティ上の意思決定となるようになった。Hookとプロトコルの他のコントラクトとの相互作用の連鎖は非常に長く、Hookコントラクトのみを監査しても、コントラクト間の組み合わせ問題を発見することはできない。逆に、周辺のコントラクトを監査してHookをスコープ外に置くと、v4時代における最大の攻撃面を見落とすことになる。

4. 反省

プロトコルメカニズムとCork攻撃を並べて振り返ることで、v4 Hookセキュリティモデルのいくつかの核心的なポイントを要約できる:

(1) Hook コールバック関数が PoolManager が提供する呼び出しコンテキストに依存する場合、明示的に PoolManager からのみ呼び出されることを制限する必要があります。BaseHook は開発者に代わってこれを実行しません。これは v4 と一般的な契約監査の経験との間で最も衝突しやすい設計上の落とし穴です。

(2) フックとプールのバインド関係はPoolManagerの制限を受けません。開発者はbeforeInitialize内でプールのホワイトリストまたは単一プールバインドを自ら実装する必要があります。

(3) ホークアドレスの権限ビットは、関数実装と厳密に一致しなければなりません。計算されたアドレスには、今後の拡張で追加される可能性のあるすべての権限ビットを事前に含める必要があります。

(4) Async / Custom Curve Hook は、完全にカスタム化されたスワップ実装です。Uniswap プロトコルレベルでの保護は一切なく、”完全な自己管理型金融契約”の基準で監査される必要があります。

(5) デルタの会計的な「保存」は「正しさ」を意味しない。NonzeroDeltaCount == 0 は帳簿の最終的なバランスを保証するだけで、帳簿の内容が悪意を持って操作されていないことを保証しない。

(6) クロスマーケットでのトークンタイプの混同は、v4時代の新たな攻撃面である。プロトコルがユーザーに市場の作成を許可する場合、トークンのセマンティック検証は必須であり、インターフェースチェックのみに頼ってはならない。

各フックは独立した信頼領域であり、各プールのセキュリティはそのバインドされたフックによって決まります。したがって、フックのセキュリティ監査の複雑さは「1つのコードを監査する」から「1つの完全なサブプロトコルを監査する」へと変化し、これはプロジェクト側と監査側双方にとって方法論の進化を意味します。

元の記事を表示

免責事項: 本ページの情報はサードパーティからのものであり、必ずしもKuCoinの見解や意見を反映しているわけではありません。この内容は一般的な情報提供のみを目的として提供されており、いかなる種類の表明や保証もなく、金融または投資助言として解釈されるものでもありません。KuCoinは誤記や脱落、またはこの情報の使用に起因するいかなる結果に対しても責任を負いません。 デジタル資産への投資にはリスクが伴います。商品のリスクとリスク許容度をご自身の財務状況に基づいて慎重に評価してください。詳しくは利用規約およびリスク開示を参照してください。