如何编写第一个 hook
2024-10-21 14:07
Antalpha Labs
2024-10-21 14:07
订阅此专栏
收藏此文章

引言

在去中心化金融(DeFi)的世界中,Uniswap 占据着不可或缺的地位。随着 Uniswap 的不断演进,V4 版本引入了 Hook 机制,使得开发者能够在流动性池和交易之间插入自定义的逻辑。在上一篇文章中,我们已经详细介绍了 Hook 的核心概念及其作用。为了帮助大家更好地理解和应用 Hook,本篇将手把手教大家如何从头编写一个 Hook,并通过详细的代码示例和测试用例帮助您快速上手。


安装开发工具

编写 Uniswap V4 Hook,首先需要搭建一个开发环境。Uniswap 官方从 V3 开始便推荐使用 Foundry 进行合约开发,Foundry 是一个现代化的智能合约开发工具,基于 Rust 开发,提供编译、测试、运行、部署一站式服务。开发环境使用纯 Solidity ,不引入 Javascript 或者 Python。

Foundry 可以通过多种方式安装,官方提供了详细的安装文档:

https://book.getfoundry.sh/getting-started/installation

安装完成后,就可以下一步的工作了。


初始化项目

安装好开发环境后,接下来我们使用 Foundry 初始化一个新的项目。初始化命令非常简单:

forge init v4-hook-demo
cd v4-hook-demo

初始化后的项目结构非常完善,包含了 Foundry 的必要配置文件,并且自动生成了 GitHub 的 workflow 文件,这意味着如果您使用 GitHub 托管项目,已经完成了部分持续集成的配置工作。如果您希望将项目推送到 GitHub,可以按照以下步骤操作:

如果需要连接到 github,可以先创建一个 git 项目,然后通过下面的命令同步:

git remote add origin git@github.com:32ethers/v4-hook-demo.git
git push --set-upstream origin master

这个项目中包含一个简单的合约示例 Counter.sol,它演示了如何编写一个基础的智能合约。我们可以浏览一下代码,了解它的结构和功能。如果不需要的话可以将其删除:

rm ./**/Counter*.sol

安装依赖

接下来,为了使项目支持 Uniswap V4,我们需要安装相应的依赖库。以下命令将帮助我们安装核心库 v4-core 和外设库 v4-periphery

forge install Uniswap/v4-core
forge install Uniswap/v4-periphery

为了简化导入路径,避免在每次编写代码时都写冗长的 import 语句,我们可以使用 forge remappings 生成一个重定向文件:

forge remappings > remappings.txt

生成的 remappings.txt 文件内容如下:

@ensdomains/=lib/v4-core/node_modules/@ensdomains/
@openzeppelin/=lib/v4-core/lib/openzeppelin-contracts/
@openzeppelin/contracts/=lib/v4-core/lib/openzeppelin-contracts/contracts/
@uniswap/v4-core/=lib/v4-periphery/lib/v4-core/
ds-test/=lib/v4-core/lib/forge-std/lib/ds-test/src/
erc4626-tests/=lib/v4-core/lib/openzeppelin-contracts/lib/erc4626-tests/
forge-gas-snapshot/=lib/v4-core/lib/forge-gas-snapshot/src/
forge-std/=lib/forge-std/src/
hardhat/=lib/v4-core/node_modules/hardhat/
openzeppelin-contracts/=lib/v4-core/lib/openzeppelin-contracts/
permit2/=lib/v4-periphery/lib/permit2/
solmate/=lib/v4-core/lib/solmate/
v4-core/=lib/v4-core/src/
v4-periphery/=lib/v4-periphery/

这里有一个小问题需要注意:我们需要将 v4-core/=lib/v4-core/src/ 修改为 v4-core/=lib/v4-periphery/lib/v4-core/src/。这样做可以确保我们编写的 Hook 所依赖的 v4-core 版本与 v4-periphery 引用的版本保持一致,避免因版本冲突导致的编译错误。


设置项目环境

由于 Uniswap V4 引入了「临时存储(Transient Storage)」机制,因此我们需要指定运行环境为即将到来的坎昆硬分叉版本,并确保 Solidity 编译器版本大于 0.8.24。您可以在项目根目录的 foundry.toml 文件中添加以下三行代码来完成配置:

solc_version = "0.8.26"
evm_version = "cancun"
ffi = true

至此,环境搭建部分已经完成,接下来我们就可以开始编写 Hook 代码。


创建 hook

在 Uniswap V4 中,Hook 的作用是允许开发者在流动性管理和交易过程中插入自定义逻辑。我们接下来将编写一个简单的 Hook,用于记录每次 swap 交易时的操作次数。首先,我们需要新建一个合约文件 FirstHook.sol, 然后添加引用。

