UniswapV3Pool 合约,其引用的库合约就达到了 13 个,通过 using 方式使用的也达到了 9 个,如下所示:

using LowGasSafeMath for uint256;
using LowGasSafeMath for int256;
using SafeCast for uint256;
using SafeCast for int256;
using Tick for mapping(int24 => Tick.Info);
using TickBitmap for mapping(int16 => uint256);
using Position for mapping(bytes32 => Position.Info);
using Position for Position.Info;
using Oracle for Oracle.Observation[65535];

LowGasSafeMath 是用于加减乘除算法计算的,SafeCast 用于类型转换,Tick 和 TickBitmap 用于管理 tick 处理相关的操作和计算,Position 则主要用于更新流动性的头寸,Oracle 则是用于预言机计算的。

然后分析状态变量

address public immutable override factory;
address public immutable override token0;
address public immutable override token1;
uint24 public immutable override fee;
int24 public immutable override tickSpacing;      
uint128 public immutable override maxLiquidityPerTick;

struct Slot0 {
    // the current price
    uint160 sqrtPriceX96;
    // the current tick
    int24 tick;
    // the most-recently updated index of the observations array
    uint16 observationIndex;
    // the current maximum number of observations that are being stored
    uint16 observationCardinality;
    // the next maximum number of observations to store, triggered in observations.write
    uint16 observationCardinalityNext;
    // the current protocol fee as a percentage of the swap fee taken on withdrawal
    // represented as an integer denominator (1/x)%
    uint8 feeProtocol;
    // whether the pool is locked
    bool unlocked;
}
Slot0 public override slot0;

uint256 public override feeGrowthGlobal0X128;
uint256 public override feeGrowthGlobal1X128;

// accumulated protocol fees in token0/token1 units
struct ProtocolFees {
    uint128 token0;
    uint128 token1;
}
ProtocolFees public override protocolFees;

uint128 public override liquidity;

mapping(int24 => Tick.Info) public override ticks;
mapping(int16 => uint256) public override tickBitmap;
mapping(bytes32 => Position.Info) public override positions;
Oracle.Observation[65535] public override observations;


1、tickSpacing 可以理解为就是 tick 变动的最小单位;

2、maxLiquidityPerTick 表示每个 tick 能接受的最大流动性,是在构造函数中根据 tickSpacing 计算出来的

3、slot0 记录了当前的一些状态值,都封装在了结构体 Slot0 中,其共有 7 个字段。

4、sqrtPriceX96 是当前价格,记录的是根号价格,且做了扩展,准确来说:sqrtPriceX96 = (token1数量 / token0数量) ^ 0.5 * 2^96。换句话说,这个值代表的是 token0 和 token1 数量比例的平方根,经过放大以获得更高的精度。这样设计的目的是为了方便和优化合约中的一些计算。如果想从 sqrtPriceX96 得出具体的价格,还需要做一些额外的计算。tick 记录了当前价格对应的价格点。observationIndexobservationCardinality 和 observationCardinalityNext 是跟 observations 数组有关的,也是计算预言机价格时需要的,

5、feeProtocol 则用来存储协议费率,初始化时为 0,可通过 setFeeProtocol 函数来重置该值。

6、unlocked 记录池子的锁定状态,初始化时为 true,主要作为一个防止重入锁来使用。

7、feeGrowthGlobal0X128 和 feeGrowthGlobal1X128 记录两个 token 的每单位流动性所获取的手续费。

8、protocolFees 则记录了两个 token 的累计未被领取的协议手续费。

9、liquidity 记录了池子当前可用的流动性。注意,这里不是指注入池子里的所有流动性总量,而是包含了当前价格的那些有效头寸的流动性总量。

10、ticks 记录池子里每个 tick 的详细信息,key 为 tick 的序号,value 就是详细信息。

11、tickBitmap 记录已初始化的 tick 的位图。如果一个 tick 没有被用作流动性区间的边界点,即该 tick 没有被初始化,那在交易过程中可以跳过这个 tick。而为了更高效地寻找下一个已初始化的 tick,就使用了 tickBitmap 来记录已初始化的 tick。如果 tick 已被初始化,位图中对应于该 tick 序号的位置设置为 1,否则为 0。

positions 记录每个流动性头寸的详细信息,具体信息如下:

library Position {
    // 用于存储每个用户的头寸信息
    struct Info {
        // 当前头寸的总流动性
        uint128 liquidity;
        // 截止最后一次更新流动性或所欠费用时,每单位流动性的费用增长
        uint256 feeGrowthInside0LastX128;
        uint256 feeGrowthInside1LastX128;
        // 欠头寸所有者的费用
        uint128 tokensOwed0;
        uint128 tokensOwed1;
    }
    ...
}

2. 第二笔交易

swap 函数

swap 函数是实现交易的底层函数,其代码逻辑复杂很多,我们对其进行逐步拆解来看。

首先,其入参有 5 个:

function swap(
    // 收款地址
    address recipient,
    // 交易方向,true表示用token0交换token1,false则相反
    bool zeroForOne,
    // 指定的交易数额,如果是正数则为指定的输入,负数则为指定的输出
    int256 amountSpecified,
    // 限定的价格
    uint160 sqrtPriceLimitX96,
    // 传给回调函数的参数
    bytes calldata data
) external override noDelegateCall returns (int256 amount0, int256 amount1)

