v4-security-foundations について
Uniswap v4 の Hook を開発する人向けのセキュリティガイドです。権限のリスク、トークン処理、Delta 決済、デプロイ前の確認項目を扱い、特に危険な return-delta 権限と会計上の問題を解説します。
主な機能
- 01Hook の脅威モデルと権限リスク
- 02NoOp 攻撃の分析と検出
- 03Delta 会計と決済パターン
- 04アクセス制御とルーター検証
- 05トークンの危険性とチェックリスト
- 06ガス予算とリスク評価
活用シーン
- 01Uniswap v4 Hook の実装前に設計を確認
- 02beforeSwapReturnDelta を有効にした Hook の評価
- 03コールバック権限と Delta 決済ロジックの確認
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ uniswap/ uniswap-ai --skill v4-security-foundationsインストール後、Claude Code で/v4-security-foundationsを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
references/audit-checklist.mdbase-hook-template.mdvulnerabilities-catalog.md詳細
v4 フックのセキュリティ基礎
Uniswap v4 フックを構築するためのセキュリティ優先ガイドです。フックの脆弱性によってユーザー資金が流出する可能性があります。フックのコードを書く前に、これらの概念を理解してください。
脅威モデル
コードを書く前に、v4 のセキュリティ状況を理解してください。
| 脅威領域 | 説明 | 緩和策 |
|---|---|---|
| 呼び出し元の検証 | フック関数を呼び出すのは PoolManager のみにする必要があります | msg.sender == address(poolManager) を検証する |
| 送信者の識別 | msg.sender は常に PoolManager であり、エンドユーザーではありません | ユーザーの識別には sender パラメーターを使用します |
| ルーターのコンテキスト | sender パラメーターが識別するのはユーザーではなくルーターです | ルーターの許可リストを実装します |
| 状態の公開 | トランザクション実行中にフックの状態を読み取ることができます | オンチェーンに機密データを保存しないようにします |
| 再入可能性のリスク | フックからの外部呼び出しによって再入可能性が生じる可能性があります | 再入可能性ガードを使用し、外部呼び出しを最小限に抑えます |
パーミッションフラグのリスクマトリクス
リスクレベルを付した、14 個すべてのフックパーミッションです。
| パーミッションフラグ | リスクレベル | 説明 | セキュリティ上の注意 |
|---|---|---|---|
beforeInitialize | 低 | プール作成前に呼び出されます | プールパラメーターを検証します |
afterInitialize | 低 | プール作成後に呼び出されます | 状態の初期化に安全です |
beforeAddLiquidity | 中 | LP 預け入れ前 | 正当な LP をブロックする可能性があります |
afterAddLiquidity | 低 | LP 預け入れ後 | 追跡や報酬に安全です |
beforeRemoveLiquidity | 高 | LP 引き出し前 | ユーザー資金を引き出せなくする可能性があります |
afterRemoveLiquidity | 低 | LP 引き出し後 | 追跡に安全です |
beforeSwap | 高 | スワップ実行前 | 価格を操作する可能性があります |
afterSwap | 中 | スワップ実行後 | 最終状態を観察できます |
beforeDonate | 低 | 寄付前 | アクセス制御のみ |
afterDonate | 低 | 寄付後 | 追跡に安全です |
beforeSwapReturnDelta | 重大 | カスタムのスワップ数量を返します | NoOp 攻撃ベクトル |
afterSwapReturnDelta | 高 | スワップ後の数量を変更します | 価値を抜き取る可能性があります |
afterAddLiquidityReturnDelta | 高 | LP トークン数量を変更します | LP に不利益を与える可能性があります |
afterRemoveLiquidityReturnDelta | 高 | 引き出し数量を変更します | 資金を盗む可能性があります |
リスクのしきい値
- 低:資金損失を引き起こす可能性は低いです
- 中:慎重な実装が必要です
- 高:実装を誤ると資金損失を引き起こす可能性があります
- 重大:資金の完全な窃取を可能にするおそれがあります
重大:NoOp ラグプル攻撃
BEFORE_SWAP_RETURNS_DELTA パーミッション(ビット 3)は、最も危険なフックパーミッションです。悪意のあるフックは、次のことができます。
- スワップ全体を処理したと主張するデルタを返す
- PoolManager がこれを受け入れ、取引を決済する
- フックは出力トークンを渡さずに入力トークンをすべて保持する
- ユーザーはスワップ額の全額を失う
攻撃パターン
// MALICIOUS - DO NOT USE
function beforeSwap(
address,
PoolKey calldata,
IPoolManager.SwapParams calldata params,
bytes calldata
) external override returns (bytes4, BeforeSwapDelta, uint24) {
// Claim to handle the swap but steal tokens
int128 amountSpecified = int128(params.amountSpecified);
BeforeSwapDelta delta = toBeforeSwapDelta(amountSpecified, 0);
return (BaseHook.beforeSwap.selector, delta, 0);
}
検出
beforeSwapReturnDelta: true を持つフックとやり取りする前に、必ず以下を実施してください。
- フックのコードを監査する - 正当なユースケースであることを確認します
- 所有権を確認する - アップグレード可能ですか?誰がアップグレードできますか?
- 実績を確認する - 信頼できる監査会社による監査を受けていますか?
- 少額から始める - まず最小額でテストします
正当な用途
NoOp パターンは、以下の用途で有効です。
- ジャストインタイム流動性(JIT)
- カスタム AMM カーブ
- インテントベースの取引システム
- RFQ/PMM との統合
ただし、それぞれ慎重な実装と監査が必要です。
デルタ会計の基礎
v4 では、PoolManager を通じた貸借システムが使用されます。
コア不変条件
For every transaction: sum(deltas) == 0
PoolManager は、各アドレスが支払うべき額、または受け取るべき額を追跡します。トランザクションの終了時には、すべての債務を清算する必要があります。
主な関数
| 関数 | 目的 | 方向 |
|---|---|---|
take(currency, to, amount) | PoolManager からトークンを引き出す | トークンを受け取ります |
settle(currency) | PoolManager にトークンを支払う | トークンを送ります |
sync(currency) | PoolManager の残高追跡を更新する | 清算の準備 |
清算パターン
// Correct pattern: sync before settle
poolManager.sync(currency);
currency.transfer(address(poolManager), amount);
poolManager.settle(currency);
よくある間違い
- sync を忘れる:sync しないと清算に失敗します
- 順序を間違える:sync → transfer → settle の順に実行する必要があります
- 一部のみ清算する:トランザクションが無効な状態で残ります
- 二重に清算する:会計上のエラーが発生します
アクセス制御パターン
PoolManager の検証
すべてのフックコールバックで、呼び出し元を必ず検証する必要があります。
modifier onlyPoolManager() {
require(msg.sender == address(poolManager), "Not PoolManager");
_;
}
function beforeSwap(
address sender,
PoolKey calldata key,
IPoolManager.SwapParams calldata params,
bytes calldata hookData
) external override onlyPoolManager returns (bytes4, BeforeSwapDelta, uint24) {
// Safe to proceed
}
重要な理由
このチェックがないと、以下の問題が発生します。
- 誰でもフック関数を直接呼び出せます
- 攻撃者がフックの状態を操作できます
- 偽のコールバックを通じて資金が流出する可能性があります
ルーター検証パターン
sender パラメーターはエンドユーザーではなく、ルーターを示します。ユーザー ID が必要なフックでは、以下を使用します。
許可リストパターン
mapping(address => bool) public allowedRouters;
function beforeSwap(
address sender, // This is the router
PoolKey calldata key,
IPoolManager.SwapParams calldata params,
bytes calldata hookData
) external override onlyPoolManager returns (bytes4, BeforeSwapDelta, uint24) {
require(allowedRouters[sender], "Router not allowed");
// Proceed with swap
}
hookData によるユーザー ID の取得
function beforeSwap(
address sender,
PoolKey calldata key,
IPoolManager.SwapParams calldata params,
bytes calldata hookData
) external override onlyPoolManager returns (bytes4, BeforeSwapDelta, uint24) {
// Decode user address from hookData (router must include it)
address user = abi.decode(hookData, (address));
// CAUTION: Router must be trusted to provide accurate user
}
msg.sender の落とし穴
// WRONG - msg.sender is always PoolManager in hooks
function beforeSwap(...) external {
require(msg.sender == someUser); // Always fails or wrong
}
// CORRECT - Use sender parameter
function beforeSwap(address sender, ...) external {
require(allowedRouters[sender], "Invalid router");
}
トークン処理の危険性
すべてのトークンが標準的な ERC-20 のように動作するわけではありません。
| トークンの種類 | 危険性 | 対策 |
|---|---|---|
| 手数料徴収型 | 受取額が送金額を下回る | 実際の残高変化を測定する |
| リベース型 | 送金なしで残高が変化する | 生の残高を保存しない |
| ERC-777 | 送金コールバックによってリエントランシーが可能になる | リエントランシーガードを使用する |
| 一時停止可能 | 送金がブロックされる | 送金失敗を適切に処理する |
| ブロックリスト | 特定のアドレスがブロックされる | 本番環境のアドレスでテストする |
| 小数桁が少ない | 計算で精度が失われる | 適切なスケーリングを使用する |
安全な残高確認パターン
function safeTransferIn(
IERC20 token,
address from,
uint256 amount
) internal returns (uint256 received) {
uint256 balanceBefore = token.balanceOf(address(this));
token.safeTransferFrom(from, address(this), amount);
received = token.balanceOf(address(this)) - balanceBefore;
}
基本フックのテンプレート
すべての権限を無効にした状態から始め、必要な権限のみを有効にします。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {BaseHook} from "v4-periphery/src/base/hooks/BaseHook.sol";
import {Hooks} from "v4-core/src/libraries/Hooks.sol";
import {IPoolManager} from "v4-core/src/interfaces/IPoolManager.sol";
import {PoolKey} from "v4-core/src/types/PoolKey.sol";
import {BeforeSwapDelta, BeforeSwapDeltaLibrary} from "v4-core/src/types/BeforeSwapDelta.sol";
contract SecureHook is BaseHook {
constructor(IPoolManager _poolManager) BaseHook(_poolManager) {}
function getHookPermissions() public pure override returns (Hooks.Permissions memory) {
return Hooks.Permissions({
beforeInitialize: false,
afterInitialize: false,
beforeAddLiquidity: false,
afterAddLiquidity: false,
beforeRemoveLiquidity: false,
afterRemoveLiquidity: false,
beforeSwap: false, // Enable only if needed
afterSwap: false, // Enable only if needed
beforeDonate: false,
afterDonate: false,
beforeSwapReturnDelta: false, // DANGER: NoOp attack vector
afterSwapReturnDelta: false, // DANGER: Can extract value
afterAddLiquidityReturnDelta: false,
afterRemoveLiquidityReturnDelta: false
});
}
// Implement only the callbacks you enabled above
}
完全な実装テンプレートについては、references/base-hook-template.md を参照してください。
セキュリティチェックリスト
フックをデプロイする前に、以下を確認してください。
| # | チェック項目 | 状況 |
|---|---|---|
| 1 | すべてのフックコールバックで msg.sender == poolManager を検証している | [ ] |
| 2 | 必要に応じてルーターの許可リストを実装している | [ ] |
| 3 | OOG の原因となる、上限のないループがない | [ ] |
| 4 | 外部呼び出しに対する再入防止策を講じている | [ ] |
| 5 | デルタの合計がゼロになる | [ ] |
| 6 | 送金時に手数料が発生するトークンに対応している | [ ] |
| 7 | アドレスをハードコードしていない | [ ] |
| 8 | スリッページパラメーターを遵守している | [ ] |
| 9 | 機密データをオンチェーンに保存していない | [ ] |
| 10 | アップグレードの仕組みが保護されている(該当する場合) | [ ] |
| 11 | 有効にする場合、beforeSwapReturnDelta の使用理由が明確である | [ ] |
| 12 | ファズテストが完了している | [ ] |
| 13 | 不変条件テストが完了している | [ ] |
ガス予算のガイドライン
フックのコールバックは、PoolManager のトランザクションコンテキスト内で実行されます。ガス消費が過剰になると、スワップがリバートしたり、経済的に成立しなくなったりする可能性があります。
コールバック別のガス予算
| コールバック | 目標予算 | 上限 | 備考 |
|---|---|---|---|
beforeSwap | < 50,000 gas | 150,000 gas | すべてのスワップで実行されるため、軽量に保つ |
afterSwap | < 30,000 gas | 100,000 gas | 分析/トラッキングのみ |
beforeAddLiquidity | < 50,000 gas | 200,000 gas | アクセス制御を含む場合がある |
afterAddLiquidity | < 30,000 gas | 100,000 gas | 報酬のトラッキング |
beforeRemoveLiquidity | < 50,000 gas | 200,000 gas | ロックの検証 |
afterRemoveLiquidity | < 30,000 gas | 100,000 gas | トラッキング/会計 |
| 外部呼び出しを伴うコールバック | < 100,000 gas | 300,000 gas | 外部 DEX へのルーティング、オラクル |
よくあるガス関連の落とし穴
- 上限のないループ:動的配列(例:すべてのアクティブなポジション)を反復処理すると、ブロックのガス上限を超える可能性があります。配列サイズに上限を設けるか、ページネーションを使用してください。
- ホットパスでの SSTORE:新しいストレージスロットごとに約 20,000 gas かかります。トランザクション終了後に保持する必要のないデータには、一時ストレージ(
tstore/tload)を優先してください。Solidity >= 0.8.24 が必要で、EVM ターゲットをcancun以降に設定する必要があります。 - 外部呼び出し:コントラクト間の呼び出しごとに、約 2,600 gas の基本コストに加えて、呼び出し先の実行コストがかかります。可能な場合は呼び出しをまとめてください。
- 文字列操作:コールバック内での
string操作は避け、識別子にはbytes32を使用してください。 - 冗長な読み取り:
poolManagerの呼び出しをキャッシュしてください。getSlot0()やgetLiquidity()を繰り返し読み取るたびにガスがかかります。
ガスの測定
# Profile a specific hook callback with Foundry
forge test --match-test test_beforeSwapGas --gas-report
# Snapshot gas usage across all tests
forge snapshot --match-contract MyHookTest
リスクスコアリングシステム
フックのリスクスコアを計算してください(0~33)。
| カテゴリ | 点数 | 基準 |
|---|---|---|
| 権限 | 0-14 | 有効にした権限のリスクレベルの合計 |
| 外部呼び出し | 0-5 | 外部とのやり取りの数と種類 |
| 状態の複雑さ | 0-5 | 変更可能な状態の量 |
| アップグレードの仕組み | 0-5 | プロキシ、管理者関数など |
| トークンの取り扱い | 0-4 | 標準外トークンへの対応 |
監査ティアの推奨事項
| スコア | リスクレベル | 推奨事項 |
|---|---|---|
| 0-5 | 低 | セルフ監査 + ピアレビュー |
| 6-12 | 中 | プロフェッショナル監査を推奨 |
| 13-20 | 高 | プロフェッショナル監査が必須 |
| 21-33 | 重大 | 複数回の監査が必須 |
絶対禁止事項
フックでは、次のことを絶対に行わないでください。
- ユーザーの識別に
msg.senderを絶対に使わないでください - 常に PoolManager です - NoOp 攻撃を理解せずに
beforeSwapReturnDeltaを絶対に有効にしないでください - パスワード、キー、個人を特定できる情報をオンチェーンに絶対に保存しないでください
- ETH の送金に
transfer()を絶対に使わないでください -call{value:}("")を使用してください - トークンの小数桁数を絶対に決めつけないでください - 必ずトークンに問い合わせてください
- 乱数生成に
block.timestampを絶対に使わないでください - 呼び出しでガス上限を絶対にハードコードしないでください
- 外部呼び出しの戻り値を絶対に無視しないでください
- 認可に
tx.originを絶対に使わないでください - フィッシング攻撃の経路となります。悪意のあるコントラクトは、元のユーザーのtx.originを維持したまま呼び出しを中継できます
デプロイ前監査チェックリスト
| # | 項目 | 必須対象 |
|---|---|---|
| 1 | セキュリティ重視の開発者によるコードレビュー | すべてのフック |
| 2 | すべてのコールバックの単体テスト | すべてのフック |
| 3 | Foundry によるファズテスト | すべてのフック |
| 4 | 不変条件テスト | デルタを返すフック |
| 5 | メインネットのフォークテスト | すべてのフック |
| 6 | ガスプロファイリング | すべてのフック |
| 7 | フォーマル検証 | 重大なフック |
| 8 | Slither/Mythril による分析 | すべてのフック |
| 9 | 外部監査 | 中リスク以上のフック |
| 10 | バグ報奨金プログラム | 高リスク以上のフック |
| 11 | 監視/アラートのセットアップ | すべての本番フック |
詳細な監査要件については、references/audit-checklist.md を参照してください。
本番フックの参考例
監査済みの本番フックから学びましょう。
| プロジェクト | 説明 | 主なセキュリティ機能 |
|---|---|---|
| Flaunch | トークンローンチプラットフォーム | マルチシグ管理者、タイムロック |
| EulerSwap | レンディング連携 | 市場ごとのリスク分離 |
| Zaha TWAMM | 時間加重 AMM | 段階的な実行により MEV を低減 |
| Bunni | LP 管理 | 集中流動性ガード |
外部リソース
公式ドキュメント
セキュリティリソース
コミュニティ
- @igoryuzo による v4-hooks-skill - このガイドの着想となったコミュニティスキル
- v4hooks.dev - コミュニティのフック関連リソース
追加の参考資料
- 基本フックテンプレート - 実装用の完全なスターター
- 脆弱性カタログ - 一般的なパターンと緩和策
- 監査チェックリスト - 詳細なデプロイ前チェックリスト