本文介绍了如何在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 将在池操作的不同节点调用的函数。以下是它可能的简化版本:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
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 后,单击 创建一个端点 按钮,然后选择 Ethereum 链和 Sepolia 网络。
创建端点后,复制 HTTP 提供者 URL 链接并保留,以便在本地测试网部分使用。
现在我们已经介绍了核心概念并完成了先决条件,让我们深入创建你的第一个 Uniswap V4 hook。我们将使用 v4-template 作为起始点,它为 hook 开发提供了坚实的基础。
使用 v4-template 作为起始点。你可以通过在 GitHub 仓库上点击 “使用此模板” 来实现,也可以通过克隆来实现:
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。我们将实现一个 “Swap Limiter” 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];
}
}
这个“Swap Limiter” hook 合约实现了以下功能:
MAX_SWAPS_PER_HOUR
配置)。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) // 命名空间 hook 以避免冲突
);
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
函数:
beforeSwap
模拟 hook 触发beforeSwap
调用被回退testSwapLimitReachedEvent
函数:
beforeSwap
4 次SwapLimitReached
事件在第 5 次调用时被触发beforeSwap
使用以下命令执行测试:
forge test
测试结束时你会看到:
Ran 3 tests for test/SwapLimiterHookTest.t.sol:SwapLimiterHookTest
[PASS] testDirectBeforeSwap() (gas: 74596)
[PASS] testSwapLimitReachedEvent() (gas: 58075)
[PASS] testSwapLimiter() (gas: 506599)
Suite result: ok. 3 passed; 0 failed; 0 skipped; finished in 24.54ms (4.06ms CPU time)
如果你想查看 console.log 语句,你可以在测试命令中添加 -vv
标志。可选地,通过多次传递 v
来增加详细程度(例如 -v, -vv, -vvv),以查看更深入的信息(例如,打印执行痕迹)。
根据你的具体实现和你可能添加到 hook 中的任何附加功能,记得根据需要调整测试。
为了在更真实的环境中测试你的 hook,你可以使用 Anvil,一个以 QuickNode 为基础的本地测试节点。
首先,在单独的终端窗口中启动 Anvil,并从你的 QuickNode RPC 进行分叉:
anvil --fork-url https://your-quicknode-endpoint.quiknode.pro/your-api-key/
这将启动一个本地测试节点,其状态是你所连接的 QuickNode RPC 当前状态的分叉。
然后更新 Anvil.s.sol
文件,并将现有代码替换为以下内容:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import "forge-std/Script.sol";
import "forge-std/console.sol";
import {IHooks} from "v4-core/src/interfaces/IHooks.sol";
import {Hooks} from "v4-core/src/libraries/Hooks.sol";
import {PoolManager} from "v4-core/src/PoolManager.sol";
import {IPoolManager} from "v4-core/src/interfaces/IPoolManager.sol";
import {PoolModifyLiquidityTest} from "v4-core/src/test/PoolModifyLiquidityTest.sol";
import {PoolSwapTest} from "v4-core/src/test/PoolSwapTest.sol";
import {PoolDonateTest} from "v4-core/src/test/PoolDonateTest.sol";
import {PoolKey} from "v4-core/src/types/PoolKey.sol";
import {MockERC20} from "solmate/src/test/utils/mocks/MockERC20.sol";
import {Constants} from "v4-core/src/../test/utils/Constants.sol";
import {TickMath} from "v4-core/src/libraries/TickMath.sol";
import {CurrencyLibrary, Currency} from "v4-core/src/types/Currency.sol";
import {SwapLimiterHook} from "../src/SwapLimiterHook.sol";
import {HookMiner} from "../test/utils/HookMiner.sol";
contract SwapLimiterScript is Script {
address constant CREATE2_DEPLOYER = address(0x4e59b44847b379578588920cA78FbF26c0B4956C);
function setUp() public {}
function run() public {
vm.broadcast();
IPoolManager manager = deployPoolManager();
uint160 permissions = uint160(Hooks.BEFORE_SWAP_FLAG);
(address hookAddress, bytes32 salt) = HookMiner.find(
CREATE2_DEPLOYER,
permissions,
type(SwapLimiterHook).creationCode,
abi.encode(address(manager))
);
vm.broadcast();
SwapLimiterHook swapLimiter = new SwapLimiterHook{salt: salt}(manager);
require(address(swapLimiter) == hookAddress, "SwapLimiterScript: hook address mismatch");
vm.startBroadcast();
(PoolModifyLiquidityTest lpRouter, PoolSwapTest swapRouter,) = deployRouters(manager);
vm.stopBroadcast();
vm.startBroadcast();
testLifecycle(manager, address(swapLimiter), lpRouter, swapRouter);
vm.stopBroadcast();
}
function deployPoolManager() internal returns (IPoolManager) {
return IPoolManager(address(new PoolManager()));
}
function deployRouters(IPoolManager manager)
internal
returns (PoolModifyLiquidityTest lpRouter, PoolSwapTest swapRouter, PoolDonateTest donateRouter)
{
lpRouter = new PoolModifyLiquidityTest(manager);
swapRouter = new PoolSwapTest(manager);
donateRouter = new PoolDonateTest(manager);
}
function deployTokens() internal returns (MockERC20 token0, MockERC20 token1) {
MockERC20 tokenA = new MockERC20("MockA", "A", 18);
MockERC20 tokenB = new MockERC20("MockB", "B", 18);
if (uint160(address(tokenA)) < uint160(address(tokenB))) {
token0 = tokenA;
token1 = tokenB;
} else {
token0 = tokenB;
token1 = tokenA;
}
}
function testLifecycle(
IPoolManager manager,
address hook,
PoolModifyLiquidityTest lpRouter,
PoolSwapTest swapRouter
) internal {
(MockERC20 token0, MockERC20 token1) = deployTokens();
token0.mint(msg.sender, 100_000 ether);
token1.mint(msg.sender, 100_000 ether);
bytes memory ZERO_BYTES = new bytes(0);
int24 tickSpacing = 60;
PoolKey memory poolKey = PoolKey(
Currency.wrap(address(token0)),
Currency.wrap(address(token1)),
3000,
tickSpacing,
IHooks(hook)
);
manager.initialize(poolKey, Constants.SQRT_PRICE_1_1, ZERO_BYTES);
token0.approve(address(lpRouter), type(uint256).max);
token1.approve(address(lpRouter), type(uint256).max);
token0.approve(address(swapRouter), type(uint256).max);
token1.approve(address(swapRouter), type(uint256).max);
lpRouter.modifyLiquidity(
poolKey,
IPoolManager.ModifyLiquidityParams(
TickMath.minUsableTick(tickSpacing),
TickMath.maxUsableTick(tickSpacing),
100 ether,
0
),
ZERO_BYTES
);
console.log("Starting swap tests...");
for (uint256 i = 0; i < 6; i++) {
console.log("Attempting swap %d", i + 1);
try swapRouter.swap(
poolKey,
IPoolManager.SwapParams({
zeroForOne: true,
amountSpecified: 1 ether,
sqrtPriceLimitX96: TickMath.MIN_SQRT_PRICE + 1
}),
PoolSwapTest.TestSettings({takeClaims: false, settleUsingBurn: false}),
new bytes(0)
) {
console.log("Swap %d successful", i + 1);
} catch Error(string memory reason) {
console.log("Swap %d failed: %s", i + 1, reason);
} catch (bytes memory /*lowLevelData*/) {
console.log("Swap %d failed", i + 1);
}
}
console.log("Swap tests completed.");
SwapLimiterHook swapLimiter = SwapLimiterHook(hook);
uint256 remainingSwaps = swapLimiter.getRemainingSwaps(address(swapRouter));
console.log("Remaining swaps for the sender: %d", remainingSwaps);
}
}
然后,在另一个新版终端窗口中运行以下命令:
forge script script/Anvil.s.sol \
--rpc-url http://localhost:8545 \
--private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 \
--broadcast
上述命令在端口为 8545 的本地测试网节点上执行 Anvil.s.sol
脚本。私钥对应于运行本地测试网代码时生成的账户。
你会看到像这样的响应:
注意日志语句,它们显示了每个交换后的状态,直到交换因达到交换限制而回退。
恭喜!你现在已经学习了如何为 Uniswap V4 创建、测试和部署自定义 hook。从理解 hooks 的基础知识到实现自定义 hook,并将其部署到 Sepolia 测试网,你已经涵盖了 Uniswap V4 hook 开发的基本步骤。
通过在 Twitter (@QuickNode) 上关注 QuickNode 或加入 Discord 社区,保持对区块链开发最新动态的了解。
告诉我们 如果你有任何反馈或对新主题的请求。我们很乐意听到你的想法。
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!