如何在 Uniswap V4 上创建钩子

  • QuickNode
  • 发布于 2024-09-15 13:11
  • 阅读 13

本文介绍了如何在Uniswap V4中使用Hooks添加自定义逻辑,详细讲解了Hooks的生命周期、IHook接口和Hook标志,并提供了一个简单的Swap Limiter Hook的实现和测试方法。

概述

Uniswap V4 引入了 hooks,这是一种在池操作的重要节点添加自定义逻辑的强大方式。Hooks 是与 Uniswap 池配合工作的独立智能合约,允许开发者在不改变整个协议的情况下改变交换行为、创建复杂策略并实现自定义的 AMM 逻辑。

在本指南中,我们将展示如何构建一个添加基本功能的简单 hook。这将帮助你学习如何开发更复杂的自定义 hooks。让我们开始吧!

你将做什么

  • 了解 Uniswap V4 Hooks
  • 创建一个自定义 Hook 智能合约
  • 使用脚本和 Anvil 测试 Hook 智能合约
  • 在测试网上部署并测试 Hook 智能合约

你将需要什么

  • 对以太坊和 DeFi 的经验
  • 已安装 Foundry

Uniswap Hooks

Uniswap V4 hooks 是在交换过程中特定时间介入的智能合约,允许开发者自定义和扩展流动性池的功能。这些 hooks 在设定时间与主协议配合工作,允许在不降低核心合约安全性的情况下增加更多功能。

关于 Uniswap V4 hooks 的关键点:

  1. Hooks 可以在重要操作(如交换、添加或移除流动性、启动池)之前或之后触发
  2. 每个 hook 都是独立的智能合约,因此可以独立开发和测试
  3. 你可以将多个 hooks 结合使用以创建复杂的池行为
  4. 自定义逻辑在交换交易中运行,比单独的合约消耗更少的 gas
  5. 开发者可以在不改变主 Uniswap 协议的情况下添加新的 hooks

Hooks 允许你详细控制池操作。它们允许在池逻辑中实现自定义费用结构、改变流动性分布和复杂的交易策略。通过在交换过程的关键节点介入,hooks 可以增加风险控制、提高资本使用效率,甚至帮助在区块链上创建新的金融工具。

一些你可以用 hooks 实现的功能示例:

  • 仅在某些条件下触发的订单(如限价订单或止损订单)
  • 根据市场条件改变费用
  • 自动流动性管理策略
  • 在不同池之间运行套利
  • 创建新的 AMM 曲线或定价方式

这种定制级别允许你创建适合特定市场条件或交易需求的池。你可以单独开发和使用的 hooks,并将它们结合以创建复杂的池行为,在交换过程中运行自定义逻辑以节省 gas。这种设计允许你添加通常需要多个交易或链下协调的功能。

在接下来的部分中,我们将介绍你所需了解的 hooks 核心概念,如 Hooks 生命周期、IHook 接口和 Hook 标志。

Hook 生命周期

Hooks 可以在交换过程中的几个重要节点介入:

  • Before Initialize: 在池启动之前运行
  • After Initialize: 在池启动之后运行
  • Before Swap: 在交换发生之前运行
  • After Swap: 在交换发生之后运行
  • Before Add Liquidity: 在向池添加流动性之前运行
  • After Add Liquidity: 在向池添加流动性之后运行
  • Before Remove Liquidity: 从池中移除流动性之前运行
  • After Remove Liquidity: 从池中移除流动性之后运行

通过创建这些 hook 函数,开发者可以在池操作的特定时间实现自定义行为。

IHook 接口

IHook 接口是你的 hook 合约需要实现的接口。它定义了 Uniswap V4 在池操作的不同节点调用的函数。以下是一个简化版本:

interface IHook {
    function beforeInitialize(address sender, uint160 sqrtPriceX96) external returns (bytes4);
    function afterInitialize(address sender, uint160 sqrtPriceX96) external returns (bytes4);
    function beforeSwap(address sender, address recipient, bool zeroForOne, uint256 amountSpecified, uint160 sqrtPriceLimitX96) external returns (bytes4);
    function afterSwap(address sender, address recipient, bool zeroForOne, uint256 amountSpecified, uint160 sqrtPriceLimitX96) external returns (bytes4);
    function beforeAddLiquidity(address sender, uint256 amount0, uint256 amount1) external returns (bytes4);
    function afterAddLiquidity(address sender, uint256 amount0, uint256 amount1) external returns (bytes4);
    function beforeRemoveLiquidity(address sender, uint256 amount0, uint256 amount1) external returns (bytes4);
    function afterRemoveLiquidity(address sender, uint256 amount0, uint256 amount1) external returns (bytes4);
}

