前回の記事でも強調したように、Awesome Uniswap v4 Hooks リポジトリ[1]に掲載されているプロジェクトの 30%以上 に脆弱性が存在します。ここで言及する脆弱性は、Uniswap v4 のインタラクションに特有のものであることに注意が必要です。そこで本記事では、以下の2つの観点からセキュアなフックのインタラクションロジックを精査します:
- 不適切なアクセス制御
- 不適切な入力検証
各カテゴリについて、脆弱性の分析から始め、対応するProof-of-Concept(PoC)を提示することで、その悪用可能性を示します。その後、潜在的な緩和策についての議論を行います。
不適切なアクセス制御
一般的に、Uniswap v4 のフックに関連するインタラクションは、フックがロッカーとして機能するかどうか、すなわちプールで操作を実行するために PoolManager でロックを取得するかどうかによって分類できます。適切なアクセス制御が必要な主要なインタラクションシナリオは2つあります:
- Hook-PoolManager インタラクション:公式のコールバック関数と
PoolManagerとの間のインタラクションです。コールバック関数には、8つのプールアクションコールバック(すなわち、initialize、modifyPosition、swap、donate)とロックコールバック(すなわち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))
}
}
カスタムロジックを実行するために、lockAcquired は data バイトを受け取り、その 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 コールバック関数を検証してみましょう:

明らかに、上記の関数は機密性の高い操作を実行するように設計されています。しかし、不適切なアクセス制御により、悪意のあるアクターが引数(例えば key や params)を操作することで悪用され、予期しない動作を引き起こす可能性があります。例えば、afterSwap コールバックは PoolManager でスワップが既に行われたという前提で動作する可能性があります。その後、現在の価格や収集されたスワップ手数料などの重要な状態情報を記録するためのアクションを開始する可能性があります。しかし、afterSwap が PoolManager からの呼び出しのみに厳密に制限されていない場合、悪意のあるアクターが params パラメータを偽造し、記録された状態が歪められる可能性があります。
エクスプロイト & PoC
簡潔さのために、基本的な PoC を使用してこのアクセス制御の問題を説明します。一般的に、フックの beforeInitialize は PoolKey 型パラメータを受け取ります。このパラメータの hooks フィールドにはこのフックのアドレスが含まれている必要があります(PoolManager はこのフィールドを使用して呼び出すフックアドレスを決定するため)。
スクリーンショットは、DiamondHookPoC [3] で見られるような、不適切なアクセス制御を持つフックの悪用を実証する PoC を示しています。
beforeInitialize コールバック関数にアクセス制限がないため、悪意のあるアクターはこの関数に任意の poolKey を渡すことができます。フックは、この poolKey のフックが現在のフックアドレスと一致するかどうかを検証しません。

