深入理解 Uniswap v3 合约代码 (一)

tags: uniswap solidity logarithm uniswap-v3 tick core

概述

与Uniswap v2一样,Uniswap v3的合约也分为两类:

  • Uniswap-v3-core
    • Uniswap v3的核心代码,实现了协议定义的所有功能,外部合约可直接与core合约交互
  • Uniswap-v3-periphery
    • 基于使用场景封装接口,如头寸(Position)管理、多路径代币交换等功能,Uniswap界面即与periphery合约交互

如果你希望以用户场景角度阅读本文,请直接从Uniswap-v3-periphery开始,它包含了创建头寸、修改头寸流动性、交换代币等常用功能。

如果你希望从底层核心模块开始阅读,请从Uniswap-v3-core开始。

Uniswap-v3-core

UniswapV3Factory.sol

工厂合约主要包含三个功能:

createPool

创建一个Uniswap v3交易对池子,注意,由于Uniswap v3支持不同手续费等级,如0.05%、0.30%、1.00%等,因此一个交易对合约由tokenAtokenBfee(手续费)唯一确定。

计算交易对合约还需要:factory工厂合约地址、合约初始化代码的hash。

/// @inheritdoc IUniswapV3Factory
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);
}

因为传入的tokenAtokenB是无序的,首先对tokenAtokenB排序,确保tokenA < tokenB

通过手续费等级获取对应的tickSpacing

int24 tickSpacing = feeAmountTickSpacing[fee];

我们在《深入理解Uniswap v3白皮书》中介绍过tickSpacing的作用,每个手续费等级对应一个tickSpacing,只有被tickSpacing整除的tick才允许被初始化,tickSpacing越大,每个tick流动性越多,tick之间滑点越大,但会节省跨tick操作的gas。这里作为Pool的参数保存起来。

确认该交易对对应的手续费等级没有创建过:

require(getPool[token0][token1][fee] == address(0));

创建(部署)交易对合约:

pool = deploy(address(this), token0, token1, fee, tickSpacing);

deploy代码如下:

/// @dev Deploys a pool with the given parameters by transiently setting the parameters storage slot and then
/// clearing it after deploying the pool.
/// @param factory The contract address of the Uniswap V3 factory
/// @param token0 The first token of the pool by address sort order
/// @param token1 The second token of the pool by address sort order
/// @param fee The fee collected upon every swap in the pool, denominated in hundredths of a bip
/// @param tickSpacing The spacing between usable ticks
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;
}

我们在Uniswap v2中提到,为了确保交易对合约地址的可计算性和唯一性,Uniswap v2使用CREATE2操作码创建交易对合约;从Solidity 0.6.2版本开始(Github PR),支持在new方法中传递salt参数实现CREATE2功能;salt参数确保了合约地址的唯一性和可计算性。从代码可知,Uniswap v3交易对合约使用token0、token1、fee唯一确定一个交易对合约,比如,根据ETH-USDC 0.05%手续费(以及工厂合约地址、初始化代码hash)等信息,可计算交易对合约地址。

最后,保存交易对合约地址到getPool变量中:

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;

setOwner

设置工厂合约owner,owner具有以下权限:

首先判断请求由当前owner发起,确认后修改owner:

/// @inheritdoc IUniswapV3Factory
function setOwner(address _owner) external override {
    require(msg.sender == owner);
    emit OwnerChanged(owner, _owner);
    owner = _owner;
}

enableFeeAmount

Uniswap v3默认支持三种手续费等级:0.05%、0.30%和1.00%,对应的fee值分别为500、3000和10000;fee的基本单位是百分之一基点,即0.01 bp = \(10^{-6}\)

手续费百分比计算公式为:

\[ f_{ratio} = \frac{fee}{1,000,000} \]

/// @inheritdoc IUniswapV3Factory
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);
}

UniswapV3Pool.sol

这是Uniswap v3的主要代码,定义了交易对池子的功能:

此外,factory(工厂合约)owner还可以调用以下两个方法:

initialize

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

该方法初始化slot0变量:

/// @inheritdoc IUniswapV3PoolActions
/// @dev not locked because it initializes unlocked
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:交易对当前的开根号价格\(\sqrt{P}\)
  • tick:当前\(\sqrt{P}\)对应的tick,使用getTickAtSqrtRatio计算得出
  • observationIndex:最近更新的(预言机)观测点数组序号
  • observationCardinality:(预言机)观测点数组容量,最大65536,初始时为1
  • observationCardinalityNext:下一个(预言机)观测点数组容量,如果手动扩容容量,会更新这个值,初始时为1
  • feeProtocol:协议手续费比例,可以分别为token0token1设置交易手续费中分给协议的比例
  • unlocked:当前交易对合约是否非锁定状态

mint

该方法实现添加流动性功能。实际上,首次添加流动性和后续增加流动性,都会使用该方法。

mint方法参数如下:

  • recipient:头寸接收者(owner)
  • tickLower:流动性区间低点
  • tickUpper:流动性区间高点
  • amount:流动性数量
  • data:回调参数
/// @inheritdoc IUniswapV3PoolActions
/// @dev noDelegateCall is applied indirectly via _modifyPosition
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);
}

mint方法主要的逻辑都在_modifyPosition中,其返回的amount0Intamount1Int表示:如果添加amount数量的流动性,则需要分别向交易对合约转入的token0token1的代币数量。

调用方需在uniswapV3MintCallback完成代币的转入操作;调用mint方法的合约需要实现IUniswapV3MintCallback接口,Uniswap v3在periphery合约的NonfungiblePositionManager.sol实现该接口。

因为mint调用方需要实现接口方法,因此个人ETH账户无法调用该方法。

IUniswapV3MintCallback(msg.sender).uniswapV3MintCallback(amount0, amount1, data);
_modifyPosition

继续来看_modifyPosition

/// @dev Effect some changes to a position
/// @param params the position details and the change to the position's liquidity to effect
/// @return position a storage pointer referencing the position with the given owner and tick range
/// @return amount0 the amount of token0 owed to the pool, negative if the pool should pay the recipient
/// @return amount1 the amount of token1 owed to the pool, negative if the pool should pay the recipient
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; // SLOAD for gas optimization

    position = _updatePosition(
        params.owner,
        params.tickLower,
        params.tickUpper,
        params.liquidityDelta,
        _slot0.tick
    );

先通过_updatePosition更新头寸信息,我们在下一节会具体介绍。

    if (params.liquidityDelta != 0) {
        if (_slot0.tick < params.tickLower) {
            // current tick is below the passed range; liquidity can only become in range by crossing from left to
            // right, when we'll need _more_ token0 (it's becoming more valuable) so user must provide it
            amount0 = SqrtPriceMath.getAmount0Delta(
                TickMath.getSqrtRatioAtTick(params.tickLower),
                TickMath.getSqrtRatioAtTick(params.tickUpper),
                params.liquidityDelta
            );
        } else if (_slot0.tick < params.tickUpper) {
            // current tick is inside the passed range
            uint128 liquidityBefore = liquidity; // SLOAD for gas optimization

            // write an oracle entry
            (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 {
            // current tick is above the passed range; liquidity can only become in range by crossing from right to
            // left, when we'll need _more_ token1 (it's becoming more valuable) so user must provide it
            amount1 = SqrtPriceMath.getAmount1Delta(
                TickMath.getSqrtRatioAtTick(params.tickLower),
                TickMath.getSqrtRatioAtTick(params.tickUpper),
                params.liquidityDelta
            );
        }
    }
}

代码的下半部分则主要通过getAmount0DeltagetAmount1Delta计算该流动性需要分别提供的token0token1的数量,即amount0amount1

具体地,当你提供流动性的区间大于当前tick\(i_c\)时,因为tick大小与\(\sqrt{P}\)(即\(\sqrt{\frac{y}{x}}\))成正比,意味着在大于\(i_c\)的区间,\(x\)的价值更高(需要更少的\(x\)),因此添加流动性时需在该部分提供\(x\)代币,即amount0数量的token0;反之,则提供\(y\)代币,即amount1token1

如下所示:

\[ \begin{cases}i_c, ..., \overbrace{i_l, ..., i_u}^{amount0} & \text{$i_c < i_l$}\\ \overbrace{i_l, ...}^{amount1}, i_c, \overbrace{..., i_u}^{amount0} & \text{$i_l \leq i_c < i_u$}\\ \overbrace{i_l, ..., i_u}^{amount1}, ..., i_c & \text{$i_u \leq i_c$}\end{cases} \]

其中,\(i_l\), \(i_u\)为提供流动性价格区间的边界,\(i_c\)为当前价格对应的tick

如果当前价格在区间中,即\(i_l \leq i_c < i_u\)时,_modifyPosition会记录一次(预言机)观测点数据,因为此时区间的流动性发生了变化,需要记录每流动性的持续时间secondsPerLiquidityCumulativeX128

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

在计算amount0amount1后,更新当前交易对的全局活跃流动性liquidity

liquidity = LiquidityMath.addDelta(liquidityBefore, params.liquidityDelta);

这个全局流动性会在swap时用到。

_updatePosition

_modifyPosition中的_updatePosition代码如下:

/// @dev Gets and updates a position with the given liquidity delta
/// @param owner the owner of the position
/// @param tickLower the lower tick of the position's tick range
/// @param tickUpper the upper tick of the position's tick range
/// @param tick the current tick, passed to avoid sloads
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; // SLOAD for gas optimization
    uint256 _feeGrowthGlobal1X128 = feeGrowthGlobal1X128; // SLOAD for gas optimization

    // if we need to update the ticks, do it
    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
            );

observations.observeSingle计算从最后一次观测点到现在的累积ticktickCumulative和累积每份流动性的持续时间secondsPerLiquidityCumulativeX128

        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);
        }
    }

接着使用ticks.update分别更新tickLower(价格区间低点)和tickUpper(价格区间高点)的状态,具体请参考Tick.update

如果对应tick的流动性从从0到有,或从有到0,则表示该tick需要被翻转。如果该tick未被标记为初始化,则标记为初始化;否则,将其取消初始化;这里用到tickBitmap.flipTick方法,请参考TickBitmap.flipTick

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

接着,计算该价格区间的累积每流动性手续费。

    position.update(liquidityDelta, feeGrowthInside0X128, feeGrowthInside1X128);

更新头寸(Position)信息,这里主要更新了头寸的应收手续费tokensOwed0tokensOwed1,以及头寸流动性liquidity,请参考Position.update

    // clear any tick data that is no longer needed
    if (liquidityDelta < 0) {
        if (flippedLower) {
            ticks.clear(tickLower);
        }
        if (flippedUpper) {
            ticks.clear(tickUpper);
        }
    }
}

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

最后,回到mint方法,调用者需要确保在uniswapV3MintCallback方法中,将这里计算出的amount0amount1数量的token0token1代币转入交易对合约。

总结mint方法的主要工作如下:

  1. 更新价格区间端点(lower, upper)的信息:ticks.update
  2. 如果Tick状态翻转,则更新位图的标识位,设置为“已初始化”或“未初始化”:tickBitmap.flipTick
  3. 更新头寸(Position)信息:positions.update
  4. 如果当前tick位于价格区间中,则:
    • 写入一次预言机观测点:observations.write
    • 更新全局活跃流动性:liquidity
  5. 调用方向交易对合约转账:uniswapV3MintCallback

burn

销毁流动性(burn)的逻辑与添加流动性(mint)几乎完全一样,唯一的区别是liquidityDelta是负的。

