关于 v4-security-foundations
面向 Uniswap v4 Hook 开发者的安全指南,涵盖威胁模型、权限风险、代币处理和部署前检查。重点说明高危权限与 Delta 结算中的常见漏洞。
主要功能
- 01Hook 威胁模型与权限风险
- 02NoOp 攻击分析与检测
- 03Delta 记账和结算模式
- 04访问控制与路由器验证
- 05代币处理风险与安全检查表
- 06Gas 预算与风险评分
适用场景
- 01编写 Uniswap v4 Hook 前检查安全设计
- 02审查启用 beforeSwapReturnDelta 的 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 Hook 安全基础知识
这是一份以安全为先的指南,介绍如何构建 Uniswap v4 hook。Hook 漏洞可能导致用户资金被盗——编写任何 hook 代码前,请先了解这些概念。
威胁模型
编写代码前,请先了解 v4 的安全背景:
| 威胁领域 | 描述 | 缓解措施 |
|---|---|---|
| 调用方验证 | 只有 PoolManager 应调用 hook 函数 | 验证 msg.sender == address(poolManager) |
| 发送方身份 | msg.sender 始终是 PoolManager,而非最终用户 | 使用 sender 参数标识用户 |
| 路由器上下文 | sender 参数标识的是路由器,而非用户 | 实施路由器许可名单 |
| 状态暴露 | 在交易执行过程中,hook 状态可被读取 | 避免在链上存储敏感数据 |
| 重入攻击面 | hook 发起外部调用可能导致重入 | 使用重入保护;尽量减少外部调用 |
权限标志风险矩阵
全部 14 项 hook 权限及其对应的风险等级:
| 权限标志 | 风险等级 | 描述 | 安全说明 |
|---|---|---|---|
beforeInitialize | 低 | 在创建池之前调用 | 验证池参数 |
afterInitialize | 低 | 在创建池之后调用 | 适用于初始化状态 |
beforeAddLiquidity | 中 | 在 LP 存入前调用 | 可能阻止合法的 LP |
afterAddLiquidity | 低 | 在 LP 存入后调用 | 适用于追踪/奖励 |
beforeRemoveLiquidity | 高 | 在 LP 提取前调用 | 可能锁住用户资金 |
afterRemoveLiquidity | 低 | 在 LP 提取后调用 | 适用于追踪 |
beforeSwap | 高 | 在执行交换前调用 | 可能操纵价格 |
afterSwap | 中 | 在执行交换后调用 | 可以观察最终状态 |
beforeDonate | 低 | 在捐赠前调用 | 仅用于访问控制 |
afterDonate | 低 | 在捐赠后调用 | 适用于追踪 |
beforeSwapReturnDelta | 严重 | 返回自定义交换金额 | NoOp 攻击向量 |
afterSwapReturnDelta | 高 | 修改交换后的金额 | 可能提取价值 |
afterAddLiquidityReturnDelta | 高 | 修改 LP 代币金额 | 可能少付 LP |
afterRemoveLiquidityReturnDelta | 高 | 修改提取金额 | 可能盗取资金 |
风险等级说明
- 低:不太可能造成资金损失
- 中:需要谨慎实现
- 高:实现不当可能造成资金损失
- 严重:可能导致资金被完全盗取
严重:NoOp Rug Pull 攻击
BEFORE_SWAP_RETURNS_DELTA 权限(位 3)是最危险的 hook 权限。恶意 hook 可以:
- 返回一个 delta,声称自己已处理整笔交换
- PoolManager 接受该 delta 并结算交易
- Hook 留下所有输入代币,却不提供任何输出
- 用户损失整笔交换金额
攻击模式
// 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 集成
但每种用途都需要谨慎实现并经过审计。
Delta 记账基础
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 参数是路由器,而非最终用户。对于需要用户身份的钩子:
许可名单模式
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 获取用户身份
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。
安全检查清单
部署任何 hook 之前:
| # | 检查项 | 状态 |
|---|---|---|
| 1 | 所有 hook 回调都验证 msg.sender == poolManager | [ ] |
| 2 | 如有需要,实现路由器许可名单 | [ ] |
| 3 | 没有可能导致 OOG 的无界循环 | [ ] |
| 4 | 外部调用设有重入保护 | [ ] |
| 5 | Delta 记账之和为零 | [ ] |
| 6 | 已处理转账收费型代币 | [ ] |
| 7 | 没有硬编码地址 | [ ] |
| 8 | 遵守滑点参数 | [ ] |
| 9 | 链上未存储敏感数据 | [ ] |
| 10 | 升级机制安全(如适用) | [ ] |
| 11 | 已启用 beforeSwapReturnDelta 时有充分理由 | [ ] |
| 12 | 已完成模糊测试 | [ ] |
| 13 | 已完成不变量测试 | [ ] |
Gas 预算指南
Hook 回调在 PoolManager 的交易上下文中执行。Gas 消耗过高可能导致交换回滚,或在经济上不可行。
各回调的 Gas 预算
| 回调 | 目标预算 | 硬性上限 | 说明 |
|---|---|---|---|
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 路由、预言机 |
常见的 Gas 陷阱
- 无界循环:遍历动态数组(例如所有活跃头寸)可能超过区块 Gas 限制。限制数组大小或使用分页。
- 热点路径中的 SSTORE:每个新的存储槽约消耗 20,000 gas。对于交易结束后无需保留的数据,优先使用瞬态存储(
tstore/tload)。需要 Solidity >= 0.8.24,且 EVM 目标设为cancun或更高版本。 - 外部调用:每次跨合约调用都会增加约 2,600 gas 的基础成本,此外还有被调用方的执行开销。尽可能批量调用。
- 字符串操作:避免在回调中操作
string;标识符使用bytes32。 - 冗余读取:缓存对
poolManager的调用——重复读取getSlot0()或getLiquidity()都会消耗 Gas。
测量 Gas
# 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
风险评分系统
计算 hook 的风险分数(0-33):
| 类别 | 分数 | 标准 |
|---|---|---|
| 权限 | 0-14 | 已启用权限的风险等级总和 |
| 外部调用 | 0-5 | 外部交互的数量和类型 |
| 状态复杂度 | 0-5 | 可变状态的数量 |
| 升级机制 | 0-5 | 代理、管理员函数等 |
| 代币处理 | 0-4 | 对非标准代币的支持 |
审计等级建议
| 分数 | 风险等级 | 建议 |
|---|---|---|
| 0-5 | 低 | 自行审计 + 同行评审 |
| 6-12 | 中 | 建议进行专业审计 |
| 13-20 | 高 | 必须进行专业审计 |
| 21-33 | 严重 | 必须进行多次审计 |
绝对禁止事项
切勿在 Hook 中执行以下操作:
- 切勿依赖
msg.sender识别用户身份 - 它始终是 PoolManager - 不了解 NoOp 攻击时,切勿启用
beforeSwapReturnDelta - 切勿在链上存储密码、密钥或 PII
- 切勿使用
transfer()发送 ETH - 使用call{value:}("") - 切勿假设代币的小数位数 - 始终查询代币
- 切勿使用
block.timestamp生成随机数 - 切勿在调用中硬编码 Gas 限额
- 切勿忽略外部调用的返回值
- 切勿使用
tx.origin进行授权 - 这会带来钓鱼风险;恶意合约可以通过转发调用,保留原用户的tx.origin
部署前审计清单
| # | 项目 | 适用于 |
|---|---|---|
| 1 | 由专注于安全的开发者进行代码审查 | 所有 Hook |
| 2 | 为所有回调编写单元测试 | 所有 Hook |
| 3 | 使用 Foundry 进行模糊测试 | 所有 Hook |
| 4 | 不变量测试 | 返回 delta 的 Hook |
| 5 | 在主网上进行分叉测试 | 所有 Hook |
| 6 | Gas 性能分析 | 所有 Hook |
| 7 | 形式化验证 | 关键 Hook |
| 8 | Slither/Mythril 分析 | 所有 Hook |
| 9 | 外部审计 | 中等及以上风险的 Hook |
| 10 | 漏洞赏金计划 | 高风险及以上的 Hook |
| 11 | 监控/告警设置 | 所有生产环境 Hook |
详细审计要求请参阅 references/audit-checklist.md。
生产环境 Hook 参考
借鉴经过审计并已投入生产的 Hook:
| 项目 | 描述 | 值得关注的安全特性 |
|---|---|---|
| Flaunch | 代币发行平台 | 多签管理员、时间锁 |
| EulerSwap | 借贷集成 | 每个市场的风险隔离 |
| Zaha TWAMM | 时间加权 AMM | 渐进执行可减少 MEV |
| Bunni | LP 管理 | 集中流动性防护 |
外部资源
官方文档
安全资源
社区
- v4-hooks-skill by @igoryuzo - 为本指南提供灵感的社区技能
- v4hooks.dev - 社区 Hook 资源
其他参考资料
- 基础 Hook 模板 - 完整的实现起始模板
- 漏洞目录 - 常见模式和缓解措施
- 审计清单 - 详细的部署前检查清单