« 上一篇下一篇 »

UniswapV3简析(三)

NonfungiblePositionManager合约是直接与链下前端进行交互的外围合约, 主要对外提供比如创建流动池, 添加流动性, 删除流动性等API接口, 同时也会为每个流动性仓位生成一个唯一的NFT给到用户, 可以说是Uniswap协议中最为重要的一个管理器, 这里有一个关键点前文已经说过, 这里需要再次强调一下, 就是NonfungiblePositionManager合约可以同时管理多个流动池Pool(每个流动池包含一个交易对), 其由(token0,token1,fee)唯一标识, 而每个流动池又是一个流动性聚合器其同时包含多个流动性仓位, 而每个流动性仓位又可以由(owner,tickLower,tickUpper)唯一进行标识, 如下图: 
NonfungiblePositionManager合约对外提供一些重要的API:

createAndInitializePoolIfNecessary,  部署一个新的流动池合约

selfPermitAllowed/selfPermit, 授权合约有转账相应token的权限

mint, 给指定流动池添加一个新的流动性仓位, 并且生成一个代表此仓位的NFT给到用户

increaseLiquidity, 给指定流动池某个已经存在的流动性仓位增加流动性

decreaseLiquidity, 指定流动池某个已经存在的流动性仓位减少流动性

collect, 提取闲置token及其手续费

multicall, 将多个合约调用打包进一次合约调用中, 这个其实是为了达到类似数据库事务(transaction)的效果, 一个事务应该满足:
1.原子性(Atomicity): 事务中的全部操作在数据库中是不可分割的, 要么全部完成, 要么全部不执行
2.一致性(Consistency): 几个并行执行的事务, 其执行结果必须与按某一顺序串行执行的结果相一致
3.隔离性(Isolation): 事务的执行不受其他事务的干扰, 事务执行的中间结果对其他事务必须是透明的
4.持久性(Durability): 对于任意已提交事务, 系统必须保证该事务对数据库的改变不被丢失, 即使数据库出现故障


部署新流动池

createAndInitializePoolIfNecessary函数用于部署一个新的流动池合约, 其代码位于contracts\base\PoolInitializer.sol中, 参考代码如下:

function createAndInitializePoolIfNecessary(
    address token0,
    address token1,
    uint24 fee,
    uint160 sqrtPriceX96
) external payable override returns (address pool) {
    require(token0 < token1);
    pool = IUniswapV3Factory(factory).getPool(token0, token1, fee); #查询流动池是否存在
    if (pool == address(0)) { #流动池不存在
        pool = IUniswapV3Factory(factory).createPool(token0, token1, fee); #部署新的流动池合约
        IUniswapV3Pool(pool).initialize(sqrtPriceX96); #设置流动池初始价格
    } else { #流动池已经存在
        (uint160 sqrtPriceX96Existing, , , , , , ) = IUniswapV3Pool(pool).slot0(); #获取流动池当前价格
        if (sqrtPriceX96Existing == 0) { #价格不存在就设置它
            IUniswapV3Pool(pool).initialize(sqrtPriceX96);
        }
    }
}

基本逻辑挺简单, 就是利用(token0, token1, fee)三个参数去工厂合约查询相应的流动池是否已经存在, 如果不存在就调用工厂合约的createPool函数在链上部署一个新的流动池合约, 然后设置这个池子的初始价格.


给指定流动池添加一个新的流动性仓位

mint函数给指定流动池添加一个新的流动性仓位, 并且生成一个代表此仓位的NFT给到用户, 其代码位于contracts\NonfungiblePositionManager.sol, 参考代码如下:

