剖析DeFi交易产品之UniswapV4:创建池子
创建池子的底层函数是 PoolManager 合约的 initialize
函数,其代码实现并不复杂,如下所示:
function initialize(PoolKey memory key, uint160 sqrtPriceX96, bytes calldata hookData)
external
override
onlyByLocker
returns (int24 tick)
{
if (key.fee.isStaticFeeTooLarge()) revert FeeTooLarge();
// see TickBitmap.sol for overflow conditions that can arise from tick spacing being too large
if (key.tickSpacing > MAX_TICK_SPACING) revert TickSpacingTooLarge();
if (key.tickSpacing < MIN_TICK_SPACING) revert TickSpacingTooSmall();
if (key.currency0 >= key.currency1) revert CurrenciesOutOfOrderOrEqual();
if (!key.hooks.isValidHookAddress(key.fee)) revert Hooks.HookAddressNotValid(address(key.hooks));
if (key.hooks.shouldCallBeforeInitialize()) {
if (key.hooks.beforeInitialize(msg.sender, key, sqrtPriceX96, hookData) != IHooks.beforeInitialize.selector)
{
revert Hooks.InvalidHookResponse();
}
}
PoolId id = key.toId();
uint24 swapFee = key.fee.isDynamicFee() ? _fetchDynamicSwapFee(key) : key.fee.getStaticFee();
tick = pools[id].initialize(sqrtPriceX96, _fetchProtocolFees(key), _fetchHookFees(key), swapFee);
if (key.hooks.shouldCallAfterInitialize()) {
if (
key.hooks.afterInitialize(msg.sender, key, sqrtPriceX96, tick, hookData)
!= IHooks.afterInitialize.selector
) {
revert Hooks.InvalidHookResponse();
}
}
// On intitalize we emit the key's fee, which tells us all fee settings a pool can have: either a static swap fee or dynamic swap fee and if the hook has enabled swap or withdraw fees.
emit Initialize(id, key.currency0, key.currency1, key.fee, key.tickSpacing, key.hooks);
}
不过,里面有很多信息,我们需要一一拆解才能理解。
先来看入参,有三个:key
、sqrtPriceX96
、hookData
。key
指定了一个池子的唯一组成,sqrtPriceX96
是要初始化的根号价格,hookData
是需要传给 hooks 合约的初始化数据。
关于池子的唯一组成,前文我们已经讲过,PoolKey
包含了五个字段:
currency0
:token0currency1
:token1fee
:费率tickSpacing
:tick 间隔hooks
:hooks 地址
currency0
和 currency1
和以前版本的 token0
和 token1
一样,是经过排序的,currency0
为数值较小的代币,currency1
则为数值较大的代币。tickSpacing
和 UniswapV3 的一样,就不再解释了。hooks
是自定义的地址,具体如何实现后面再细说。
fee
则和之前的版本不一样了。UniswapV3 的 fee
只指定了固定的交易费率,但 UniswapV4 的 fee
其实还包含了动态费用、hook 交易费用、hook 提现费用等标志。fee
总共 24 位(bit),前 4 位用来作为不同的标志位,具体解析在 FeeLibrary 里实现,以下是其代码实现:
// SPDX-License-Identifier: GPL-2.0-or-later
pragma solidity ^0.8.20;
library FeeLibrary {
// 静态费率掩码
uint24 public constant STATIC_FEE_MASK = 0x0FFFFF;
// 支持动态费用的标志位
uint24 public constant DYNAMIC_FEE_FLAG = 0x800000; // 1000
// 支持hook交易费用的标志位
uint24 public constant HOOK_SWAP_FEE_FLAG = 0x400000; // 0100
// 支持hook提现费用的标志位
uint24 public constant HOOK_WITHDRAW_FEE_FLAG = 0x200000; // 0010
// 是否支持动态费用
function isDynamicFee(uint24 self) internal pure returns (bool) {
return self & DYNAMIC_FEE_FLAG != 0;
}
// 是否支持hook交易费用
function hasHookSwapFee(uint24 self) internal pure returns (bool) {
return self & HOOK_SWAP_FEE_FLAG != 0;
}
// 是否支持hook提现费用
function hasHookWithdrawFee(uint24 self) internal pure returns (bool) {
return self & HOOK_WITHDRAW_FEE_FLAG != 0;
}
// 静态费率是否超过最大值
function isStaticFeeTooLarge(uint24 self) internal pure returns (bool) {
return self & STATIC_FEE_MASK >= 1000000;
}
// 获取出静态手续费率
function getStaticFee(uint24 self) internal pure returns (uint24) {
return self & STATIC_FEE_MASK;
}
}
静态费率最大值为 1000000,表示 100% 费用。那么要设置 0.3% 的费率的话那就是 3000,这个精度和 UniswapV3 是一致的。
那如果是要支持静态费率,就假设静态费率为 0.3%,同时又要支持 hook 交易费和提现费,则需要同时设置这两个标志位,那 fee
字段用 16 进制表示的值为 0xC01778
。其二进制表示为:11000000000101110111000
,前面两个 1 就是两个标志位,后面的 101110111000
其实就是十进制数 3000 的二进制数。
另外,UniswapV3 的费率只能在指定支持的几个费率中选择一个,而 UniswapV4 取消了这个限制,费率完全放开了,由池子的创建者自己去决定要设置多少费率。
回到 initialize
函数,函数声明里还有一个函数修饰器 onlyByLocker
,这也是需要展开说明的一个地方。我们先来看这个函数修饰器的代码:
modifier onlyByLocker() {
address locker = Lockers.getCurrentLocker();
if (msg.sender != locker) revert LockedBy(locker);
_;
}
它要求调用者需是当前的 locker。要成为 locker,需要调用 PoolManager 合约的 lock()
函数。以下是 lock() 函数的实现:
function lock(bytes calldata data) external override returns (bytes memory result) {
//把调用者添加到locker队列里
Lockers.push(msg.sender);
//需在这个回调函数里完成所有事情,包括支付等操作
result = ILockCallback(msg.sender).lockAcquired(data);
if (Lockers.length() == 1) {//只有一个locker的情况下,做清理操作
if (Lockers.nonzeroDeltaCount() != 0) revert CurrencyNotSettled();
Lockers.clear();
} else {//不止一个locker的情况下,移出顶部的locker
Lockers.pop();
}
}
其中,Lockers 是封装了锁定操作的库合约,push()
函数会把当前调用者添加到锁定者队列里,具体实现用到了 EIP-1153 所引入的 tstore
瞬态存储操作码。具体原理不在这里展开。
而下一步是调用了 msg.sender
的回调函数 lockAcquired()
,这一步非常关键,透露出很多信息。首先,这说明了,调用者需是一个合约才行,而不能是一个 EOA 账户。然后,调用者需实现 ILockCallback 接口,该接口只定义了一个函数,就是 lockAcquired()
函数。最后,调用者合约需在 lockAcquired()
函数里实现所有事情,包括完成支付和各种不同的交易场景,其实也包括了调用 initialize
函数。
我的理解,lock()
函数调用者应该是一个路由合约,或不同功能模块用不同的合约实现,比如可以加一个工厂合约用于完成创建池子的操作,但目前 UniswapV4 还没看到关于路由合约或工厂合约的实现,所以具体逻辑不得而知。
总而言之,到了这里,我们就已经知道了,创建池子的调用者需是一个实现了 ILockCallback 接口的合约,先调用 lock()
函数成为 locker
,再通过 lockAcquired()
回调函数调其 initialize
函数来完成初始化池子。
回到 initialize
函数的具体实现。前面是一些基本的校验,我们摘出来看一下:
// 静态费率不能超过最大值
if (key.fee.isStaticFeeTooLarge()) revert FeeTooLarge();
// tickSpacing需在限定的有效范围内
if (key.tickSpacing > MAX_TICK_SPACING) revert TickSpacingTooLarge();
if (key.tickSpacing < MIN_TICK_SPACING) revert TickSpacingTooSmall();
// currency0需小于currency1
if (key.currency0 >= key.currency1) revert CurrenciesOutOfOrderOrEqual();
// hooks地址需是符合条件的有效地址
if (!key.hooks.isValidHookAddress(key.fee)) revert Hooks.HookAddressNotValid(address(key.hooks));
接着,判断是否需要调用 beforeInitialize
的钩子函数,如下:
if (key.hooks.shouldCallBeforeInitialize()) {
if (key.hooks.beforeInitialize(msg.sender, key, sqrtPriceX96, hookData) != IHooks.beforeInitialize.selector)
{
revert Hooks.InvalidHookResponse();
}
}
钩子函数需返回该函数的 selector
。
之后的三行代码实现初始化逻辑,代码如下:
// 把key转为id
PoolId id = key.toId();
// 读取出交易费率
uint24 swapFee = key.fee.isDynamicFee() ? _fetchDynamicSwapFee(key) : key.fee.getStaticFee();
// 执行实际的初始化操作
tick = pools[id].initialize(sqrtPriceX96, _fetchProtocolFees(key), _fetchHookFees(key), swapFee);
这里面有好几个跟费用相关的函数,有必要说明一下。
isDynamicFee()
就是前面所说的 FeeLibrary 库合约的函数,判断是否设置了支持动态费用的标志位。如果不支持,则通过 getStaticFee()
读取出静态费率;如果支持动态费用,则通过 _fetchDynamicSwapFee()
获取费率。 _fetchDynamicSwapFee()
函数是在抽象合约 Fees 里实现的,其实现非常简单,就两行代码,如下所示:
function _fetchDynamicSwapFee(PoolKey memory key) internal view returns (uint24 dynamicSwapFee) {
dynamicSwapFee = IDynamicFeeManager(address(key.hooks)).getFee(msg.sender, key);
if (dynamicSwapFee >= MAX_SWAP_FEE) revert FeeTooLarge();
}
可见,其实是调用了 hooks 合约的 getFee()
函数。即是说,要支持动态费用,则 hooks 合约需要实现 IDynamicFeeManager 接口的 getFee()
函数。
_fetchHookFees()
函数也类似,需要 hooks 合约实现 IHookFeeManager 接口的 getHookFees()
函数。不过 getHookFees()
的返回值里其实是由两个费用组合而成的,一个是交易费,一个是提现费。返回值是 24 位,前 12 位是交易费,后 12 位是提现费。
_fetchProtocolFees()
函数则是用于获取协议费,这就和 hooks 合约没有关系了,是由一个实现了 IProtocolFeeController 接口的合约进行管理的。只有合约 owner 可以设置这个合约地址。目前 UniswapV4 还没有提供关于该合约的实现,短期内应该也不会开启收取协议费。
最后,通过调用 pools[id].initialize()
函数完成内部的初始化工作。这里的关键就是 pools
状态变量,新建的池子状态最终其实也是存储在了 pools
里。它是一个 mapping
类型的变量,如下:
mapping(PoolId id => Pool.State) public pools;
其 value 存的是一个 Pool.State
对象,这是一个定义在 Pool 库合约里的结构体,具体包含了如下数据:
struct State {
Slot0 slot0;
uint256 feeGrowthGlobal0X128;
uint256 feeGrowthGlobal1X128;
uint128 liquidity;
mapping(int24 => TickInfo) ticks;
mapping(int16 => uint256) tickBitmap;
mapping(bytes32 => Position.Info) positions;
}
如果和 UniswapV3 对比就会发现,其实就是将 UniswapV3Pool 里的大部分状态变量移到了 State
里。另外,slot0
的字段与 UniswapV3Pool 的有所不同,以下是其具体字段:
struct Slot0 {
// the current price
uint160 sqrtPriceX96;
// the current tick
int24 tick;
uint24 protocolFees;
uint24 hookFees;
// used for the swap fee, either static at initialize or dynamic via hook
uint24 swapFee;
}
可看到,与 UniswapV3Pool 的 Slot0
相比,没有了预言机相关的状态数据。另外,关于费用的字段总共有三个:protocolFees
、 hookFees
和 swapFee
。
pools[id].initialize()
函数的实现是在 Pool
库合约里,其代码逻辑很简单,就是初始化了 slot0
,代码如下:
function initialize(State storage self, uint160 sqrtPriceX96, uint24 protocolFees, uint24 hookFees, uint24 swapFee)
internal
returns (int24 tick)
{
//当前状态下的根号价格不为0,说明已经初始化过了
if (self.slot0.sqrtPriceX96 != 0) revert PoolAlreadyInitialized();
//根据根号价格算出tick
tick = TickMath.getTickAtSqrtRatio(sqrtPriceX96);
//初始化slot0
self.slot0 = Slot0({
sqrtPriceX96: sqrtPriceX96,
tick: tick,
protocolFees: protocolFees,
hookFees: hookFees,
swapFee: swapFee
});
}
再回到 PoolManager 合约自身的 initialize()
函数,还剩下最后一段代码如下:
if (key.hooks.shouldCallAfterInitialize()) {
if (
key.hooks.afterInitialize(msg.sender, key, sqrtPriceX96, tick, hookData)
!= IHooks.afterInitialize.selector
) {
revert Hooks.InvalidHookResponse();
}
}
//发送事件
emit Initialize(id, key.currency0, key.currency1, key.fee, key.tickSpacing, key.hooks);
完成了 PoolManager 自身的初始化逻辑之后,就是判断是否需要再调用 hooks 合约的 afterInitialize
钩子函数了。最后发送事件,整个创建池子的流程就完成了。