Back to Blog

致命的な統合:危険な相互作用によるフックの脆弱性

Code Auditing
November 20, 2023
10 min read

前回の記事でも強調したように、Awesome Uniswap v4 Hooks リポジトリ[1]に掲載されているプロジェクトの 30%以上 に脆弱性が存在します。ここで言及する脆弱性は、Uniswap v4 のインタラクションに特有のものであることに注意が必要です。そこで本記事では、以下の2つの観点からセキュアなフックのインタラクションロジックを精査します:

  • 不適切なアクセス制御
  • 不適切な入力検証

各カテゴリについて、脆弱性の分析から始め、対応するProof-of-Concept(PoC)を提示することで、その悪用可能性を示します。その後、潜在的な緩和策についての議論を行います。

不適切なアクセス制御

一般的に、Uniswap v4 のフックに関連するインタラクションは、フックがロッカーとして機能するかどうか、すなわちプールで操作を実行するために PoolManager でロックを取得するかどうかによって分類できます。適切なアクセス制御が必要な主要なインタラクションシナリオは2つあります:

  • Hook-PoolManager インタラクション:公式のコールバック関数と PoolManager との間のインタラクションです。コールバック関数には、8つのプールアクションコールバック(すなわち、initializemodifyPositionswapdonate)とロックコールバック(すなわち lockAcquired)が含まれます。
  • Hook-Internal インタラクション:ロッカーとして機能するフックコントラクト内で発生するインタラクションに関するものです。

Hook-PoolManager インタラクションは比較的シンプルです。ここではフックは純粋なフックとして機能し、8つのプールアクションコールバックを受け付けます。フック内のロジックは関連するプールに影響を与えないため、フックとプールの間に資金の流れはありません。コールバック関数が提供するパラメータは、必要なストレージの変更や重要な関数パラメータとして使用されます。重要な考慮点は、コールバックパラメータが操作される可能性があるかどうかです。

Hook-Internal インタラクションは、やや複雑です。実際、多くのフックのプロトタイプは純粋なフック以上の機能を持っています。開発者の中には、ユーザーのための資金管理機能をフックに提供させる者もいます。これらの機能はフックコントラクトに実装されていない場合もありますが、ここでは総称してフックと見なすことができます。このような場合、フックはユーザーの資金を受け取り、流動性管理やスワップといったプール操作を実行します。これはコントラクトが PoolManager からロックを取得する必要があることを意味し、フックはロッカーとなります。 Uniswap Foundation はこの状況を考慮し、フックテンプレートに関数を統合しています。具体的には、BaseHook テンプレートは以下のようにロックコールバックとして lockAcquired 関数を提供しています:

    function lockAcquired(bytes calldata data) external virtual poolManagerOnly
returns (bytes memory) {
        (bool success, bytes memory returnData) = address(this).call(data);
        if (success) return returnData;
        if (returnData.length == 0) revert LockFailure();
        // if the call failed, bubble up the reason
        /// @solidity memory-safe-assembly
        assembly {
            revert(add(returnData, 32), mload(returnData))
        }
    }

カスタムロジックを実行するために、lockAcquireddata バイトを受け取り、その data を使って自身に対してローレベルコールを実行します。data はフックのビジネスロジックに依存し、ユーザーによって操作される可能性があるため、lockAcquired によってトリガーされる Hook-Internal インタラクションによるセキュリティ上の問題が生じる可能性があります。フックの設計は非常に柔軟であるため、この状況においてすべての可能なシナリオを網羅することはできません。ここでは主に ロックを取得するフックとその後の内部インタラクション に焦点を当てます。他の潜在的なビジネスロジックを掘り下げると、この議論には複雑すぎる状況になります。

両シナリオにおいて、これらの関数には明確なインタラクションエンティティが存在するため、悪用につながる可能性のある不適切なアクセス制御に対処することが優先事項となります。以降のサブセクションでは、各シナリオを順次検証し、より安全なインタラクションロジックを確保するために必要なアクセス制御について議論します。

脆弱性分析

アクセス制御は、多くのプロジェクトにとって非常に効率的でわかりやすいセキュリティソリューションです。ある関数が特定のエンティティによって呼び出されるように設計されている場合、アクセス制御を組み込む必要があります。アクセス制御の最もよく知られた例は、OpenZeppelin ライブラリの Ownable コントラクトであり、特権的な関数はコントラクトオーナーのみが呼び出せるように要求します。上述した2つのシナリオは、このタイプの制御が適切なケースであることは明らかです。