function mint(MintParams calldata params)
    external
    payable
    override
    checkDeadline(params.deadline)
    returns (
        uint256 tokenId,
        uint128 liquidity,
        uint256 amount0,
        uint256 amount1
    )
{
    IUniswapV3Pool pool;
    (liquidity, amount0, amount1, pool) = addLiquidity( #此函数内部真正添加流动性,并完成token的转账充值
        AddLiquidityParams({
            token0: params.token0, #唯一标识一个流动池
            token1: params.token1, #唯一标识一个流动池
            fee: params.fee, #唯一标识一个流动池
            recipient: address(this), #唯一标识该池中的一个流动性仓位
            tickLower: params.tickLower, #唯一标识该池中的一个流动性仓位
            tickUpper: params.tickUpper, #唯一标识该池中的一个流动性仓位
            amount0Desired: params.amount0Desired, #要充值的token0数量
            amount1Desired: params.amount1Desired, #要充值的token1数量
            amount0Min: params.amount0Min,
            amount1Min: params.amount1Min
        })
    );

    _mint(params.recipient, (tokenId = _nextId++)); #生成一个代表流动性仓位的NFT给到用户

    bytes32 positionKey = PositionKey.compute(address(this), params.tickLower, params.tickUpper);
    (, uint256 feeGrowthInside0LastX128, uint256 feeGrowthInside1LastX128, , ) = pool.positions(positionKey);

    // idempotent set
    uint80 poolId =
        cachePoolKey(
            address(pool),
            PoolAddress.PoolKey({token0: params.token0, token1: params.token1, fee: params.fee})
        );

    _positions[tokenId] = Position({ #将NFT与对应的流动性仓位进行关联
        nonce: 0,
        operator: address(0),
        poolId: poolId, #该NFT所代表的流动性仓位属于哪个流动池
        tickLower: params.tickLower, #该NFT所代表的流动性仓位做市区间最低点位
        tickUpper: params.tickUpper, #该NFT所代表的流动性仓位做市区间最高点位
        liquidity: liquidity, #该NFT所代表的流动性仓位的流动性大小, 也就是L值
        feeGrowthInside0LastX128: feeGrowthInside0LastX128, #计算手续费相关
        feeGrowthInside1LastX128: feeGrowthInside1LastX128, #计算手续费相关
        tokensOwed0: 0,
        tokensOwed1: 0
    });

    emit IncreaseLiquidity(tokenId, liquidity, amount0, amount1);
}

代码整体流程很清晰, 这里关键就是addLiquidity函数, 其代码位于contracts\base\LiquidityManagement.sol, 参考代码如下:
function addLiquidity(AddLiquidityParams memory params)
    internal
    returns (
        uint128 liquidity,
        uint256 amount0,
        uint256 amount1,
        IUniswapV3Pool pool
    )
{
    PoolAddress.PoolKey memory poolKey =
        PoolAddress.PoolKey({token0: params.token0, token1: params.token1, fee: params.fee});

    pool = IUniswapV3Pool(PoolAddress.computeAddress(factory, poolKey)); #通过(token0,token1,fee)定位流动池合约地址

    // compute the liquidity amount
    {
        (uint160 sqrtPriceX96, , , , , , ) = pool.slot0(); #流动池当前价格
        uint160 sqrtRatioAX96 = TickMath.getSqrtRatioAtTick(params.tickLower); #做市区间最低价
        uint160 sqrtRatioBX96 = TickMath.getSqrtRatioAtTick(params.tickUpper); #做市区间最高价
        #这里其实就是前文提过关于L,P,x,y四者的相互运算
        liquidity = LiquidityAmounts.getLiquidityForAmounts(
            sqrtPriceX96,
            sqrtRatioAX96,
            sqrtRatioBX96,
            params.amount0Desired,
            params.amount1Desired
        );
    }
    #调用流动池的mint函数完成流动性添加,此函数内部会回调我们的uniswapV3MintCallback函数完成充值,代码在下面
    (amount0, amount1) = pool.mint(
        params.recipient,
        params.tickLower,
        params.tickUpper,
        liquidity,
        abi.encode(MintCallbackData({poolKey: poolKey, payer: msg.sender}))
    );

    require(amount0 >= params.amount0Min && amount1 >= params.amount1Min, 'Price slippage check');
}


