{"content":{"title":"如何在 Uniswap V4 上创建 Hook","body":"## 概述\r\n\r\nUniswap V4 引入了 hooks，这是一种在池操作的重要节点添加自定义逻辑的强大方式。Hooks 是与 Uniswap 池一起工作的独立智能合约，允许开发者更改交换行为，创建复杂策略，并在不改变整个协议的情况下实现自定义 AMM 逻辑。\r\n\r\n在本指南中，我们将向你展示如何构建一个简单的 hook，以添加基本功能。这将帮助你学习如何开发更复杂的自定义 hooks。让我们开始吧！\r\n\r\n### 你将要做什么\r\n\r\n- 了解 Uniswap V4 Hooks\r\n- 创建自定义 Hook 智能合约\r\n- 使用脚本和 Anvil 测试 Hook 智能合约\r\n- 在测试网上部署和测试 Hook 智能合约\r\n\r\n### 你将需要什么\r\n\r\n- 对以太坊和 DeFi 的经验\r\n- 已安装 [Foundry](https://github.com/foundry-rs/foundry)\r\n\r\n## Uniswap Hooks\r\n\r\n[Uniswap](https://learnblockchain.cn/article/11741#what-is-uniswap) V4 hooks 是可以在交换的特定时间插入的智能合约，允许开发者自定义和扩展流动性池的功能。这些 hooks 在设定的时间与主协议协作，允许更多功能而不会使核心合约变得不安全。\r\n\r\n关于 Uniswap V4 hooks 的关键点：\r\n\r\n1. Hooks 可以在交换、添加或移除流动性、以及启动池等重要动作之前或之后被触发\r\n2. 每个 hook 是自己的智能合约，因此你可以单独开发和测试它\r\n3. 你可以将多个 hooks 一起使用，创建复杂的池行为\r\n4. 自定义逻辑在交换交易中运行，使用的 gas 少于单独的合约\r\n5. 开发者可以在不改变主要 Uniswap 协议的情况下添加新的 hooks\r\n\r\nHooks 允许你详细控制池操作。它们可以自定义费用结构，改变流动性分配，甚至在池逻辑中实现复杂的交易策略。通过在交换过程中的关键时刻介入，hooks 可以添加风险控制，使资本使用更高效，甚至帮助在区块链上创建新的金融工具。\r\n\r\n以下是你可以使用 hooks 做到的一些示例：\r\n\r\n- 仅在特定条件下发生的订单（如限价订单或止损单）\r\n- 根据市场条件变化的费用\r\n- 自动流动性管理策略\r\n- 不同池之间的套利\r\n- 创建新的 AMM 曲线或定价方式\r\n\r\n这种级别的定制使你能够创建适应特定市场条件或交易需求的池。你可以单独开发和使用 hooks，并将它们组合以创建复杂的池行为，直接在交换过程中运行自定义逻辑以节省 gas。这样的设计使你能够添加通常需要多个交易或区块链外协调的功能。\r\n\r\n在接下来的章节中，我们将介绍你需要了解的 hooks 核心概念，如 Hooks 生命周期、IHook 接口和 Hook 标志。\r\n\r\n### Hook 生命周期\r\n\r\nHooks 可以在多个重要节点与交换过程一起工作：\r\n\r\n- 初始化前：在池启动前运行\r\n- 初始化后：在池启动后运行\r\n- 交换前：在交换发生前运行\r\n- 交换后：在交换发生后运行\r\n- 添加流动性前：在向池添加流动性前运行\r\n- 添加流动性后：在向池添加流动性后运行\r\n- 移除流动性前：在从池中移除流动性前运行\r\n- 移除流动性后：在从池中移除流动性后运行\r\n\r\n通过创建这些 hook 函数，开发者可以在池操作的特定时刻实现自定义行为。\r\n\r\n### IHook 接口\r\n\r\nIHook 接口是你的 hook 合约需要实现的内容。它定义了 Uniswap V4 将在池操作的不同节点调用的函数。以下是它可能的简化版本：\r\n\r\n```\r\n// SPDX-License-Identifier: MIT\r\npragma solidity ^0.8.24;\r\n\r\ninterface IHook {\r\n    function beforeInitialize(address sender, uint160 sqrtPriceX96) external returns (bytes4);\r\n    function afterInitialize(address sender, uint160 sqrtPriceX96) external returns (bytes4);\r\n    function beforeSwap(address sender, address recipient, bool zeroForOne, uint256 amountSpecified, uint160 sqrtPriceLimitX96) external returns (bytes4);\r\n    function afterSwap(address sender, address recipient, bool zeroForOne, uint256 amountSpecified, uint160 sqrtPriceLimitX96) external returns (bytes4);\r\n    function beforeAddLiquidity(address sender, uint256 amount0, uint256 amount1) external returns (bytes4);\r\n    function afterAddLiquidity(address sender, uint256 amount0, uint256 amount1) external returns (bytes4);\r\n    function beforeRemoveLiquidity(address sender, uint256 amount0, uint256 amount1) external returns (bytes4);\r\n    function afterRemoveLiquidity(address sender, uint256 amount0, uint256 amount1) external returns (bytes4);\r\n}\r\n\r\n```\r\n\r\n### Hook 标志\r\n\r\n当你创建一个 hook 时，你需要指定它实现了哪些函数。这是通过 hook 标志来完成的。这些标志是位标志，指示哪些 hook 函数处于激活状态。例如：\r\n\r\n```\r\nuint160 constant BEFORE_SWAP_FLAG = 1 << 0;\r\nuint160 constant AFTER_SWAP_FLAG = 1 << 1;\r\nuint160 constant BEFORE_ADD_LIQUIDITY_FLAG = 1 << 2;\r\n// ... 其他 hook 函数的相应标志\r\n\r\n```\r\n\r\n你可以组合这些标志以指示你的 hook 实现了哪些函数。例如，如果你的 hook 实现了 beforeSwap 和 afterSwap，你将使用：\r\n\r\n```\r\nuint160 public constant FLAGS = BEFORE_SWAP_FLAG | AFTER_SWAP_FLAG;\r\n\r\n```\r\n\r\n在接下来的部分中，我们将逐步介绍创建一个简单 hook 的过程，展示如何在实践中使用这些工具。\r\n\r\n## 项目先决条件：创建 QuickNode 端点\r\n\r\n在进入代码之前，先设置一些先决条件，如获取一个 RPC URL。你可以使用公共节点或自行部署和管理基础设施；但是，如果你希望获得 8 倍的响应时间，你可以将重任交给我们。点击 [这里](https://www.quicknode.com/signup?utm_source=internal&utm_campaign=guides&utm_content=how-to-create-uniswap-v4-hooks) 注册一个免费帐户。\r\n\r\n登录 QuickNode 后，单击 **创建一个端点** 按钮，然后选择 **Ethereum** 链和 **Sepolia** 网络。\r\n\r\n创建端点后，复制 **HTTP 提供者 URL** 链接并保留，以便在本地测试网部分使用。\r\n\r\n![QuickNode 端点](https://img.learnblockchain.cn/2025/03/01/1-c9e74c5ed42adb413f1cb4237e35127e.png)\r\n\r\n## 实现你的第一个 Uniswap Hook\r\n\r\n现在我们已经介绍了核心概念并完成了先决条件，让我们深入创建你的第一个 Uniswap V4 hook。我们将使用 [v4-template](https://github.com/uniswapfoundation/v4-template) 作为起始点，它为 hook 开发提供了坚实的基础。\r\n\r\n### 目录设置\r\n\r\n使用 v4-template 作为起始点。你可以通过在 GitHub 仓库上点击 “使用此模板” 来实现，也可以通过克隆来实现：\r\n\r\n```\r\ngit clone git@github.com:uniswapfoundation/v4-template.git\r\ncd v4-template\r\n\r\n```\r\n\r\n该模板包含一个示例 hook `Counter.sol`，展示了 `beforeSwap()` 和 `afterSwap()` hooks（可以随意先看一眼）。测试模板 `Counter.t.sol` 预先配置了 v4 池管理器、测试代币和测试流动性，这将有助于测试我们的 hook。\r\n\r\n### 安装依赖\r\n\r\n首先，确保你已安装并更新 Foundry：\r\n\r\n```\r\nfoundryup\r\n\r\n```\r\n\r\n接下来，安装项目依赖：\r\n\r\n```\r\nforge install\r\n\r\n```\r\n\r\n虽然现在技术上你可以运行 `forge tests` 命令，但我们将在添加自己的自定义 hook 逻辑后再进行。\r\n\r\n### 自定义 Hook 合约\r\n\r\n现在，让我们创建自己的 hook。我们将实现一个 “Swap Limiter” hook，该 hook 限制单个地址在特定时间框架内可以进行的交换次数。这为池添加了基本的速率限制。\r\n\r\n首先，创建一个名为 `src/SwapLimiterHook.sol` 的文件。然后，打开该文件并包含以下代码：\r\n\r\n```\r\n// SPDX-License-Identifier: MIT\r\npragma solidity ^0.8.24;\r\n\r\nimport {BaseHook} from \"v4-periphery/src/base/hooks/BaseHook.sol\";\r\nimport {Hooks} from \"v4-core/src/libraries/Hooks.sol\";\r\nimport {IPoolManager} from \"v4-core/src/interfaces/IPoolManager.sol\";\r\nimport {PoolKey} from \"v4-core/src/types/PoolKey.sol\";\r\nimport {PoolId, PoolIdLibrary} from \"v4-core/src/types/PoolId.sol\";\r\nimport {BeforeSwapDelta, BeforeSwapDeltaLibrary} from \"v4-core/src/types/BeforeSwapDelta.sol\";\r\n\r\ncontract SwapLimiterHook is BaseHook {\r\n    using PoolIdLibrary for PoolKey;\r\n\r\n    uint256 public constant MAX_SWAPS_PER_HOUR = 5;\r\n    uint256 public constant HOUR = 3600;\r\n\r\n    mapping(address => uint256) public lastResetTime;\r\n    mapping(address => uint256) public swapCount;\r\n\r\n    event SwapLimitReached(address indexed user, uint256 timestamp);\r\n\r\n    constructor(IPoolManager _poolManager) BaseHook(_poolManager) {}\r\n\r\n    function getHookPermissions() public pure override returns (Hooks.Permissions memory) {\r\n        return Hooks.Permissions({\r\n            beforeInitialize: false,\r\n            afterInitialize: false,\r\n            beforeAddLiquidity: false,\r\n            afterAddLiquidity: false,\r\n            beforeRemoveLiquidity: false,\r\n            afterRemoveLiquidity: false,\r\n            beforeSwap: true,\r\n            afterSwap: false,\r\n            beforeDonate: false,\r\n            afterDonate: false,\r\n            beforeSwapReturnDelta: false,\r\n            afterSwapReturnDelta: false,\r\n            afterAddLiquidityReturnDelta: false,\r\n            afterRemoveLiquidityReturnDelta: false\r\n        });\r\n    }\r\n\r\n    // 强制实施交换限制的主函数\r\n    function beforeSwap(address sender, PoolKey calldata, IPoolManager.SwapParams calldata, bytes calldata)\r\n        external\r\n        override\r\n        returns (bytes4, BeforeSwapDelta, uint24)\r\n    {\r\n        uint256 currentTime = block.timestamp;\r\n        if (currentTime - lastResetTime[sender] >= HOUR) {\r\n            swapCount[sender] = 0;\r\n            lastResetTime[sender] = currentTime;\r\n        }\r\n\r\n        require(swapCount[sender] < MAX_SWAPS_PER_HOUR, \"Swap limit reached for this hour\");\r\n\r\n        swapCount[sender]++;\r\n\r\n        if (swapCount[sender] == MAX_SWAPS_PER_HOUR) {\r\n            emit SwapLimitReached(sender, currentTime);\r\n        }\r\n\r\n        return (BaseHook.beforeSwap.selector, BeforeSwapDeltaLibrary.ZERO_DELTA, 0);\r\n    }\r\n\r\n    function getRemainingSwaps(address user) public view returns (uint256) {\r\n        if (block.timestamp - lastResetTime[user] >= HOUR) {\r\n            return MAX_SWAPS_PER_HOUR;\r\n        }\r\n        return MAX_SWAPS_PER_HOUR - swapCount[user];\r\n    }\r\n}\r\n\r\n```\r\n\r\n这个“Swap Limiter” hook 合约实现了以下功能：\r\n\r\n- 交换限制：它限制每个地址在一小时内最多进行 5 次交换（可以通过 `MAX_SWAPS_PER_HOUR` 配置）。\r\n- 基于时间的重置：每个地址的交换计数每小时重置。\r\n- beforeSwap Hook：此函数检查发送者是否超过了当前小时的交换限制。如果没有，它将增加他们的交换计数。\r\n- 剩余交换检查：`getRemainingSwaps` 函数允许用户（或前端）检查他们在当前小时内剩余的交换次数。\r\n- 事件日志：当用户达到其交换限制时，会发出事件，这对于监控或警告非常有用。\r\n\r\n可选地，你可以运行 `forge compile` 命令来检查你的文件语法是否设置正确。\r\n\r\n### 创建测试文件\r\n\r\n现在我们已经创建了我们的 hook，我们需要创建一个测试文件。首先，创建一个名为 `test/SwapLimiterHook.t.sol` 的文件。然后，包含以下代码：\r\n\r\n```\r\n// SPDX-License-Identifier: MIT\r\npragma solidity ^0.8.24;\r\n\r\nimport \"forge-std/Test.sol\";\r\nimport {IHooks} from \"v4-core/src/interfaces/IHooks.sol\";\r\nimport {Hooks} from \"v4-core/src/libraries/Hooks.sol\";\r\nimport {TickMath} from \"v4-core/src/libraries/TickMath.sol\";\r\nimport {IPoolManager} from \"v4-core/src/interfaces/IPoolManager.sol\";\r\nimport {PoolKey} from \"v4-core/src/types/PoolKey.sol\";\r\nimport {BalanceDelta} from \"v4-core/src/types/BalanceDelta.sol\";\r\nimport {PoolId, PoolIdLibrary} from \"v4-core/src/types/PoolId.sol\";\r\nimport {CurrencyLibrary, Currency} from \"v4-core/src/types/Currency.sol\";\r\nimport {PoolSwapTest} from \"v4-core/src/test/PoolSwapTest.sol\";\r\nimport {SwapLimiterHook} from \"../src/SwapLimiterHook.sol\";\r\nimport {StateLibrary} from \"v4-core/src/libraries/StateLibrary.sol\";\r\n\r\nimport {IPositionManager} from \"v4-periphery/src/interfaces/IPositionManager.sol\";\r\nimport {EasyPosm} from \"./utils/EasyPosm.sol\";\r\nimport {Fixtures} from \"./utils/Fixtures.sol\";\r\n\r\ncontract SwapLimiterHookTest is Test, Fixtures {\r\n    using EasyPosm for IPositionManager;\r\n    using PoolIdLibrary for PoolKey;\r\n    using CurrencyLibrary for Currency;\r\n    using StateLibrary for IPoolManager;\r\n\r\n    SwapLimiterHook hook;\r\n    PoolId poolId;\r\n\r\n    uint256 tokenId;\r\n    int24 tickLower;\r\n    int24 tickUpper;\r\n\r\n    event SwapLimitReached(address indexed user, uint256 timestamp);\r\n\r\n    function setUp() public {\r\n        // 创建池管理器、实用路由器和测试代币\r\n        deployFreshManagerAndRouters();\r\n        deployMintAndApprove2Currencies();\r\n\r\n        deployAndApprovePosm(manager);\r\n\r\n        // 在具有正确标志的地址上部署 hook\r\n        address flags = address(\r\n            uint160(Hooks.BEFORE_SWAP_FLAG) ^ (0x4444 << 144) // 命名空间 hook 以避免冲突\r\n        );\r\n        bytes memory constructorArgs = abi.encode(manager);\r\n        deployCodeTo(\"SwapLimiterHook.sol:SwapLimiterHook\", constructorArgs, flags);\r\n        hook = SwapLimiterHook(flags);\r\n\r\n        // 创建池\r\n        key = PoolKey(currency0, currency1, 3000, 60, IHooks(hook));\r\n        poolId = key.toId();\r\n        manager.initialize(key, SQRT_PRICE_1_1, ZERO_BYTES);\r\n\r\n        // 向池提供全范围流动性\r\n        tickLower = TickMath.minUsableTick(key.tickSpacing);\r\n        tickUpper = TickMath.maxUsableTick(key.tickSpacing);\r\n\r\n        (tokenId,) = posm.mint(\r\n            key,\r\n            tickLower,\r\n            tickUpper,\r\n            10_000e18,\r\n            MAX_SLIPPAGE_ADD_LIQUIDITY,\r\n            MAX_SLIPPAGE_ADD_LIQUIDITY,\r\n            address(this),\r\n            block.timestamp,\r\n            ZERO_BYTES\r\n        );\r\n    }\r\n\r\n    function testDirectBeforeSwap() public {\r\n        address sender = address(this);\r\n        IPoolManager.SwapParams memory params;\r\n        bytes memory hookData;\r\n\r\n        for (uint i = 0; i < 5; i++) {\r\n            (bytes4 selector,,) = hook.beforeSwap(sender, key, params, hookData);\r\n            assertEq(selector, SwapLimiterHook.beforeSwap.selector);\r\n            console.log(\"Swap %d, Remaining swaps: %d\", i + 1, hook.getRemainingSwaps(sender));\r\n        }\r\n\r\n        vm.expectRevert(\"Swap limit reached for this hour\");\r\n        hook.beforeSwap(sender, key, params, hookData);\r\n    }\r\n\r\n    function testSwapLimiter() public {\r\n        bool zeroForOne = true;\r\n        int256 amountSpecified = -1e18; // 负数表示精确输入交换\r\n\r\n        console.log(\"Initial remaining swaps: %d\", hook.getRemainingSwaps(address(this)));\r\n\r\n        // 进行 5 次交换（应成功）\r\n        for (uint i = 0; i < 5; i++) {\r\n            // 手动调用 beforeSwap 模拟 hook 触发\r\n            (bytes4 selector,,) = hook.beforeSwap(address(this), key, IPoolManager.SwapParams({zeroForOne: zeroForOne, amountSpecified: amountSpecified, sqrtPriceLimitX96: 0}), ZERO_BYTES);\r\n            assertEq(selector, SwapLimiterHook.beforeSwap.selector);\r\n\r\n            BalanceDelta swapDelta = swap(key, zeroForOne, amountSpecified, ZERO_BYTES);\r\n            assertEq(int256(swapDelta.amount0()), amountSpecified);\r\n            console.log(\"Swap %d succeeded. Remaining swaps: %d\", i + 1, hook.getRemainingSwaps(address(this)));\r\n        }\r\n\r\n        // 第 6 次交换应回退\r\n        vm.expectRevert(\"Swap limit reached for this hour\");\r\n        hook.beforeSwap(address(this), key, IPoolManager.SwapParams({zeroForOne: zeroForOne, amountSpecified: amountSpecified, sqrtPriceLimitX96: 0}), ZERO_BYTES);\r\n\r\n        // 尝试第 6 次交换（应失败）\r\n        vm.expectRevert(abi.encodeWithSignature(\"Wrap__FailedHookCall(address,bytes)\", address(hook), abi.encodeWithSignature(\"Error(string)\", \"Swap limit reached for this hour\")));\r\n        swap(key, zeroForOne, amountSpecified, ZERO_BYTES);\r\n\r\n        // 检查剩余交换次数\r\n        uint256 remainingSwaps = hook.getRemainingSwaps(address(this));\r\n        console.log(\"Final remaining swaps: %d\", remainingSwaps);\r\n        assertEq(remainingSwaps, 0, \"Should have 0 remaining swaps\");\r\n    }\r\n\r\n    function testSwapLimitReachedEvent() public {\r\n        address sender = address(this);\r\n        IPoolManager.SwapParams memory params;\r\n        bytes memory hookData;\r\n\r\n        for (uint i = 0; i < 4; i++) {\r\n            hook.beforeSwap(sender, key, params, hookData);\r\n        }\r\n\r\n        vm.expectEmit(true, false, false, true);\r\n        emit SwapLimitReached(sender, block.timestamp);\r\n        hook.beforeSwap(sender, key, params, hookData);\r\n    }\r\n}\r\n\r\n```\r\n\r\n这些测试覆盖了我们 `SwapLimiterHook` 合约的主要功能，它强制实施交换限制，在小时重置交换计数，并检查用户的剩余交换次数。\r\n\r\n让我们回顾一下测试代码中的主要功能。\r\n\r\n- `testDirectBeforeSwap` 函数：\r\n  - 直接调用 hook 的 `beforeSwap` 函数 5 次\r\n  - 检查每个调用返回正确的选择器\r\n  - 记录每次调用后的剩余交换次数\r\n  - 期望第六次调用被回退，提示 `\"Swap limit reached for this hour\"`\r\n  \r\n- `testSwapLimiter` 函数：\r\n  - 记录初始剩余交换次数\r\n  - 进行 5 次交换，每次：\r\n    - 手动调用 `beforeSwap` 模拟 hook 触发\r\n    - 执行实际交换\r\n    - 记录成功的交换和剩余交换次数\r\n  - 期望第六次 `beforeSwap` 调用被回退\r\n  - 期望第六次实际交换被回退并带有包裹的错误消息\r\n  - 检查最终剩余交换次数为 0\r\n\r\n- `testSwapLimitReachedEvent` 函数：\r\n  - 调用 `beforeSwap` 4 次\r\n  - 期望 `SwapLimitReached` 事件在第 5 次调用时被触发\r\n  - 执行第 5 次调用 `beforeSwap`\r\n\r\n使用以下命令执行测试：\r\n\r\n```\r\nforge test\r\n\r\n```\r\n\r\n测试结束时你会看到：\r\n\r\n```\r\nRan 3 tests for test/SwapLimiterHookTest.t.sol:SwapLimiterHookTest\r\n[PASS] testDirectBeforeSwap() (gas: 74596)\r\n[PASS] testSwapLimitReachedEvent() (gas: 58075)\r\n[PASS] testSwapLimiter() (gas: 506599)\r\nSuite result: ok. 3 passed; 0 failed; 0 skipped; finished in 24.54ms (4.06ms CPU time)\r\n```\r\n\r\n如果你想查看 console.log 语句，你可以在测试命令中添加 `-vv` 标志。可选地，通过多次传递 `v` 来增加详细程度（例如 -v, -vv, -vvv），以查看更深入的信息（例如，打印执行痕迹）。\r\n\r\n根据你的具体实现和你可能添加到 hook 中的任何附加功能，记得根据需要调整测试。\r\n\r\n## 使用 Anvil 和 QuickNode 进行本地开发\r\n\r\n为了在更真实的环境中测试你的 hook，你可以使用 Anvil，一个以 QuickNode 为基础的本地测试节点。\r\n\r\n首先，在单独的终端窗口中启动 Anvil，并从你的 QuickNode RPC 进行分叉：\r\n\r\n```\r\nanvil --fork-url https://your-quicknode-endpoint.quiknode.pro/your-api-key/\r\n\r\n```\r\n\r\n这将启动一个本地测试节点，其状态是你所连接的 QuickNode RPC 当前状态的分叉。\r\n\r\n然后更新 `Anvil.s.sol` 文件，并将现有代码替换为以下内容：\r\n\r\n```\r\n// SPDX-License-Identifier: MIT\r\npragma solidity ^0.8.24;\r\n\r\nimport \"forge-std/Script.sol\";\r\nimport \"forge-std/console.sol\";\r\nimport {IHooks} from \"v4-core/src/interfaces/IHooks.sol\";\r\nimport {Hooks} from \"v4-core/src/libraries/Hooks.sol\";\r\nimport {PoolManager} from \"v4-core/src/PoolManager.sol\";\r\nimport {IPoolManager} from \"v4-core/src/interfaces/IPoolManager.sol\";\r\nimport {PoolModifyLiquidityTest} from \"v4-core/src/test/PoolModifyLiquidityTest.sol\";\r\nimport {PoolSwapTest} from \"v4-core/src/test/PoolSwapTest.sol\";\r\nimport {PoolDonateTest} from \"v4-core/src/test/PoolDonateTest.sol\";\r\nimport {PoolKey} from \"v4-core/src/types/PoolKey.sol\";\r\nimport {MockERC20} from \"solmate/src/test/utils/mocks/MockERC20.sol\";\r\nimport {Constants} from \"v4-core/src/../test/utils/Constants.sol\";\r\nimport {TickMath} from \"v4-core/src/libraries/TickMath.sol\";\r\nimport {CurrencyLibrary, Currency} from \"v4-core/src/types/Currency.sol\";\r\nimport {SwapLimiterHook} from \"../src/SwapLimiterHook.sol\";\r\nimport {HookMiner} from \"../test/utils/HookMiner.sol\";\r\n\r\ncontract SwapLimiterScript is Script {\r\n    address constant CREATE2_DEPLOYER = address(0x4e59b44847b379578588920cA78FbF26c0B4956C);\r\n\r\n    function setUp() public {}\r\n\r\n    function run() public {\r\n        vm.broadcast();\r\n        IPoolManager manager = deployPoolManager();\r\n\r\n        uint160 permissions = uint160(Hooks.BEFORE_SWAP_FLAG);\r\n\r\n        (address hookAddress, bytes32 salt) = HookMiner.find(\r\n            CREATE2_DEPLOYER,\r\n            permissions,\r\n            type(SwapLimiterHook).creationCode,\r\n            abi.encode(address(manager))\r\n        );\r\n\r\n        vm.broadcast();\r\n        SwapLimiterHook swapLimiter = new SwapLimiterHook{salt: salt}(manager);\r\n        require(address(swapLimiter) == hookAddress, \"SwapLimiterScript: hook address mismatch\");\r\n\r\n        vm.startBroadcast();\r\n        (PoolModifyLiquidityTest lpRouter, PoolSwapTest swapRouter,) = deployRouters(manager);\r\n        vm.stopBroadcast();\r\n\r\n        vm.startBroadcast();\r\n        testLifecycle(manager, address(swapLimiter), lpRouter, swapRouter);\r\n        vm.stopBroadcast();\r\n    }\r\n\r\n    function deployPoolManager() internal returns (IPoolManager) {\r\n        return IPoolManager(address(new PoolManager()));\r\n    }\r\n\r\n    function deployRouters(IPoolManager manager)\r\n        internal\r\n        returns (PoolModifyLiquidityTest lpRouter, PoolSwapTest swapRouter, PoolDonateTest donateRouter)\r\n    {\r\n        lpRouter = new PoolModifyLiquidityTest(manager);\r\n        swapRouter = new PoolSwapTest(manager);\r\n        donateRouter = new PoolDonateTest(manager);\r\n    }\r\n\r\n    function deployTokens() internal returns (MockERC20 token0, MockERC20 token1) {\r\n        MockERC20 tokenA = new MockERC20(\"MockA\", \"A\", 18);\r\n        MockERC20 tokenB = new MockERC20(\"MockB\", \"B\", 18);\r\n        if (uint160(address(tokenA)) < uint160(address(tokenB))) {\r\n            token0 = tokenA;\r\n            token1 = tokenB;\r\n        } else {\r\n            token0 = tokenB;\r\n            token1 = tokenA;\r\n        }\r\n    }\r\n\r\n    function testLifecycle(\r\n        IPoolManager manager,\r\n        address hook,\r\n        PoolModifyLiquidityTest lpRouter,\r\n        PoolSwapTest swapRouter\r\n    ) internal {\r\n        (MockERC20 token0, MockERC20 token1) = deployTokens();\r\n        token0.mint(msg.sender, 100_000 ether);\r\n        token1.mint(msg.sender, 100_000 ether);\r\n\r\n        bytes memory ZERO_BYTES = new bytes(0);\r\n\r\n        int24 tickSpacing = 60;\r\n        PoolKey memory poolKey = PoolKey(\r\n            Currency.wrap(address(token0)),\r\n            Currency.wrap(address(token1)),\r\n            3000,\r\n            tickSpacing,\r\n            IHooks(hook)\r\n        );\r\n        manager.initialize(poolKey, Constants.SQRT_PRICE_1_1, ZERO_BYTES);\r\n\r\n        token0.approve(address(lpRouter), type(uint256).max);\r\n        token1.approve(address(lpRouter), type(uint256).max);\r\n        token0.approve(address(swapRouter), type(uint256).max);\r\n        token1.approve(address(swapRouter), type(uint256).max);\r\n\r\n        lpRouter.modifyLiquidity(\r\n            poolKey,\r\n            IPoolManager.ModifyLiquidityParams(\r\n                TickMath.minUsableTick(tickSpacing),\r\n                TickMath.maxUsableTick(tickSpacing),\r\n                100 ether,\r\n                0\r\n            ),\r\n            ZERO_BYTES\r\n        );\r\n\r\n        console.log(\"Starting swap tests...\");\r\n\r\n        for (uint256 i = 0; i < 6; i++) {\r\n            console.log(\"Attempting swap %d\", i + 1);\r\n            try swapRouter.swap(\r\n                poolKey,\r\n                IPoolManager.SwapParams({\r\n                    zeroForOne: true,\r\n                    amountSpecified: 1 ether,\r\n                    sqrtPriceLimitX96: TickMath.MIN_SQRT_PRICE + 1\r\n                }),\r\n                PoolSwapTest.TestSettings({takeClaims: false, settleUsingBurn: false}),\r\n                new bytes(0)\r\n            ) {\r\n                console.log(\"Swap %d successful\", i + 1);\r\n            } catch Error(string memory reason) {\r\n                console.log(\"Swap %d failed: %s\", i + 1, reason);\r\n            } catch (bytes memory /*lowLevelData*/) {\r\n                console.log(\"Swap %d failed\", i + 1);\r\n            }\r\n        }\r\n\r\n        console.log(\"Swap tests completed.\");\r\n\r\n        SwapLimiterHook swapLimiter = SwapLimiterHook(hook);\r\n        uint256 remainingSwaps = swapLimiter.getRemainingSwaps(address(swapRouter));\r\n        console.log(\"Remaining swaps for the sender: %d\", remainingSwaps);\r\n    }\r\n}\r\n\r\n```\r\n\r\n然后，在另一个新版终端窗口中运行以下命令：\r\n\r\n```\r\nforge script script/Anvil.s.sol \\\r\n    --rpc-url http://localhost:8545 \\\r\n    --private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 \\\r\n    --broadcast\r\n\r\n```\r\n\r\n上述命令在端口为 8545 的本地测试网节点上执行 `Anvil.s.sol` 脚本。私钥对应于运行本地测试网代码时生成的账户。\r\n\r\n你会看到像这样的响应：\r\n\r\n![Anvil 响应](https://img.learnblockchain.cn/2025/03/01/2-0d7ce31cd04d5886536ad3a12cffa4de.png)\r\n\r\n注意日志语句，它们显示了每个交换后的状态，直到交换因达到交换限制而回退。\r\n\r\n## 总结\r\n\r\n恭喜！你现在已经学习了如何为 Uniswap V4 创建、测试和部署自定义 hook。从理解 hooks 的基础知识到实现自定义 hook，并将其部署到 Sepolia 测试网，你已经涵盖了 Uniswap V4 hook 开发的基本步骤。\r\n\r\n \r\n#### 我们 ❤️ 反馈！\r\n\r\n[告诉我们](https://airtable.com/shrKKKP7O1Uw3ZcUB?prefill_Guide+Name=How%20to%20Create%20Hooks%20on%20Uniswap%20V4) 如果你有任何反馈或对新主题的请求。我们很乐意听到你的想法。"},"author":{"user":"https://learnblockchain.cn/people/25306","address":null},"history":null,"timestamp":1740975684,"version":1}