引言
在去中心化金融(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 代码。
在 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) {}
}
需要注意:
afterSwapCount
使用了一个 mapping
类型,可以针对每个 pool 分别记录。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;
}
同时,还要声明一些类变量,包括
pool_key
,pool_id
: 我们将会创建一个虚拟的 pool 供测试使用。接下来是 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 机制。
参考资料 / 相关资源
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
【免责声明】市场有风险,投资需谨慎。本文不构成投资建议,用户应考虑本文中的任何意见、观点或结论是否符合其特定状况。据此投资,责任自负。