function uniswapV3MintCallback(
    uint256 amount0Owed, #要充值的token0数量
    uint256 amount1Owed, #要充值的token1数量
    bytes calldata data
) external override {
    MintCallbackData memory decoded = abi.decode(data, (MintCallbackData));
    CallbackValidation.verifyCallback(factory, decoded.poolKey);
    #将token转账给流动池合约地址, 完成充值
    if (amount0Owed > 0) pay(decoded.poolKey.token0, decoded.payer, msg.sender, amount0Owed);
    if (amount1Owed > 0) pay(decoded.poolKey.token1, decoded.payer, msg.sender, amount1Owed);
}


给指定流动池某个已经存在的流动性仓位增加流动性

increaseLiquidity函数用于给指定流动池某个已经存在的流动性仓位增加流动性, 其代码位于contracts\NonfungiblePositionManager.sol, 参考代码如下:
function increaseLiquidity(IncreaseLiquidityParams calldata params)
    external
    payable
    override
    checkDeadline(params.deadline)
    returns (
        uint128 liquidity,
        uint256 amount0,
        uint256 amount1
    )
{
    Position storage position = _positions[params.tokenId]; #根据传入的 NFT id 获取对应的流动性仓位信息

    PoolAddress.PoolKey memory poolKey = _poolIdToPoolKey[position.poolId]; #获取该流动性仓位所属的流动池

    IUniswapV3Pool pool;
    (liquidity, amount0, amount1, pool) = addLiquidity( #向该流动池添加流动性, 不在赘述
        AddLiquidityParams({
            token0: poolKey.token0,
            token1: poolKey.token1,
            fee: poolKey.fee,
            tickLower: position.tickLower,
            tickUpper: position.tickUpper,
            amount0Desired: params.amount0Desired,
            amount1Desired: params.amount1Desired,
            amount0Min: params.amount0Min,
            amount1Min: params.amount1Min,
            recipient: address(this)
        })
    );

    bytes32 positionKey = PositionKey.compute(address(this), position.tickLower, position.tickUpper);

    // this is now updated to the current transaction
    (, uint256 feeGrowthInside0LastX128, uint256 feeGrowthInside1LastX128, , ) = pool.positions(positionKey); #获取该流动池中对应的这个流动性仓位

    position.tokensOwed0 += uint128( #更新这个流动性仓位之前获取的做市手续费
        FullMath.mulDiv(
            feeGrowthInside0LastX128 - position.feeGrowthInside0LastX128,
            position.liquidity,
            FixedPoint128.Q128
        )
    );
    position.tokensOwed1 += uint128( #更新这个流动性仓位之前获取的做市手续费
        FullMath.mulDiv(
            feeGrowthInside1LastX128 - position.feeGrowthInside1LastX128,
            position.liquidity,
            FixedPoint128.Q128
        )
    );

    position.feeGrowthInside0LastX128 = feeGrowthInside0LastX128; #更新手续费计算相关
    position.feeGrowthInside1LastX128 = feeGrowthInside1LastX128; #更新手续费计算相关
    position.liquidity += liquidity; #更新L值

    emit IncreaseLiquidity(params.tokenId, liquidity, amount0, amount1);
}


给指定流动池某个已经存在的流动性仓位减少流动性

