# 剖析DeFi交易产品之UniswapV3：工厂合约

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

---

**UniswapV3Factory** 合约主要用来创建不同代币对的流动性池子合约，其代码实现并不复杂，以下就是代码实现：

    contract UniswapV3Factory is IUniswapV3Factory, UniswapV3PoolDeployer, NoDelegateCall {
        address public override owner;
    
        mapping(uint24 => int24) public override feeAmountTickSpacing;
        mapping(address => mapping(address => mapping(uint24 => address))) public override getPool;
    
        constructor() {
            owner = msg.sender;
            emit OwnerChanged(address(0), msg.sender);
            // 初始化支持的费率以及对应的tickSpacing
            feeAmountTickSpacing[500] = 10;
            emit FeeAmountEnabled(500, 10);
            feeAmountTickSpacing[3000] = 60;
            emit FeeAmountEnabled(3000, 60);
            feeAmountTickSpacing[10000] = 200;
            emit FeeAmountEnabled(10000, 200);
        }
    
        function createPool(
            address tokenA,
            address tokenB,
            uint24 fee
        ) external override noDelegateCall returns (address pool) {
            require(tokenA != tokenB);
            // 对两个token进行排序，小的排前面
            (address token0, address token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA);
            require(token0 != address(0));
            int24 tickSpacing = feeAmountTickSpacing[fee];
            require(tickSpacing != 0); //为0则说明该费率并不支持
            require(getPool[token0][token1][fee] == address(0));
            // 实际的部署新池子函数
            pool = deploy(address(this), token0, token1, fee, tickSpacing);
            // 两个方向的token都存储，方便查询
            getPool[token0][token1][fee] = pool;
            getPool[token1][token0][fee] = pool;
            emit PoolCreated(token0, token1, fee, tickSpacing, pool);
        }
    
        function setOwner(address _owner) external override {
            require(msg.sender == owner);
            emit OwnerChanged(owner, _owner);
            owner = _owner;
        }
    
        function enableFeeAmount(uint24 fee, int24 tickSpacing) public override {
            require(msg.sender == owner);
            require(fee < 1000000);
            // tick spacing is capped at 16384 to prevent the situation where tickSpacing is so large that
            // TickBitmap#nextInitializedTickWithinOneWord overflows int24 container from a valid tick
            // 16384 ticks represents a >5x price change with ticks of 1 bips
            require(tickSpacing > 0 && tickSpacing < 16384);
            require(feeAmountTickSpacing[fee] == 0);
    
            feeAmountTickSpacing[fee] = tickSpacing;
            emit FeeAmountEnabled(fee, tickSpacing);
        }
    }
    

UniswapV3Factory 除了继承其 interface **IUniswapV3Factory** 之外，还继承了另外两个合约 **UniswapV3PoolDeployer** 和 **NoDelegateCall**。这两个合约后面再讲，先来看看构造函数。构造函数除了初始化 `owner` 之外，最主要就是初始化 `feeAmountTickSpacing` 状态变量。这个变量是用来存储支持的交易手续费率的配置的，key 代表费率，value 代表 tickSpacing。初始的费率值分别设为了 500、3000、10000，分别代表了 0.05%、0.3%、1%。**tickSpacing** 的概念需要解释一下。

当添加流动性时，虽然 UI 交互上选择的是一个价格区间，但实际调用合约时，传入的参数其实是一个 tick 区间。而如果低价或/和高价的 tick 还没有被已存在的头寸用作边界点时，该 tick 将被初始化。tickSpacing 就是用来限制哪些 tick 可以被初始化的。只有那些序号能够被 tickSpacing 整除的 tick 才能被初始化。当 tickSpacing = 10 的时候，则只有可以被 10 整除的 tick (..., -30, -20, -10, 0, 10, 20, 30, ...) 才可以被初始化；当 tickSpacing = 200 时，则只有可以被 200 整除的 tick (..., -600, -400, -200, 0, 200, 400, 600, ...) 才可被初始化。tickSpacing 越小，则说明可设置的价格区间精度越高，但可能会使得每次交易时损耗的 gas 也越高，因为每次交易穿越一个初始化的 tick 时，都会给交易者带来 gas 消耗。

为了更直观地理解 tickSpacing，我再用更具体的示例进行说明。我们知道，在 UniswapV2 中，在智能合约层面，价格精度其实可以达到 18 位小数，交易精度是可以非常小的。但是，在中心化交易所，不同代币的价格精度则是不一样的，比如 BTC 和 ETH 的价格精度大多为两个小数，MEME 的精度为 6 位小数，SHIB 的精度则为 8 位小数，这个价格精度也就是价格的最小变动单位，BTC 和 ETH 的最小变动单位为 0.01，SHIB 的最小变动单位为 0.00000001。类似地，**tickSpacing 可以理解为就是 tick 变动的最小单位**。而我们知道，每一个 tick 其实也对应了每一个价格点，因此 tickSpacing 其实和中心化交易所的价格精度类似，是用于限制每个池子的最小价格变动范围的。也因此，当你在 Uniswap 官网上添加流动性时，当你输入的区间价格为整数时，比如 1700，最终会变成 1699.4004，就是因为 1699.4004 才是符合 tickSpacing 限制的有效价格点。

