Back to Blog

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

Code Auditing
November 20, 2023
11 min read

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

  • 不適切なアクセス制御
  • 不適切な入力バリデーション

各カテゴリについて、まず脆弱性の分析を行い、対応する概念実証(PoC)を提供することで、潜在的な悪用の可能性を実演します。続いて、考えられる緩和戦略について議論します。

不適切なアクセス制御

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

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

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

Hook-Internal インタラクションはいくぶん複雑です。実際には、多くのフック・プロトタイプが純粋なフックとして以上の働きをします。開発者の中には、ユーザーに対して資金管理機能を提供するフックを許可するケースもあります。これらの機能はフックコントラクト自体には実装されていないかもしれませんが、この文脈ではそれらをまとめてフックとみなすことができます。このような場合、フックはユーザーの資金を受け入れ、流動性管理やスワップなどのプール操作を実行します。これは、コントラクトが PoolManager からロックを取得する必要があり、フックがロッカーに変貌することを意味します。 Uniswap財団はこの状況を考慮し、フックテンプレートに関数を統合しました。具体的には、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 を使用して自分自身に対して低レベル(low-level)コールを実行します。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 リポジトリにリストされている例の一つ、Stop Loss Order を例に挙げます[2]:

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

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

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

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

悪用とPoC

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

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

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

このシナリオでの悪用が必ずしもフックに金銭的損失を与えない可能性があることに留意する必要はありますが、それでも保護されていないコールバック関数を通じてフックの状態がいかに操作され得るかを劇的に示しています。

緩和策

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

幸いなことに、Uniswap v4は v4-periphery リポジトリにおいて BaseHook を介したベストプラクティスを提供しています[4]。 BaseHook は、poolManagerOnly 修飾子を提供し、PoolManager からの呼び出しのみを厳密に制限します:

    /// @dev PoolManagerのみがこの関数を呼び出せる
    modifier poolManagerOnly() {
        if (msg.sender != address(poolManager)) revert NotPoolManager();
        _;
    }

この修飾子は、機密性の高いフックおよびロックコールバックに対して適切なアクセス制御を強制するために効果的に使用できます。

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

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

    /// @dev このアドレスのみがこの関数を呼び出せる
    modifier selfOnly() {
        if (msg.sender != address(this)) revert NotSelf();
        _;
    }

要約すると、BaseHook を継承することで、カスタムフックはこれらの組み込みアクセス制御修飾子とコールバックを活用し、適切なアクセス制御を強制できます。

不適切な入力バリデーション

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

デフォルトでは、フックは PoolManagerinitialize 関数を介して任意のプールの登録を許可しています。しかし、フックが登録プール内の基盤となる資産を検証できない場合、悪意のあるユーザーが偽のトークンを含むプールを登録し、トークンの transfer 関数を介してフックに再入(リエントラント)できるようになります。

この脆弱性は、フック自体が悪意のあるロジックを実行するわけではないため、非常に巧妙です。しかし、フックが PoolManager を呼び出す際、PoolManager と悪意のあるプールの基礎資産との間のやり取りが、PoolManager 内の take 関数を介して攻撃者に制御フローを明け渡す可能性があります。

    /// @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 関数を見てみましょう。この関数は、ロックを取得した後にテイクプロフィット注文を満たすためのスワップを実行します。

図3: Take Profits Hookの_handleSwap関数[5]
図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 が何らかのアクセス制限のない外部関数であるために可能です。すでにロックは取得されているため、攻撃者はフックが十分な基礎資産を保持している限り、任意のプールで強制的にスワップを実行させることが可能です。攻撃者はその後、スワップをサンドイッチして、他のユーザーを犠牲にして利益を得ることができます。

以下の詳細図は攻撃の流れを示しています。

図4: 攻撃フロー
図4: 攻撃フロー

前述の通り、フック自体が悪意のあるロジックを呼び出すわけではありません。唯一のミスは、フックが信頼できないトークンプールが 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

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

Best Security Auditor for Web3

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

BlockSec Audit