Hook 标志

当你创建一个 hook 时,你需要指定它实现了哪些函数。这是通过 hook 标志完成的。这些标志是位标志,指示哪些 hook 函数是活跃的。例如:

uint160 constant BEFORE_SWAP_FLAG = 1 << 0;
uint160 constant AFTER_SWAP_FLAG = 1 << 1;
uint160 constant BEFORE_ADD_LIQUIDITY_FLAG = 1 << 2;
// ... 其他 hook 函数以此类推

你可以组合这些标志来指示你的 hook 实现了哪些函数。例如,如果你的 hook 实现了 beforeSwapafterSwap,你可以使用:

uint160 public constant FLAGS = BEFORE_SWAP_FLAG | AFTER_SWAP_FLAG;

在接下来的部分中,我们将通过创建一个简单的 hook 来展示如何在实践中使用这些工具。

项目前提:创建一个 QuickNode 端点

在深入代码之前,让我们设置一些前提条件,比如获取一个 RPC URL。你可以使用公共节点或部署和管理自己的基础设施;但是,如果你希望获得 8 倍更快的响应时间,可以将繁重的工作交给我们。在此注册一个免费账户。

登录 QuickNode 后,点击 Create an endpoint 按钮,然后选择 Ethereum 链和 Sepolia 网络。

创建你的端点后,复制 HTTP Provider URL 链接并保存好,因为在本地测试网部分你将需要它。

QuickNode endpoint

实现你的第一个 Uniswap Hook

现在我们已经介绍了核心概念并完成了前提条件,让我们深入创建你的第一个 Uniswap V4 hook。我们将使用 v4-template 作为起点,它为 hook 开发提供了一个坚实的基础。

目录设置

使用 v4-template 作为起点。你可以通过点击 GitHub 仓库中的 "Use this Template" 或克隆它来完成:

git clone git@github.com:uniswapfoundation/v4-template.git
cd v4-template

模板包括一个示例 hook Counter.sol,它展示了 beforeSwap()afterSwap() hooks(现在可以随意浏览一下)。测试模板 Counter.t.sol 预配置了 v4 池管理器、测试代币和测试流动性,这将有助于测试我们的 hook。

安装依赖项

首先,确保你已经安装并更新了 Foundry:

foundryup

接下来,安装项目依赖项:

forge install

虽然你现在可以运行 forge tests 命令,但我们将等到添加我们自己的自定义 hook 逻辑后再进行。

自定义 Hook 合约

现在,让我们创建我们自己的 hook。我们将实现一个“交换限制器”hook,它限制单个地址在特定时间范围内可以执行的交换次数。这为池添加了一种基本的速率限制。

首先,创建一个名为 src/SwapLimiterHook.sol 的文件。然后,打开文件并包含以下代码:

// 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 {PoolId, PoolIdLibrary} from "v4-core/src/types/PoolId.sol";
import {BeforeSwapDelta, BeforeSwapDeltaLibrary} from "v4-core/src/types/BeforeSwapDelta.sol";

