Uniswap v3分析(二)

Uniswapv3分析(二)本文将分析Uniswapv3的核心模块Uniswap-v3-core。UniswapV3Factory.sol工厂合约主要包含二个方法:createPool方法:用于创建交易池enableFeeAmount方法:用于设置手续费等级createP

  • Uniswap v3分析(二)

    本文将分析Uniswap v3的核心模块Uniswap-v3-core

UniswapV3Factory.sol

工厂合约主要包含二个方法:

createPool方法:用于创建交易池

enableFeeAmount方法:用于设置手续费等级

createPool

 function createPool(
        address tokenA,
        address tokenB,
        uint24 fee
    ) external override noDelegateCall returns (address pool) {
        require(tokenA != tokenB);
        (address token0, address token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA);
        require(token0 != address(0));
        int24 tickSpacing = feeAmountTickSpacing[fee];
        require(tickSpacing != 0);
        require(getPool[token0][token1][fee] == address(0));
        pool = deploy(address(this), token0, token1, fee, tickSpacing);
        getPool[token0][token1][fee] = pool;
        // populate mapping in the reverse direction, deliberate choice to avoid the cost of comparing addresses
        getPool[token1][token0][fee] = pool;
        emit PoolCreated(token0, token1, fee, tickSpacing, pool);
    }

由于V3支持不同手续费等级,所以不同于V2只使用TokenA,TokenB来唯一确定一个交易池,V3还需要用到fee。

因此在创建池的过程中多了一个检查fee是否合规的操作。

enableFeeAmount

Uniswap v3默认支持三种手续费等级:0.05%、0.30%和1.00%。若要新增,就要用到这个方法。

 function enableFeeAmount(uint24 fee, int24 tickSpacing) public override {
        require(msg.sender == owner);
        require(fee < 1000000);
        require(tickSpacing > 0 && tickSpacing < 16384);
        require(feeAmountTickSpacing[fee] == 0);

        feeAmountTickSpacing[fee] = tickSpacing;
        emit FeeAmountEnabled(fee, tickSpacing);
    }

逻辑很简单,接收两个参数fee(手续费),tickSpacing手续费等级。先判断调用者是否为合约的owner,再判断fee和tickSpacing是否合法, 再判断是否已经被添加,最后改变状态。

UniswapV3Pool.sol

交易池合约主要包含以下方法: initialize:初始化交易 mint:添加流动性 burn:移除流动性 swap:交换代币 flash:闪电贷 collect:取回代币 setFeeProtocol:修改某个交易对的协议手续费比例 collectProtocol:收集某个交易对的协议手续费

initialize

创建完交易对后,需要调用initialize方法初始化slot0,才能正常使用。

function initialize(uint160 sqrtPriceX96) external override {
        require(slot0.sqrtPriceX96 == 0, 'AI');

        int24 tick = TickMath.getTickAtSqrtRatio(sqrtPriceX96);

        (uint16 cardinality, uint16 cardinalityNext) = observations.initialize(_blockTimestamp());

        slot0 = Slot0({
            sqrtPriceX96: sqrtPriceX96,
            tick: tick,
            observationIndex: 0,
            observationCardinality: cardinality,
            observationCardinalityNext: cardinalityNext,
            feeProtocol: 0,
            unlocked: true
        });

        emit Initialize(sqrtPriceX96, tick);
    }

slot0结构如下:

sqrtPriceX96:交易对当前的开根号价格

tick:当前的tick

observationIndex:最近更新的观测点数组序号

observationCardinality:观测点数组容量(最大65536,最小1)

observationCardinalityNext:下一个观测点数组容量。

feeProtocol:协议手续费比例,可以分别为token0和token1设置交易手续费中分给协议的比例

unlocked:当前交易对合约是否非锁定状态

mint

