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