import {BaseHook} from "v4-periphery/src/base/hooks/BaseHook.sol";
import {Hooks} from "v4-core/libraries/Hooks.sol";
import {IPoolManager} from "v4-core/interfaces/IPoolManager.sol";
import {PoolKey} from "v4-core/types/PoolKey.sol";
import {PoolId,PoolIdLibrary} from "v4-core/types/PoolId.sol";
import {BalanceDelta} from "v4-core/types/BalanceDelta.sol";
import {BeforeSwapDelta,BeforeSwapDeltaLibrary} from "v4-core/types/BeforeSwapDelta.sol";

然后声明一个 hook 类以及类变量,并初始化构造函数。

contract CountingHook is BaseHook {
using PoolIdLibrary for PoolKey;
mapping(PoolId => uint256 count) public afterSwapCount;
constructor(IPoolManager _poolManager) BaseHook(_poolManager) {}
}

需要注意:

  1. 所有的 hook 都继承自 BaseHook。
  2. Hook 合约可以被多个 pool 引用,所以我们的类变量 afterSwapCount 使用了一个 mapping 类型,可以针对每个 pool 分别记录。
  3. Hook 合约的构造函数必须包含 IPoolManager 参数,IPoolManager 是 Hook 的管理接口,能进行很多操作。v4 的库也提供了 onlyByPoolManager 修饰符,限制某些函数只能由管理员操作。

接下来是 getHookPermissions() 函数,这个函数是必须重载的。它的作用很简单,就是定义启动哪些 Hook。我们想在 swap 交易之后,给计数器加 1,所以将 afterSwap 设置为 true,其他设置为 false。

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

最后,重载 afterSwap 函数,让计数器加一。对于这些函数的说明,见IHooks.sol[1]

function afterSwap(address,PoolKey calldata key,IPoolManager.SwapParams calldata,BalanceDelta,bytes calldata)
external
override
returns (bytes4,int128)
{
afterSwapCount[key.toId()]++;
return (BaseHook.afterSwap.selector,0);
}

最后贴一下完整的例子。

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

import {BaseHook} from "v4-periphery/src/base/hooks/BaseHook.sol";

import {Hooks} from "v4-core/libraries/Hooks.sol";
import {IPoolManager} from "v4-core/interfaces/IPoolManager.sol";
import {PoolKey} from "v4-core/types/PoolKey.sol";
import {PoolId,PoolIdLibrary} from "v4-core/types/PoolId.sol";
import {BalanceDelta} from "v4-core/types/BalanceDelta.sol";
import {BeforeSwapDelta,BeforeSwapDeltaLibrary} from "v4-core/types/BeforeSwapDelta.sol";

contract CountingHook is BaseHook {
using PoolIdLibrary for PoolKey;

mapping(PoolId => uint256 count) public afterSwapCount;

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

function afterSwap(address,PoolKey calldata key,IPoolManager.SwapParams calldata,BalanceDelta,bytes calldata)
external
override
returns (bytes4,int128)
{
afterSwapCount[key.toId()]++;
return (BaseHook.afterSwap.selector,0);
}
}


创建测试用例

测试用例也是 hook 开发的重要内容。在为 hook 开发测试用例时,需要设置测试 pool 并添加流动性。这里演示如何开发测试用例。

首先添加引用。

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.26;

import {Deployers} from "@uniswap/v4-core/test/utils/Deployers.sol";
import {PoolSwapTest} from "v4-core/test/PoolSwapTest.sol";
import {MockERC20} from "solmate/src/test/utils/mocks/MockERC20.sol";

import {PoolManager} from "v4-core/PoolManager.sol";
import {IPoolManager} from "v4-core/interfaces/IPoolManager.sol";

import {Currency,CurrencyLibrary} from "v4-core/types/Currency.sol";

import {Hooks} from "v4-core/libraries/Hooks.sol";
import {TickMath} from "v4-core/libraries/TickMath.sol";
import {SqrtPriceMath} from "v4-core/libraries/SqrtPriceMath.sol";
import {LiquidityAmounts} from "@uniswap/v4-core/test/utils/LiquidityAmounts.sol";

import "forge-std/Test.sol";
import {CountingHook} from "../src/FirstHook.sol";
import {PoolKey} from "v4-core/types/PoolKey.sol";
import {PoolId} from "v4-core/types/PoolId.sol";

然后声明测试类,这个类需要继承 Test,由于我们还需要部署虚拟类,还需要继承 Deployers 类,这个类中提供了很多有用的变量。

contract CountingHookTest is Test,Deployers {
using CurrencyLibrary for Currency;

CountingHook public hook;

PoolKey pool_key;
PoolId pool_id;

Currency token0;
Currency token1;
}

