# 剖析DeFi交易产品之UniswapV4：创建池子

By [Keegan小钢](https://paragraph.com/@keeganlee) · 2023-11-27

---

创建池子的底层函数是 **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` 钩子函数了。最后发送事件，整个创建池子的流程就完成了。

---

*Originally published on [Keegan小钢](https://paragraph.com/@keeganlee/defi-uniswapv4-3)*