/// @inheritdoc IUniswapV3PoolActions
/// @dev noDelegateCall is applied indirectly via _modifyPosition
function burn(
    int24 tickLower,
    int24 tickUpper,
    uint128 amount
) external override lock returns (uint256 amount0, uint256 amount1) {
    (Position.Info storage position, int256 amount0Int, int256 amount1Int) =
        _modifyPosition(
            ModifyPositionParams({
                owner: msg.sender,
                tickLower: tickLower,
                tickUpper: tickUpper,
                liquidityDelta: -int256(amount).toInt128()
            })
        );

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

    if (amount0 > 0 || amount1 > 0) {
        (position.tokensOwed0, position.tokensOwed1) = (
            position.tokensOwed0 + uint128(amount0),
            position.tokensOwed1 + uint128(amount1)
        );
    }

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

这里与mint使用同一个_modifyPosition方法。

需注意,当销毁(部分)流动性后,代币并没有转回到调用方,而是以未领取代币的形式记在头寸(Position)上。

swap

swap方法是Uniswap v3代码的核心,该方法实现两个代币的交换,从token0交换到token1,或者相反。

相比Uniswap v2的同质化流动性,我们重点关注在swap过程中,价格如何变化,以及如何影响流动性的。

先来看swap方法的几个参数:

  • recipient:交易后的代币接收者
  • zeroForOne:如果从token0交换token1则为true,从token1交换token0则为false
  • amountSpecified: 指定的代币数量,如果为正,表示希望输入的代币数量;如果为负,则表示希望输出的代币数量
  • sqrtPriceLimitX96:能够承受的价格上限(或下限),格式为Q64.96;如果从token0token1,则表示swap过程中的价格下限;如果从token1token0,则表示价格上限;如果价格超过该值,则swap失败
  • data:回调参数
/// @inheritdoc IUniswapV3PoolActions
function swap(
    address recipient,
    bool zeroForOne,
    int256 amountSpecified,
    uint160 sqrtPriceLimitX96,
    bytes calldata data
) external override noDelegateCall returns (int256 amount0, int256 amount1) {
    require(amountSpecified != 0, 'AS');

    Slot0 memory slot0Start = slot0;

    require(slot0Start.unlocked, 'LOK');
    require(
        zeroForOne
            ? sqrtPriceLimitX96 < slot0Start.sqrtPriceX96 && sqrtPriceLimitX96 > TickMath.MIN_SQRT_RATIO
            : sqrtPriceLimitX96 > slot0Start.sqrtPriceX96 && sqrtPriceLimitX96 < TickMath.MAX_SQRT_RATIO,
        'SPL'
    );

    slot0.unlocked = false;

    SwapCache memory cache =
        SwapCache({
            liquidityStart: liquidity,
            blockTimestamp: _blockTimestamp(),
            feeProtocol: zeroForOne ? (slot0Start.feeProtocol % 16) : (slot0Start.feeProtocol >> 4),
            secondsPerLiquidityCumulativeX128: 0,
            tickCumulative: 0,
            computedLatestObservation: false
        });

    bool exactInput = amountSpecified > 0;

    SwapState memory state =
        SwapState({
            amountSpecifiedRemaining: amountSpecified,
            amountCalculated: 0,
            sqrtPriceX96: slot0Start.sqrtPriceX96,
            tick: slot0Start.tick,
            feeGrowthGlobalX128: zeroForOne ? feeGrowthGlobal0X128 : feeGrowthGlobal1X128,
            protocolFee: 0,
            liquidity: cache.liquidityStart
        });

上面代码主要是初始化状态相关的。

因为\(\sqrt{P} = \sqrt{\frac{y}{x}}\),当zeroForOne = true,即从token0token1时,swap过程中\(x\)变多,\(y\)变少,因此\(\sqrt{P}\)逐渐减小,所以指定的价格极限sqrtPriceLimitX96需要小于当前市场价格sqrtPriceX96

另外,需要注意几个关键数据:

  • 初始交易价格state.sqrtPriceX96为:slot0.sqrtPriceX96
    • 这里并没有使用slot0.tick计算初始价格,因为计算出来的值与slot0.sqrtPriceX96可能不一致,我们在后面代码会看到,slot0.tick不能作为当前价格
  • 初始可用流动性state.liquidity为:liquidity,也就是我们在mintburn时更新的全局可用流动性

根据zeroForOneexactInput,可以有四种swap组合:

zeroForOne exactInput swap
true true 输入固定数量token0,输出最大数量token1
true false 出入最小数量token0,输出固定数量token1
false true 输入固定数量token1,输出最大数量token0
false false 输入最小数量token1,输出固定数量token0

一个完整的swap可以由多个step组成,代码如下:

// continue swapping as long as we haven't used the entire input/output and haven't reached the price limit
while (state.amountSpecifiedRemaining != 0 && state.sqrtPriceX96 != sqrtPriceLimitX96) {
    StepComputations memory step;

    step.sqrtPriceStartX96 = state.sqrtPriceX96;

    (step.tickNext, step.initialized) = tickBitmap.nextInitializedTickWithinOneWord(
        state.tick,
        tickSpacing,
        zeroForOne
    );

    // ensure that we do not overshoot the min/max tick, as the tick bitmap is not aware of these bounds
    if (step.tickNext < TickMath.MIN_TICK) {
        step.tickNext = TickMath.MIN_TICK;
    } else if (step.tickNext > TickMath.MAX_TICK) {
        step.tickNext = TickMath.MAX_TICK;
    }

    // get the price for the next tick
    step.sqrtPriceNextX96 = TickMath.getSqrtRatioAtTick(step.tickNext);

    // compute values to swap to the target tick, price limit, or point where input/output amount is exhausted
    (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
    );

    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 the protocol fee is on, calculate how much is owed, decrement feeAmount, and increment protocolFee
    if (cache.feeProtocol > 0) {
        uint256 delta = step.feeAmount / cache.feeProtocol;
        step.feeAmount -= delta;
        state.protocolFee += uint128(delta);
    }

    // update global fee tracker
    if (state.liquidity > 0)
        state.feeGrowthGlobalX128 += FullMath.mulDiv(step.feeAmount, FixedPoint128.Q128, state.liquidity);

    // shift tick if we reached the next price
    if (state.sqrtPriceX96 == step.sqrtPriceNextX96) {
        // if the tick is initialized, run the tick transition
        if (step.initialized) {
            // check for the placeholder value, which we replace with the actual value the first time the swap
            // crosses an initialized tick
            if (!cache.computedLatestObservation) {
                (cache.tickCumulative, cache.secondsPerLiquidityCumulativeX128) = observations.observeSingle(
                    cache.blockTimestamp,
                    0,
                    slot0Start.tick,
                    slot0Start.observationIndex,
                    cache.liquidityStart,
                    slot0Start.observationCardinality
                );
                cache.computedLatestObservation = true;
            }
            int128 liquidityNet =
                ticks.cross(
                    step.tickNext,
                    (zeroForOne ? state.feeGrowthGlobalX128 : feeGrowthGlobal0X128),
                    (zeroForOne ? feeGrowthGlobal1X128 : state.feeGrowthGlobalX128),
                    cache.secondsPerLiquidityCumulativeX128,
                    cache.tickCumulative,
                    cache.blockTimestamp
                );
            // if we're moving leftward, we interpret liquidityNet as the opposite sign
            // safe because liquidityNet cannot be type(int128).min
            if (zeroForOne) liquidityNet = -liquidityNet;

            state.liquidity = LiquidityMath.addDelta(state.liquidity, liquidityNet);
        }

        state.tick = zeroForOne ? step.tickNext - 1 : step.tickNext;
    } else if (state.sqrtPriceX96 != step.sqrtPriceStartX96) {
        // recompute unless we're on a lower tick boundary (i.e. already transitioned ticks), and haven't moved
        state.tick = TickMath.getTickAtSqrtRatio(state.sqrtPriceX96);
    }
}

整理成伪代码(pseudo code)如下:

loop if 剩余代币 != 0 and 当前价格 != 最小(或最大)价格:
    // step
    初始价格 := 上一个step的价格
    下一个tick := 根据当前tick,寻找最近的已初始化的tick,或者本组最后一个未初始化的tick
    目标价格 := 根据下一个tick计算的价格
    交换后的价格, 消耗的输入代币数量, 得到的输出代币数量, 交易手续费 := 完成一步交换(初始价格, 目标价格, 可用流动性, 剩余代币)

    更新 剩余代币
    更新 协议手续费

    if 交换后价格 == 目标价格:
        if tick已初始化:
            价格穿越该tick,更新tick相关字段
            更新 可用流动性

        当前tick := 下一个tick - 1
    else if 交换后价格 != 初始价格:
        当前tick := 根据交换后价格计算tick

首先需要根据当前tick寻找下一个tick,即tickNext,具体逻辑可参考:tickBitmap.nextInitializedTickWithinOneWord

计算当前step的目标价格:

// get the price for the next tick
step.sqrtPriceNextX96 = TickMath.getSqrtRatioAtTick(step.tickNext);

计算本次(step)交换的输入输出(即执行一次交换):

// compute values to swap to the target tick, price limit, or point where input/output amount is exhausted
(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
);

SwapMath.computeSwapStep将根据当前价格、目标价格、可用流动性、可用输入代币等数据,计算本次交换能最多成交的输入代币数量(amountIn),输出代币数量(amountOut),手续费(feeAmount)和成交后价格(sqrtRatioNextX96)。请参考computeSwapStep

保存本次交易的amountInamountOut

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());
}
  • 如果是指定输入代币数量(token0token1
    • amountSpecifiedRemaining表示(扣除手续费后)剩余可用输入代币数量
    • amountCalculated表示已输出代币数量(注意,这里是负值)
  • 如果是指定输出代币数量(token0token1
    • amountSpecifiedRemaining表示剩余需要输出的代币数量(初始为负值,因此每次交换后需要+= step.amountOut),直到为0
    • amountCalculated表示(加入手续费后)已使用的输入代币数量

计算协议手续费:

// if the protocol fee is on, calculate how much is owed, decrement feeAmount, and increment protocolFee
if (cache.feeProtocol > 0) {
    uint256 delta = step.feeAmount / cache.feeProtocol;
    step.feeAmount -= delta;
    state.protocolFee += uint128(delta);
}

如果开启了协议手续费,则从交易手续费中拆出协议手续费。注意,协议手续费的值feeProtocol表示交易手续费的\(\frac{1}{n}\)

// update global fee tracker
if (state.liquidity > 0)
    state.feeGrowthGlobalX128 += FullMath.mulDiv(step.feeAmount, FixedPoint128.Q128, state.liquidity);

计算(每流动性)全局累积手续费。

// shift tick if we reached the next price
if (state.sqrtPriceX96 == step.sqrtPriceNextX96) {
    // if the tick is initialized, run the tick transition
    if (step.initialized) {
        // check for the placeholder value, which we replace with the actual value the first time the swap
        // crosses an initialized tick
        if (!cache.computedLatestObservation) {
            (cache.tickCumulative, cache.secondsPerLiquidityCumulativeX128) = observations.observeSingle(
                cache.blockTimestamp,
                0,
                slot0Start.tick,
                slot0Start.observationIndex,
                cache.liquidityStart,
                slot0Start.observationCardinality
            );
            cache.computedLatestObservation = true;
        }
        int128 liquidityNet =
            ticks.cross(
                step.tickNext,
                (zeroForOne ? state.feeGrowthGlobalX128 : feeGrowthGlobal0X128),
                (zeroForOne ? feeGrowthGlobal1X128 : state.feeGrowthGlobalX128),
                cache.secondsPerLiquidityCumulativeX128,
                cache.tickCumulative,
                cache.blockTimestamp
            );
        // if we're moving leftward, we interpret liquidityNet as the opposite sign
        // safe because liquidityNet cannot be type(int128).min
        if (zeroForOne) liquidityNet = -liquidityNet;

        state.liquidity = LiquidityMath.addDelta(state.liquidity, liquidityNet);
    }

    state.tick = zeroForOne ? step.tickNext - 1 : step.tickNext;
} else if (state.sqrtPriceX96 != step.sqrtPriceStartX96) {
    // recompute unless we're on a lower tick boundary (i.e. already transitioned ticks), and haven't moved
    state.tick = TickMath.getTickAtSqrtRatio(state.sqrtPriceX96);
}
  • 如果本次交换后的价格达到目标价格(即根据寻找的下一个tick计算的价格):
    • 如果该tick已经初始化,则:
      • 通过ticks.cross方法穿越该tick,反向设置相关Outside变量的数据
      • 使用tick净流动性liquidityNet更新可用流动性state.liquidity
    • 移动当前tick到下一个tick
  • 如果交换后的价格没有达到本次目标价格,但是又不等于初始价格,即表示此时交易结束:
    • 使用交换后的价格计算最新的tick

重复上述step,直到交换完全结束。

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

// update tick and write an oracle entry if the tick change
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 {
    // otherwise just update the price
    slot0.sqrtPriceX96 = state.sqrtPriceX96;
}
  • 如果交换后的tick与交换前的tick不同:
    • 记录一次(预言机)观测点数据,因为tickCumulative发生了改变
    • 更新slot0.sqrtPriceX96, slot0.tick等值,注意此时sqrtPriceX96tick并不一定对应,sqrtPriceX96才能准确反映当前价格
  • 如果交换前后tick值相同,则只需要修改价格:
    • 更新slot0.sqrtPriceX96

同样,如果全局流动性发生改变,则更新liquidity

// update liquidity if it changed
if (cache.liquidityStart != state.liquidity) liquidity = state.liquidity;

更新累积手续费和协议手续费:

// update fee growth global and, if necessary, protocol fees
// overflow is acceptable, protocol has to withdraw before it hits type(uint128).max fees
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);

计算本次交换需要的具体amount0amount1

// do the transfers and collect payment
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');
}

