{"content":{"title":"剖析DeFi交易产品之UniswapV3：工厂合约","body":"本文首发于公众号：[Keegan小钢](https://mp.weixin.qq.com/s?__biz=MzA5OTI1NDE0Mw==&mid=2652494659&idx=1&sn=c7ef5ba71ffee1ee17b339e5dfaba0bb&chksm=8b685153bc1fd845ef5bd30536b0fc82a6003a020cf8164c9b6b021f2a953fa1e4664378d774&token=2071110718&lang=zh_CN#rd)\r\n***\r\n**UniswapV3Factory** 合约主要用来创建不同代币对的流动性池子合约，其代码实现并不复杂，以下就是代码实现：\r\n\r\n```java\r\ncontract UniswapV3Factory is IUniswapV3Factory, UniswapV3PoolDeployer, NoDelegateCall {\r\n    address public override owner;\r\n\r\n    mapping(uint24 => int24) public override feeAmountTickSpacing;\r\n    mapping(address => mapping(address => mapping(uint24 => address))) public override getPool;\r\n\r\n    constructor() {\r\n        owner = msg.sender;\r\n        emit OwnerChanged(address(0), msg.sender);\r\n        // 初始化支持的费率以及对应的tickSpacing\r\n        feeAmountTickSpacing[500] = 10;\r\n        emit FeeAmountEnabled(500, 10);\r\n        feeAmountTickSpacing[3000] = 60;\r\n        emit FeeAmountEnabled(3000, 60);\r\n        feeAmountTickSpacing[10000] = 200;\r\n        emit FeeAmountEnabled(10000, 200);\r\n    }\r\n\r\n    function createPool(\r\n        address tokenA,\r\n        address tokenB,\r\n        uint24 fee\r\n    ) external override noDelegateCall returns (address pool) {\r\n        require(tokenA != tokenB);\r\n        // 对两个token进行排序，小的排前面\r\n        (address token0, address token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA);\r\n        require(token0 != address(0));\r\n        int24 tickSpacing = feeAmountTickSpacing[fee];\r\n        require(tickSpacing != 0); //为0则说明该费率并不支持\r\n        require(getPool[token0][token1][fee] == address(0));\r\n        // 实际的部署新池子函数\r\n        pool = deploy(address(this), token0, token1, fee, tickSpacing);\r\n        // 两个方向的token都存储，方便查询\r\n        getPool[token0][token1][fee] = pool;\r\n        getPool[token1][token0][fee] = pool;\r\n        emit PoolCreated(token0, token1, fee, tickSpacing, pool);\r\n    }\r\n\r\n    function setOwner(address _owner) external override {\r\n        require(msg.sender == owner);\r\n        emit OwnerChanged(owner, _owner);\r\n        owner = _owner;\r\n    }\r\n\r\n    function enableFeeAmount(uint24 fee, int24 tickSpacing) public override {\r\n        require(msg.sender == owner);\r\n        require(fee < 1000000);\r\n        // tick spacing is capped at 16384 to prevent the situation where tickSpacing is so large that\r\n        // TickBitmap#nextInitializedTickWithinOneWord overflows int24 container from a valid tick\r\n        // 16384 ticks represents a >5x price change with ticks of 1 bips\r\n        require(tickSpacing > 0 && tickSpacing < 16384);\r\n        require(feeAmountTickSpacing[fee] == 0);\r\n\r\n        feeAmountTickSpacing[fee] = tickSpacing;\r\n        emit FeeAmountEnabled(fee, tickSpacing);\r\n    }\r\n}\r\n```\r\n\r\nUniswapV3Factory 除了继承其 interface **IUniswapV3Factory** 之外，还继承了另外两个合约 **UniswapV3PoolDeployer** 和 **NoDelegateCall**。这两个合约后面再讲，先来看看构造函数。构造函数除了初始化 `owner` 之外，最主要就是初始化 `feeAmountTickSpacing` 状态变量。这个变量是用来存储支持的交易手续费率的配置的，key 代表费率，value 代表 tickSpacing。初始的费率值分别设为了 500、3000、10000，分别代表了 0.05%、0.3%、1%。**tickSpacing** 的概念需要解释一下。\r\n\r\n当添加流动性时，虽然 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 消耗。\r\n\r\n为了更直观地理解 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 限制的有效价格点。\r\n\r\n从构造函数中可看出，三个不同费率对应的 tickSpacing 分别为 10、60 和 200。费率越高，tickSpacing 越高，即是说，费率越高，价格变动的最小单位也越高。\r\n\r\n在 2021 年 11 月通过 DAO 治理增加了另一个手续费率配置，费率为 0.01%，tickSpacing 为 1，是通过调用了 `enableFeeAmount` 函数添加的。该函数只有 `owner` 才有权限调用，而 owner 其实是个 **Timelock** 合约。\r\n\r\n`createPool` 是最核心的创建新池子的函数，其三个入参就是组成一个池子唯一性的 **tokenA**、**tokenB** 和 **fee**。代码实现里，各种 require 的检验都非常好理解，而实际的创建池子逻辑其实封装在了 `deploy` 内部函数里，而这个函数是在 **UniswapV3PoolDeployer** 合约中实现的。`deploy` 函数返回 `pool` 后，会存储到 `getPool` 状态变量里。\r\n\r\n下面，来看看 **UniswapV3PoolDeployer** 合约实现，其代码如下：\r\n\r\n```javascript\r\ncontract UniswapV3PoolDeployer is IUniswapV3PoolDeployer {\r\n    struct Parameters {\r\n        address factory;\r\n        address token0;\r\n        address token1;\r\n        uint24 fee;\r\n        int24 tickSpacing;\r\n    }\r\n\r\n    Parameters public override parameters;\r\n\r\n    function deploy(\r\n        address factory,\r\n        address token0,\r\n        address token1,\r\n        uint24 fee,\r\n        int24 tickSpacing\r\n    ) internal returns (address pool) {\r\n        parameters = Parameters({factory: factory, token0: token0, token1: token1, fee: fee, tickSpacing: tickSpacing});\r\n        pool = address(new UniswapV3Pool{salt: keccak256(abi.encode(token0, token1, fee))}());\r\n        delete parameters;\r\n    }\r\n}\r\n```\r\n\r\n这套代码还是比较有意思的。首先，其定义了结构体 `Parameters` 和该结构体类型的状态变量 `parameters`。然后，在 `deploy` 函数里，先对 `parameters` 进行赋值，接着通过 `new UniswapV3Pool` 部署了新池子合约，使用 token0、token1 和 fee 三个字段拼接的哈希值作为盐值。最后再将  `parameters` 删除。总共就三行代码。但其中有两个用法，是在以前的项目中还没出现过的。\r\n\r\n第一，使用 `new UniswapV3Pool` 部署新合约时，还可以指定 `salt`。这其实也是 `create2` 的一种新写法，相比于 UniswapV2Factory 中使用内联汇编的方式，明显简化了很多。\r\n\r\n第二，`parameters` 其实是传给 `UniswapV3Pool` 的参数，在 UniswapV3Pool 的构造函数里，是如下所示来接收这些参数的：\r\n\r\n```\r\nconstructor() {\r\n    int24 _tickSpacing;\r\n    (factory, token0, token1, fee, _tickSpacing) = IUniswapV3PoolDeployer(msg.sender).parameters();\r\n    tickSpacing = _tickSpacing;\r\n\r\n    maxLiquidityPerTick = Tick.tickSpacingToMaxLiquidityPerTick(_tickSpacing);\r\n}\r\n```\r\n\r\n可见，其实就是通过调用了 `IUniswapV3PoolDeployer(msg.sender).parameters()` 来获取到几个参数。其中，`msg.sender` 其实就是工厂合约。\r\n\r\n看了这段代码才明白，原来合约间传递参数还可以这么用。\r\n\r\n回到 **UniswapV3Factory** 合约的 `createPool` 函数，函数体里还有加了 `noDelegateCall` 的函数修饰器，这是在 **NoDelegateCall** 抽象合约中定义的。以下是 **NoDelegateCall** 的代码实现：\r\n\r\n```typescript\r\nabstract contract NoDelegateCall {\r\n    /// @dev The original address of this contract\r\n    address private immutable original;\r\n\r\n    constructor() {\r\n        // Immutables are computed in the init code of the contract, and then inlined into the deployed bytecode.\r\n        // In other words, this variable won't change when it's checked at runtime.\r\n        original = address(this);\r\n    }\r\n\r\n    /// @dev Private method is used instead of inlining into modifier because modifiers are copied into each method,\r\n    ///     and the use of immutable means the address bytes are copied in every place the modifier is used.\r\n    function checkNotDelegateCall() private view {\r\n        require(address(this) == original);\r\n    }\r\n\r\n    /// @notice Prevents delegatecall into the modified method\r\n    modifier noDelegateCall() {\r\n        checkNotDelegateCall();\r\n        _;\r\n    }\r\n}\r\n```\r\n\r\n这其实就是为了阻止用 **delegatecall** 来调用所修饰的函数。当使用 delegatecall 调用 `createPool` 函数的时候，那 `address(this)` 将是发起 delegatecall 的地址，而不是当前的工厂合约地址。\r\n\r\n至此，我们就讲解完了 UniswapV3 的工厂合约。"},"author":{"user":"https://learnblockchain.cn/people/96","address":null},"history":null,"timestamp":1699240298,"version":1}