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

2023-11-07 15:54:19 浏览数 (1)

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

代码语言:javascript复制
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 用于类型转换,TickTickBitmap 用于管理 tick 处理相关的操作和计算,Position 则主要用于更新流动性的头寸,Oracle 则是用于预言机计算的。

接着,来看看定义了哪些状态变量:

代码语言:javascript复制
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;

前 5 个变量我们都已经了解过了,第 6 个变量 maxLiquidityPerTick 表示每个 tick 能接受的最大流动性,是在构造函数中根据 tickSpacing 计算出来的。

slot0 记录了当前的一些状态值,都封装在了结构体 Slot0 中,其共有 7 个字段。sqrtPriceX96 是当前价格,记录的是根号价格,且做了扩展,准确来说:sqrtPriceX96 = (token1数量 / token0数量) ^ 0.5 * 2^96。换句话说,这个值代表的是 token0 和 token1 数量比例的平方根,经过放大以获得更高的精度。这样设计的目的是为了方便和优化合约中的一些计算。如果想从 sqrtPriceX96 得出具体的价格,还需要做一些额外的计算。tick 记录了当前价格对应的价格点。observationIndexobservationCardinalityobservationCardinalityNext 是跟 observations 数组有关的,也是计算预言机价格时需要的,这在之前的文章《价格预言机的使用总结(三):UniswapV3篇》讲解 UniswapV3 预言机时已经介绍过,这里不再赘述。feeProtocol 则用来存储协议费率,初始化时为 0,可通过 setFeeProtocol 函数来重置该值。unlocked 记录池子的锁定状态,初始化时为 true,主要作为一个防止重入锁来使用。

feeGrowthGlobal0X128feeGrowthGlobal1X128 记录两个 token 的每单位流动性所获取的手续费。

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

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

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

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

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

observations 则是存储了计算预言机价格相关的累加值,包括 tick 累加值和流动性累加值。具体用法在《价格预言机的使用总结(三):UniswapV3篇》一文中已经介绍过,这里也不再赘述。

接下来就到合约函数了,UniswapV3Pool 核心的函数在 IUniswapV3PoolActions 接口里有定义,该接口共定义了 7 个函数:

  • initialize:初始化 slot0 状态
  • mint:添加流动性
  • collect:提取收益
  • burn:移除流动性
  • swap:兑换
  • flash:闪电贷
  • increaseObservationCardinalityNext:扩展 observations 数组可存储的容量

initialize 通常会在第一次添加流动性时被调用,主要会初始化 slot0 状态变量,其中 sqrtPriceX96 是直接作为入参传入的,因为第一次添加流动性时,价格其实是由 LP 自己定的。初始的 tick 则是根据 sqrtPriceX96 计算出来的。而最后一个函数increaseObservationCardinalityNext 是用于预言机的,因为默认的 observations 数组实际存储的容量只是 1,需要扩展这个容量才可计算预言机价格。

mint 函数

mint 是添加流动性的底层函数,以下是其代码实现:

代码语言:javascript复制
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);
}

其有 5 个入参:

  • recipient:流动性的接收者地址
  • tickLower:区间价格下限的 tick 序号
  • tickUpper:区间价格上限的 tick 序号
  • amount:待添加的流动性数量
  • data:传给回调函数的数据

其中,tick 的上下限和 amount 其实都是通过前端 SDK 根据用户的输入计算好对应的值,通常是通过流动性管理的入口合约 NonfungiblePositionManager 合约下传进来的。关于 NonfungiblePositionManager 合约的实现后面文章再详解。

添加流动性的主要操作其实是在 _modifyPosition 私有函数里,执行完该函数后,返回值包括了需要添加到池子里的两种 token 的具体数额 amount0amount1。之后,查询并临时记录下两种 token 在池子里的当前余额。然后,调用 msg.sender 的回调函数 uniswapV3MintCallback,在回调函数中需要完成两种 token 的支付。msg.sender 一般是 NonfungiblePositionManager 合约,所以 NonfungiblePositionManager 合约会实现该回调函数来完成支付。执行完回调函数之后,那池子里两种 token 的余额就会发生变化,判断其前后余额即可。

_modifyPosition 封装了主要的处理逻辑,其代码如下:

代码语言:javascript复制
function _modifyPosition(ModifyPositionParams memory params)
    private
    noDelegateCall
    returns (
        Position.Info storage position,
        int256 amount0,
        int256 amount1
    )
{
    // 检查Tick的上下限是否符合边界条件
    checkTicks(params.tickLower, params.tickUpper);
    // 从storage位置转存到内存中,后续访问可节省gas
    Slot0 memory _slot0 = slot0;
    // 第一步核心操作
    position = _updatePosition(
        params.owner,
        params.tickLower,
        params.tickUpper,
        params.liquidityDelta,
        _slot0.tick
    );

    if (params.liquidityDelta != 0) {
        if (_slot0.tick < params.tickLower) {
            // 当前报价低于传递的范围;流动性只能通过从左到右交叉而进入范围内,需要提供更多token0
            amount0 = SqrtPriceMath.getAmount0Delta(
                TickMath.getSqrtRatioAtTick(params.tickLower),
                TickMath.getSqrtRatioAtTick(params.tickUpper),
                params.liquidityDelta
            );
        } else if (_slot0.tick < params.tickUpper) {
            // 当前报价在传递的范围内
            uint128 liquidityBefore = liquidity;
            // 更新预言机相关状态数据
            (slot0.observationIndex, slot0.observationCardinality) = observations.write(
                _slot0.observationIndex,
                _blockTimestamp(),
                _slot0.tick,
                liquidityBefore,
                _slot0.observationCardinality,
                _slot0.observationCardinalityNext
            );
            // 计算当前价格到价格区间上限之间需支付的amount0
            amount0 = SqrtPriceMath.getAmount0Delta(
                _slot0.sqrtPriceX96,
                TickMath.getSqrtRatioAtTick(params.tickUpper),
                params.liquidityDelta
            );
            // 计算从价格区间下限到当前价格之间需支付的amount1
            amount1 = SqrtPriceMath.getAmount1Delta(
                TickMath.getSqrtRatioAtTick(params.tickLower),
                _slot0.sqrtPriceX96,
                params.liquidityDelta
            );
            // 当前有效头寸的总流动性增加
            liquidity = LiquidityMath.addDelta(liquidityBefore, params.liquidityDelta);
        } else {
            // 当前报价高于传递的范围;流动性只能通过从右到左交叉而进入范围内,需要提供更多token1
            amount1 = SqrtPriceMath.getAmount1Delta(
                TickMath.getSqrtRatioAtTick(params.tickLower),
                TickMath.getSqrtRatioAtTick(params.tickUpper),
                params.liquidityDelta
            );
        }
    }
}

其中,第一步的核心操作是调用 _updatePosition 函数,先更新头寸。之后的核心操作是计算此次调整头寸流动性时对应的 amount0 和 amount1,这需要根据三种不同情况分别计算:

  • 当前 tick 小于头寸的 tick 区间下限时,则只需要更多 token0,所以也只需要计算 amount0
  • 当前 tick 大于头寸的 tick 区间上限时,则只需要更多 token1,所以也只需要计算 amount1
  • 当前 tick 处于头寸的 tick 区间内时,分别计算 amount0 和 amount1,且池子里处于激活状态的总流动性也跟着调整

前两种状态,添加的流动性都是没有激活的,所以不需要把添加的流动性追加到当前的 liquidity 里。

下面,再来看看私有函数 _updatePosition 的代码实现逻辑,如下所示:

代码语言:javascript复制
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

    // 是否需要将tick从初始化翻转为未初始化,或者反之亦然
    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
            );
        // 更新tickLower的数据
        flippedLower = ticks.update(
            tickLower,
            tick,
            liquidityDelta,
            _feeGrowthGlobal0X128,
            _feeGrowthGlobal1X128,
            secondsPerLiquidityCumulativeX128,
            tickCumulative,
            time,
            false,
            maxLiquidityPerTick
        );
        // 更新tickUpper的数据
        flippedUpper = ticks.update(
            tickUpper,
            tick,
            liquidityDelta,
            _feeGrowthGlobal0X128,
            _feeGrowthGlobal1X128,
            secondsPerLiquidityCumulativeX128,
            tickCumulative,
            time,
            true,
            maxLiquidityPerTick
        );
        if (flippedLower) {
            // 在tick位图中翻转lower tick的状态
            tickBitmap.flipTick(tickLower, tickSpacing);
        }
        if (flippedUpper) {
            // 在tick位图中翻转upper tick的状态
            tickBitmap.flipTick(tickUpper, tickSpacing);
        }
    }
    // 计算增长的手续费
    (uint256 feeGrowthInside0X128, uint256 feeGrowthInside1X128) =
        ticks.getFeeGrowthInside(tickLower, tickUpper, tick, _feeGrowthGlobal0X128, _feeGrowthGlobal1X128);
    // 更新头寸元数据
    position.update(liquidityDelta, feeGrowthInside0X128, feeGrowthInside1X128);
    // 清理不再需要用到的tick数据
    if (liquidityDelta < 0) {
        if (flippedLower) {
            ticks.clear(tickLower);
        }
        if (flippedUpper) {
            ticks.clear(tickUpper);
        }
    }
}

我们看到有五个入参,其中,ownertickLowertickUpper 这三个组合起来的哈希值其实就是状态变量 positions 的 key。实际上,key 的计算是通过 keccak256 算法所得的:

代码语言:javascript复制
keccak256(abi.encodePacked(owner, tickLower, tickUpper))

实现代码的第一行,就是通过这三个参数得到 Position.Info 类型的 position 变量,从而得到待更新的头寸数据。另外,owner 其实是 NonfungiblePositionManager 合约。其实,对于底层 Pool 合约来说,所有的头寸 owner 都是 NonfungiblePositionManager 合约,而每个用户的头寸则是在 NonfungiblePositionManager 合约里进行区分管理的。

