# 剖析DeFi交易产品之UniswapV4:创建池子 **Published by:** [Keegan小钢](https://paragraph.com/@keeganlee/) **Published on:** 2023-11-27 **URL:** https://paragraph.com/@keeganlee/defi-uniswapv4-3 ## Content 创建池子的底层函数是 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:token0 currency1:token1 fee:费率 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 钩子函数了。最后发送事件,整个创建池子的流程就完成了。 ## Publication Information - [Keegan小钢](https://paragraph.com/@keeganlee/): Publication homepage - [All Posts](https://paragraph.com/@keeganlee/): More posts from this publication - [RSS Feed](https://api.paragraph.com/blogs/rss/@keeganlee): Subscribe to updates - [Twitter](https://twitter.com/KKeeganLee): Follow on Twitter