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

iconMetaEra
共有
AI summary icon概要
Uniswap v4はHookによりプログラマブルな流動性を実現しているが、権限コーディングやアクセス制御などの欠陥により攻撃の標的となりやすく、Async Hookおよび会計ロジックの誤用リスクに注意が必要である。

文章作成者、出典:Beosin

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

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

Uniswap v4 フック セキュリティ

1. Introduction

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 フラッシュ会計の核心的な制約です。どのフックも実行中に一時的に勘定を不均衡にすることが可能ですが、トランザクション終了前に自らセットルしなければなりません。そうでない場合、トランザクション全体がロールバックされます。

各プールはPoolKey構造体によって一意に識別され、この構造体にはhooksフィールドが含まれます:

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

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

2.3 BaseHook と、しばしば見過ごされるアクセス制御の罠

Uniswap v4 periphery が提供する旧バージョンの 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時代における最大の攻撃面を見落とすことになる。

5. 反省

プロトコルメカニズムと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つの完全なサブプロトコルを監査する」へと変化し、これはプロジェクト側と監査側双方にとって方法論の進化を意味します。

参考資料

(1) Cork Protocol。「2025年5月28日 攻撃後の検証報告。」2025-06-04。https://www.cork.tech/blog/post-mortem

(2) OWASP スマートコントラクトセキュリティトップ10 2026、SC01:アクセス制御脆弱性。https://scs.owasp.org/sctop10/SC01-AccessControlVulnerabilities/

(3) Uniswap v4 ホワイトペーパー。https://app.uniswap.org/whitepaper-v4.pdf

(4) Uniswap v4-core。https://github.com/Uniswap/v4-core

(5) Uniswap v4-periphery。https://github.com/Uniswap/v4-periphery

Beosinは、形式化検証に取り組む世界で最も早い時期のブロックチェーンセキュリティ企業であり、「セキュリティ+コンプライアンス」のエコシステム全体を手がけています。世界10か国以上に拠点を設置し、プロジェクト上場前のコードセキュリティ監査、運用中のセキュリティリスク監視とブロック、盗難資産の回収、仮想資産のマネーロンダリング対策(AML)、および各国の規制要件に準拠したコンプライアンス評価など、「ワンストップ」のブロックチェーンコンプライアンス製品とセキュリティサービスを提供しています。公式アカウントのメッセージボックスからお気軽にお問い合わせください。

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