用于添加流动性。

  function mint(
        address recipient,
        int24 tickLower,
        int24 tickUpper,
        uint128 amount,
        bytes calldata data
    ) external override lock returns (uint256 amount0, uint256 amount1) {
        require(amount > 0);
        (, int256 amount0Int, int256 amount1Int) =
            _modifyPosition(
                ModifyPositionParams({
                    owner: recipient,
                    tickLower: tickLower,
                    tickUpper: tickUpper,
                    liquidityDelta: int256(amount).toInt128()
                })
            );

        amount0 = uint256(amount0Int);
        amount1 = uint256(amount1Int);

        uint256 balance0Before;
        uint256 balance1Before;
        if (amount0 > 0) balance0Before = balance0();
        if (amount1 > 0) balance1Before = balance1();
        IUniswapV3MintCallback(msg.sender).uniswapV3MintCallback(amount0, amount1, data);
        if (amount0 > 0) require(balance0Before.add(amount0) <= balance0(), 'M0');
        if (amount1 > 0) require(balance1Before.add(amount1) <= balance1(), 'M1');

        emit Mint(msg.sender, recipient, tickLower, tickUpper, amount, amount0, amount1);
    }

首先通过_modifyPosition计算添加amount0的流动性所需要的token0的数量amount0Int和token1的数量amount1Int。

然后就是转账,改变状态没什么可说的。

值得注意的是转账通过回调msg.sender进行,因此个人EOA账户无法调用mint。

接下来分析_modifyPosition是如何计算的

    function _modifyPosition(ModifyPositionParams memory params)
        private
        noDelegateCall
        returns (
            Position.Info storage position,
            int256 amount0,
            int256 amount1
        )
    {
        checkTicks(params.tickLower, params.tickUpper);
        Slot0 memory _slot0 = slot0;
        position = _updatePosition(
            params.owner,
            params.tickLower,
            params.tickUpper,
            params.liquidityDelta,
            _slot0.tick
        );
        if (params.liquidityDelta != 0) {
            if (_slot0.tick < params.tickLower) {
                amount0 = SqrtPriceMath.getAmount0Delta(
                    TickMath.getSqrtRatioAtTick(params.tickLower),
                    TickMath.getSqrtRatioAtTick(params.tickUpper),
                    params.liquidityDelta
                );
            } else if (_slot0.tick < params.tickUpper) {
                uint128 liquidityBefore = liquidity; 

                (slot0.observationIndex, slot0.observationCardinality) = observations.write(
                    _slot0.observationIndex,
                    _blockTimestamp(),
                    _slot0.tick,
                    liquidityBefore,
                    _slot0.observationCardinality,
                    _slot0.observationCardinalityNext
                );

                amount0 = SqrtPriceMath.getAmount0Delta(
                    _slot0.sqrtPriceX96,
                    TickMath.getSqrtRatioAtTick(params.tickUpper),
                    params.liquidityDelta
                );
                amount1 = SqrtPriceMath.getAmount1Delta(
                    TickMath.getSqrtRatioAtTick(params.tickLower),
                    _slot0.sqrtPriceX96,
                    params.liquidityDelta
                );

                liquidity = LiquidityMath.addDelta(liquidityBefore, params.liquidityDelta);
            } else {
                amount1 = SqrtPriceMath.getAmount1Delta(
                    TickMath.getSqrtRatioAtTick(params.tickLower),
                    TickMath.getSqrtRatioAtTick(params.tickUpper),
                    params.liquidityDelta
                );
            }
        }
    }

首先检查流动性区间是否合规。

然后通过_updatePosition函数更新头寸。(这个后面会说)

最后通过getAmount0Delta和getAmount1Delta计算需要提供的token0的数量amount0和token1的数量amount1。

具体计算逻辑如下:

  当你提供流动性的区间的最小值大于当前tick_ic时,也就是区间在c的上方时,用户只需要提供x代币即可(amount0)。

反之,提供y代币(amount1)。

  当tick_ic被包含在你提供流动性的区间时,也就是c在区间内部时,用户需要提供x代币和y代币。

值得注意的时当tick_ic被包含在你提供流动性的区间时,会记录一次观测点数据(也就是此时资产价格),且会更新当前交易对的全局活跃流动性liquidity(后面会用)。

接下来分析_updatePosition。