Hook-PoolManager インタラクション:PoolManager との安全なインタラクションのために、フックはこれらのコールバック関数に必要なアクセス制御を強制する必要があります。具体的には、これらのコールバックは PoolManager のみが呼び出せるようにし、他のアカウントは呼び出せないようにする必要があります。このような制御を確立しなければ、これらの機密インターフェースが悪意のあるアクターによる悪用にさらされる可能性があります。

8つのプールアクションコールバックに加え、PoolManager からロックを取得した後にカスタムロジックを実行するロック(すなわち lockAcquired)コールバックも、この問題に対処する必要があります。

Hook-Internal インタラクション:フック内部のインタラクションに関わる関数も、特定の呼び出し元によって呼び出されるように設計されています。前述の通り、このシナリオには2つのフェーズがあります。まず、ロッカーの lockAcquired 関数が PoolManager によって呼び出されます。これはその関数が msg.sender を PoolManager と要求する必要があることを示しています。次に、フックはそれに応じて関数呼び出しをディスパッチします。BaseHook の設計に基づき、これはフック自身へのローレベルコールによって実装されています。これは、それらの関数が external として定義され、呼び出し元をフックのアドレスに限定する必要があることを示しています。

Awesome Uniswap v4 Hooks リポジトリに掲載されている例の1つ、すなわち Stop Loss Order を例として取り上げます[2]:

Uniswap V4 プールに直接統合されており、ストップロス注文はオンチェーンに投稿され、afterSwap() フックを通じて実行されます。実行を保証するために外部のボットやアクターは必要ありません。

その afterSwap コールバック関数を検証してみましょう:

Figure 1: The afterSwap function of Stop Loss Order
Figure 1: The afterSwap function of Stop Loss Order

明らかに、上記の関数は機密性の高い操作を実行するように設計されています。しかし、不適切なアクセス制御により、悪意のあるアクターが引数(例えば keyparams)を操作することで悪用され、予期しない動作を引き起こす可能性があります。例えば、afterSwap コールバックは PoolManager でスワップが既に行われたという前提で動作する可能性があります。その後、現在の価格や収集されたスワップ手数料などの重要な状態情報を記録するためのアクションを開始する可能性があります。しかし、afterSwapPoolManager からの呼び出しのみに厳密に制限されていない場合、悪意のあるアクターが params パラメータを偽造し、記録された状態が歪められる可能性があります。

エクスプロイト & PoC

簡潔さのために、基本的な PoC を使用してこのアクセス制御の問題を説明します。一般的に、フックの beforeInitializePoolKey 型パラメータを受け取ります。このパラメータの hooks フィールドにはこのフックのアドレスが含まれている必要があります(PoolManager はこのフィールドを使用して呼び出すフックアドレスを決定するため)。

スクリーンショットは、DiamondHookPoC [3] で見られるような、不適切なアクセス制御を持つフックの悪用を実証する PoC を示しています。 beforeInitialize コールバック関数にアクセス制限がないため、悪意のあるアクターはこの関数に任意の poolKey を渡すことができます。フックは、この poolKey のフックが現在のフックアドレスと一致するかどうかを検証しません。

Figure 2: PoolKey.hooks can be set to a zero address beforeInitialize_poolKey_no_hooks_validation.webp

このシナリオでのエクスプロイトがフックに金銭的損失をもたらさない可能性があることに注意することは重要ですが、それでも保護されていないコールバック関数を通じてフックの状態がどのように操作され得るかを劇的に示しています。

緩和方法

Hook-PoolManager インタラクションのセキュリティを確保するために、フックコールバックとロックコールバックの両方が PoolManager のみにアクセスを制限する必要があります。

幸いなことに、Uniswap v4 は v4-periphery リポジトリ[4]の BaseHook を通じてベストプラクティスを提供しています。 BaseHookPoolManager からの呼び出しのみに厳密に制限する poolManagerOnly モディファイアを提供しています:

    /// @dev Only the pool manager may call this function
    modifier poolManagerOnly() {
        if (msg.sender != address(poolManager)) revert NotPoolManager();
        _;
    }

このモディファイアは、機密性の高いフックおよびロックコールバックに適切なアクセス制御を適用するために効果的に使用できます。

一方、Hook-Internal インタラクションの存在により、BaseHook で指定された lockAcquired コールバックを通じて呼び出される重要な状態変更関数は、任意に呼び出せないようにする必要があります。

