关于 uniswap-v4-hooks
面向 Uniswap v4 hook 开发的安全技能,涵盖权限、调用方验证、delta 记账、代币风险和测试,适用于实现或审查拦截池操作的 hook。
主要功能
- 01评估 hook 回调威胁模型
- 02配置权限标志与地址编码
- 03验证 PoolManager 和路由器
- 04处理 delta 记账与代币
- 05使用 Foundry 进行模糊与不变量测试
- 06部署前风险评估
适用场景
- 01规划 hook 回调的访问控制
- 02审查交换 delta 与结算逻辑
- 03评估代币及价格计算风险
- 04准备 hook 测试和部署检查
安装
复制下面的命令,在终端中运行。
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 hooks。Hook 是在特定生命周期节点拦截池操作的外部智能合约。安全至关重要——Hook 漏洞可能导致用户资金被盗。
安全思维
编写任何 Hook 代码之前,都要评估威胁模型:
谁会调用你的 Hook?
- 只有
PoolManager应调用 Hook 函数 - Hook 中的
msg.sender始终是PoolManager,绝不会是用户 sender参数是路由器,而不是最终用户
会暴露哪些状态?
- Hook 在交易过程中执行——状态可能在回调之间被操纵
- 外部调用可能引发重入
- 多个池共用同一个 Hook 时,共享存储可能导致意外故障
Delta 可能出现哪些问题?
- 每种代币的流入量都必须等于流出量(Delta 记账)
- 迭代操作中的舍入误差会累积
- BeforeSwapDelta 可以完全绕过常规交换逻辑
严重:NoOp Rug Pull 攻击途径
启用 BEFORE_SWAP_RETURNS_DELTA_FLAG 的 Hook 可以窃取用户资金。这是风险最高的 Hook 权限。
// 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 时:
- Hook 可以完全取代交换逻辑
- 如果 Delta 等于 amountSpecified,池的数学计算会被跳过
- 用户资金会流向 Hook,而不是经过 AMM 曲线
**防御措施:**除非是在实现合法的自定义 AMM,否则绝不要启用 beforeSwapReturnDelta。用户应在交换前检查 Hook 权限。
权限标志(地址编码)
Hook 权限编码在合约地址中。地址必须设置特定的位:
| 位 | 权限 | 风险级别 |
|---|---|---|
| 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 | 中 |
**地址挖掘:**使用 CREATE2 并通过不断尝试 salt,在具有正确权限位的地址上部署。工具: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 进行身份验证(仅可用于防止自我推荐等反博弈检查)。
Delta 记账规则
Delta 记录 Hook 欠付或应收的金额。Delta 总额必须为零。
// 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
常见 Delta 错误:
- 取出后忘记结算
- 舍入方向错误(始终对用户/Hook 不利的方向舍入)
- 未处理两个交换方向(
zeroForOne为 true 和 false)
Hook 实现检查清单
在实现任何 Hook 之前:
- 访问控制:只有
PoolManager能调用 Hook 函数 - Delta 平衡:每次取出都要有对应的结算
- 路由器验证:未经允许列表检查,绝不信任 sender
- 溢出保护:价格计算使用
mulDiv,绝不直接相乘 - 重入保护:进行外部调用时添加
- 代币类型处理:说明不支持的代币(重基代币、转账收费代币)
- 权限标志:仅启用功能所需的最少权限
Hook 基础模板
// 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();
代币处理风险
不支持的代币类型(如果你的钩子无法处理,请明确记录):
- 转账扣费型:实际收到的数量与转账数量不同
- 重基型:余额会在没有转账的情况下发生变化
- ERC-777:可通过转账钩子触发重入
- 可暂停型:转账可能意外回滚
- 封锁名单型:部分地址可能被封锁
// 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 次由数学专家进行)+ 必须设置漏洞赏金 + 监控
生产环境钩子参考
截至 2025 年 1 月,Uniswap 上列入许可名单的钩子:
- Flaunch(Meme 币启动平台,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的 rug pull 风险时,绝不要启用它 - 未验证路由器时,绝不要信任
sender参数 - 绝不要在价格或数量计算中使用
unchecked代码块 - 绝不要假设所有代币都遵循 ERC20 标准行为
- 绝不要跳过 delta 结算
- 未验证地址权限时,绝不要部署
- 绝不要存储可在交换过程中读取的敏感状态
- 绝不要使用
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 - 确认所有路径中的 delta 均净额为零
- 数学运算不存在溢出可能
- 如果需要识别用户,已实现路由器许可名单
- 已明确记录代币类型
- 外部调用均设有重入保护
- 已进行时间戳验证
- 权限标志最少且正确
- 边界情况的模糊测试通过
- 已针对 delta 记账编写不变量测试
- 已测试两个交换方向
- 费用计算符合 Uniswap 的公式