emit Swap(msg.sender, recipient, amount0, amount1, state.sqrtPriceX96, state.liquidity, state.tick);
slot0.unlocked = true;

合约将输出代币转账给recipient,同时,调用方需要在uniswapV3SwapCallback方法将输入代币转给交易对合约:

至此,整个swap流程就结束了。

flash

本方法实现Uniswap v3闪电贷功能。

方法参数:

  • recipient:闪电贷接收者
  • amount0:借出token0的数量
  • amount1:借出token1的数量
  • data:回调方法参数
/// @inheritdoc IUniswapV3PoolActions
function flash(
    address recipient,
    uint256 amount0,
    uint256 amount1,
    bytes calldata data
) external override lock noDelegateCall {
    uint128 _liquidity = liquidity;
    require(_liquidity > 0, 'L');

    uint256 fee0 = FullMath.mulDivRoundingUp(amount0, fee, 1e6);
    uint256 fee1 = FullMath.mulDivRoundingUp(amount1, fee, 1e6);

闪电贷手续费与swap手续费相同,都是\(\frac{fee}{10^6}\)

    uint256 balance0Before = balance0();
    uint256 balance1Before = balance1();

    if (amount0 > 0) TransferHelper.safeTransfer(token0, recipient, amount0);
    if (amount1 > 0) TransferHelper.safeTransfer(token1, recipient, amount1);

    IUniswapV3FlashCallback(msg.sender).uniswapV3FlashCallback(fee0, fee1, data);

    uint256 balance0After = balance0();
    uint256 balance1After = balance1();

    require(balance0Before.add(fee0) <= balance0After, 'F0');
    require(balance1Before.add(fee1) <= balance1After, 'F1');

向收款人转入贷出的代币数量,flash方法的调用方需实现IUniswapV3FlashCallback.uniswapV3FlashCallback接口方法,并在该方法中归还代币,包含手续费。

    // sub is safe because we know balanceAfter is gt balanceBefore by at least fee
    uint256 paid0 = balance0After - balance0Before;
    uint256 paid1 = balance1After - balance1Before;

    if (paid0 > 0) {
        uint8 feeProtocol0 = slot0.feeProtocol % 16;
        uint256 fees0 = feeProtocol0 == 0 ? 0 : paid0 / feeProtocol0;
        if (uint128(fees0) > 0) protocolFees.token0 += uint128(fees0);
        feeGrowthGlobal0X128 += FullMath.mulDiv(paid0 - fees0, FixedPoint128.Q128, _liquidity);
    }
    if (paid1 > 0) {
        uint8 feeProtocol1 = slot0.feeProtocol >> 4;
        uint256 fees1 = feeProtocol1 == 0 ? 0 : paid1 / feeProtocol1;
        if (uint128(fees1) > 0) protocolFees.token1 += uint128(fees1);
        feeGrowthGlobal1X128 += FullMath.mulDiv(paid1 - fees1, FixedPoint128.Q128, _liquidity);
    }

    emit Flash(msg.sender, recipient, amount0, amount1, paid0, paid1);
}

根据收取的手续费,计算协议手续费(注意,token0token1的协议手续费是单独设置的,参考:setFeeProtocol),最后更新protocolFeesfeeGrowthGlobal1X128

collect

该方法实现取回代币功能,包括销毁流动性记录的代币和手续费代币。

参数如下:

  • recipient:代币接收者
  • tickLower:头寸低点
  • tickUpper:头寸高点
  • amount0Requested:请求取回的token0数量
  • amount1Requested:请求取回的token1数量
/// @inheritdoc IUniswapV3PoolActions
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);
}

上述代码比较简单,这里不再展开。需要注意,如果希望取回所有代币,则需要指定比tokensOwned更大的数,比如可以使用type(uint128).max

increaseObservationCardinalityNext

扩容预言机观测点的可写入空间,该方法调用Oracle.solgrow方法实现扩容。

/// @inheritdoc IUniswapV3PoolActions
function increaseObservationCardinalityNext(uint16 observationCardinalityNext)
    external
    override
    lock
    noDelegateCall
{
    uint16 observationCardinalityNextOld = slot0.observationCardinalityNext; // for the event
    uint16 observationCardinalityNextNew =
        observations.grow(observationCardinalityNextOld, observationCardinalityNext);
    slot0.observationCardinalityNext = observationCardinalityNextNew;
    if (observationCardinalityNextOld != observationCardinalityNextNew)
        emit IncreaseObservationCardinalityNext(observationCardinalityNextOld, observationCardinalityNextNew);
}

observe

批量获取指定时间的观测点数据,该方法调用Oracle.solobserve实现。

/// @inheritdoc IUniswapV3PoolDerivedState
function observe(uint32[] calldata secondsAgos)
    external
    view
    override
    noDelegateCall
    returns (int56[] memory tickCumulatives, uint160[] memory secondsPerLiquidityCumulativeX128s)
{
    return
        observations.observe(
            _blockTimestamp(),
            secondsAgos,
            slot0.tick,
            slot0.observationIndex,
            liquidity,
            slot0.observationCardinality
        );
}

setFeeProtocol

设置协议手续费的比例,该方法仅允许工厂合约的owner执行。

注意,需要分别设置token0token1的协议手续费比例,该比例是交易手续费的占比,合法值为0(不开启协议手续费)或者\(4 \leq n \leq 10\),也就是可以设置协议手续费为交易手续费的\(\frac{1}{n}\)

/// @inheritdoc IUniswapV3PoolOwnerActions
function setFeeProtocol(uint8 feeProtocol0, uint8 feeProtocol1) external override lock onlyFactoryOwner {
    require(
        (feeProtocol0 == 0 || (feeProtocol0 >= 4 && feeProtocol0 <= 10)) &&
            (feeProtocol1 == 0 || (feeProtocol1 >= 4 && feeProtocol1 <= 10))
    );
    uint8 feeProtocolOld = slot0.feeProtocol;
    slot0.feeProtocol = feeProtocol0 + (feeProtocol1 << 4);
    emit SetFeeProtocol(feeProtocolOld % 16, feeProtocolOld >> 4, feeProtocol0, feeProtocol1);
}

slot0.feeProtocol类型为uint8,保存两种代币的协议手续费比例,高4位为token1,低4位为token0

\[ slot0.feeProtocol = \overbrace{0000}^{fee1}\overbrace{0000}^{fee0} \]

因此,feeProtocolOld % 16表示token0的协议手续费比例fee0feeProtocolOld >> 4表示token1的协议手续费比例fee1

collectProtocol

取回协议手续费,该方法仅允许工厂合约的owner执行。

协议手续费有两个来源:

  • swap产生的交易手续费
  • flash产生的闪电贷手续费
/// @inheritdoc IUniswapV3PoolOwnerActions
function collectProtocol(
    address recipient,
    uint128 amount0Requested,
    uint128 amount1Requested
) external override lock onlyFactoryOwner returns (uint128 amount0, uint128 amount1) {
    amount0 = amount0Requested > protocolFees.token0 ? protocolFees.token0 : amount0Requested;
    amount1 = amount1Requested > protocolFees.token1 ? protocolFees.token1 : amount1Requested;

    if (amount0 > 0) {
        if (amount0 == protocolFees.token0) amount0--; // ensure that the slot is not cleared, for gas savings
        protocolFees.token0 -= amount0;
        TransferHelper.safeTransfer(token0, recipient, amount0);
    }
    if (amount1 > 0) {
        if (amount1 == protocolFees.token1) amount1--; // ensure that the slot is not cleared, for gas savings
        protocolFees.token1 -= amount1;
        TransferHelper.safeTransfer(token1, recipient, amount1);
    }

    emit CollectProtocol(msg.sender, recipient, amount0, amount1);
}

上述代码比较简单,此处不再展开。

Tick.sol

Tick.sol管理Tick内部状态。

tickSpacingToMaxLiquidityPerTick

根据tickSpacing计算每个tick最大流动性,只有能够被tickSpacing整除的tick才能够存放流动性:

/// @notice Derives max liquidity per tick from given tick spacing
/// @dev Executed within the pool constructor
/// @param tickSpacing The amount of required tick separation, realized in multiples of `tickSpacing`
///     e.g., a tickSpacing of 3 requires ticks to be initialized every 3rd tick i.e., ..., -6, -3, 0, 3, 6, ...
/// @return The max liquidity per tick
function tickSpacingToMaxLiquidityPerTick(int24 tickSpacing) internal pure returns (uint128) {
    int24 minTick = (TickMath.MIN_TICK / tickSpacing) * tickSpacing;
    int24 maxTick = (TickMath.MAX_TICK / tickSpacing) * tickSpacing;
    uint24 numTicks = uint24((maxTick - minTick) / tickSpacing) + 1;
    return type(uint128).max / numTicks;
}

getFeeGrowthInside

计算两个tick区间内部的每流动性累积手续费,该方法实现白皮书公式6.17-6.19:

\[ f_a(i) = \begin{cases} f_g - f_o(i) & \text{$i_c \geq i$}\\ f_o(i) & \text{$i_c < i$} \end{cases} \tag{6.17} \]

\[ f_b(i) = \begin{cases} f_o(i) & \text{$i_c \geq i$}\\ f_g - f_o(i) & \text{$i_c < i$}\end{cases} \tag{6.18} \]

\[ f_r = f_g - f_b(i_l) - f_a(i_u) \tag{6.19} \]

代码如下:

/// @notice Retrieves fee growth data
/// @param self The mapping containing all tick information for initialized ticks
/// @param tickLower The lower tick boundary of the position
/// @param tickUpper The upper tick boundary of the position
/// @param tickCurrent The current tick
/// @param feeGrowthGlobal0X128 The all-time global fee growth, per unit of liquidity, in token0
/// @param feeGrowthGlobal1X128 The all-time global fee growth, per unit of liquidity, in token1
/// @return feeGrowthInside0X128 The all-time fee growth in token0, per unit of liquidity, inside the position's tick boundaries
/// @return feeGrowthInside1X128 The all-time fee growth in token1, per unit of liquidity, inside the position's tick boundaries
function getFeeGrowthInside(
    mapping(int24 => Tick.Info) storage self,
    int24 tickLower,
    int24 tickUpper,
    int24 tickCurrent,
    uint256 feeGrowthGlobal0X128,
    uint256 feeGrowthGlobal1X128
) internal view returns (uint256 feeGrowthInside0X128, uint256 feeGrowthInside1X128) {
    Info storage lower = self[tickLower];
    Info storage upper = self[tickUpper];

    // calculate fee growth below
    uint256 feeGrowthBelow0X128;
    uint256 feeGrowthBelow1X128;
    if (tickCurrent >= tickLower) {
        feeGrowthBelow0X128 = lower.feeGrowthOutside0X128;
        feeGrowthBelow1X128 = lower.feeGrowthOutside1X128;
    } else {
        feeGrowthBelow0X128 = feeGrowthGlobal0X128 - lower.feeGrowthOutside0X128;
        feeGrowthBelow1X128 = feeGrowthGlobal1X128 - lower.feeGrowthOutside1X128;
    }

    // calculate fee growth above
    uint256 feeGrowthAbove0X128;
    uint256 feeGrowthAbove1X128;
    if (tickCurrent < tickUpper) {
        feeGrowthAbove0X128 = upper.feeGrowthOutside0X128;
        feeGrowthAbove1X128 = upper.feeGrowthOutside1X128;
    } else {
        feeGrowthAbove0X128 = feeGrowthGlobal0X128 - upper.feeGrowthOutside0X128;
        feeGrowthAbove1X128 = feeGrowthGlobal1X128 - upper.feeGrowthOutside1X128;
    }

    feeGrowthInside0X128 = feeGrowthGlobal0X128 - feeGrowthBelow0X128 - feeGrowthAbove0X128;
    feeGrowthInside1X128 = feeGrowthGlobal1X128 - feeGrowthBelow1X128 - feeGrowthAbove1X128;
}

