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

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 之外,还继承了另外两个合约 UniswapV3PoolDeployerNoDelegateCall。这两个合约后面再讲,先来看看构造函数。构造函数除了初始化 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 是最核心的创建新池子的函数,其三个入参就是组成一个池子唯一性的 tokenAtokenBfee。代码实现里,各种 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 的工厂合约。

Subscribe to Keegan小钢
Receive the latest updates directly to your inbox.
Mint this entry as an NFT to add it to your collection.
Verification
This entry has been permanently stored onchain and signed by its creator.