function _updatePosition(
        address owner,
        int24 tickLower,
        int24 tickUpper,
        int128 liquidityDelta,
        int24 tick
    ) private returns (Position.Info storage position) {
        position = positions.get(owner, tickLower, tickUpper);

        uint256 _feeGrowthGlobal0X128 = feeGrowthGlobal0X128;
        uint256 _feeGrowthGlobal1X128 = feeGrowthGlobal1X128;

        bool flippedLower;
        bool flippedUpper;
        if (liquidityDelta != 0) {
            uint32 time = _blockTimestamp();
            (int56 tickCumulative, uint160 secondsPerLiquidityCumulativeX128) =
                observations.observeSingle(
                    time,
                    0,
                    slot0.tick,
                    slot0.observationIndex,
                    liquidity,
                    slot0.observationCardinality
                );

            flippedLower = ticks.update(
                tickLower,
                tick,
                liquidityDelta,
                _feeGrowthGlobal0X128,
                _feeGrowthGlobal1X128,
                secondsPerLiquidityCumulativeX128,
                tickCumulative,
                time,
                false,
                maxLiquidityPerTick
            );
            flippedUpper = ticks.update(
                tickUpper,
                tick,
                liquidityDelta,
                _feeGrowthGlobal0X128,
                _feeGrowthGlobal1X128,
                secondsPerLiquidityCumulativeX128,
                tickCumulative,
                time,
                true,
                maxLiquidityPerTick
            );

            if (flippedLower) {
                tickBitmap.flipTick(tickLower, tickSpacing);
            }
            if (flippedUpper) {
                tickBitmap.flipTick(tickUpper, tickSpacing);
            }
        }

        (uint256 feeGrowthInside0X128, uint256 feeGrowthInside1X128) =
            ticks.getFeeGrowthInside(tickLower, tickUpper, tick, _feeGrowthGlobal0X128, _feeGrowthGlobal1X128);

        position.update(liquidityDelta, feeGrowthInside0X128, feeGrowthInside1X128);

        if (liquidityDelta < 0) {
            if (flippedLower) {
                ticks.clear(tickLower);
            }
            if (flippedUpper) {
                ticks.clear(tickUpper);
            }
        }
    }

首先获取用户的流动性头寸,token0和token1的手续费(Pool的总手续费,所有头寸的总和)

然后通过observeSingle更新Oracle数据,接着使用ticks.update分别更新价格区间低点(tickLower)和价格区间高点(tickUpper)的状态。

然后计算该价格区间的累积每流动性手续费。

然后更新头寸信息,包括头寸的应收手续费tokensOwed0和tokensOwed1,以及头寸流动性liquidity。

如果是移除流动性,并且tick被翻转,则调用clear清空tick状态。

burn

销毁流动性的逻辑与添加流动性几乎相同,唯一的区别是liquidityDelta是负的。就不再赘述了。

swap

swap的逻辑十分复杂,让我们一步一步来分析。