其中,如果 zeroForOne 为 true 的话,那交易后的价格不能小于 sqrtPriceLimitX96;如果 zeroForOne 为 false,则交易后的价格不能大于 sqrtPriceLimitX96。返回值 amount0 和 amount1 是交易后两个 token 的实际成交数额。

下面我们只摘取一些重要代码添加注解进行说明,以下是执行实际交易前的一些准备工作:

// 将状态变量保存在内存中,后续访问通过 MLOAD 完成,可以节省 gas
Slot0 memory slot0Start = slot0;
// 防止重入
slot0.unlocked = false;
// 缓存交易前的数据,以节省 gas
SwapCache memory cache =
    SwapCache({
        liquidityStart: liquidity,
        blockTimestamp: _blockTimestamp(),
        feeProtocol: zeroForOne ? (slot0Start.feeProtocol % 16) : (slot0Start.feeProtocol >> 4),
        secondsPerLiquidityCumulativeX128: 0,
        tickCumulative: 0,
        computedLatestObservation: false
    });
// 如果 amountSpecified 为正数,则指定的是确定的输入数额
bool exactInput = amountSpecified > 0;
// 缓存交易过程中需要用到的临时变量
SwapState memory state =
    SwapState({
        // 剩余可交易金额
        amountSpecifiedRemaining: amountSpecified,
        // 已交易互换的金额,指与 amountSpecifiedRemaining 互换的 token
        amountCalculated: 0,
        sqrtPriceX96: slot0Start.sqrtPriceX96,
        tick: slot0Start.tick,
        feeGrowthGlobalX128: zeroForOne ? feeGrowthGlobal0X128 : feeGrowthGlobal1X128,
        protocolFee: 0,
        liquidity: cache.liquidityStart
    });

while 循环中处理实际的交易逻辑:

// 当剩余可交易金额为零,或交易后价格达到了限定的价格之后才退出循环
while (state.amountSpecifiedRemaining != 0 && state.sqrtPriceX96 != sqrtPriceLimitX96) {
    // 缓存每一次循环的状态变量
    StepComputations memory step;
    // 交易的起始价格
    step.sqrtPriceStartX96 = state.sqrtPriceX96;
    // 通过 tick 位图找到下一个已初始化的 tick,即下一个流动性边界点
    (step.tickNext, step.initialized) = tickBitmap.nextInitializedTickWithinOneWord(
        state.tick,
        tickSpacing,
        zeroForOne
    );
    ...
    // 将上一步找到的下一个 tick 转为根号价格
    step.sqrtPriceNextX96 = TickMath.getSqrtRatioAtTick(step.tickNext);
    // 在当前价格和下一口价格之间计算交易结果,返回最新价格、消耗的 amountIn、输出的 amountOut 和手续费 feeAmount
    (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) {
        // 此时的剩余可交易金额为正数,需减去消耗的输入 amountIn 和手续费 feeAmount
        state.amountSpecifiedRemaining -= (step.amountIn + step.feeAmount).toInt256();
        // 此时该值表示 tokenOut 的累加值,结果为负数
        state.amountCalculated = state.amountCalculated.sub(step.amountOut.toInt256());
    } else {
        // 此时的剩余可交易金额为负数,需加上输出的 amountOut
        state.amountSpecifiedRemaining += step.amountOut.toInt256();
        // 此时该值表示 tokenIn 的累加值,结果为正数
        state.amountCalculated = state.amountCalculated.add((step.amountIn + step.feeAmount).toInt256());
    }
    ...
    // 如果达到了下一个价格,则需要移动 tick
    if (state.sqrtPriceX96 == step.sqrtPriceNextX96) {
        // 如果 tick 已经初始化,则需要执行 tick 的转换
        if (step.initialized) {
            ...
            // 转换到下一个 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
        state.tick = zeroForOne ? step.tickNext - 1 : step.tickNext;
    } else if (state.sqrtPriceX96 != step.sqrtPriceStartX96) {
        // 如果不需要移动 tick,则根据最新价格换算成最新的 tick
        state.tick = TickMath.getTickAtSqrtRatio(state.sqrtPriceX96);
    }
}

3. 跨tick交易

一笔交易有时候会跨越多个流动性区间,所以需要使用循环处理在每一个区间内的交易。当剩余可交易金额已经消耗完,或价格已经达到了指定的限定价格后,循环也就结束了,即交易主流程结束了。

之后就是一些交易收尾的工作了,包括更新 tick、价格、流动性、手续费增长系数等。最后很关键的一步就是做转账和支付,以下是最后的代码:

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

// 发送 Swap 事件
emit Swap(msg.sender, recipient, amount0, amount1, state.sqrtPriceX96, state.tick);
// 解除防止重入的锁
slot0.unlocked = true;

4. 多池子交易

单池和多池交易 #

在我们的现有实现中,管理合约中的 swap 函数仅支持单池交易,并且会需要池子地址作为参数:

function swap(
    address poolAddress_,
    bool zeroForOne,
    uint256 amountSpecified,
    uint160 sqrtPriceLimitX96,
    bytes calldata data
) public returns (int256, int256) { ... }