uniswap-v4-hooks について
Uniswap v4 hook開発向けのセキュリティスキルです。権限、呼び出し元の検証、デルタ会計、トークンのリスク、テストを扱い、プール操作に介入するhookの実装やレビューを支援します。
主な機能
- 01hookコールバックの脅威分析
- 02権限フラグとアドレスエンコード
- 03PoolManagerとルーターの検証
- 04デルタ会計とトークン処理
- 05Foundryによるファズテストと不変条件テスト
- 06デプロイ前のリスク評価
活用シーン
- 01hookコールバックのアクセス制御を設計する
- 02スワップのデルタと精算処理を確認する
- 03トークンや価格計算のリスクを評価する
- 04hookのテストとデプロイ確認を準備する
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ igoryuzo/ uniswapv4-hooks-skill --skill uniswap-v4-hooksインストール後、Claude Code で/uniswap-v4-hooksを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
.gitignoreCONTRIBUTING.mdREADME.mdLICENSEwebsite/index.html詳細
このスキルでは、Uniswap v4 フックを安全に開発する方法を説明します。フックは、ライフサイクルの特定の時点でプールの操作を横取りする外部スマートコントラクトです。セキュリティは最優先事項です。フックに脆弱性があると、ユーザーの資金が流出するおそれがあります。
セキュリティについて考える
フックのコードを書く前に、必ず脅威モデルを評価してください。
誰がフックを呼び出しますか?
- フック関数を呼び出すのは
PoolManagerのみにしてください - フック内の
msg.senderは常にPoolManagerであり、ユーザーではありません senderパラメーターはルーターであり、エンドユーザーではありません
どのような状態が公開されていますか?
- フックはトランザクションの途中で実行されるため、コールバックの間に状態が操作される可能性があります
- 外部呼び出しを介したリエントランシーが発生する可能性があります
- 同じフックを使う複数のプールでストレージを共有すると、予期せぬ問題が発生する可能性があります
デルタで何が問題になる可能性がありますか?
- 入力トークンの量は、必ず出力トークンの量と一致させてください(デルタ計算)
- 反復処理では丸め誤差が蓄積します
- BeforeSwapDelta によって通常のスワップロジックを完全に回避できる可能性があります
重大:NoOp ラグプルの攻撃ベクトル
BEFORE_SWAP_RETURNS_DELTA_FLAG を有効にしたフックは、ユーザーの資金を盗む可能性があります。これは最も危険なフック権限です。
// MALICIOUS EXAMPLE - DO NOT USE
// This hook takes user tokens and returns nothing
function beforeSwap(...) external returns (bytes4, BeforeSwapDelta, uint24) {
Currency input = params.zeroForOne ? key.currency0 : key.currency1;
// Take all user funds
input.take(poolManager, address(this), uint256(-params.amountSpecified), false);
// Return delta saying we took everything, returning zero
return (
BaseHook.beforeSwap.selector,
toBeforeSwapDelta(int128(-params.amountSpecified), 0), // THEFT
0
);
}
beforeSwapReturnDelta: true の場合:
- フックはスワップロジックを完全に置き換えられます
- デルタが amountSpecified と等しい場合、プールの計算はスキップされます
- ユーザーの資金は AMM のカーブを通らず、フックに送られます
防御策: 正当なカスタム AMM を実装する場合を除き、beforeSwapReturnDelta を有効にしないでください。ユーザーはスワップ前にフックの権限を確認してください。
権限フラグ(アドレスエンコーディング)
フックの権限はコントラクトアドレスにエンコードされます。アドレスには特定のビットを設定する必要があります。
| ビット | 権限 | リスクレベル |
|---|---|---|
| 0 | beforeInitialize | 低 |
| 1 | afterInitialize | 低 |
| 2 | beforeAddLiquidity | 中 |
| 3 | afterAddLiquidity | 中 |
| 4 | beforeRemoveLiquidity | 中 |
| 5 | afterRemoveLiquidity | 中 |
| 6 | beforeSwap | 高 |
| 7 | afterSwap | 中 |
| 8 | beforeDonate | 低 |
| 9 | afterDonate | 低 |
| 10 | beforeSwapReturnDelta | 重大 |
| 11 | afterSwapReturnDelta | 高 |
| 12 | afterAddLiquidityReturnDelta | 中 |
| 13 | afterRemoveLiquidityReturnDelta | 中 |
アドレスの探索: salt を試行して CREATE2 を使用し、必要な権限ビットが設定されたアドレスにデプロイします。ツール:hook-mine-and-sinker、v4-hook-address-miner。
アクセス制御パターン
// REQUIRED: Verify caller is PoolManager
modifier onlyPoolManager() {
require(msg.sender == address(poolManager), "Not PoolManager");
_;
}
function beforeSwap(
address sender, // This is the ROUTER, not the user
PoolKey calldata key,
IPoolManager.SwapParams calldata params,
bytes calldata hookData
) external onlyPoolManager returns (bytes4, BeforeSwapDelta, uint24) {
// ...
}
実際のユーザーの特定(msg.sender の問題)
sender パラメーターはルーターコントラクトであり、エンドユーザーではありません。実際のユーザーを取得するには、次のようにします。
// 1. Define interface for trusted routers
interface IMsgSender {
function msgSender() external view returns (address);
}
// 2. Maintain allowlist of trusted routers
mapping(address => bool) public trustedRouters;
// 3. Query router safely in hook
function afterSwap(
address sender, // This is the router
PoolKey calldata key,
IPoolManager.SwapParams calldata params,
BalanceDelta delta,
bytes calldata hookData
) external override returns (bytes4, int128) {
// CRITICAL: Only trust verified routers
if (!trustedRouters[sender]) {
revert UntrustedRouter(sender);
}
// Safe to query actual user
try IMsgSender(sender).msgSender() returns (address user) {
// Use `user` for rewards, tracking, etc.
} catch {
revert RouterDoesNotImplementMsgSender();
}
return (this.afterSwap.selector, 0);
}
認証に tx.origin を決して信頼しないでください(自己紹介による不正防止など、不正利用対策の確認に限り許容されます)。
デルタ計算のルール
デルタは、フックが支払うべき額、または受け取るべき額を追跡します。必ず合計がゼロになるようにしてください。
// Taking tokens FROM PoolManager (hook receives tokens)
currency.take(poolManager, address(this), amount, false);
// Settling tokens TO PoolManager (hook sends tokens)
currency.settle(poolManager, address(this), amount, false);
// INVARIANT: All deltas must balance before unlock completes
よくあるデルタのバグ:
- take の後に settle し忘れる
- 丸める方向を誤る(常にユーザーまたはフックに不利な方向に丸める)
- スワップの両方向(zeroForOne が true の場合と false の場合)に対応していない
フック実装チェックリスト
フックを実装する前に、必ず次の項目を確認してください。
- アクセス制御:フック関数を呼び出せるのは PoolManager のみです
- デルタの均衡:すべての take に対応する settle があります
- ルーターの検証:許可リストを確認せずに sender を信頼しないでください
- オーバーフロー保護:価格計算には
mulDivを使用し、通常の乗算は使わないでください - リエントランシーガード:外部呼び出しを行う場合は追加してください
- トークンタイプの処理:サポート対象外のトークン(リベース、送金手数料あり)を明記してください
- 権限フラグ:機能に必要な最小限の権限だけを設定してください
基本的なフックのテンプレート
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
import {BaseHook} from "v4-periphery/src/base/hooks/BaseHook.sol";
import {IPoolManager} from "v4-core/interfaces/IPoolManager.sol";
import {PoolKey} from "v4-core/types/PoolKey.sol";
import {BalanceDelta} from "v4-core/types/BalanceDelta.sol";
import {Hooks} from "v4-core/libraries/Hooks.sol";
contract MyHook is BaseHook {
constructor(IPoolManager _manager) BaseHook(_manager) {}
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,
afterSwap: true, // Enable only what you need
beforeDonate: false,
afterDonate: false,
beforeSwapReturnDelta: false, // DANGEROUS - enable with caution
afterSwapReturnDelta: false,
afterAddLiquidityReturnDelta: false,
afterRemoveLiquidityReturnDelta: false
});
}
function _afterSwap(
address sender,
PoolKey calldata key,
IPoolManager.SwapParams calldata params,
BalanceDelta delta,
bytes calldata hookData
) internal override returns (bytes4, int128) {
// Your logic here
return (this.afterSwap.selector, 0);
}
}
よくある脆弱性のパターン
1. 価格計算のオーバーフロー
// BAD: Can overflow
uint256 price = uint256(sqrtPriceX96) ** 2;
// GOOD: Use mulDiv
uint256 price = FullMath.mulDiv(uint256(sqrtPriceX96), uint256(sqrtPriceX96), 1 << 96);
2. 手数料計算の誤り
// BAD: Simple addition is wrong
uint256 totalFee = protocolFee + lpFee;
// GOOD: Protocol fee taken first, then LP fee from remainder
// protocolFee + lpFee * (1_000_000 - protocolFee) / 1_000_000
3. タイムスタンプ検証の欠落
// BAD: No future check
function submitOrder(uint256 expiration) external {
orders[msg.sender] = Order(expiration, ...);
}
// GOOD: Validate expiration is in future
function submitOrder(uint256 expiration) external {
require(expiration > block.timestamp, "Expiration must be in future");
require(expiration % interval == 0, "Must align to interval");
orders[msg.sender] = Order(expiration, ...);
}
4. 検証せずに sender を信頼する
// BAD: sender could be malicious contract
address user = sender;
// GOOD: Verify router and query actual user
require(trustedRouters[sender], "Untrusted router");
address user = IMsgSender(sender).msgSender();
トークン処理に関する危険性
サポート対象外のトークンタイプ(フックが処理できない場合は明記してください):
- Fee-on-transfer:実際の受取額が送金額と異なります
- Rebasing:送金なしで残高が変化します
- ERC-777:送金フックを介した再入が発生します
- Pausable:送金が予期せずリバートすることがあります
- Blocklist:一部のアドレスがブロックされる場合があります
// If handling non-standard tokens, validate actual balances
uint256 balanceBefore = token.balanceOf(address(this));
token.transferFrom(user, address(this), amount);
uint256 actualReceived = token.balanceOf(address(this)) - balanceBefore;
テスト要件
包括的なテストには Foundry を使用してください:
// Invariant test: Deltas always balance
function invariant_deltasBalance() public {
assertEq(poolManager.currencyDelta(address(hook), currency0), 0);
assertEq(poolManager.currencyDelta(address(hook), currency1), 0);
}
// Fuzz test: No overflow in price calculations
function testFuzz_priceCalculation(uint160 sqrtPriceX96) public {
vm.assume(sqrtPriceX96 >= TickMath.MIN_SQRT_PRICE);
vm.assume(sqrtPriceX96 <= TickMath.MAX_SQRT_PRICE);
// Should not revert
hook.calculatePrice(sqrtPriceX96);
}
// Test both swap directions
function test_swapZeroForOne() public { ... }
function test_swapOneForZero() public { ... }
リスク評価(自己採点)
デプロイ前に、フックを採点してください(0〜33 点):
| 項目 | スコア範囲 | フックのスコア |
|---|---|---|
| コードの複雑さ | 0-5 | |
| 独自の計算処理 | 0-5 | |
| 外部依存関係 | 0-3 | |
| 外部流動性 | 0-3 | |
| TVL の可能性 | 0-5 | |
| チームの成熟度 | 0-3 | |
| アップグレード可能性 | 0-3 | |
| 自律的な更新 | 0-3 | |
| 価格に影響する処理 | 0-3 |
リスクレベル:
- 低(0-6):監査 1 回 + AI 静的解析
- 中(7-17):監査 1〜2 回 + バグ報奨金を推奨
- 高(18-33):監査 2 回(うち 1 回は計算処理の専門家)+ バグ報奨金必須 + 監視
本番環境のフックの参考例
Uniswap で許可リストに登録されているフック(2025 年 1 月時点):
- Flaunch(ミームコインのローンチパッド、手数料の 100% をクリエイターに還元)
- EulerSwap(レンディングを活用した DEX)
- Zaha Studios TWAMM(時間加重注文)
- Coinbase Verified Pools
- Panoptic Oracle Hook
- AEGIS Dynamic Fee Mechanism
パターンを学ぶには、以下を参照してください:https://raw.githubusercontent.com/fewwwww/awesome-uniswap-hooks/refs/heads/main/README.md
絶対にしてはいけないこと
- ラグプルのリスクを理解せずに
beforeSwapReturnDeltaを有効にしないでください - ルーターを検証せずに
senderパラメーターを信頼しないでください - 価格や数量の計算に
uncheckedブロックを使用しないでください - すべてのトークンが ERC20 標準の動作をすると想定しないでください
- デルタの清算を省略しないでください
- アドレスの権限を検証せずにデプロイしないでください
- スワップ中に読み取り可能な機密状態を保存しないでください
- 認証に
tx.originを使用しないでください
リソース
- v4-core:https://github.com/Uniswap/v4-core
- v4-periphery:https://github.com/Uniswap/v4-periphery
- セキュリティフレームワーク:https://docs.uniswap.org/contracts/v4/security
- フックの例:https://github.com/fewwwww/awesome-uniswap-hooks
- アドレスマイニング:https://github.com/hensha256/hook-mine-and-sinker
- 例で学ぶ v4:https://v4-by-example.org
監査チェックリスト(デプロイ前)
- すべてのフック関数で
msg.sender == poolManagerを確認している - すべての処理経路でデルタの合計がゼロになることを検証している
- 数値演算でオーバーフローが発生しない
- ユーザーを識別する場合はルーターの許可リストを実装している
- トークンタイプを明示的に記載している
- 外部呼び出しに再入防止策を講じている
- タイムスタンプの検証を行っている
- 権限フラグを最小限かつ正確に設定している
- エッジケースのファズテストに合格している
- デルタ会計の不変条件テストを実施している
- 両方のスワップ方向をテストしている
- 手数料の計算が Uniswap の公式と一致している