contract SwapLimiterHook is BaseHook {
    using PoolIdLibrary for PoolKey;

    uint256 public constant MAX_SWAPS_PER_HOUR = 5;
    uint256 public constant HOUR = 3600;

    mapping(address => uint256) public lastResetTime;
    mapping(address => uint256) public swapCount;

    event SwapLimitReached(address indexed user, uint256 timestamp);

    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: true,
            afterSwap: false,
            beforeDonate: false,
            afterDonate: false,
            beforeSwapReturnDelta: false,
            afterSwapReturnDelta: false,
            afterAddLiquidityReturnDelta: false,
            afterRemoveLiquidityReturnDelta: false
        });
    }

    // 强制执行交换限制的主要函数
    function beforeSwap(address sender, PoolKey calldata, IPoolManager.SwapParams calldata, bytes calldata)
        external
        override
        returns (bytes4, BeforeSwapDelta, uint24)
    {
        uint256 currentTime = block.timestamp;
        if (currentTime - lastResetTime[sender] >= HOUR) {
            swapCount[sender] = 0;
            lastResetTime[sender] = currentTime;
        }

        require(swapCount[sender] < MAX_SWAPS_PER_HOUR, "Swap limit reached for this hour");

        swapCount[sender]++;

        if (swapCount[sender] == MAX_SWAPS_PER_HOUR) {
            emit SwapLimitReached(sender, currentTime);
        }

        return (BaseHook.beforeSwap.selector, BeforeSwapDeltaLibrary.ZERO_DELTA, 0);
    }

    function getRemainingSwaps(address user) public view returns (uint256) {
        if (block.timestamp - lastResetTime[user] >= HOUR) {
            return MAX_SWAPS_PER_HOUR;
        }
        return MAX_SWAPS_PER_HOUR - swapCount[user];
    }
}

这个“交换限制器”hook 合约实现了以下功能:

  • 交换限制:它将每个地址限制为每小时最多 5 次交换(可通过 MAX_SWAPS_PER_HOUR 配置)。
  • 基于时间的重置:每个地址的交换计数每小时重置一次。
  • beforeSwap Hook:此函数检查发送者是否在当前小时内超过了他们的交换限制。如果没有,则增加他们的交换计数。
  • 剩余交换检查:getRemainingSwaps 函数允许用户(或前端)检查他们在当前小时内剩余的交换次数。
  • 事件日志:当用户达到他们的交换限制时,会发出一个事件,这对于监控或警报很有用。

可选地,为了检查你的文件语法设置是否正确,你可以运行 forge compile 命令。

创建测试文件

现在我们已经创建了我们的 hook,我们需要创建一个测试文件。首先,创建一个名为 test/SwapLimiterHook.t.sol 的文件。然后,包含以下代码:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import "forge-std/Test.sol";
import {IHooks} from "v4-core/src/interfaces/IHooks.sol";
import {Hooks} from "v4-core/src/libraries/Hooks.sol";
import {TickMath} from "v4-core/src/libraries/TickMath.sol";
import {IPoolManager} from "v4-core/src/interfaces/IPoolManager.sol";
import {PoolKey} from "v4-core/src/types/PoolKey.sol";
import {BalanceDelta} from "v4-core/src/types/BalanceDelta.sol";
import {PoolId, PoolIdLibrary} from "v4-core/src/types/PoolId.sol";
import {CurrencyLibrary, Currency} from "v4-core/src/types/Currency.sol";
import {PoolSwapTest} from "v4-core/src/test/PoolSwapTest.sol";
import {SwapLimiterHook} from "../src/SwapLimiterHook.sol";
import {StateLibrary} from "v4-core/src/libraries/StateLibrary.sol";

import {IPositionManager} from "v4-periphery/src/interfaces/IPositionManager.sol";
import {EasyPosm} from "./utils/EasyPosm.sol";
import {Fixtures} from "./utils/Fixtures.sol";