decreaseLiquidity函数用于指定流动池某个已经存在的流动性仓位减少流动性, 其代码位于contracts\NonfungiblePositionManager.sol, 参考代码如下:
function decreaseLiquidity(DecreaseLiquidityParams calldata params)
    external
    payable
    override
    isAuthorizedForToken(params.tokenId)
    checkDeadline(params.deadline)
    returns (uint256 amount0, uint256 amount1)
{
    require(params.liquidity > 0);
    Position storage position = _positions[params.tokenId]; #根据传入的 NFT id 获取对应的流动性仓位信息

    uint128 positionLiquidity = position.liquidity;
    require(positionLiquidity >= params.liquidity);

    PoolAddress.PoolKey memory poolKey = _poolIdToPoolKey[position.poolId]; #获取该流动性仓位所属的流动池key
    IUniswapV3Pool pool = IUniswapV3Pool(PoolAddress.computeAddress(factory, poolKey)); #获取该流动性仓位所属的流动池合约地址
    (amount0, amount1) = pool.burn(position.tickLower, position.tickUpper, params.liquidity); #调用该流动池合约的burn函数,真正完成减少流动性

    require(amount0 >= params.amount0Min && amount1 >= params.amount1Min, 'Price slippage check');

    bytes32 positionKey = PositionKey.compute(address(this), position.tickLower, position.tickUpper); #获取该流动性仓位的key
    // this is now updated to the current transaction
    (, uint256 feeGrowthInside0LastX128, uint256 feeGrowthInside1LastX128, , ) = pool.positions(positionKey); #从指定流动池中获取该流动性仓位的手续费相关信息

    position.tokensOwed0 +=
        uint128(amount0) +
        uint128(
            FullMath.mulDiv(
                feeGrowthInside0LastX128 - position.feeGrowthInside0LastX128,
                positionLiquidity,
                FixedPoint128.Q128
            )
        ); #减少流动性后,会空闲出多余token,记录之,同时也会更新之前的做市手续费收入
    position.tokensOwed1 +=
        uint128(amount1) +
        uint128(
            FullMath.mulDiv(
                feeGrowthInside1LastX128 - position.feeGrowthInside1LastX128,
                positionLiquidity,
                FixedPoint128.Q128
            )
        ); #减少流动性后,会空闲出多余token,记录之,同时也会更新之前的做市手续费收入

    position.feeGrowthInside0LastX128 = feeGrowthInside0LastX128; #更新手续费计算相关
    position.feeGrowthInside1LastX128 = feeGrowthInside1LastX128; #更新手续费计算相关
    // subtraction is safe because we checked positionLiquidity is gte params.liquidity
    position.liquidity = positionLiquidity - params.liquidity; #更新L值

    emit DecreaseLiquidity(params.tokenId, params.liquidity, amount0, amount1);
}


提取闲置token及其手续费

collect函数用于提取闲置token及其手续费, 其代码位于contracts\NonfungiblePositionManager.sol, 参考代码如下:
function collect(CollectParams calldata params)
    external
    payable
    override
    isAuthorizedForToken(params.tokenId)
    returns (uint256 amount0, uint256 amount1)
{
    require(params.amount0Max > 0 || params.amount1Max > 0);
    // allow collecting to the nft position manager address with address 0
    address recipient = params.recipient == address(0) ? address(this) : params.recipient;

    Position storage position = _positions[params.tokenId]; #根据传入的 NFT id 获取对应的流动性仓位信息

    PoolAddress.PoolKey memory poolKey = _poolIdToPoolKey[position.poolId]; #根据流动性仓位信息找所属的流动池

    IUniswapV3Pool pool = IUniswapV3Pool(PoolAddress.computeAddress(factory, poolKey)); #获取该流动池合约地址

    (uint128 tokensOwed0, uint128 tokensOwed1) = (position.tokensOwed0, position.tokensOwed1); #真正可提取的token数量

    // trigger an update of the position fees owed and fee growth snapshots if it has any liquidity
    if (position.liquidity > 0) {
        pool.burn(position.tickLower, position.tickUpper, 0); #这里并不是减少流动性,而仅仅只是为了更新手续费
        (, uint256 feeGrowthInside0LastX128, uint256 feeGrowthInside1LastX128, , ) =
            pool.positions(PositionKey.compute(address(this), position.tickLower, position.tickUpper));

        tokensOwed0 += uint128(
            FullMath.mulDiv(
                feeGrowthInside0LastX128 - position.feeGrowthInside0LastX128,
                position.liquidity,
                FixedPoint128.Q128
            )
        ); #把最新的可提取的手续费收入也累加进来
        tokensOwed1 += uint128(
            FullMath.mulDiv(
                feeGrowthInside1LastX128 - position.feeGrowthInside1LastX128,
                position.liquidity,
                FixedPoint128.Q128
            )
        ); #把最新的可提取的手续费收入也累加进来

        position.feeGrowthInside0LastX128 = feeGrowthInside0LastX128;
        position.feeGrowthInside1LastX128 = feeGrowthInside1LastX128;
    }

    // compute the arguments to give to the pool#collect method
    (uint128 amount0Collect, uint128 amount1Collect) =
        (
            params.amount0Max > tokensOwed0 ? tokensOwed0 : params.amount0Max,
            params.amount1Max > tokensOwed1 ? tokensOwed1 : params.amount1Max
        );

    // the actual amounts collected are returned
    (amount0, amount1) = pool.collect( #调用流动池合约的collect函数,真正完成转账提取token的操作
        recipient, #用户收钱地址
        position.tickLower, #用于标识对应的流动性仓位
        position.tickUpper, #用于标识对应的流动性仓位
        amount0Collect, #期望提取的token数量
        amount1Collect  #期望提取的token数量
    );

    // sometimes there will be a few less wei than expected due to rounding down in core, but we just subtract the full amount expected
    // instead of the actual amount so we can burn the token
    (position.tokensOwed0, position.tokensOwed1) = (tokensOwed0 - amount0Collect, tokensOwed1 - amount1Collect);

    emit Collect(params.tokenId, recipient, amount0Collect, amount1Collect);
}