首先根据当前tickCurrent,分别计算tickLowertickUpper\(f_a\), \(f_b\),最后计算出区间内手续费\(f_r\)

\[ \underbrace{\overbrace{..., i_l - 1}^{f_b(i_l)}, \overbrace{i_l, i_l + 1, ..., i_u - 1, i_u}^{f_r}, \overbrace{i_u + 1, ...}^{f_a(i_u)}}_{f_g} \]

为什么需要计算区间内每流动性累计手续费呢?因为每个头寸(Position)会在mint/burn时根据该值计算自己的应收手续费:

\[ liquidityDelta \cdot (feeGrowthInside - feeGrowthInsideLast) \]

update

更新tick状态,并返回该tick是否翻转flipped

/// @notice Updates a tick and returns true if the tick was flipped from initialized to uninitialized, or vice versa
/// @param self The mapping containing all tick information for initialized ticks
/// @param tick The tick that will be updated
/// @param tickCurrent The current tick
/// @param liquidityDelta A new amount of liquidity to be added (subtracted) when tick is crossed from left to right (right to left)
/// @param feeGrowthGlobal0X128 The all-time global fee growth, per unit of liquidity, in token0
/// @param feeGrowthGlobal1X128 The all-time global fee growth, per unit of liquidity, in token1
/// @param secondsPerLiquidityCumulativeX128 The all-time seconds per max(1, liquidity) of the pool
/// @param tickCumulative The tick * time elapsed since the pool was first initialized
/// @param time The current block timestamp cast to a uint32
/// @param upper true for updating a position's upper tick, or false for updating a position's lower tick
/// @param maxLiquidity The maximum liquidity allocation for a single tick
/// @return flipped Whether the tick was flipped from initialized to uninitialized, or vice versa
function update(
    mapping(int24 => Tick.Info) storage self,
    int24 tick,
    int24 tickCurrent,
    int128 liquidityDelta,
    uint256 feeGrowthGlobal0X128,
    uint256 feeGrowthGlobal1X128,
    uint160 secondsPerLiquidityCumulativeX128,
    int56 tickCumulative,
    uint32 time,
    bool upper,
    uint128 maxLiquidity
) internal returns (bool flipped) {
    Tick.Info storage info = self[tick];

    uint128 liquidityGrossBefore = info.liquidityGross;
    uint128 liquidityGrossAfter = LiquidityMath.addDelta(liquidityGrossBefore, liquidityDelta);

    require(liquidityGrossAfter <= maxLiquidity, 'LO');

    flipped = (liquidityGrossAfter == 0) != (liquidityGrossBefore == 0);

如果tick从无流动性到有流动性,或者从有流动性变成无流动性,则表示tick需要翻转flipped

    if (liquidityGrossBefore == 0) {
        // by convention, we assume that all growth before a tick was initialized happened _below_ the tick
        if (tick <= tickCurrent) {
            info.feeGrowthOutside0X128 = feeGrowthGlobal0X128;
            info.feeGrowthOutside1X128 = feeGrowthGlobal1X128;
            info.secondsPerLiquidityOutsideX128 = secondsPerLiquidityCumulativeX128;
            info.tickCumulativeOutside = tickCumulative;
            info.secondsOutside = time;
        }
        info.initialized = true;
    }

如果tick之前没有流动性,则进行初始化;对于小于当前tickCurrenttick,设置Outside等变量。

    info.liquidityGross = liquidityGrossAfter;

liquidityGross表示总流动性,用于判断tick是否需要翻转:

  • 如果mint,则增加流动性;如果burn,则减少流动性
  • 该变量与tick在不同头寸中是否作为边界低点或高点无关,只与mintburn操作有关
    // when the lower (upper) tick is crossed left to right (right to left), liquidity must be added (removed)
    info.liquidityNet = upper
        ? int256(info.liquidityNet).sub(liquidityDelta).toInt128()
        : int256(info.liquidityNet).add(liquidityDelta).toInt128();
}

liquidityNet表示净流动性,当swap穿越tick时,用于更新全局可用流动性liquidity

  • 如果作为tickLower,即边界低点(左边界点),则增加liquidityDeltamint时为正,burn时为负)
  • 如果作为tickUpper,即边界高点(右边界点),则减少liquidityDeltamint时为正,burn时为负)

clear

tick翻转后,如果没有流动性关联该tick,即liquidityGross = 0,则清空tick状态:

/// @notice Clears tick data
/// @param self The mapping containing all initialized tick information for initialized ticks
/// @param tick The tick that will be cleared
function clear(mapping(int24 => Tick.Info) storage self, int24 tick) internal {
    delete self[tick];
}

cross

tick被穿越时,需要翻转Outside等变量的方向,如白皮书公式6.20:

\[ f_o(i) := f_g - f_o(i) \tag{6.20} \]

这些变量在getFeeGrowthInside等方法被用到。

/// @notice Transitions to next tick as needed by price movement
/// @param self The mapping containing all tick information for initialized ticks
/// @param tick The destination tick of the transition
/// @param feeGrowthGlobal0X128 The all-time global fee growth, per unit of liquidity, in token0
/// @param feeGrowthGlobal1X128 The all-time global fee growth, per unit of liquidity, in token1
/// @param secondsPerLiquidityCumulativeX128 The current seconds per liquidity
/// @param tickCumulative The tick * time elapsed since the pool was first initialized
/// @param time The current block.timestamp
/// @return liquidityNet The amount of liquidity added (subtracted) when tick is crossed from left to right (right to left)
function cross(
    mapping(int24 => Tick.Info) storage self,
    int24 tick,
    uint256 feeGrowthGlobal0X128,
    uint256 feeGrowthGlobal1X128,
    uint160 secondsPerLiquidityCumulativeX128,
    int56 tickCumulative,
    uint32 time
) internal returns (int128 liquidityNet) {
    Tick.Info storage info = self[tick];
    info.feeGrowthOutside0X128 = feeGrowthGlobal0X128 - info.feeGrowthOutside0X128;
    info.feeGrowthOutside1X128 = feeGrowthGlobal1X128 - info.feeGrowthOutside1X128;
    info.secondsPerLiquidityOutsideX128 = secondsPerLiquidityCumulativeX128 - info.secondsPerLiquidityOutsideX128;
    info.tickCumulativeOutside = tickCumulative - info.tickCumulativeOutside;
    info.secondsOutside = time - info.secondsOutside;
    liquidityNet = info.liquidityNet;
}

TickMath.sol

TickMath主要包含两个方法:

getSqrtRatioAtTick

该方法对应白皮书公式6.2:

\[ \sqrt{p}(i) = \sqrt{1.0001}^i = 1.0001^{\frac{i}{2}} \]

其中,\(i\)即为tick

因为Uniswap v3支持的价格(\(\frac{token1}{token0}\))区间为\([2^{-128}, 2^{128}]\),根据白皮书公式6.1:

\[ p(i) = 1.0001^i \]

因此,对应的最大tick(MAX_TICK)为:

\[ i = \lfloor log_{1.0001}{p(i)} \rfloor = \lfloor log_{1.0001}{2^{128}} \rfloor = \lfloor 887272.7517970635 \rfloor = 887272 \]

最小tick(MIN_TICK)为:

\[ i = \lceil log_{1.0001}{2^{-128}} \rceil = \lceil -887272.7517970635 \rceil = -887272 \]

假设\(i\) \(\geq 0\),对于一个给定的tick \(i\),它总可以表示为二进制,因此以下式子总是成立:

\[ \begin{cases} i = \sum_{n=0}^{19}{(x_n \cdot 2^n)} = x_0 \cdot 1 + x_1 \cdot 2 + x_2 \cdot 4 + ... + x_{19}\cdot 524288 \\ \forall x_n \in \{0, 1\} \end{cases} \tag{1.1} \]

其中,\(x_n\)\(i\)的二进制位。如\(i=6\),其对应的二进制为:000000000000000000000110,则\(x_1 = 1, x_2 = 1\),其余\(x_n\)均为0。

同样可以推出\(i < 0\)也可以用类似的公式表示。

我们先看\(i < 0\)的情况:

如果 \(i < 0\),则:

\[ \sqrt{p}(i) = 1.0001^{\frac{i}{2}} = 1.0001^{-\frac{|i|}{2}} = \frac{1}{1.0001^{\frac{|i|}{2}}} = \frac{1}{1.0001^{\frac{1}{2}(\sum_{n=0}^{19}{(x_n \cdot 2^n)})}} \\ = \frac{1}{1.0001^{\frac{1}{2} \cdot x_0}} \cdot \frac{1}{1.0001^{\frac{2}{2} \cdot x_1}} \cdot \frac{1}{1.0001^{\frac{4}{2} \cdot x_2}} \cdot ... \cdot \frac{1}{1.0001^{\frac{524288}{2} \cdot x_{19}}} \]

根据二进制位\(x_n\)的值,可以总结如下:

\[ \frac{1}{1.0001^{\frac{x_n \cdot 2^n}{2}}} \begin{cases} = 1 & \text{$x_n = 0, n \geq 0, i < 0$}\\ < 1 & \text{$x_n = 1, n \geq 0, i < 0$} \end{cases} \]

为了最小化精度误差,在计算过程中,使用Q128.128(128位定点数)表示中间价格,对于每一个价格\(p\),均需要左移128位。由于\(i < 0, x_n = 1\)时,\(\frac{1}{1.0001^{\frac{x_n \cdot 2^n}{2}}} < 1\),因此在连续乘积过程中不会有溢出问题。

可以总结计算\(\sqrt{p}(i)\)的方法:

  • 初始值为1,从第0位开始,从低位到高位(从右往左)循环遍历\(i\)的二进制比特位
  • 如果该位不为0,则乘以对应的\(\frac{2^{128}}{1.0001^{\frac{2^n}{2}}}\),其中\(2^{128}\)表示左移128位
  • 如果该位为0,则乘以1,可以省略