contract SwapLimiterHookTest is Test, Fixtures {
    using EasyPosm for IPositionManager;
    using PoolIdLibrary for PoolKey;
    using CurrencyLibrary for Currency;
    using StateLibrary for IPoolManager;

    SwapLimiterHook hook;
    PoolId poolId;

    uint256 tokenId;
    int24 tickLower;
    int24 tickUpper;

    event SwapLimitReached(address indexed user, uint256 timestamp);

    function setUp() public {
        // 创建池管理器、实用路由器和测试代币
        deployFreshManagerAndRouters();
        deployMintAndApprove2Currencies();

        deployAndApprovePosm(manager);

        // 将 hook 部署到具有正确标志的地址
        address flags = address(
            uint160(Hooks.BEFORE_SWAP_FLAG) ^ (0x4444 << 144) // 命名空间以避免冲突
        );
        bytes memory constructorArgs = abi.encode(manager);
        deployCodeTo("SwapLimiterHook.sol:SwapLimiterHook", constructorArgs, flags);
        hook = SwapLimiterHook(flags);

        // 创建池
        key = PoolKey(currency0, currency1, 3000, 60, IHooks(hook));
        poolId = key.toId();
        manager.initialize(key, SQRT_PRICE_1_1, ZERO_BYTES);

        // 向池提供全范围流动性
        tickLower = TickMath.minUsableTick(key.tickSpacing);
        tickUpper = TickMath.maxUsableTick(key.tickSpacing);

        (tokenId,) = posm.mint(
            key,
            tickLower,
            tickUpper,
            10_000e18,
            MAX_SLIPPAGE_ADD_LIQUIDITY,
            MAX_SLIPPAGE_ADD_LIQUIDITY,
            address(this),
            block.timestamp,
            ZERO_BYTES
        );
    }

    function testDirectBeforeSwap() public {
        address sender = address(this);
        IPoolManager.SwapParams memory params;
        bytes memory hookData;

        for (uint i = 0; i < 5; i++) {
            (bytes4 selector,,) = hook.beforeSwap(sender, key, params, hookData);
            assertEq(selector, SwapLimiterHook.beforeSwap.selector);
            console.log("Swap %d, Remaining swaps: %d", i + 1, hook.getRemainingSwaps(sender));
        }

        vm.expectRevert("Swap limit reached for this hour");
        hook.beforeSwap(sender, key, params, hookData);
    }

    function testSwapLimiter() public {
        bool zeroForOne = true;
        int256 amountSpecified = -1e18; // 负数表示精确输入交换

        console.log("Initial remaining swaps: %d", hook.getRemainingSwaps(address(this)));

        // 执行 5 次交换(应该成功)
        for (uint i = 0; i < 5; i++) {
            // 手动调用 beforeSwap 以模拟 hook 被触发
            (bytes4 selector,,) = hook.beforeSwap(address(this), key, IPoolManager.SwapParams({zeroForOne: zeroForOne, amountSpecified: amountSpecified, sqrtPriceLimitX96: 0}), ZERO_BYTES);
            assertEq(selector, SwapLimiterHook.beforeSwap.selector);

            BalanceDelta swapDelta = swap(key, zeroForOne, amountSpecified, ZERO_BYTES);
            assertEq(int256(swapDelta.amount0()), amountSpecified);
            console.log("Swap %d succeeded. Remaining swaps: %d", i + 1, hook.getRemainingSwaps(address(this)));
        }

        // 第 6 次交换应该回滚
        vm.expectRevert("Swap limit reached for this hour");
        hook.beforeSwap(address(this), key, IPoolManager.SwapParams({zeroForOne: zeroForOne, amountSpecified: amountSpecified, sqrtPriceLimitX96: 0}), ZERO_BYTES);

        // 尝试第 6 次交换(应该失败)
        vm.expectRevert(abi.encodeWithSignature("Wrap__FailedHookCall(address,bytes)", address(hook), abi.encodeWithSignature("Error(string)", "Swap limit reached for this hour")));
        swap(key, zeroForOne, amountSpecified, ZERO_BYTES);

        // 检查剩余交换
        uint256 remainingSwaps = hook.getRemainingSwaps(address(this));
        console.log("Final remaining swaps: %d", remainingSwaps);
        assertEq(remainingSwaps, 0, "Should have 0 remaining swaps");
    }

    function testSwapLimitReachedEvent() public {
        address sender = address(this);
        IPoolManager.SwapParams memory params;
        bytes memory hookData;

        for (uint i = 0; i < 4; i++) {
            hook.beforeSwap(sender, key, params, hookData);
        }

        vm.expectEmit(true, false, false, true);
        emit SwapLimitReached(sender, block.timestamp);
        hook.beforeSwap(sender, key, params, hookData);
    }
}

这些测试涵盖了我们的 SwapLimiterHook 合约的主要功能,强制执行交换限制,每小时重置交换计数,并检查用户的剩余交换次数。

让我们回顾一下测试代码中的主要功能。

  • testDirectBeforeSwap 函数:
    • 直接调用 hook 的 beforeSwap 函数 5 次
    • 检查每次调用是否返回正确的选择器
    • 在每次调用后记录剩余交换次数
    • 预期第 6 次调用会回滚,并显示 "Swap limit reached for this hour"
  • testSwapLimiter 函数:
  • 原文链接: quicknode.com/guides/def...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
QuickNode
QuickNode
江湖只有他的大名,没有他的介绍。