このシナリオでのエクスプロイトがフックに金銭的損失をもたらさない可能性があることに注意することは重要ですが、それでも保護されていないコールバック関数を通じてフックの状態がどのように操作され得るかを劇的に示しています。
緩和方法
Hook-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();
_;
}
このモディファイアは、機密性の高いフックおよびロックコールバックに適切なアクセス制御を適用するために効果的に使用できます。
一方、Hook-Internal インタラクションの存在により、BaseHook で指定された lockAcquired コールバックを通じて呼び出される重要な状態変更関数は、任意に呼び出せないようにする必要があります。
この要件を満たすために、BaseHook は selfOnly モディファイアを提供しています。このモディファイアは、宣言された関数のアクセシビリティをフック自身に限定し、外部コントラクトがこれらの機密関数を悪意ある目的で直接呼び出すことを禁止します。
/// @dev Only this address may call this function
modifier selfOnly() {
if (msg.sender != address(this)) revert NotSelf();
_;
}
まとめると、BaseHook を継承することで、カスタムフックはこれらの組み込みアクセス制御モディファイアとコールバックを活用して、適切なアクセス制御を強制することができます。
不適切な入力検証
v4-periphery[4] の BaseHook は、フック開発者が活用できる安全なインタラクションロジックのためのソリューションを提供しています。しかし、既存のフックにおける不適切な使用例が依然として観察され、新たな攻撃ベクトルの可能性を生み出しています。
デフォルトでは、フックは PoolManager の initialize 関数を通じて任意のプールが登録されることを許可しています。しかし、フックが登録プールの基礎となるアセットを検証しない場合、悪意のあるユーザーが偽のトークンを含むプールを登録し、トークンの 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 関数を見てみましょう。この関数はロックを取得した後、テイクプロフィット注文を満たすためにスワップを実行します。
![Figure 3: The _handleSwap function of Take Profits Hook[5]](https://assets.blocksec.com/frontend/blocksec-strapi-online/take_profit_handle_Swap_36133dc1fe.jpg)
この関数はアクセス制御モディファイアで保護されていないことに気づくかもしれません。しかし、250行目がアクセスを効果的に制限しており、PoolManager からロックを取得した後にのみこの関数を呼び出せるようにしています。そうでなければ、オペレーターが最新のロッカーでないため、poolManager.swap は失敗します。言い換えれば、登録されたプールが検証されている場合、_handleSwap は特定の順序で呼び出される必要があります。残念ながら、フックはそのような検証を実装していません。
この不適切な実装により、フックは再入攻撃に対して脆弱となっています。この脆弱性により、攻撃者はユーザーが預けた資金を使って任意のスワップを強制的に実行できる可能性があります。
エクスプロイト & PoC
具体的には、攻撃は以下のステップで実行できます:
- 攻撃者が偽のトークンを含む悪意のあるプールを登録し、Take Profits Hook をプールのフックとして指定します。
- 攻撃者はフックを通じて悪意のあるプールにストッププロフィット注文を設定します。
- 攻撃者は悪意のあるプールでスワップを実行し、
afterSwapコールバックのfillOrderをトリガーして攻撃者のストッププロフィット注文を満たします。 - フックは
PoolManagerのlock関数を呼び出してロックを要求し、lockAcquiredコールバックで_handleSwap関数を呼び出します。 _handleSwap関数において、トークンの転送が偽のトークンコントラクトの悪意のあるロジックをトリガーし、_handleSwap関数に再入します。_handleSwapはアクセス制限のない外部関数であるため、これが可能です。ロックがすでに取得されているため、攻撃者はフックが十分な基礎資産を保有している限り、任意のプールで任意のスワップを強制実行できます。攻撃者はその後、スワップをサンドイッチして他のユーザーを犠牲にして利益を得ることができます。
以下の詳細な図は、攻撃のフローを示しています。

前述の通り、フック自体は悪意のあるロジックを呼び出しません。唯一の問題は、フックが信頼できないトークンプールが PoolManager コントラクトに登録されることを防いでいないことです。間接的に、偽のトークンコントラクトの悪意のあるロジックがトークン転送操作を通じて呼び出されますが、これも一種の信頼できない外部呼び出しです。
緩和方法
不適切な入力検証による潜在的な攻撃を緩和するための3つの実行可能なアプローチがあります:
-
適切なアクセス制御。
BaseHookのビルディングブロックを活用することで、フックは関数のアクセシビリティを厳密に管理できます。これにより、任意のアカウントが機密関数を呼び出すことを防ぎます。 -
再入ロック。上記の攻撃シナリオでは、このアプローチは悪意のあるトークンロジックが機密関数に再入することを防ぐことができます。ただし、場合によってはフックの設計がフック自体の再入を必要とすることがあります。 具体的には、フックがいくつかのプールアクションを実行する必要がある場合、これらのアクションを完了するために
PoolManagerがそのコールバックに再入することを許可する必要があります。再入ロックはこの意図した機能を破壊する可能性があります。 -
ホワイトリスト方式。これは、特権的な管理者がフックで承認されたプールをホワイトリストに登録することを要求します。管理者は、ホワイトリストに登録されたプールが潜在的なリスクをもたらさないことを確認します。ただし、制限として、フックユーザーはフックを通じて管理者が承認した限られた数のプールでのみ操作を実行できます。 ホワイトリスト方式はセキュリティを向上させますが、フックの機能を著しく制限します。
フックのセキュリティと使いやすさのバランスをとる完璧なソリューションを見つけることは困難です。いくつかの緩和アプローチについて議論しますが、開発者はフックの設計においてトレードオフを慎重に検討する必要があります。目標は、意図した機能を維持しながら、潜在的なリスクをできる限り軽減することであるべきです。さらに、ここでの議論は Uniswap v4 の機能に特有のインタラクションに存在する可能性のある脆弱性のみを対象としています。実際のアプリケーションは間違いなくより包括的なものになるでしょう。コントラクトの一行一行を理解するようにし、常に SAFU でいてください!
結論
本記事では、フックのインタラクションロジックにおいて発生する脆弱性を探求し、特に2つのシナリオ(不適切なアクセス制御と不適切な入力検証)に焦点を当てました。詳細な脆弱性分析を提示し、PoC とともに潜在的な悪用方法を示し、潜在的な緩和策について議論しました。これらの知見がフックの安全な開発と使用に貢献し、脆弱性検出における将来の取り組みを導くことを願っています。
参考文献
[2] Stop Loss Order
[3] DiamondHookPoC
[4] v4-periphery
[5] Take Profits