/// @notice Calculates sqrt(1.0001^tick) * 2^96
/// @dev Throws if |tick| > max tick
/// @param tick The input tick for the above formula
/// @return sqrtPriceX96 A Fixed point Q64.96 number representing the sqrt of the ratio of the two assets (token1/token0)
/// at the given tick
function getSqrtRatioAtTick(int24 tick) internal pure returns (uint160 sqrtPriceX96) {
    uint256 absTick = tick < 0 ? uint256(-int256(tick)) : uint256(int256(tick));
    require(absTick <= uint256(MAX_TICK), 'T');

    // 如果第0位非0,则ratio = 0xfffcb933bd6fad37aa2d162d1a594001 ,即:2^128 / 1.0001^0.5
    uint256 ratio = absTick & 0x1 != 0 ? 0xfffcb933bd6fad37aa2d162d1a594001 : 0x100000000000000000000000000000000;
    // 如果第1位非0,则乘以 0xfff97272373d413259a46990580e213a ,即:2^128 / 1.0001^1,因为两个乘数均为Q128.128,最终结果多乘了2^128,因此需要右移128
    if (absTick & 0x2 != 0) ratio = (ratio * 0xfff97272373d413259a46990580e213a) >> 128;
    // 如果第2位非0,则乘以 0xfff2e50f5f656932ef12357cf3c7fdcc ,即:2^128 / 1.0001^2,
    if (absTick & 0x4 != 0) ratio = (ratio * 0xfff2e50f5f656932ef12357cf3c7fdcc) >> 128;
    // 以此类推
    if (absTick & 0x8 != 0) ratio = (ratio * 0xffe5caca7e10e4e61c3624eaa0941cd0) >> 128;
    if (absTick & 0x10 != 0) ratio = (ratio * 0xffcb9843d60f6159c9db58835c926644) >> 128;
    if (absTick & 0x20 != 0) ratio = (ratio * 0xff973b41fa98c081472e6896dfb254c0) >> 128;
    if (absTick & 0x40 != 0) ratio = (ratio * 0xff2ea16466c96a3843ec78b326b52861) >> 128;
    if (absTick & 0x80 != 0) ratio = (ratio * 0xfe5dee046a99a2a811c461f1969c3053) >> 128;
    if (absTick & 0x100 != 0) ratio = (ratio * 0xfcbe86c7900a88aedcffc83b479aa3a4) >> 128;
    if (absTick & 0x200 != 0) ratio = (ratio * 0xf987a7253ac413176f2b074cf7815e54) >> 128;
    if (absTick & 0x400 != 0) ratio = (ratio * 0xf3392b0822b70005940c7a398e4b70f3) >> 128;
    if (absTick & 0x800 != 0) ratio = (ratio * 0xe7159475a2c29b7443b29c7fa6e889d9) >> 128;
    if (absTick & 0x1000 != 0) ratio = (ratio * 0xd097f3bdfd2022b8845ad8f792aa5825) >> 128;
    if (absTick & 0x2000 != 0) ratio = (ratio * 0xa9f746462d870fdf8a65dc1f90e061e5) >> 128;
    if (absTick & 0x4000 != 0) ratio = (ratio * 0x70d869a156d2a1b890bb3df62baf32f7) >> 128;
    if (absTick & 0x8000 != 0) ratio = (ratio * 0x31be135f97d08fd981231505542fcfa6) >> 128;
    if (absTick & 0x10000 != 0) ratio = (ratio * 0x9aa508b5b7a84e1c677de54f3e99bc9) >> 128;
    if (absTick & 0x20000 != 0) ratio = (ratio * 0x5d6af8dedb81196699c329225ee604) >> 128;
    if (absTick & 0x40000 != 0) ratio = (ratio * 0x2216e584f5fa1ea926041bedfe98) >> 128;
    // 如果第19位非0,因为(2^19 = 0x80000=524288),则乘以 0x2216e584f5fa1ea926041bedfe98,即:2^128 / 1.0001^(524288/2)
    // tick的最大值为887272,因此其二进制最多只需要20位表示,从0开始计数,最后一位为第19位。
    if (absTick & 0x80000 != 0) ratio = (ratio * 0x48a170391f7dc42444e8fa2) >> 128;

    if (tick > 0) ratio = type(uint256).max / ratio;

    // this divides by 1<<32 rounding up to go from a Q128.128 to a Q128.96.
    // we then downcast because we know the result always fits within 160 bits due to our tick input constraint
    // we round up in the division so getTickAtSqrtRatio of the output price is always consistent
    sqrtPriceX96 = uint160((ratio >> 32) + (ratio % (1 << 32) == 0 ? 0 : 1));
}

假设 \(i > 0\) 时:

\[ \sqrt{p_{Q128128}(i)} = 2^{128} \cdot \sqrt{p(i)} = 2^{128} \cdot 1.0001^{\frac{i}{2}} \\ = \frac{2^{128}}{1.0001^{-\frac{i}{2}}} = \frac{2^{256}}{2^{128} \cdot \sqrt{p(-i)}} = \frac{2^{256}}{\sqrt{p_{Q128128}(-i)}} \]

因此,只需要算出 \(i < 0\) 时的 ratio 值,使用\(2^{256}\)除以ratio即可得出 \(i > 0\) 时,使用Q128.128表示的ratio值:

if (tick > 0) ratio = type(uint256).max / ratio;

代码最后一行将ratio右移32位,转化为Q128.96格式的定点数:

sqrtPriceX96 = uint160((ratio >> 32) + (ratio % (1 << 32) == 0 ? 0 : 1));

这里算的是开根号价格\(\sqrt{p}\),由于价格\(p\)最大为\(2^{128}\),因此\(\sqrt{p}\)最大为\(2^{64}\),也就是整数部分最大只需要64位表示,因此最终的sqrtPriceX96一定可以用160位(64+96,即Q64.96格式的定点数)表示。

getTickAtSqrtRatio

该方法对应白皮书中的公式6.8:

\[ i_c = \lfloor \log_{\sqrt{1.0001}} \sqrt{P} \rfloor \]

本方法涉及在Solidity中计算对数,根据对数公式,可以推出:

\[ \log_{\sqrt{1.0001}} \sqrt{P} = \frac{log_2{\sqrt{P}}}{log_2{\sqrt{1.0001}}} = log_2{\sqrt{P}} \cdot log_{\sqrt{1.0001}}{2} \]

由于 \(log_{\sqrt{1.0001}}{2}\) 是一个常数,因此我们只需要计算 \(log_2{\sqrt{P}}\) 即可。

将输入的参数为\(\sqrt{P}\)看作\(x\),问题转化为求 \(log_2{x}\)

把结果分为整数部分\(n\)和小数部分\(m\),则:

\[ n \leq log_2{x} = n + m < n + 1 \]

整数部分

对于\(n\),因为:

\[ 2^n \leq x < 2^{n+1} \]

可以通过二分查找找到\(n\)值:

  • 对于256位的数,\(0 \leq n < 256\),可以用8位比特表示\(n\)
  • 从二进制表示的第8位(k=7)到第1位(k=0)(从最高位到最低位),依次比较\(x\)是否大于\(2^{2^{k}} - 1\),如果大于则标记该位为1,并右移\(2^k\)位;否则标记0
  • 最终标记后的8位二进制即为\(n\)

使用Python代码描述如下:

def find_msb(x):
    msb = 0
    for k in reversed(range(8)): // k = 7, 6, 5. 4, 3, 2, 1, 0
        if x > 2 ** (2 ** k) - 1:
            msb += 2 ** k // 标记该位为1,即加上 2 ** k
            x /= 2 ** (2 ** k) // 右移 2 ** k 位
    return msb

Uniswap v3中的Solidity代码如下:

/// @notice Calculates the greatest tick value such that getRatioAtTick(tick) <= ratio
/// @dev Throws in case sqrtPriceX96 < MIN_SQRT_RATIO, as MIN_SQRT_RATIO is the lowest value getRatioAtTick may
/// ever return.
/// @param sqrtPriceX96 The sqrt ratio for which to compute the tick as a Q64.96
/// @return tick The greatest tick for which the ratio is less than or equal to the input ratio
function getTickAtSqrtRatio(uint160 sqrtPriceX96) internal pure returns (int24 tick) {
    // second inequality must be < because the price can never reach the price at the max tick
    require(sqrtPriceX96 >= MIN_SQRT_RATIO && sqrtPriceX96 < MAX_SQRT_RATIO, 'R');
    uint256 ratio = uint256(sqrtPriceX96) << 32; // 右移32位,转化为Q128.128格式

    uint256 r = ratio;
    uint256 msb = 0;

    assembly {
        // 如果大于2 ** (2 ** 7) - 1,则保存临时变量:2 ** 7
        let f := shl(7, gt(r, 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF))
        // msb += 2 ** 7
        msb := or(msb, f)
        // r /= (2 ** (2 ** 7)),即右移 2 ** 7
        r := shr(f, r)
    }
    assembly {
        let f := shl(6, gt(r, 0xFFFFFFFFFFFFFFFF))
        msb := or(msb, f)
        r := shr(f, r)
    }
    assembly {
        let f := shl(5, gt(r, 0xFFFFFFFF))
        msb := or(msb, f)
        r := shr(f, r)
    }
    assembly {
        let f := shl(4, gt(r, 0xFFFF))
        msb := or(msb, f)
        r := shr(f, r)
    }
    assembly {
        let f := shl(3, gt(r, 0xFF))
        msb := or(msb, f)
        r := shr(f, r)
    }
    assembly {
        let f := shl(2, gt(r, 0xF))
        msb := or(msb, f)
        r := shr(f, r)
    }
    assembly {
        let f := shl(1, gt(r, 0x3))
        msb := or(msb, f)
        r := shr(f, r)
    }
    assembly {
        let f := gt(r, 0x1)
        msb := or(msb, f)
    }
小数部分

对于小数部分\(m\)

\[ 0 \leq m = log_2{x} - n = log_2{\frac{x}{2^n}} < 1 \tag{1.2} \]

其中,\(n\)为上文算出的msb,即整数部分。

我们先将\(\frac{x}{2^n}\)看做一个整体\(r\),则:

\[ 0 \leq log_2{r} < 1 \]

\[ 1 \leq r = \frac{x}{2^n} < 2 \]

这里我们希望求出\(log_2{r}\),如果能够将\(log_2{r}\)表示成一个不断收敛的数列,当小数位足够多时,就可以近似求出\(log_2{r}\)的值。

根据对数公式,我们可以推导以下两个等式:

\[ log_2{r} = \frac{2 \cdot log_2{r}}{2} = \frac{log_2{r^2}}{2} \tag{1.3} \]

\[ log_2{r} = log_2{2 \cdot \frac{r}{2}} = 1 + log_2{\frac{r}{2}} \tag{1.4} \]

我们循环套用上述两个公式,可以整理以下方法:

  1. 因为初始时 \(log_2{r} < 1\),因此先应用公式1.3,将问题转化为求\(log_2{r^2}\),注意此时基数为\(\frac{1}{2}\)
    • 事实上,每一次进入步骤1,新的基数都是上一次基数的\(\frac{1}{2}\),比如第二次进入步骤1的基数为\(\frac{1}{4}\),以此类推。
  2. 如果\(r^2\) >= 2,则应用公式1.4,分离出1,并将问题转化为求\(log_2{\frac{r^2}{2}}\)
    • 因为公式1.4是在公式1.3之后判断,因此这里的1需要乘以上一次步骤1的基数,如果是第一次则记录\(\frac{1}{2}\),第二次则记录\(\frac{1}{4}\),以此类推;
    • 因为\(1 \leq r < 2\),且\(2 \leq r^2 < 4\),因此\(1 \leq \frac{r^2}{2} < 2\),将\(\frac{r^2}{2}\)看做一个整体\(r\),又回到步骤1求解\(log_2{r}\),并且\(1 \leq r < 2\)
  3. 如果\(r^2 < 2\),则回到步骤1继续。

可以将上述步骤总结为以下公式:

\[ log_2{r} = m_1 \cdot \frac{1}{2} + m_2 \cdot \frac{1}{4} + ... + m_n \cdot \frac{1}{2^n} = \sum^{\infty}_{i=1}(m_i \cdot \frac{1}{2^i}) \tag{1.5} \]

其中,\(\forall m_i \in \{0, 1\}\)

这其实就是小数的二进制表示法,小数的二进制第一位表示为\(2^{-1}\),第二位为\(2^{-2}\),以此类推。而在我们上述计算\(log_2{r}\)的步骤中,如果进入步骤2,则相当于标记该位为1;如果进入步骤3,则相当于标记该位为0。

重复以上步骤的过程,即为确认小数部分二进制位从高位到低位(从左到右)每一位的值,每一个循环确认一位。循环次数越多,计算得出的\(log_2{r}\)精度越高。

我们继续看Uniswap v3中计算小数部分的代码:

        if (msb >= 128) r = ratio >> (msb - 127);
        else r = ratio << (127 - msb);

这里msb即为整数部分\(n\)。因为ratio是Q128.128,如果msb >= 128则表示ratio >= 1,因此需要右移整数位数得到小数部分ratio >> msb-127表示左移127位,使用Q129.127表示小数部分;同样,如果msb < 128,则表示ratio < 1,其本身就只有小数部分,因此通过左移127 - msb位,将小数部分凑齐127位,也用Q129.127表示小数部分。

实际上,ratio >> msb即为公式1.2中的\(\frac{x}{2^n}\),也就是步骤1中的\(r\),在后续迭代算法(步骤1-3)中需要用到。

    int256 log_2 = (int256(msb) - 128) << 64;

因为msb是基于Q128.128的ratio计算的,int256(msb) - 128表示\(n\)的真正值。<< 64使用Q192.64表示\(n\) 这一行代码实际上是使用Q192.64保存整数部分的值。

下面代码循环计算二进制表示的小数部分的前14位小数:

    assembly {
        // 根据步骤1,计算r^2,右移127位是因为两个r都是Q129.127
        r := shr(127, mul(r, r))
        // 因为1 <= r^2 < 4,仅需2位表示r^2的整数,
        // 因此从右往左数第129和128位表示r^2的整数部分,
        // 右移128位,仅剩129位,
        // 该值为1,则表示r >= 2;该值为0,则表示r < 2
        let f := shr(128, r)
        // 如果f == 1,则log_2 += Q192.64的1/2
        log_2 := or(log_2, shl(63, f))
        // 根据步骤2(即公式1.4),如果r >= 2(即f == 1),则r /= 2;否则不操作,即步骤3
        r := shr(f, r)
    }
    // 重复进行上述过程
    assembly {
        r := shr(127, mul(r, r))
        let f := shr(128, r)
        log_2 := or(log_2, shl(62, f))
        r := shr(f, r)
    }
    assembly {
        r := shr(127, mul(r, r))
        let f := shr(128, r)
        log_2 := or(log_2, shl(61, f))
        r := shr(f, r)
    }
    assembly {
        r := shr(127, mul(r, r))
        let f := shr(128, r)
        log_2 := or(log_2, shl(60, f))
        r := shr(f, r)
    }
    assembly {
        r := shr(127, mul(r, r))
        let f := shr(128, r)
        log_2 := or(log_2, shl(59, f))
        r := shr(f, r)
    }
    assembly {
        r := shr(127, mul(r, r))
        let f := shr(128, r)
        log_2 := or(log_2, shl(58, f))
        r := shr(f, r)
    }
    assembly {
        r := shr(127, mul(r, r))
        let f := shr(128, r)
        log_2 := or(log_2, shl(57, f))
        r := shr(f, r)
    }
    assembly {
        r := shr(127, mul(r, r))
        let f := shr(128, r)
        log_2 := or(log_2, shl(56, f))
        r := shr(f, r)
    }
    assembly {
        r := shr(127, mul(r, r))
        let f := shr(128, r)
        log_2 := or(log_2, shl(55, f))
        r := shr(f, r)
    }
    assembly {
        r := shr(127, mul(r, r))
        let f := shr(128, r)
        log_2 := or(log_2, shl(54, f))
        r := shr(f, r)
    }
    assembly {
        r := shr(127, mul(r, r))
        let f := shr(128, r)
        log_2 := or(log_2, shl(53, f))
        r := shr(f, r)
    }
    assembly {
        r := shr(127, mul(r, r))
        let f := shr(128, r)
        log_2 := or(log_2, shl(52, f))
        r := shr(f, r)
    }
    assembly {
        r := shr(127, mul(r, r))
        let f := shr(128, r)
        log_2 := or(log_2, shl(51, f))
        r := shr(f, r)
    }
    assembly {
        r := shr(127, mul(r, r))
        let f := shr(128, r)
        log_2 := or(log_2, shl(50, f))
    }

上述计算的log_2即为Q192.64表示的\(log_2{\sqrt{P}}\),精度为\(2^{-14}\)

    int256 log_sqrt10001 = log_2 * 255738958999603826347141; // 128.128 number

因为:

\[ \log_{\sqrt{1.0001}} \sqrt{P} = \frac{log_2{\sqrt{P}}}{log_2{\sqrt{1.0001}}} = log_2{\sqrt{P}} \cdot log_{\sqrt{1.0001}}{2} \]

这里255738958999603826347141即为\(log_{\sqrt{1.0001}}{2} \cdot 2^{64}\),两个Q192.64乘以的结果为Q128.64(不会发生溢出)。

由于这里算出的\(log_2{\sqrt{P}}\)精度为\(2^{-14}\),乘以255738958999603826347141后误差进一步放大,因此需要修正并确保结果是最接近给定价格的tick。

    int24 tickLow = int24((log_sqrt10001 - 3402992956809132418596140100660247210) >> 128);
    int24 tickHi = int24((log_sqrt10001 + 291339464771989622907027621153398088495) >> 128);

    tick = tickLow == tickHi ? tickLow : getSqrtRatioAtTick(tickHi) <= sqrtPriceX96 ? tickHi : tickLow;

其中,3402992956809132418596140100660247210表示0.01000049749154292 << 128291339464771989622907027621153398088495表示0.8561697375276566 << 128

参考abdk的这篇文章,当精度为\(2^{-14}\)时,tick的最小误差为\(− 0.85617\),最大误差为\(0.0100005\)。同时,这篇文章也从理论上证明了,只有当精度等于(或高于)\(2^{-14}\)时,只需要一次计算即可得出所需的tick值。

我们的目的是寻找满足当前条件的最大tick,使得tick对应的\(\sqrt{P}\)小于等于传入的值。因此如果补偿后的tickHi满足要求,则优先使用tickHi;否则使用tickLow。

以下是本节参考文章,有兴趣的朋友请扩展阅读:

TickBitmap.sol

TickBitmap使用Bitmap(位图)保存Tick的初始化状态,提供以下几个方法:

position

/// @notice Computes the position in the mapping where the initialized bit for a tick lives
/// @param tick The tick for which to compute the position
/// @return wordPos The key in the mapping containing the word in which the bit is stored
/// @return bitPos The bit position in the word where the flag is stored
function position(int24 tick) private pure returns (int16 wordPos, uint8 bitPos) {
    wordPos = int16(tick >> 8);
    bitPos = uint8(tick % 256);
}

只有能被tickSpacing整除的tick才能记录在位图中,因此此处的参数:\(tick = \frac{tick}{tickSpacing}\)

tick类型为int24,其二进制从右到左,从低位到高位,前8位表示bitPos,后16位表示wordPos,如下图所示:

\[ \overbrace{23,...,8}^{wordPos},\overbrace{7,...,0}^{bitPos} \]

tick的bitmap表示为:self[wordPos] ^= 1 << bitPos

flipTick

tick翻转初始化状态时,如果其位图的值为0,则需要修改为1;否则,修改为0;即对该位“取反”。

/// @notice Flips the initialized state for a given tick from false to true, or vice versa
/// @param self The mapping in which to flip the tick
/// @param tick The tick to flip
/// @param tickSpacing The spacing between usable ticks
function flipTick(
    mapping(int16 => uint256) storage self,
    int24 tick,
    int24 tickSpacing
) internal {
    require(tick % tickSpacing == 0); // ensure that the tick is spaced
    (int16 wordPos, uint8 bitPos) = position(tick / tickSpacing);
    uint256 mask = 1 << bitPos;
    self[wordPos] ^= mask;
}

首先获取tick对应的wordPosbitPos,由于最后针对bitPos执行按位“异或”操作:

因为1与任何值b(0或1)异或等于~b;0与任何值b(0或1)异或等于b。

  • 对于tick对应的位(bit),mask为1
    • 如果旧值为1,1^1=0,因此tick状态由“初始化”变成“未初始化”;
    • 如果旧值为0,0^1=1,因此tick状态由“未初始化”变成“初始化”
  • 对于非tick对应的位,mask为0
    • 如果旧值为1,1^0=1,因此状态不变
    • 如果旧值为0,0^0=0,因此状态不变

所以,上述代码实现了tick位取反的效果。

nextInitializedTickWithinOneWord

根据参数tick,寻找位图上最近一个已初始化的tick,如未找到,返回本组最后一个未初始化的tick。

/// @notice Returns the next initialized tick contained in the same word (or adjacent word) as the tick that is either
/// to the left (less than or equal to) or right (greater than) of the given tick
/// @param self The mapping in which to compute the next initialized tick
/// @param tick The starting tick
/// @param tickSpacing The spacing between usable ticks
/// @param lte Whether to search for the next initialized tick to the left (less than or equal to the starting tick)
/// @return next The next initialized or uninitialized tick up to 256 ticks away from the current tick
/// @return initialized Whether the next tick is initialized, as the function only searches within up to 256 ticks
function nextInitializedTickWithinOneWord(
    mapping(int16 => uint256) storage self,
    int24 tick,
    int24 tickSpacing,
    bool lte
) internal view returns (int24 next, bool initialized) {
    int24 compressed = tick / tickSpacing;
    if (tick < 0 && tick % tickSpacing != 0) compressed--; // round towards negative infinity

    if (lte) {
        (int16 wordPos, uint8 bitPos) = position(compressed);
        // all the 1s at or to the right of the current bitPos
        uint256 mask = (1 << bitPos) - 1 + (1 << bitPos);
        uint256 masked = self[wordPos] & mask;

        // if there are no initialized ticks to the right of or at the current tick, return rightmost in the word
        initialized = masked != 0;
        // overflow/underflow is possible, but prevented externally by limiting both tickSpacing and tick
        next = initialized
            ? (compressed - int24(bitPos - BitMath.mostSignificantBit(masked))) * tickSpacing
            : (compressed - int24(bitPos)) * tickSpacing;
    } else {
        // start from the word of the next tick, since the current tick state doesn't matter
        (int16 wordPos, uint8 bitPos) = position(compressed + 1);
        // all the 1s at or to the left of the bitPos
        uint256 mask = ~((1 << bitPos) - 1);
        uint256 masked = self[wordPos] & mask;

        // if there are no initialized ticks to the left of the current tick, return leftmost in the word
        initialized = masked != 0;
        // overflow/underflow is possible, but prevented externally by limiting both tickSpacing and tick
        next = initialized
            ? (compressed + 1 + int24(BitMath.leastSignificantBit(masked) - bitPos)) * tickSpacing
            : (compressed + 1 + int24(type(uint8).max - bitPos)) * tickSpacing;
    }
}

如果lte == true,即寻找小于等于当前tick的值,因此问题转化为:寻找低比特位上是否有1

if (lte) {
    (int16 wordPos, uint8 bitPos) = position(compressed);
    // all the 1s at or to the right of the current bitPos
    uint256 mask = (1 << bitPos) - 1 + (1 << bitPos);
    uint256 masked = self[wordPos] & mask;
}

其中,mask的值为所有小于等于bitPos的位全部置1,比如bitPos = 7,则mask二进制 = 1111111;masked保留位图中小于等于bitPos的位值,比如self[wordPos] = 110101011,则masked = 110101011 & 1111111 = 000101011

如果masked != 0,则表示当前方向(lte)该wordPos上有初始化的tick;否则,则表示该方向都是未初始化的tick。

// if there are no initialized ticks to the right of or at the current tick, return rightmost in the word
initialized = masked != 0;

如果存在已初始化的tick,则需要定位到masked中最高位的1;如果不存在,则返回当前方向最后一个tick

// overflow/underflow is possible, but prevented externally by limiting both tickSpacing and tick
next = initialized
    ? (compressed - int24(bitPos - BitMath.mostSignificantBit(masked))) * tickSpacing
    : (compressed - int24(bitPos)) * tickSpacing;

BitMath.mostSignificantBit(masked)通过二分查找法找到masked最高位的1,关于该算法的具体说明,可参考本文TickMath对数计算部分。简单而言,mostSignificantBit(masked)会返回一个数n,使得:

\[ 2^n \leq masked < 2^{n+1} \]

比如,如果masked = 000101011mostSignificantBit将返回5,因为最高位的1在(从0开始)第5位:000101011。

compressed - int24(bitPos)表示当前wordPos第一个tickcompressed - int24(bitPos - BitMath.mostSignificantBit(masked))表示该最高位bitPos对应的tick* tickSpacing将恢复到原始tick值,因为保存位图时,tick需要先除以tickSpacing

同样,如果向大于当前tick方向寻找第一个已初始化的tick时,方法和上述类似,只是mostSignificantBit需要换成leastSignificantBit,即从tick比特位开始(不含),往高位寻找第一个bitPos位为1的tick

else {
    // start from the word of the next tick, since the current tick state doesn't matter
    (int16 wordPos, uint8 bitPos) = position(compressed + 1);
    // all the 1s at or to the left of the bitPos
    uint256 mask = ~((1 << bitPos) - 1);
    uint256 masked = self[wordPos] & mask;

    // if there are no initialized ticks to the left of the current tick, return leftmost in the word
    initialized = masked != 0;
    // overflow/underflow is possible, but prevented externally by limiting both tickSpacing and tick
    next = initialized
        ? (compressed + 1 + int24(BitMath.leastSignificantBit(masked) - bitPos)) * tickSpacing
        : (compressed + 1 + int24(type(uint8).max - bitPos)) * tickSpacing;
}

Position.sol

Position.sol管理头寸相关信息,包括以下方法:

  • get:获取头寸对象
  • update:更新头寸

一个头寸(Position)可由“所有者”owner、“区间低点”tickLower和“区间高点”tickUpper唯一确定,对于同一个交易对池子,每个用户可以创建多个不同价格区间的头寸,但只能创建一个相同价格区间的头寸;不同用户可以创建相同价格区间的头寸。

get

根据ownertickLowertickUpper返回一个头寸对象:

/// @notice Returns the Info struct of a position, given an owner and position boundaries
/// @param self The mapping containing all user positions
/// @param owner The address of the position owner
/// @param tickLower The lower tick boundary of the position
/// @param tickUpper The upper tick boundary of the position
/// @return position The position info struct of the given owners' position
function get(
    mapping(bytes32 => Info) storage self,
    address owner,
    int24 tickLower,
    int24 tickUpper
) internal view returns (Position.Info storage position) {
    position = self[keccak256(abi.encodePacked(owner, tickLower, tickUpper))];
}

update

更新头寸的流动性和可取回代币,注意,该方法只会在mintburn时被触发,swap并不会更新头寸信息。

/// @notice Credits accumulated fees to a user's position
/// @param self The individual position to update
/// @param liquidityDelta The change in pool liquidity as a result of the position update
/// @param feeGrowthInside0X128 The all-time fee growth in token0, per unit of liquidity, inside the position's tick boundaries
/// @param feeGrowthInside1X128 The all-time fee growth in token1, per unit of liquidity, inside the position's tick boundaries
function update(
    Info storage self,
    int128 liquidityDelta,
    uint256 feeGrowthInside0X128,
    uint256 feeGrowthInside1X128
) internal {
    Info memory _self = self;

    uint128 liquidityNext;
    if (liquidityDelta == 0) {
        require(_self.liquidity > 0, 'NP'); // disallow pokes for 0 liquidity positions
        liquidityNext = _self.liquidity;
    } else {
        liquidityNext = LiquidityMath.addDelta(_self.liquidity, liquidityDelta);
    }

更新流动性:

  • 如果是mint,则liquidityDelta > 0
  • 如果是burn,则liquidityDelta < 0
    // calculate accumulated fees
    uint128 tokensOwed0 =
        uint128(
            FullMath.mulDiv(
                feeGrowthInside0X128 - _self.feeGrowthInside0LastX128,
                _self.liquidity,
                FixedPoint128.Q128
            )
        );
    uint128 tokensOwed1 =
        uint128(
            FullMath.mulDiv(
                feeGrowthInside1X128 - _self.feeGrowthInside1LastX128,
                _self.liquidity,
                FixedPoint128.Q128
            )
        );

根据头寸区间自上一次更新后的每流动性手续费增长值,分别计算token0token1的应收手续费。

    // update the position
    if (liquidityDelta != 0) self.liquidity = liquidityNext;
    self.feeGrowthInside0LastX128 = feeGrowthInside0X128;
    self.feeGrowthInside1LastX128 = feeGrowthInside1X128;
    if (tokensOwed0 > 0 || tokensOwed1 > 0) {
        // overflow is acceptable, have to withdraw before you hit type(uint128).max fees
        self.tokensOwed0 += tokensOwed0;
        self.tokensOwed1 += tokensOwed1;
    }
}

更新头寸流动性、本次每流动性手续费和可取回代币数。

SwapMath.sol

computeSwapStep

SwapMath只有一个方法,即computeSwapStep,计算单步交换的输入输出。

参数如下:

  • sqrtRatioCurrentX96:当前价格
  • sqrtRatioTargetX96:目标价格
  • liquidity:可用流动性
  • amountRemaining:剩余输入代币
  • feePips:手续费

返回值:

  • sqrtRatioNextX96:交换后价格
  • amountIn:本次交换消耗的输入代币
  • amountOut:本次交换消耗的输出代币
  • feeAmount:交易手续费(含协议手续费)
function computeSwapStep(
    uint160 sqrtRatioCurrentX96,
    uint160 sqrtRatioTargetX96,
    uint128 liquidity,
    int256 amountRemaining,
    uint24 feePips
)
    internal
    pure
    returns (
        uint160 sqrtRatioNextX96,
        uint256 amountIn,
        uint256 amountOut,
        uint256 feeAmount
    )
{
    bool zeroForOne = sqrtRatioCurrentX96 >= sqrtRatioTargetX96;
    bool exactIn = amountRemaining >= 0;

我们在swap章节提到,对于一个交换操作,根据zeroForOneexactIn的值,有四种组合:

zeroForOne exactInput swap
true true 输入固定数量token0,输出最大数量token1
true false 输入最小数量token0,输出固定数量token1
false true 输入固定数量token1,输出最大数量token0
false false 输入最小数量token1,输出固定数量token0
if (exactIn) {
    uint256 amountRemainingLessFee = FullMath.mulDiv(uint256(amountRemaining), 1e6 - feePips, 1e6);
    amountIn = zeroForOne
        ? SqrtPriceMath.getAmount0Delta(sqrtRatioTargetX96, sqrtRatioCurrentX96, liquidity, true)
        : SqrtPriceMath.getAmount1Delta(sqrtRatioCurrentX96, sqrtRatioTargetX96, liquidity, true);
    if (amountRemainingLessFee >= amountIn) sqrtRatioNextX96 = sqrtRatioTargetX96;
    else
        sqrtRatioNextX96 = SqrtPriceMath.getNextSqrtPriceFromInput(
            sqrtRatioCurrentX96,
            liquidity,
            amountRemainingLessFee,
            zeroForOne
        );
}

如果是输入固定数量,则交易手续费需要从输入代币中扣除。

因为feePips的单位是百分之一基点,即\(\frac{1}{10^6}\),因此,按照如下公式扣除手续费:

\[ amountRemaining \cdot (1 - \frac{feePips}{10^6}) \]

使用getAmount0DeltagetAmount1Delta,根据当前价格、目标价格和可用流动性计算所需的输入代币数量amountIn

  • 如果从token0交换token1,则输入代币为token0,因此计算amount0
  • 如果从token1交换token0,则输入代币为token1,因此计算amount1

注意,方法的最后一个参数roundUp,当计算amountIn时需要设置roundUp = true,也就是合约只能多收钱,而不能少收,否则合约资金将出现损失;同样,当计算amountOut时设置roundUp = false,也就是合约可以少付钱,而不能多付。

  • 如果可用代币数量大于amountIn,则表示该步交易可以完成,因此交换后的价格等于目标价格
  • 否则,根据当前价格,可用流动性和可用代币amountRemainingLessFee,计算交换后价格,我们在SqrtPriceMath.getNextSqrtPriceFromInput会具体介绍计算方法
else {
    amountOut = zeroForOne
        ? SqrtPriceMath.getAmount1Delta(sqrtRatioTargetX96, sqrtRatioCurrentX96, liquidity, false)
        : SqrtPriceMath.getAmount0Delta(sqrtRatioCurrentX96, sqrtRatioTargetX96, liquidity, false);
    if (uint256(-amountRemaining) >= amountOut) sqrtRatioNextX96 = sqrtRatioTargetX96;
    else
        sqrtRatioNextX96 = SqrtPriceMath.getNextSqrtPriceFromOutput(
            sqrtRatioCurrentX96,
            liquidity,
            uint256(-amountRemaining),
            zeroForOne
        );
}

如果不是输入固定数量,也就是输出固定数量,需要将amountRemaining作为输出代币数量使用:

  • 如果从token0交换token1,则输出代币为token1,因此计算amount1
  • 如果从token1交换token0,则输出代币为token0,因此计算amount0

首先根据当前价格、目标价格和可用流动性,计算可产生的输出代币数量amountOut,注意,这里与上面相反,如果是token0交换token1,需要计算token1的数量,需使用SqrtPriceMath.getAmount1Delta方法。

当表示固定输出代币时,传入的参数amountRemaining是负值:

  • 如果amountOut的绝对值小于应输出代币数量,则表示可完全交换,交换后价格等于目标价格
  • 否则,根据可用输出amountRemaining,计算交换后价格
    bool max = sqrtRatioTargetX96 == sqrtRatioNextX96;

如果交换后价格等于目标价格,则表示完全交换。

// get the input/output amounts
if (zeroForOne) {
    amountIn = max && exactIn
        ? amountIn
        : SqrtPriceMath.getAmount0Delta(sqrtRatioNextX96, sqrtRatioCurrentX96, liquidity, true);
    amountOut = max && !exactIn
        ? amountOut
        : SqrtPriceMath.getAmount1Delta(sqrtRatioNextX96, sqrtRatioCurrentX96, liquidity, false);
} else {
    amountIn = max && exactIn
        ? amountIn
        : SqrtPriceMath.getAmount1Delta(sqrtRatioCurrentX96, sqrtRatioNextX96, liquidity, true);
    amountOut = max && !exactIn
        ? amountOut
        : SqrtPriceMath.getAmount0Delta(sqrtRatioCurrentX96, sqrtRatioNextX96, liquidity, false);
}

计算本次交换所需输入amountIn和所得输出amountOut

  • 如果从token0交换token1
    • 所需输入amountIn
      • 如果完全交换,并且固定输入,则所需输入即为amountIn
      • 否则(非完全交换,或者固定输出),则使用getAmount0Delta,根据价格区间和流动性计算所需输入
    • 所得输出amountOut
      • 如果完全交换,并且固定输出,则所得输出即为amountOut
      • 否则(非完全交换,或者固定输入),则使用getAmount1Delta,根据价格区间和流动性计算所得输出
  • 如果从token1交换token1
    • 所需输入amountIn
      • 如果完全交换,并且固定输入,则所需输入即为amountIn
      • 否则(非完全交换,或者固定输出),则使用getAmount1Delta,根据价格区间和流动性计算所需输入
    • 所得输出amountOut
      • 如果完全交换,并且固定输出,则所得输出即为amountOut
      • 否则(非完全交换,或者固定输入),则使用getAmount0Delta,根据价格区间和流动性计算所得输出
// cap the output amount to not exceed the remaining output amount
if (!exactIn && amountOut > uint256(-amountRemaining)) {
    amountOut = uint256(-amountRemaining);
}

确认所得输出没有超过指定输出。

if (exactIn && sqrtRatioNextX96 != sqrtRatioTargetX96) {
    // we didn't reach the target, so take the remainder of the maximum input as fee
    feeAmount = uint256(amountRemaining) - amountIn;
} else {
    feeAmount = FullMath.mulDivRoundingUp(amountIn, feePips, 1e6 - feePips);
}

计算交易手续费(包括协议手续费):

  • 如果固定输入,并且没有达到目标价格,相当于是最后一次交换,则原始输入代币amountRemaining扣除本次交换所需输入amountIn,即为手续费;
  • 否则,需要根据amountIn计算手续费;注意,除了上述最后一次交换外,这里的amountInamountOut都是不包括手续费的,因此计算手续费需要除以1e6 - feePips,而非1e6

SqrtPriceMath.sol

getNextSqrtPriceFromAmount0RoundingUp

根据当前价格、liquidity\(\Delta{x}\),计算目标价格。

根据白皮书公式6.16:

\[ \Delta{x} = \Delta{\frac{1}{\sqrt{P}}} \cdot L \]

假设\(\sqrt{P_a} > \sqrt{P_b}\),则\(x_a < x_b\)

\[ \Delta{x} = x_b - x_a \]

如果已知\(\sqrt{P_a}\)计算\(\sqrt{P_b}\),则:

\[ \frac{1}{\sqrt{P_b}} = \frac{\Delta{x}}{L} + \frac{1}{\sqrt{P_a}} = \frac{1}{L} \cdot (\Delta{x} + \frac{L}{\sqrt{P_a}}) \tag{1.1} \]

\[ \sqrt{P_b} = \frac{L}{\Delta{x} + \frac{L}{\sqrt{P_a}}} \tag{1.2} \]

\[ {\sqrt{P_b}} = \frac{L \cdot \sqrt{P_a}}{L + \Delta{x} \cdot \sqrt{P_a}} \tag{1.3} \]

如果已知\(\sqrt{P_b}\)计算\(\sqrt{P_a}\),则:

\[ \frac{1}{\sqrt{P_a}} = \frac{1}{\sqrt{P_b}} - \frac{\Delta{x}}{L} \tag{1.4} \]

\[ {\sqrt{P_a}} = \frac{L \cdot \sqrt{P_b}}{L - \Delta{x} \cdot \sqrt{P_b}} \tag{1.5} \]

/// @notice Gets the next sqrt price given a delta of token0
/// @dev Always rounds up, because in the exact output case (increasing price) we need to move the price at least
/// far enough to get the desired output amount, and in the exact input case (decreasing price) we need to move the
/// price less in order to not send too much output.
/// The most precise formula for this is liquidity * sqrtPX96 / (liquidity +- amount * sqrtPX96),
/// if this is impossible because of overflow, we calculate liquidity / (liquidity / sqrtPX96 +- amount).
/// @param sqrtPX96 The starting price, i.e. before accounting for the token0 delta
/// @param liquidity The amount of usable liquidity
/// @param amount How much of token0 to add or remove from virtual reserves
/// @param add Whether to add or remove the amount of token0
/// @return The price after adding or removing amount, depending on add
function getNextSqrtPriceFromAmount0RoundingUp(
    uint160 sqrtPX96,
    uint128 liquidity,
    uint256 amount,
    bool add
) internal pure returns (uint160) {
    // we short circuit amount == 0 because the result is otherwise not guaranteed to equal the input price
    if (amount == 0) return sqrtPX96;
    uint256 numerator1 = uint256(liquidity) << FixedPoint96.RESOLUTION;

    if (add) {
        uint256 product;
        if ((product = amount * sqrtPX96) / amount == sqrtPX96) {
            uint256 denominator = numerator1 + product;
            if (denominator >= numerator1)
                // always fits in 160 bits
                return uint160(FullMath.mulDivRoundingUp(numerator1, sqrtPX96, denominator));
        }

        return uint160(UnsafeMath.divRoundingUp(numerator1, (numerator1 / sqrtPX96).add(amount)));
    } else {
        uint256 product;
        // if the product overflows, we know the denominator underflows
        // in addition, we must check that the denominator does not underflow
        require((product = amount * sqrtPX96) / amount == sqrtPX96 && numerator1 > product);
        uint256 denominator = numerator1 - product;
        return FullMath.mulDivRoundingUp(numerator1, sqrtPX96, denominator).toUint160();
    }
}
  • add = true时,即已知\(\sqrt{P_a}\)计算\(\sqrt{P_b}\)
    • 如果\(\Delta{x} \cdot \sqrt{P_a}\)没有发生溢出,则使用公式1.3计算\(\sqrt{P_b}\),因为这种方式精度最高
    • 否则,使用公式1.2计算\(\sqrt{P_b}\)
  • add = false时,即已知\(\sqrt{P_b}\),根据上述公式1.5计算\(\sqrt{P_a}\)

getNextSqrtPriceFromAmount1RoundingDown

根据当前价格、liquidity\(\Delta{y}\),计算目标价格。

根据白皮书公式6.13:

\[ \Delta{\sqrt{P}} = \frac{\Delta{y}}{L} \tag{6.13} \]

假设\(\sqrt{P_a} > \sqrt{P_b}\),则\(y_a > y_b\)

\[ \Delta{y} = y_a - y_b \]

如果已知\(\sqrt{P_b}\)计算\(\sqrt{P_a}\),则:

\[ \sqrt{P_a} = \sqrt{P_b} + \frac{\Delta{y}}{L} \tag{1.6} \]

如果已知\(\sqrt{P_a}\)计算\(\sqrt{P_b}\),则:

\[ \sqrt{P_b} = \sqrt{P_a} - \frac{\Delta{y}}{L} \tag{1.7} \]

/// @notice Gets the next sqrt price given a delta of token1
/// @dev Always rounds down, because in the exact output case (decreasing price) we need to move the price at least
/// far enough to get the desired output amount, and in the exact input case (increasing price) we need to move the
/// price less in order to not send too much output.
/// The formula we compute is within <1 wei of the lossless version: sqrtPX96 +- amount / liquidity
/// @param sqrtPX96 The starting price, i.e., before accounting for the token1 delta
/// @param liquidity The amount of usable liquidity
/// @param amount How much of token1 to add, or remove, from virtual reserves
/// @param add Whether to add, or remove, the amount of token1
/// @return The price after adding or removing `amount`
function getNextSqrtPriceFromAmount1RoundingDown(
    uint160 sqrtPX96,
    uint128 liquidity,
    uint256 amount,
    bool add
) internal pure returns (uint160) {
    // if we're adding (subtracting), rounding down requires rounding the quotient down (up)
    // in both cases, avoid a mulDiv for most inputs
    if (add) {
        uint256 quotient =
            (
                amount <= type(uint160).max
                    ? (amount << FixedPoint96.RESOLUTION) / liquidity
                    : FullMath.mulDiv(amount, FixedPoint96.Q96, liquidity)
            );

        return uint256(sqrtPX96).add(quotient).toUint160();
    } else {
        uint256 quotient =
            (
                amount <= type(uint160).max
                    ? UnsafeMath.divRoundingUp(amount << FixedPoint96.RESOLUTION, liquidity)
                    : FullMath.mulDivRoundingUp(amount, FixedPoint96.Q96, liquidity)
            );

        require(sqrtPX96 > quotient);
        // always fits 160 bits
        return uint160(sqrtPX96 - quotient);
    }
}
  • add = true时,即按照公式1.6,根据\(\sqrt{P_b}\)计算\(\sqrt{P_a}\)
  • add = false时,按照公式1.7,根据\(\sqrt{P_a}\)计算\(\sqrt{P_b}\)

getNextSqrtPriceFromInput

根据输入代币计算下一个价格,即添加amountIn数量的代币后的价格:

/// @notice Gets the next sqrt price given an input amount of token0 or token1
/// @dev Throws if price or liquidity are 0, or if the next price is out of bounds
/// @param sqrtPX96 The starting price, i.e., before accounting for the input amount
/// @param liquidity The amount of usable liquidity
/// @param amountIn How much of token0, or token1, is being swapped in
/// @param zeroForOne Whether the amount in is token0 or token1
/// @return sqrtQX96 The price after adding the input amount to token0 or token1
function getNextSqrtPriceFromInput(
    uint160 sqrtPX96,
    uint128 liquidity,
    uint256 amountIn,
    bool zeroForOne
) internal pure returns (uint160 sqrtQX96) {
    require(sqrtPX96 > 0);
    require(liquidity > 0);

    // round to make sure that we don't pass the target price
    return
        zeroForOne
            ? getNextSqrtPriceFromAmount0RoundingUp(sqrtPX96, liquidity, amountIn, true)
            : getNextSqrtPriceFromAmount1RoundingDown(sqrtPX96, liquidity, amountIn, true);
}

getNextSqrtPriceFromOutput

根据输出代币计算下一个价格,即移除amountOut数量的代币后的价格:

/// @notice Gets the next sqrt price given an output amount of token0 or token1
/// @dev Throws if price or liquidity are 0 or the next price is out of bounds
/// @param sqrtPX96 The starting price before accounting for the output amount
/// @param liquidity The amount of usable liquidity
/// @param amountOut How much of token0, or token1, is being swapped out
/// @param zeroForOne Whether the amount out is token0 or token1
/// @return sqrtQX96 The price after removing the output amount of token0 or token1
function getNextSqrtPriceFromOutput(
    uint160 sqrtPX96,
    uint128 liquidity,
    uint256 amountOut,
    bool zeroForOne
) internal pure returns (uint160 sqrtQX96) {
    require(sqrtPX96 > 0);
    require(liquidity > 0);

    // round to make sure that we pass the target price
    return
        zeroForOne
            ? getNextSqrtPriceFromAmount1RoundingDown(sqrtPX96, liquidity, amountOut, false)
            : getNextSqrtPriceFromAmount0RoundingUp(sqrtPX96, liquidity, amountOut, false);
}

getAmount0Delta

该方法计算白皮书中的公式6.16:

\[ \Delta{x} = \Delta{\frac{1}{\sqrt{P}}} \cdot L \]

展开公式为:

\[ amount0 = x_b - x_a = L \cdot (\frac{1}{\sqrt{P_b}} - \frac{1}{\sqrt{P_a}}) = L \cdot (\frac{\sqrt{P_a} - \sqrt{P_b}}{\sqrt{P_a} \cdot \sqrt{P_b}}) \]

/// @notice Gets the amount0 delta between two prices
/// @dev Calculates liquidity / sqrt(lower) - liquidity / sqrt(upper),
/// i.e. liquidity * (sqrt(upper) - sqrt(lower)) / (sqrt(upper) * sqrt(lower))
/// @param sqrtRatioAX96 A sqrt price
/// @param sqrtRatioBX96 Another sqrt price
/// @param liquidity The amount of usable liquidity
/// @param roundUp Whether to round the amount up or down
/// @return amount0 Amount of token0 required to cover a position of size liquidity between the two passed prices
function getAmount0Delta(
    uint160 sqrtRatioAX96,
    uint160 sqrtRatioBX96,
    uint128 liquidity,
    bool roundUp
) internal pure returns (uint256 amount0) {
    if (sqrtRatioAX96 > sqrtRatioBX96) (sqrtRatioAX96, sqrtRatioBX96) = (sqrtRatioBX96, sqrtRatioAX96);

    uint256 numerator1 = uint256(liquidity) << FixedPoint96.RESOLUTION;
    uint256 numerator2 = sqrtRatioBX96 - sqrtRatioAX96;

    require(sqrtRatioAX96 > 0);

    return
        roundUp
            ? UnsafeMath.divRoundingUp(
                FullMath.mulDivRoundingUp(numerator1, numerator2, sqrtRatioBX96),
                sqrtRatioAX96
            )
            : FullMath.mulDiv(numerator1, numerator2, sqrtRatioBX96) / sqrtRatioAX96;
}

getAmount1Delta

同样,该方法计算白皮书公式6.7:

\[ L = \frac{\Delta{Y}}{\Delta{\sqrt{P}}} \]

展开公式为:

\[ amount1 = y_b - y_a = L \cdot \Delta{\sqrt{P}} = L \cdot (\sqrt{P_b} - \sqrt{P_a}) \]

/// @notice Gets the amount1 delta between two prices
/// @dev Calculates liquidity * (sqrt(upper) - sqrt(lower))
/// @param sqrtRatioAX96 A sqrt price
/// @param sqrtRatioBX96 Another sqrt price
/// @param liquidity The amount of usable liquidity
/// @param roundUp Whether to round the amount up, or down
/// @return amount1 Amount of token1 required to cover a position of size liquidity between the two passed prices
function getAmount1Delta(
    uint160 sqrtRatioAX96,
    uint160 sqrtRatioBX96,
    uint128 liquidity,
    bool roundUp
) internal pure returns (uint256 amount1) {
    if (sqrtRatioAX96 > sqrtRatioBX96) (sqrtRatioAX96, sqrtRatioBX96) = (sqrtRatioBX96, sqrtRatioAX96);

    return
        roundUp
            ? FullMath.mulDivRoundingUp(liquidity, sqrtRatioBX96 - sqrtRatioAX96, FixedPoint96.Q96)
            : FullMath.mulDiv(liquidity, sqrtRatioBX96 - sqrtRatioAX96, FixedPoint96.Q96);

Oracle.sol

请参考:Uniswap v3 Oracle 预言机

Select a repo