同时,还要声明一些类变量,包括

  • hook: hook 对象会在 setup 中初始化,然后供各个测试函数调用。
  • pool_keypool_id: 我们将会创建一个虚拟的 pool 供测试使用。
  • token0,token1: 这是 pool 的交易对所涉及的两个 token。

接下来是 setup() 函数,它会在每个测试函数执行的时候先执行。我会将每句话的作用注释到代码中:

function setUp() public {
// 部署虚拟的 Manager 合约和 Router 合约
deployFreshManagerAndRouters();

// 使用内置的函数部署 pool 的两个 token
(token0,token1) = deployMintAndApprove2Currencies();

// 计算 hook 地址
uint160 flags = uint160(Hooks.AFTER_SWAP_FLAG);
address hookAddress = address(flags);

// 把 hook 部署到指定地址上,获得 hook 对象
deployCodeTo("FirstHook.sol",abi.encode(manager),hookAddress);
hook = CountingHook(hookAddress);

// 将两种 token approve 给 hook
MockERC20(Currency.unwrap(token0)).approve(address(hook),type(uint256).max);
MockERC20(Currency.unwrap(token1)).approve(address(hook),type(uint256).max);

// 初始化 pool
(pool_key,pool_id) = initPool(
token0,// Currency 0
token1,// Currency 1
hook,// Hook 对象
3000,// 设置手续费费率
SQRT_PRICE_1_1,// 设置初始价格,这里相当于将价格设置为 1.
ZERO_BYTES // 设置 Hook 的初始化数据为空,这个参数会被传递给 Hook 的 beforeInitialize 和 afterInitialize 函数
);

// 添加流动性,范围是 -60 ~ 60,并将流动性设置为一个很大的数字.
modifyLiquidityRouter.modifyLiquidity(
pool_key,
IPoolManager.ModifyLiquidityParams({
tickLower: -60,
tickUpper: 60,
liquidityDelta: 10 ether,
salt: bytes32(0)
}),
ZERO_BYTES // 传递给 Hook 的数据同样为空
);
}

然后是测试函数,必须以 test_ 开头。在这个函数中,我们通过 swap 交易来触发 Hook 的执行。

function test_swap() public {
// 设置 swap 参数
IPoolManager.SwapParams memory params = IPoolManager.SwapParams({
zeroForOne: true, // zeroForOne 代表 swap 方向是从 token0 向 token1
amountSpecified: 0.1 ether,// swap 的数量,注意这里是 token 数量.
sqrtPriceLimitX96: MIN_PRICE_LIMIT // swap 的价格限制,如果到达这个价格,交易会失败
});
PoolSwapTest.TestSettings memory testSettings =
PoolSwapTest.TestSettings({takeClaims: false,settleUsingBurn: false});
// 检查初始值是 0
assertEq(hook.afterSwapCount(pool_id),0);
// 进行 swap 交易
swapRouter.swap(pool_key,params,testSettings,"");
//swap 之后,计数器变为 1
assertEq(hook.afterSwapCount(pool_id),1);
}


完整的例子在这里[2]

最后,格式化代码并运行测试用例。

forge fmt
forge test -vv

总结

通过本文的讲解,我们已经完成了从环境搭建、依赖安装、项目初始化,到编写 Hook 和测试用例的整个流程。借助 Foundry 的便捷功能,开发 Uniswap V4 Hook 变得简单且高效。Uniswap 官方提供了非常详尽的文档和注释,帮助开发者更好地理解和使用 Hook 机制。


考资料 / 相关资源

[1]

IHooks.sol: https://github.com/Uniswap/v4-core/blob/main/src/interfaces/IHooks.sol

[2]

这里: https://github.com/32ethers/v4-hook-demo/blob/master/test/FirstHook.t.sol

[3]

Uniswap V4 模板项目: https://github.com/uniswapfoundation/v4-template

[4]

Uniswap 例子: https://www.v4-by-example.org/

[5]

Periphery 合约: https://github.com/Uniswap/v4-periphery

[6]

Core 合约: https://github.com/Uniswap/v4-core


笔者:Steven Sun,Zelos
点击左下方「阅读原文」/「Read More」,获取更多相关信息



Antalpha Labs 是一个非盈利的 Web3 开发者社区,致力于通过发起和支持开源软件推动 Web3 技术的创新和应用。

官网:https://labs.antalpha.com

Twitter:https://twitter.com/Antalpha_Labs

Youtube:https://www.youtube.com/channel/UCNFowsoGM9OI2NcEP2EFgrw

联系我们:hello.labs@antalpha.com

【免责声明】市场有风险,投资需谨慎。本文不构成投资建议,用户应考虑本文中的任何意见、观点或结论是否符合其特定状况。据此投资,责任自负。

Antalpha Labs
数据请求中
查看更多

推荐专栏

数据请求中
在 App 打开