入参中的 liquidityDelta 是需要增加或减少的流动性,该值为正数则表示要增加流动性,负数则是要减少流动性。

入参的 tick 是当前激活的 tick,即 slot0 中保存的 tick

该内部函数的核心操作逻辑是:先分别更新 tick 的下限和上限的元数据;如果 tick 的流动性从 0 增长为非 0 状态,或从非 0 状态减少成了为 0 的状态,则需要在 tick 位图中执行翻转操作;接着更新头寸元数据,包括流动性的加减和手续费的计算;最后将已经不再需要用到的 tick 数据给清理掉。

至此,池子底层添加流动性的 mint 函数全流程就讲解完了。

burn 函数

接下来看看做移除流动性操作的 burn 函数,其实现逻辑相对简单很多,以下是其代码实现:

代码语言:javascript复制
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);
}

该函数移除的是 msg.sender 的流动性头寸。其有三个入参,tickLowertickUpper 用来指定要移动哪个头寸,amount 指定要移除的流动性数额。

mint 的时候一样,第一步核心操作也是先 _modifyPosition。不过,因为是减少流动性,所以传入的 liquidityDelta 参数转为负数。而返回的 amount0Intamount1Int 也会是负数,所以转为 uint256 类型的 amount0amount1 时,又需要加上负号将负数再转为正数。之后,将 amount0amount1 分别累加到了头寸的 tokensOwed0tokensOwed1

这时候可能有人会产生疑问,既然是移除流动性,为什么没有转账逻辑?不是应该把 amount0amount1 转回给用户吗?其实,这也是和 UniswapV2 移除流动性时不同的地方了。UniswapV3 的处理方式并不是移除流动性时直接把两种 token 资产转给用户,而是先累加到 tokensOwed0tokensOwed1,代表这是欠用户的资产,其中也包括该头寸已赚取到的手续费。之后,用户其实是要通过 collect 函数来提取 tokensOwed0tokensOwed1 里的资产。

collect 函数

collect 函数其实很简单,以下是其代码实现:

代码语言:javascript复制
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);
}

5 个入参很好理解,recipient 就是接收 token 的地址,tickLowertickUpper 指定了头寸区间,amount0Requestedamount1Requested 是用户希望提取的数额。返回值 amount0amount1 就是实际提取的数额。

实现逻辑的第一行,通过 msg.sendertickLowertickUpper 来读取出用户的头寸。接着判断用户希望提取的数额 amount0Requested 和头寸里的 tokensOwed0 哪个值小就实际提取哪个,amount1 的也同样。之后就是从头寸的 tokensOwed 里减掉提取的数额并转账给接收地址。最后发送 Collect 事件。

swap 函数

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

首先,其入参有 5 个:

代码语言:javascript复制
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)

其中,如果 zeroForOnetrue 的话,那交易后的价格不能小于 sqrtPriceLimitX96;如果 zeroForOnefalse,则交易后的价格不能大于 sqrtPriceLimitX96。返回值 amount0amount1 是交易后两个 token 的实际成交数额。

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

代码语言:javascript复制
// 将状态变量保存在内存中,后续访问通过 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 循环中处理实际的交易逻辑:

代码语言:javascript复制
// 当剩余可交易金额为零,或交易后价格达到了限定的价格之后才退出循环
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);
    }
}

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

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

代码语言:javascript复制
// 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;

先将 tokenOut 转给了用户,然后执行了回调函数 uniswapV3SwapCallback,在回调函数里会完成 tokenIn 的支付,执行完回调函数后的余额校验是为了确保回调函数确实完成了 tokenIn 的支付。因为先将 tokenOut 转给了用户,之后才完成支付,因此在回调函数中其实还可以做和 UniswapV2 一样的 flash swap。

flash 函数

flash 函数实现了闪电贷功能,与 flash swap 不同,闪电贷借什么就需要还什么。另外,UniswapV3 的闪电贷可以两种 token 都借。

flash 函数的代码实现相对比较简单,以下是其代码实现:

代码语言:javascript复制
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);
    // 记录还款前的余额
    uint256 balance0Before = balance0();
    uint256 balance1Before = balance1();
    // 将所借 token 转给用户
    if (amount0 > 0) TransferHelper.safeTransfer(token0, recipient, amount0);
    if (amount1 > 0) TransferHelper.safeTransfer(token1, recipient, amount1);
    // 调用回调函数,在该函数里需要完成还款,包括还所借 token 和支付手续费
    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');
    // 计算出实际收到的手续费
    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);
}

入参有 4 个,recipient 是接收所贷 token 的地址,amount0amount1 是所要借贷的两个 token 数量,data 是给回调函数的参数。

还款则需在 uniswapV3FlashCallback 回调函数中完成。

最终,闪电贷赚取的手续费也是分配给 LP 和协议费。

0 人点赞