将多个合约调用打包进一次合约调用

multicall, 将多个合约调用打包进一次合约调用中, 这是事务性的关键操作, 其代码位于contracts\base\Multicall.sol, 参考代码如下:
function multicall(bytes[] calldata data) external payable override returns (bytes[] memory results) {
    results = new bytes[](data.length);
    for (uint256 i = 0; i < data.length; i++) {
        (bool success, bytes memory result) = address(this).delegatecall(data[i]);
        if (!success) {
            // Next 5 lines from https://ethereum.stackexchange.com/a/83577
            if (result.length < 68) revert();
            assembly {
                result := add(result, 0x04)
            }
            revert(abi.decode(result, (string)));
        }
        results[i] = result;
    }
}

可以看到逻辑其实很简单, 就是在一个for循环中依次调用之前打包的各个合约调用, 这里有一个非常关键的技术要点就是delegatecall, delegatecall的作用是当用户A通过合约B来delegatecall合约C的时候, 执行的是合约C的函数, 但是语境仍是合约B的: msg.sender是A的地址, 并且如果函数改变一些状态变量, 产生的效果会作用于合约B的变量上, 如下图:

这里可以参考链下前端代码, 其代码位于v3-sdk\src\nonfungiblePositionManager.ts, 比如addCallParameters函数, 参考代码如下:
public static addCallParameters(position: Position, options: AddLiquidityOptions): MethodParameters {
    ...

    // create pool if needed
    if (isMint(options) && options.createPool) {
      calldatas.push(this.encodeCreate(position.pool)) #打包createAndInitializePoolIfNecessary调用
    }

    // permits if necessary
    if (options.token0Permit) {
      calldatas.push(SelfPermit.encodePermit(position.pool.token0, options.token0Permit)) #打包selfPermitAllowed/selfPermit调用
    }
    if (options.token1Permit) {
      calldatas.push(SelfPermit.encodePermit(position.pool.token1, options.token1Permit)) #打包selfPermitAllowed/selfPermit调用
    }

    // mint
    if (isMint(options)) {
      const recipient: string = validateAndParseAddress(options.recipient)

      calldatas.push( #打包mint调用
        NonfungiblePositionManager.INTERFACE.encodeFunctionData('mint', [
            ...
        ])
      )
    } else {
      // increase
      calldatas.push( #打包increaseLiquidity调用
        NonfungiblePositionManager.INTERFACE.encodeFunctionData('increaseLiquidity', [
            ...
        ])
      )
    }

    ...

    return {
      calldata: Multicall.encodeMulticall(calldatas), #最后将上面所有调用都打包成一个multicall调用
      value
    }
}

从上面代码可以看到, 其实就是将 [创建流动池, token授权转账权限, 添加流动性] 等操作, 整体打包成一次multicall调用, 要么全部成功, 要么全部失败