先来看几个参数

        address recipient,
        bool zeroForOne,
        int256 amountSpecified,
        uint160 sqrtPriceLimitX96,
        bytes calldata data
    ) external override noDelegateCall returns (int256 amount0, int256 amount1) {

recipient:交易后的代币接收者

zeroForOne:如果从token0交换token1则为true,从token1交换token0则为false

amountSpecified: 指定的代币数量,如果为正,表示希望输入的代币数量;如果为负,则表示希望输出的代币数量

sqrtPriceLimitX96:能够承受的价格上限(或下限),格式为Q64.96;如果从token0到token1,则表示swap过程中的价格下限;如果从token1到token0,则表示价格上限;如果价格超过该值,则swap失败

data:回调参数

然后通过MLOAD(节省gas)定义一些数据。

当准备工作完成以后,才真正的开开始swap,实际的swap交易在一个循环中发生,让我们来仔细分析。

// 当剩余代币为0或价格到达用户的上限(下限)时结束循环
while (state.amountSpecifiedRemaining != 0 && state.sqrtPriceX96 != sqrtPriceLimitX96) {
    // 交易过程每一次循环的状态变量
    StepComputations memory step;
    // 交易的起始价格
    step.sqrtPriceStartX96 = state.sqrtPriceX96;
    // 通过位图找到下一个可以选的交易价格
    (step.tickNext, step.initialized) = tickBitmap.nextInitializedTickWithinOneWord(
        state.tick,
        tickSpacing,
        zeroForOne
    );

    // 确保不会超出最小/最大刻度
    if (step.tickNext < TickMath.MIN_TICK) {
        step.tickNext = TickMath.MIN_TICK;
    } else if (step.tickNext > TickMath.MAX_TICK) {
        step.tickNext = TickMath.MAX_TICK;
    }

    //根据tickNext计算下一个tick的价格
    step.sqrtPriceNextX96 = TickMath.getSqrtRatioAtTick(step.tickNext);

   // 计算交换后的价格, 消耗的输入代币数量, 得到的输出代币数量, 交易手续费
    (state.sqrtPriceX96, step.amountIn, step.amountOut, step.feeAmount) = SwapMath.computeSwapStep(
        state.sqrtPriceX96,
        (zeroForOne ? step.sqrtPriceNextX96 < sqrtPriceLimitX96 : step.sqrtPriceNextX96 > sqrtPriceLimitX96)
            ? sqrtPriceLimitX96
            : step.sqrtPriceNextX96,
        state.liquidity,
        state.amountSpecifiedRemaining,
        fee
    );
    // 更新tokenIn的余额,以及tokenOut数量
    if (exactInput) {
        state.amountSpecifiedRemaining -= (step.amountIn + step.feeAmount).toInt256();
        state.amountCalculated = state.amountCalculated.sub(step.amountOut.toInt256());
    } else {
        state.amountSpecifiedRemaining += step.amountOut.toInt256();
        state.amountCalculated = state.amountCalculated.add((step.amountIn + step.feeAmount).toInt256());
    }

    // 如果协议费用开启,则计算应支付给协议的费用
    if (cache.feeProtocol > 0) {
        uint256 delta = step.feeAmount / cache.feeProtocol;
        step.feeAmount -= delta;
        state.protocolFee += uint128(delta);
    }

    // 更新全局手续费用
    if (state.liquidity > 0)
        state.feeGrowthGlobalX128 += FullMath.mulDiv(step.feeAmount, FixedPoint128.Q128, state.liquidity);

    // 如果此时的价格 = 一个tick的价格,就更新流动性 L 的值
    if (state.sqrtPriceX96 == step.sqrtPriceNextX96) {
        // 检查如果该tick已经初始化,即是否为另一个流动性的边界
        if (step.initialized) {
            if (!cache.computedLatestObservation) {
                (cache.tickCumulative, cache.secondsPerLiquidityCumulativeX128) = observations.observeSingle(
                    cache.blockTimestamp,
                    0,
                    slot0Start.tick,
                    slot0Start.observationIndex,
                    cache.liquidityStart,
                    slot0Start.observationCardinality
                );
                cache.computedLatestObservation = true;
            }
            // 计算当价格穿过该 tick 时,处于激活的流动性需要变化的数量
            int128 liquidityNet =
                ticks.cross(
                    step.tickNext,
                    (zeroForOne ? state.feeGrowthGlobalX128 : feeGrowthGlobal0X128),
                    (zeroForOne ? feeGrowthGlobal1X128 : state.feeGrowthGlobalX128),
                    cache.secondsPerLiquidityCumulativeX128,
                    cache.tickCumulative,
                    cache.blockTimestamp
                );
            // 根据价格增加/减少,即向左或向右移动,增加/减少相应的流动性
            if (zeroForOne) liquidityNet = -liquidityNet;
            // 更新流动性
            state.liquidity = LiquidityMath.addDelta(state.liquidity, liquidityNet);
        }
        // 移动当前tick到下一个tick
        state.tick = zeroForOne ? step.tickNext - 1 : step.tickNext;
    } else if (state.sqrtPriceX96 != step.sqrtPriceStartX96) {
        // aoumtIn被耗尽,使用交换后的价格计算最新的tick值
        state.tick = TickMath.getTickAtSqrtRatio(state.sqrtPriceX96);
    }
}

在完成交换后,然后更新全局状态

if (state.tick != slot0Start.tick) {
    (uint16 observationIndex, uint16 observationCardinality) =
        observations.write(
            slot0Start.observationIndex,
            cache.blockTimestamp,
            slot0Start.tick,
            cache.liquidityStart,
            slot0Start.observationCardinality,
            slot0Start.observationCardinalityNext
        );
    (slot0.sqrtPriceX96, slot0.tick, slot0.observationIndex, slot0.observationCardinality) = (
        state.sqrtPriceX96,
        state.tick,
        observationIndex,
        observationCardinality
    );
} else {
    slot0.sqrtPriceX96 = state.sqrtPriceX96;
}

如果此时的tick与开始时的tick不同就记录一次观测点数据,更新slot0.sqrtPriceX96, slot0.tick等值。

如果交换前后tick值相同,则只需要修改价格

 if (cache.liquidityStart != state.liquidity) liquidity = state.liquidity;

更新全局流动性

        if (zeroForOne) {
            feeGrowthGlobal0X128 = state.feeGrowthGlobalX128;
            if (state.protocolFee > 0) protocolFees.token0 += state.protocolFee;
        } else {
            feeGrowthGlobal1X128 = state.feeGrowthGlobalX128;
            if (state.protocolFee > 0) protocolFees.token1 += state.protocolFee;
        }

更新累积手续费和协议手续费。(注意,如果是从token0交换token1,则只能收取token0作为手续费;反之,只能收取token1作为手续费。)

    (amount0, amount1) = zeroForOne == exactInput
        ? (amountSpecified - state.amountSpecifiedRemaining, state.amountCalculated)
        : (state.amountCalculated, amountSpecified - state.amountSpecifiedRemaining);

确定最终用户支付的 token 数和得到的 token 数。

if (zeroForOne) {
            if (amount1 < 0) TransferHelper.safeTransfer(token1, recipient, uint256(-amount1));

            uint256 balance0Before = balance0();
            IUniswapV3SwapCallback(msg.sender).uniswapV3SwapCallback(amount0, amount1, data);
            require(balance0Before.add(uint256(amount0)) <= balance0(), 'IIA');
        } else {
            if (amount0 < 0) TransferHelper.safeTransfer(token0, recipient, uint256(-amount0));

            uint256 balance1Before = balance1();
            IUniswapV3SwapCallback(msg.sender).uniswapV3SwapCallback(amount0, amount1, data);
            require(balance1Before.add(uint256(amount1)) <= balance1(), 'IIA');
        }

最后转账。

flash

与V2一样,V3版本中也又两种闪电贷,但是是通过不同的函数接口来完成的。

第一种是普通的闪电贷,借入和还贷的token相同,通过UniswapV3Pool.flash()完成。 第二种是类似V2的flash swap,借入和还贷的token不同,这个是通过UniswapV3Pool.swap()来完成的。

就不赘述了。

collect

    function collect(
        address recipient,
        int24 tickLower,
        int24 tickUpper,
        uint128 amount0Requested,
        uint128 amount1Requested
    ) external override lock returns (uint128 amount0, uint128 amount1) {
        // we don't need to checkTicks here, because invalid positions will never have non-zero tokensOwed{0,1}
        Position.Info storage position = positions.get(msg.sender, tickLower, tickUpper);

        amount0 = amount0Requested > position.tokensOwed0 ? position.tokensOwed0 : amount0Requested;
        amount1 = amount1Requested > position.tokensOwed1 ? position.tokensOwed1 : amount1Requested;

        if (amount0 > 0) {
            position.tokensOwed0 -= amount0;
            TransferHelper.safeTransfer(token0, recipient, amount0);
        }
        if (amount1 > 0) {
            position.tokensOwed1 -= amount1;
            TransferHelper.safeTransfer(token1, recipient, amount1);
        }

        emit Collect(msg.sender, recipient, tickLower, tickUpper, amount0, amount1);
    }

逻辑比较简单,首先获取用户的头寸,再计算提取的代币数量amount0与amount1,最后更新状态,转移代币。

点赞 0
收藏 1
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
不可思议之人
不可思议之人
0x46b7...98ee
江湖只有他的大名,没有他的介绍。