本文介绍了如何在Uniswap V4中使用Hooks添加自定义逻辑,详细讲解了Hooks的生命周期、IHook接口和Hook标志,并提供了一个简单的Swap Limiter Hook的实现和测试方法。
Uniswap V4 引入了 hooks,这是一种在池操作的重要节点添加自定义逻辑的强大方式。Hooks 是与 Uniswap 池配合工作的独立智能合约,允许开发者在不改变整个协议的情况下改变交换行为、创建复杂策略并实现自定义的 AMM 逻辑。
在本指南中,我们将展示如何构建一个添加基本功能的简单 hook。这将帮助你学习如何开发更复杂的自定义 hooks。让我们开始吧!
Uniswap V4 hooks 是在交换过程中特定时间介入的智能合约,允许开发者自定义和扩展流动性池的功能。这些 hooks 在设定时间与主协议配合工作,允许在不降低核心合约安全性的情况下增加更多功能。
关于 Uniswap V4 hooks 的关键点:
Hooks 允许你详细控制池操作。它们允许在池逻辑中实现自定义费用结构、改变流动性分布和复杂的交易策略。通过在交换过程的关键节点介入,hooks 可以增加风险控制、提高资本使用效率,甚至帮助在区块链上创建新的金融工具。
一些你可以用 hooks 实现的功能示例:
这种定制级别允许你创建适合特定市场条件或交易需求的池。你可以单独开发和使用的 hooks,并将它们结合以创建复杂的池行为,在交换过程中运行自定义逻辑以节省 gas。这种设计允许你添加通常需要多个交易或链下协调的功能。
在接下来的部分中,我们将介绍你所需了解的 hooks 核心概念,如 Hooks 生命周期、IHook 接口和 Hook 标志。
Hooks 可以在交换过程中的几个重要节点介入:
通过创建这些 hook 函数,开发者可以在池操作的特定时间实现自定义行为。
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 函数是活跃的。例如:
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 实现了 beforeSwap
和 afterSwap
,你可以使用:
uint160 public constant FLAGS = BEFORE_SWAP_FLAG | AFTER_SWAP_FLAG;
在接下来的部分中,我们将通过创建一个简单的 hook 来展示如何在实践中使用这些工具。
在深入代码之前,让我们设置一些前提条件,比如获取一个 RPC URL。你可以使用公共节点或部署和管理自己的基础设施;但是,如果你希望获得 8 倍更快的响应时间,可以将繁重的工作交给我们。在此注册一个免费账户。
登录 QuickNode 后,点击 Create an endpoint 按钮,然后选择 Ethereum 链和 Sepolia 网络。
创建你的端点后,复制 HTTP Provider URL 链接并保存好,因为在本地测试网部分你将需要它。
现在我们已经介绍了核心概念并完成了前提条件,让我们深入创建你的第一个 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,它限制单个地址在特定时间范围内可以执行的交换次数。这为池添加了一种基本的速率限制。
首先,创建一个名为 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 合约实现了以下功能:
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
函数:
beforeSwap
函数 5 次"Swap limit reached for this hour"
testSwapLimiter
函数:
- 原文链接: quicknode.com/guides/def...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!