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

この記事では、フックインタラクションロジック中に発生する脆弱性について、特にアクセス制御の不備と不適切な入力検証の2つのシナリオに焦点を当てて探求します。

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

この記事では、Uniswap v4 の特定のインタラクション中に発生する可能性のある 2 種類の問題に主に焦点を当てます。

前回の記事で強調したように、「Awesome Uniswap v4 Hooks」リポジトリ[1]のプロジェクトの 30% 以上に脆弱性が存在しています。ここで言及する脆弱性は、Uniswap v4 のインタラクションに固有のものであることに注意してください。したがって、この記事では、安全なフックインタラクションロジックを次の 2 つの観点から精査します。

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

各カテゴリについて、脆弱性の分析から始め、対応する Proof-of-Concept (PoC) を提供することで、その潜在的な悪用方法を実証します。その後、潜在的な軽減策について議論します。

不十分なアクセス制御

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

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

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

フック内部 インタラクションは、やや複雑です。実際には、多くのフックプロトタイプは、純粋なフックとして機能する以上のことを行います。一部の開発者は、フックがユーザーに資金管理機能を提供するようにしています。これらの機能はフックコントラクトに実装されていない場合がありますが、このコンテキストではまとめてフックと見なすことができます。これらの場合、フックはユーザー資金を受け入れ、流動性管理やスワップなどのプール操作を実行します。これは、コントラクトが PoolManager からロックを取得する必要があることを意味し、フックをロッカーに変えます。 Uniswap Foundation はこの状況を考慮し、フックテンプレートに機能 hook template を統合しました。具体的には、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 によってトリガーされるフック内部インタラクションによるセキュリティ問題につながる可能性があります。フック設計は非常に柔軟であるため、この状況のすべての可能なシナリオを網羅することはできないことに注意してください。ここでの主な焦点は、フックがロックを取得し、その後の内部インタラクションにあります。他の潜在的なビジネスロジックを掘り下げると、この議論には複雑すぎます。

どちらのシナリオでも、これらの関数には明確なインタラクションエンティティがあることを考えると、悪用される可能性のある不十分なアクセス制御に対処することが優先事項です。後続のサブセクションでは、各シナリオを順に調べ、より安全なインタラクションロジックを確保するために必要なアクセス制御について説明します。

脆弱性分析

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

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

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

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

Awesome Uniswap v4 Hooks リポジトリにリストされている例の 1 つ、つまり Stop Loss Order を例に取ります[2]。

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

その afterSwap コールバック関数を見てみましょう。

Figure 1: Stop Loss Order の afterSwap 関数[2]

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

悪用 & PoC

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

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

Figure 2: PoolKey.hooks をゼロアドレスに設定可能

このシナリオでの悪用は財政的損失を引き起こさない可能性があることに注意することが重要ですが、それでも保護されていないコールバック関数を介してフックの状態が操作される方法を劇的に強調しています。

軽減策

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

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

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

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

一方、フック内部インタラクションの存在は、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: Take Profits Hook の _handleSwap 関数[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: 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