从构造函数中可看出，三个不同费率对应的 tickSpacing 分别为 10、60 和 200。费率越高，tickSpacing 越高，即是说，费率越高，价格变动的最小单位也越高。

在 2021 年 11 月通过 DAO 治理增加了另一个手续费率配置，费率为 0.01%，tickSpacing 为 1，是通过调用了 `enableFeeAmount` 函数添加的。该函数只有 `owner` 才有权限调用，而 owner 其实是个 **Timelock** 合约。

`createPool` 是最核心的创建新池子的函数，其三个入参就是组成一个池子唯一性的 **tokenA**、**tokenB** 和 **fee**。代码实现里，各种 require 的检验都非常好理解，而实际的创建池子逻辑其实封装在了 `deploy` 内部函数里，而这个函数是在 **UniswapV3PoolDeployer** 合约中实现的。`deploy` 函数返回 `pool` 后，会存储到 `getPool` 状态变量里。

下面，来看看 **UniswapV3PoolDeployer** 合约实现，其代码如下：

    contract UniswapV3PoolDeployer is IUniswapV3PoolDeployer {
        struct Parameters {
            address factory;
            address token0;
            address token1;
            uint24 fee;
            int24 tickSpacing;
        }
    
        Parameters public override parameters;
    
        function deploy(
            address factory,
            address token0,
            address token1,
            uint24 fee,
            int24 tickSpacing
        ) internal returns (address pool) {
            parameters = Parameters({factory: factory, token0: token0, token1: token1, fee: fee, tickSpacing: tickSpacing});
            pool = address(new UniswapV3Pool{salt: keccak256(abi.encode(token0, token1, fee))}());
            delete parameters;
        }
    }
    

这套代码还是比较有意思的。首先，其定义了结构体 `Parameters` 和该结构体类型的状态变量 `parameters`。然后，在 `deploy` 函数里，先对 `parameters` 进行赋值，接着通过 `new UniswapV3Pool` 部署了新池子合约，使用 token0、token1 和 fee 三个字段拼接的哈希值作为盐值。最后再将 `parameters` 删除。总共就三行代码。但其中有两个用法，是在以前的项目中还没出现过的。

第一，使用 `new UniswapV3Pool` 部署新合约时，还可以指定 `salt`。这其实也是 `create2` 的一种新写法，相比于 UniswapV2Factory 中使用内联汇编的方式，明显简化了很多。

第二，`parameters` 其实是传给 `UniswapV3Pool` 的参数，在 UniswapV3Pool 的构造函数里，是如下所示来接收这些参数的：

    constructor() {
        int24 _tickSpacing;
        (factory, token0, token1, fee, _tickSpacing) = IUniswapV3PoolDeployer(msg.sender).parameters();
        tickSpacing = _tickSpacing;
    
        maxLiquidityPerTick = Tick.tickSpacingToMaxLiquidityPerTick(_tickSpacing);
    }
    

可见，其实就是通过调用了 `IUniswapV3PoolDeployer(msg.sender).parameters()` 来获取到几个参数。其中，`msg.sender` 其实就是工厂合约。

看了这段代码才明白，原来合约间传递参数还可以这么用。

回到 **UniswapV3Factory** 合约的 `createPool` 函数，函数体里还有加了 `noDelegateCall` 的函数修饰器，这是在 **NoDelegateCall** 抽象合约中定义的。以下是 **NoDelegateCall** 的代码实现：

    abstract contract NoDelegateCall {
        /// @dev The original address of this contract
        address private immutable original;
    
        constructor() {
            // Immutables are computed in the init code of the contract, and then inlined into the deployed bytecode.
            // In other words, this variable won't change when it's checked at runtime.
            original = address(this);
        }
    
        /// @dev Private method is used instead of inlining into modifier because modifiers are copied into each method,
        ///     and the use of immutable means the address bytes are copied in every place the modifier is used.
        function checkNotDelegateCall() private view {
            require(address(this) == original);
        }
    
        /// @notice Prevents delegatecall into the modified method
        modifier noDelegateCall() {
            checkNotDelegateCall();
            _;
        }
    }
    

这其实就是为了阻止用 **delegatecall** 来调用所修饰的函数。当使用 delegatecall 调用 `createPool` 函数的时候，那 `address(this)` 将是发起 delegatecall 的地址，而不是当前的工厂合约地址。

至此，我们就讲解完了 UniswapV3 的工厂合约。

---

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