mcpField
V
v4-security-foundations

v4-security-foundations

github.com/uniswap安全2361.2k 次安装2 天前更新
代码仓库

关于 v4-security-foundations

面向 Uniswap v4 Hook 开发者的安全指南,涵盖威胁模型、权限风险、代币处理和部署前检查。重点说明高危权限与 Delta 结算中的常见漏洞。

主要功能

  1. 01Hook 威胁模型与权限风险
  2. 02NoOp 攻击分析与检测
  3. 03Delta 记账和结算模式
  4. 04访问控制与路由器验证
  5. 05代币处理风险与安全检查表
  6. 06Gas 预算与风险评分

适用场景

  1. 01编写 Uniswap v4 Hook 前检查安全设计
  2. 02审查启用 beforeSwapReturnDelta 的 Hook
  3. 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.md
base-hook-template.md
vulnerabilities-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 可以:

  1. 返回一个 delta,声称自己已处理整笔交换
  2. PoolManager 接受该 delta 并结算交易
  3. Hook 留下所有输入代币,却不提供任何输出
  4. 用户损失整笔交换金额
攻击模式
// 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 的钩子交互之前:

  1. 审查钩子代码 - 确认用例合理
  2. 检查所有权 - 是否可升级?由谁升级?
  3. 核实过往记录 - 是否经过信誉良好的机构审计?
  4. 从小额开始 - 先用最小金额测试
合理用途

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);
常见错误
  1. 忘记 sync:没有 sync 会导致结算失败
  2. 顺序错误:必须按 sync → transfer → settle 的顺序执行
  3. 部分结算:会使交易处于无效状态
  4. 重复结算:会导致记账错误

访问控制模式

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外部调用设有重入保护[ ]
5Delta 记账之和为零[ ]
6已处理转账收费型代币[ ]
7没有硬编码地址[ ]
8遵守滑点参数[ ]
9链上未存储敏感数据[ ]
10升级机制安全(如适用)[ ]
11已启用 beforeSwapReturnDelta 时有充分理由[ ]
12已完成模糊测试[ ]
13已完成不变量测试[ ]

Gas 预算指南

Hook 回调在 PoolManager 的交易上下文中执行。Gas 消耗过高可能导致交换回滚,或在经济上不可行。

各回调的 Gas 预算
回调目标预算硬性上限说明
beforeSwap< 50,000 gas150,000 gas每次交换都会运行;保持精简
afterSwap< 30,000 gas100,000 gas仅用于分析/跟踪
beforeAddLiquidity< 50,000 gas200,000 gas可能包含访问控制
afterAddLiquidity< 30,000 gas100,000 gas奖励跟踪
beforeRemoveLiquidity< 50,000 gas200,000 gas锁定验证
afterRemoveLiquidity< 30,000 gas100,000 gas跟踪/记账
包含外部调用的回调< 100,000 gas300,000 gas外部 DEX 路由、预言机
常见的 Gas 陷阱
  1. 无界循环:遍历动态数组(例如所有活跃头寸)可能超过区块 Gas 限制。限制数组大小或使用分页。
  2. 热点路径中的 SSTORE:每个新的存储槽约消耗 20,000 gas。对于交易结束后无需保留的数据,优先使用瞬态存储(tstore/tload)。需要 Solidity >= 0.8.24,且 EVM 目标设为 cancun 或更高版本。
  3. 外部调用:每次跨合约调用都会增加约 2,600 gas 的基础成本,此外还有被调用方的执行开销。尽可能批量调用。
  4. 字符串操作:避免在回调中操作 string;标识符使用 bytes32。
  5. 冗余读取:缓存对 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 中执行以下操作:

  1. 切勿依赖 msg.sender 识别用户身份 - 它始终是 PoolManager
  2. 不了解 NoOp 攻击时,切勿启用 beforeSwapReturnDelta
  3. 切勿在链上存储密码、密钥或 PII
  4. 切勿使用 transfer() 发送 ETH - 使用 call{value:}("")
  5. 切勿假设代币的小数位数 - 始终查询代币
  6. 切勿使用 block.timestamp 生成随机数
  7. 切勿在调用中硬编码 Gas 限额
  8. 切勿忽略外部调用的返回值
  9. 切勿使用 tx.origin 进行授权 - 这会带来钓鱼风险;恶意合约可以通过转发调用,保留原用户的 tx.origin

部署前审计清单

#项目适用于
1由专注于安全的开发者进行代码审查所有 Hook
2为所有回调编写单元测试所有 Hook
3使用 Foundry 进行模糊测试所有 Hook
4不变量测试返回 delta 的 Hook
5在主网上进行分叉测试所有 Hook
6Gas 性能分析所有 Hook
7形式化验证关键 Hook
8Slither/Mythril 分析所有 Hook
9外部审计中等及以上风险的 Hook
10漏洞赏金计划高风险及以上的 Hook
11监控/告警设置所有生产环境 Hook

详细审计要求请参阅 references/audit-checklist.md。

生产环境 Hook 参考

借鉴经过审计并已投入生产的 Hook:

项目描述值得关注的安全特性
Flaunch代币发行平台多签管理员、时间锁
EulerSwap借贷集成每个市场的风险隔离
Zaha TWAMM时间加权 AMM渐进执行可减少 MEV
BunniLP 管理集中流动性防护

外部资源

官方文档
安全资源
社区

其他参考资料

评论

支持 Markdown