この要件を満たすために、BaseHookselfOnly モディファイアを提供しています。このモディファイアは、宣言された関数のアクセシビリティをフック自身に限定し、外部コントラクトがこれらの機密関数を悪意ある目的で直接呼び出すことを禁止します。

    /// @dev Only this address may call this function
    modifier selfOnly() {
        if (msg.sender != address(this)) revert NotSelf();
        _;
    }

まとめると、BaseHook を継承することで、カスタムフックはこれらの組み込みアクセス制御モディファイアとコールバックを活用して、適切なアクセス制御を強制することができます。

不適切な入力検証

v4-periphery[4] の BaseHook は、フック開発者が活用できる安全なインタラクションロジックのためのソリューションを提供しています。しかし、既存のフックにおける不適切な使用例が依然として観察され、新たな攻撃ベクトルの可能性を生み出しています。

デフォルトでは、フックは PoolManagerinitialize 関数を通じて任意のプールが登録されることを許可しています。しかし、フックが登録プールの基礎となるアセットを検証しない場合、悪意のあるユーザーが偽のトークンを含むプールを登録し、トークンの transfer 関数を通じてフックに再入することが可能になります。

この脆弱性は微妙で、フック自体が悪意のあるロジックを実行するわけではありません。しかし、フックが PoolManager を呼び出す際、PoolManager と悪意のあるプールの基礎となるアセットとのインタラクションが、PoolManagertake 関数を通じて攻撃者に制御フローを渡す可能性があります。

    /// @inheritdoc IPoolManager
    function take(Currency currency, address to, uint256 amount) external override 
noDelegateCall onlyByLocker {
        _accountDelta(currency, amount.toInt128());
        reservesOf[currency] -= amount;
        currency.transfer(to, amount);
    }

本質的に、この脆弱性はフックユーザーがインタラクションを計画している登録プールに対する不適切な検証から生じます。具体的な例を使ってこの脆弱性を掘り下げ、潜在的な緩和策について議論します。

脆弱性分析

Take Profits Hook[5] は Awesome Uniswap v4 Hooks に掲載されているフックです:

この例では、ユーザーが「テイクプロフィット」ポジションを設定できるフックを構築します。例えば、ETH/DAI プールで現在 1 ETH = 1500 DAI の場合、「1 ETH = 2000 DAI になったら ETH をすべて売る」というテイクプロフィット注文を設定でき、これが自動的に実行されます。

このフックの _handleSwap 関数を見てみましょう。この関数はロックを取得した後、テイクプロフィット注文を満たすためにスワップを実行します。

Figure 3: The _handleSwap function of Take Profits Hook[5]
Figure 3: The _handleSwap function of Take Profits Hook[5]

この関数はアクセス制御モディファイアで保護されていないことに気づくかもしれません。しかし、250行目がアクセスを効果的に制限しており、PoolManager からロックを取得した後にのみこの関数を呼び出せるようにしています。そうでなければ、オペレーターが最新のロッカーでないため、poolManager.swap は失敗します。言い換えれば、登録されたプールが検証されている場合、_handleSwap は特定の順序で呼び出される必要があります。残念ながら、フックはそのような検証を実装していません。

この不適切な実装により、フックは再入攻撃に対して脆弱となっています。この脆弱性により、攻撃者はユーザーが預けた資金を使って任意のスワップを強制的に実行できる可能性があります。

エクスプロイト & PoC

具体的には、攻撃は以下のステップで実行できます:

  1. 攻撃者が偽のトークンを含む悪意のあるプールを登録し、Take Profits Hook をプールのフックとして指定します。
  2. 攻撃者はフックを通じて悪意のあるプールにストッププロフィット注文を設定します。
  3. 攻撃者は悪意のあるプールでスワップを実行し、afterSwap コールバックの fillOrder をトリガーして攻撃者のストッププロフィット注文を満たします。
  4. フックは PoolManagerlock 関数を呼び出してロックを要求し、lockAcquired コールバックで _handleSwap 関数を呼び出します。
  5. _handleSwap 関数において、トークンの転送が偽のトークンコントラクトの悪意のあるロジックをトリガーし、_handleSwap 関数に再入します。_handleSwap はアクセス制限のない外部関数であるため、これが可能です。ロックがすでに取得されているため、攻撃者はフックが十分な基礎資産を保有している限り、任意のプールで任意のスワップを強制実行できます。攻撃者はその後、スワップをサンドイッチして他のユーザーを犠牲にして利益を得ることができます。

以下の詳細な図は、攻撃のフローを示しています。

Figure 4: The Attack Flow
Figure 4: The Attack Flow

