mcpField
U
uniswap-v4-hooks

uniswap-v4-hooks

github.com/igoryuzo开发1738 次安装38 周前更新
代码仓库

关于 uniswap-v4-hooks

面向 Uniswap v4 hook 开发的安全技能,涵盖权限、调用方验证、delta 记账、代币风险和测试,适用于实现或审查拦截池操作的 hook。

主要功能

  1. 01评估 hook 回调威胁模型
  2. 02配置权限标志与地址编码
  3. 03验证 PoolManager 和路由器
  4. 04处理 delta 记账与代币
  5. 05使用 Foundry 进行模糊与不变量测试
  6. 06部署前风险评估

适用场景

  1. 01规划 hook 回调的访问控制
  2. 02审查交换 delta 与结算逻辑
  3. 03评估代币及价格计算风险
  4. 04准备 hook 测试和部署检查

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/igoryuzo/uniswapv4-hooks-skill --skill uniswap-v4-hooks

安装后在 Claude Code 中输入/uniswap-v4-hooks确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

.gitignore
CONTRIBUTING.md
README.md
LICENSE
website/
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 权限编码在合约地址中。地址必须设置特定的位:

位权限风险级别
0beforeInitialize低
1afterInitialize低
2beforeAddLiquidity中
3afterAddLiquidity中
4beforeRemoveLiquidity中
5afterRemoveLiquidity中
6beforeSwap高
7afterSwap中
8beforeDonate低
9afterDonate低
10beforeSwapReturnDelta严重
11afterSwapReturnDelta高
12afterAddLiquidityReturnDelta中
13afterRemoveLiquidityReturnDelta中

**地址挖掘:**使用 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 进行身份验证

资源

审计清单(部署前)

  • 所有钩子函数都检查 msg.sender == poolManager
  • 确认所有路径中的 delta 均净额为零
  • 数学运算不存在溢出可能
  • 如果需要识别用户,已实现路由器许可名单
  • 已明确记录代币类型
  • 外部调用均设有重入保护
  • 已进行时间戳验证
  • 权限标志最少且正确
  • 边界情况的模糊测试通过
  • 已针对 delta 记账编写不变量测试
  • 已测试两个交换方向
  • 费用计算符合 Uniswap 的公式

评论

支持 Markdown