前述の通り、フック自体は悪意のあるロジックを呼び出しません。唯一の問題は、フックが信頼できないトークンプールが PoolManager コントラクトに登録されることを防いでいないことです。間接的に、偽のトークンコントラクトの悪意のあるロジックがトークン転送操作を通じて呼び出されますが、これも一種の信頼できない外部呼び出しです。

緩和方法

不適切な入力検証による潜在的な攻撃を緩和するための3つの実行可能なアプローチがあります:

  • 適切なアクセス制御BaseHook のビルディングブロックを活用することで、フックは関数のアクセシビリティを厳密に管理できます。これにより、任意のアカウントが機密関数を呼び出すことを防ぎます。

  • 再入ロック。上記の攻撃シナリオでは、このアプローチは悪意のあるトークンロジックが機密関数に再入することを防ぐことができます。ただし、場合によってはフックの設計がフック自体の再入を必要とすることがあります。 具体的には、フックがいくつかのプールアクションを実行する必要がある場合、これらのアクションを完了するために PoolManager がそのコールバックに再入することを許可する必要があります。再入ロックはこの意図した機能を破壊する可能性があります。

  • ホワイトリスト方式。これは、特権的な管理者がフックで承認されたプールをホワイトリストに登録することを要求します。管理者は、ホワイトリストに登録されたプールが潜在的なリスクをもたらさないことを確認します。ただし、制限として、フックユーザーはフックを通じて管理者が承認した限られた数のプールでのみ操作を実行できます。 ホワイトリスト方式はセキュリティを向上させますが、フックの機能を著しく制限します。

フックのセキュリティと使いやすさのバランスをとる完璧なソリューションを見つけることは困難です。いくつかの緩和アプローチについて議論しますが、開発者はフックの設計においてトレードオフを慎重に検討する必要があります。目標は、意図した機能を維持しながら、潜在的なリスクをできる限り軽減することであるべきです。さらに、ここでの議論は Uniswap v4 の機能に特有のインタラクションに存在する可能性のある脆弱性のみを対象としています。実際のアプリケーションは間違いなくより包括的なものになるでしょう。コントラクトの一行一行を理解するようにし、常に SAFU でいてください!

結論

本記事では、フックのインタラクションロジックにおいて発生する脆弱性を探求し、特に2つのシナリオ(不適切なアクセス制御と不適切な入力検証)に焦点を当てました。詳細な脆弱性分析を提示し、PoC とともに潜在的な悪用方法を示し、潜在的な緩和策について議論しました。これらの知見がフックの安全な開発と使用に貢献し、脆弱性検出における将来の取り組みを導くことを願っています。

参考文献

[1] Awesome Uniswap v4 Hooks

[2] Stop Loss Order

[3] DiamondHookPoC

[4] v4-periphery

[5] Take Profits

このシリーズの他の記事を読む

Sign up for the latest updates
〜598万ドルの損失:Aztec、Raydiumなど|BlockSec週次レポート
Security Insights

〜598万ドルの損失:Aztec、Raydiumなど|BlockSec週次レポート

週次ブロックチェーンセキュリティレポート(2026年6月8日〜15日)では、EthereumとSolanaで4件の重大インシデントを分析し、総損失は約598万ドル。Aztec Connectでは入力検証の欠如によりロールアップの証明経路とL1決済が不整合に。RaydiumではレガシーAMM v3の検証不備でLPトークン償還計算が操作され4プールが流出。両脆弱性は悪用前から数年間存在。入力検証不備、整数オーバーフロー、ガバナンス乗っ取りも検証。

Zcash Orchardの健全性バグ分析 | BlockSec週刊
Security Insights

Zcash Orchardの健全性バグ分析 | BlockSec週刊

2026年6月1日の週、Zcashのオーキャードシールドプールに重大な健全性脆弱性が公表された。halo2 ECCスカラー乗算ガジェットの等式制約の欠落により、二重支払いを通じたZECの検出不能な偽造が可能だった。2022年5月のオーキャード有効化から4年以上存在したこの脆弱性はAI支援監査で発見され、緊急ネットワークアップグレード(NU6.2)で修正された。

ニュースレター - 2026年5月号
Security Insights

ニュースレター - 2026年5月号

2026年5月、DeFiで3件の重大な不正流出が発生。Echo Protocolは鍵流出で約7670万ドル相当のeBTCが不正発行され、StablRは約1280万ドル相当のステーブルコインが不正発行。Verus-Ethereum Bridgeは型検証の不備により、約1170万ドル相当が不正に引き出されました。

Best Security Auditor for Web3

Validate design, code, and business logic before launch. Aligned with the highest industry security standards.

BlockSec Audit