Skip to content

Latest commit

 

History

History
1296 lines (1007 loc) · 46.2 KB

File metadata and controls

1296 lines (1007 loc) · 46.2 KB

English | 中文

深入理解 Uniswap v3 智能合约 (二)

tags: uniswap solidity logarithm uniswap-v3 tick periphery contract

Uniswap-v3-periphery

Uniswap-v3-core合约定义的是基础方法,而Uniswap-v3-periphery合约才是我们平常直接交互的合约。

比如,众所周知Uniswap v3头寸是一个NFT,这个NFT就是在periphery合约中创建和管理的,在core合约中并没有任何NFT的概念。

NonfungiblePositionManager.sol

头寸管理合约,全局仅有一个,负责管理所有交易对的头寸,主要包括以下几个方法:

需要特别注意,该合约继承了ERC721,可以mint NFT。因为每个Uniswap v3的头寸(由ownertickLowertickUpper确定)是唯一的,因此非常适合用NFT表示。

createAndInitializePoolIfNecessary

我们在Uniswap-v3-core中提到,一个交易对合约被创建后,需要初始化才能使用。

本方法就把这一系列操作合并成一个方法:创建并初始化交易对。

/// @inheritdoc IPoolInitializer
function createAndInitializePoolIfNecessary(
    address token0,
    address token1,
    uint24 fee,
    uint160 sqrtPriceX96
) external payable override returns (address pool) {
    require(token0 < token1);
    pool = IUniswapV3Factory(factory).getPool(token0, token1, fee);

    if (pool == address(0)) {
        pool = IUniswapV3Factory(factory).createPool(token0, token1, fee);
        IUniswapV3Pool(pool).initialize(sqrtPriceX96);
    } else {
        (uint160 sqrtPriceX96Existing, , , , , , ) = IUniswapV3Pool(pool).slot0();
        if (sqrtPriceX96Existing == 0) {
            IUniswapV3Pool(pool).initialize(sqrtPriceX96);
        }
    }
}

首先根据交易对代币(tokentoken1)和手续费fee获取pool对象:

  • 如果不存在,则调用Uniswap-v3-core工厂合约createPool创建该交易对并初始化
  • 如果已存在,则根据额slot0判断是否已经初始化(价格),如果没有则调用Uniswap-v3-core的initialize方法进行初始化。

mint

创建新头寸,方法接受的参数如下:

  • token0:代币0
  • token1:代币1
  • fee:手续费等级(需符合工厂合约中定义的手续费等级)
  • tickLower:价格区间低点
  • tickUpper:价格区间高点
  • amount0Desired:希望存入的代币0数量
  • amount1Desired:希望存入的代币1数量
  • amount0Min:最少存入的token0数量(防止被frontrun)
  • amount1Min:最少存入的token1数量(防止被frontrun)
  • recipient:头寸接收者
  • deadline:截止时间(超过该时间后请求无效)(防止重放攻击)

返回:

  • tokenId:每个头寸会分配一个唯一的tokenId,代表NFT
  • liquidity:头寸的流动性
  • amount0token0的数量
  • amount1token1的数量
/// @inheritdoc INonfungiblePositionManager
function mint(MintParams calldata params)
    external
    payable
    override
    checkDeadline(params.deadline)
    returns (
        uint256 tokenId,
        uint128 liquidity,
        uint256 amount0,
        uint256 amount1
    )
{
    IUniswapV3Pool pool;
    (liquidity, amount0, amount1, pool) = addLiquidity(
        AddLiquidityParams({
            token0: params.token0,
            token1: params.token1,
            fee: params.fee,
            recipient: address(this),
            tickLower: params.tickLower,
            tickUpper: params.tickUpper,
            amount0Desired: params.amount0Desired,
            amount1Desired: params.amount1Desired,
            amount0Min: params.amount0Min,
            amount1Min: params.amount1Min
        })
    );

首先通过addLiquidity方法完成流动性添加,获得实际得到的流动性liquidity,消耗的amount0amount1,以及交易对pool

    _mint(params.recipient, (tokenId = _nextId++));

通过ERC721合约的_mint方法,向接收者recipient铸造NFT,tokenId从1开始递增。

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

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

    _positions[tokenId] = Position({
        nonce: 0,
        operator: address(0),
        poolId: poolId,
        tickLower: params.tickLower,
        tickUpper: params.tickUpper,
        liquidity: liquidity,
        feeGrowthInside0LastX128: feeGrowthInside0LastX128,
        feeGrowthInside1LastX128: feeGrowthInside1LastX128,
        tokensOwed0: 0,
        tokensOwed1: 0
    });

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

最后,保存头寸信息到_positions中。

increaseLiquidity

为一个头寸添加流动性。需注意,可以修改头寸的代币数量,但是不能修改价格区间。

参数如下:

  • tokenId:创建头寸时返回的tokenId,即NFT的tokenId
  • amount0Desired:希望添加的token0数量
  • amount1Desired:希望添加的token1数量
  • amount0Min:最少添加的token0数量(防止被frontrun)
  • amount1Min:最少添加的token1数量(防止被frontrun)
  • deadline:截止时间(超过该时间后请求无效)(防止重放攻击)
/// @inheritdoc INonfungiblePositionManager
function increaseLiquidity(IncreaseLiquidityParams calldata params)
    external
    payable
    override
    checkDeadline(params.deadline)
    returns (
        uint128 liquidity,
        uint256 amount0,
        uint256 amount1
    )
{
    Position storage position = _positions[params.tokenId];

    PoolAddress.PoolKey memory poolKey = _poolIdToPoolKey[position.poolId];

    IUniswapV3Pool pool;
    (liquidity, amount0, amount1, pool) = addLiquidity(
        AddLiquidityParams({
            token0: poolKey.token0,
            token1: poolKey.token1,
            fee: poolKey.fee,
            tickLower: position.tickLower,
            tickUpper: position.tickUpper,
            amount0Desired: params.amount0Desired,
            amount1Desired: params.amount1Desired,
            amount0Min: params.amount0Min,
            amount1Min: params.amount1Min,
            recipient: address(this)
        })
    );

首先根据tokenId获取头寸信息;与mint方法一样,这里调用addLiquidity添加流动性,返回添加成功的流动性liquidity,所消耗的amount0amount1,以及交易对合约pool

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

    // this is now updated to the current transaction
    (, uint256 feeGrowthInside0LastX128, uint256 feeGrowthInside1LastX128, , ) = pool.positions(positionKey);

    position.tokensOwed0 += uint128(
        FullMath.mulDiv(
            feeGrowthInside0LastX128 - position.feeGrowthInside0LastX128,
            position.liquidity,
            FixedPoint128.Q128
        )
    );
    position.tokensOwed1 += uint128(
        FullMath.mulDiv(
            feeGrowthInside1LastX128 - position.feeGrowthInside1LastX128,
            position.liquidity,
            FixedPoint128.Q128
        )
    );

    position.feeGrowthInside0LastX128 = feeGrowthInside0LastX128;
    position.feeGrowthInside1LastX128 = feeGrowthInside1LastX128;
    position.liquidity += liquidity;

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

根据pool对象里的最新头寸信息,更新本合约的头寸状态,比如token0token1的可取回代币数tokensOwed0tokensOwed1,以及头寸当前流动性等。

decreaseLiquidity

移除流动性,可以移除部分或者所有流动性,移除后的代币将以待取回代币形式记录,需要再次调用collect方法取回代币。

参数如下:

  • tokenId:创建头寸时返回的tokenId,即NFT的tokenId
  • liquidity:希望移除的流动性数量
  • amount0Min:最少移除的token0数量(防止被frontrun)
  • amount1Min:最少移除的token1数量(防止被frontrun)
  • deadline:截止时间(超过该时间请求无效)(防止重放攻击)
/// @inheritdoc INonfungiblePositionManager
function decreaseLiquidity(DecreaseLiquidityParams calldata params)
    external
    payable
    override
    isAuthorizedForToken(params.tokenId)
    checkDeadline(params.deadline)
    returns (uint256 amount0, uint256 amount1)
{

注意,这里使用isAuthorizedForToken modifer:

modifier isAuthorizedForToken(uint256 tokenId) {
    require(_isApprovedOrOwner(msg.sender, tokenId), 'Not approved');
    _;
}

确认当前用户具备操作该tokenId的权限,否则禁止移除。

    require(params.liquidity > 0);
    Position storage position = _positions[params.tokenId];

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

确认头寸流动性大于等于待移除流动性。

    PoolAddress.PoolKey memory poolKey = _poolIdToPoolKey[position.poolId];
    IUniswapV3Pool pool = IUniswapV3Pool(PoolAddress.computeAddress(factory, poolKey));
    (amount0, amount1) = pool.burn(position.tickLower, position.tickUpper, params.liquidity);

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

调用Uniswap-v3-core的burn方法销毁流动性,返回该流动性对应的token0token1的代币数量amount0amount1,确认其符合amount0Minamount1Min的限制。

    bytes32 positionKey = PositionKey.compute(address(this), position.tickLower, position.tickUpper);
    // this is now updated to the current transaction
    (, uint256 feeGrowthInside0LastX128, uint256 feeGrowthInside1LastX128, , ) = pool.positions(positionKey);

    position.tokensOwed0 +=
        uint128(amount0) +
        uint128(
            FullMath.mulDiv(
                feeGrowthInside0LastX128 - position.feeGrowthInside0LastX128,
                positionLiquidity,
                FixedPoint128.Q128
            )
        );
    position.tokensOwed1 +=
        uint128(amount1) +
        uint128(
            FullMath.mulDiv(
                feeGrowthInside1LastX128 - position.feeGrowthInside1LastX128,
                positionLiquidity,
                FixedPoint128.Q128
            )
        );

    position.feeGrowthInside0LastX128 = feeGrowthInside0LastX128;
    position.feeGrowthInside1LastX128 = feeGrowthInside1LastX128;
    // subtraction is safe because we checked positionLiquidity is gte params.liquidity
    position.liquidity = positionLiquidity - params.liquidity;

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

increaseLiquidity相同,此处计算头寸的待取回代币等信息。

burn

销毁头寸NFT。仅当该头寸的流动性为0,并且待取回代币数量都是0时,才能销毁NFT。

同样,调用该方法需要验证当前用户拥有tokenId的权限。

/// @inheritdoc INonfungiblePositionManager
function burn(uint256 tokenId) external payable override isAuthorizedForToken(tokenId) {
    Position storage position = _positions[tokenId];
    require(position.liquidity == 0 && position.tokensOwed0 == 0 && position.tokensOwed1 == 0, 'Not cleared');
    delete _positions[tokenId];
    _burn(tokenId);
}

collect

取回待领取代币。

参数如下:

  • tokenId:创建头寸时返回的tokenId,即NFT的tokenId
  • recipient:代币接收者
  • amount0Max:最多领取的token0代币数量
  • amount1Max:最多领取的token1代币数量
/// @inheritdoc INonfungiblePositionManager
function collect(CollectParams calldata params)
    external
    payable
    override
    isAuthorizedForToken(params.tokenId)
    returns (uint256 amount0, uint256 amount1)
{
    require(params.amount0Max > 0 || params.amount1Max > 0);
    // allow collecting to the nft position manager address with address 0
    address recipient = params.recipient == address(0) ? address(this) : params.recipient;

    Position storage position = _positions[params.tokenId];

    PoolAddress.PoolKey memory poolKey = _poolIdToPoolKey[position.poolId];

    IUniswapV3Pool pool = IUniswapV3Pool(PoolAddress.computeAddress(factory, poolKey));

    (uint128 tokensOwed0, uint128 tokensOwed1) = (position.tokensOwed0, position.tokensOwed1);

获取待取回代币数量。

    // trigger an update of the position fees owed and fee growth snapshots if it has any liquidity
    if (position.liquidity > 0) {
        pool.burn(position.tickLower, position.tickUpper, 0);
        (, uint256 feeGrowthInside0LastX128, uint256 feeGrowthInside1LastX128, , ) =
            pool.positions(PositionKey.compute(address(this), position.tickLower, position.tickUpper));

        tokensOwed0 += uint128(
            FullMath.mulDiv(
                feeGrowthInside0LastX128 - position.feeGrowthInside0LastX128,
                position.liquidity,
                FixedPoint128.Q128
            )
        );
        tokensOwed1 += uint128(
            FullMath.mulDiv(
                feeGrowthInside1LastX128 - position.feeGrowthInside1LastX128,
                position.liquidity,
                FixedPoint128.Q128
            )
        );

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

如果该头寸含有流动性,则触发一次头寸状态的更新,这里使用burn 0流动性来触发。这是因为Uniswap-v3-core只在mintburn时才更新头寸状态,而collect方法可能在swap之后被调用,可能会导致头寸状态不是最新的。

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

    // the actual amounts collected are returned
    (amount0, amount1) = pool.collect(
        recipient,
        position.tickLower,
        position.tickUpper,
        amount0Collect,
        amount1Collect
    );

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

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

调用Uniswap-v3-core的collect方法取回代币,并更新头寸的待取回代币数量。

SwapRouter.sol

交换代币,包括以下几个方法:

  • exactInputSingle:单步交换,指定输入代币数量,尽可能多地获得输出代币
  • exactInput:多步交换,指定输入代币数量,尽可能多地获得输出代币
  • exactOutputSingle:单步交换,指定输出代币数量,尽可能少地提供输入代币
  • exactOutput:多步交换,指定输出代币数量,尽可能少地提供输入代币

另外,该合约也实现了:

exactInputSingle

单步交换,指定输入代币数量,尽可能多地获得输出代币。

参数如下:

  • tokenIn:输入代币地址
  • tokenOut:输出代币地址
  • fee:手续费等级
  • recipient:输出代币接收者
  • deadline:截止时间,超过该时间请求无效
  • amountIn:输入的代币数量
  • amountOutMinimum:最少收到的输出代币数量
  • sqrtPriceLimitX96:(最高或最低)限制价格

返回:

  • amountOut:输出代币数量
/// @inheritdoc ISwapRouter
function exactInputSingle(ExactInputSingleParams calldata params)
    external
    payable
    override
    checkDeadline(params.deadline)
    returns (uint256 amountOut)
{
    amountOut = exactInputInternal(
        params.amountIn,
        params.recipient,
        params.sqrtPriceLimitX96,
        SwapCallbackData({path: abi.encodePacked(params.tokenIn, params.fee, params.tokenOut), payer: msg.sender})
    );
    require(amountOut >= params.amountOutMinimum, 'Too little received');
}

该方法实际上调用exactInputInternal,最后确认输出代币数量amountOut符合最小输出代币要求amountOutMinimum

注意,SwapCallbackData中的path按照Path.sol中定义的格式编码。

exactInput

多步交换,指定输入代币数量,尽可能多地获得输出代币。

参数如下:

  • path:交换路径,格式请参考:Path.sol
  • recipient:输出代币收款人
  • deadline:交易截止时间
  • amountIn:输入代币数量
  • amountOutMinimum:最少输出代币数量

返回:

  • amountOut:输出代币
/// @inheritdoc ISwapRouter
function exactInput(ExactInputParams memory params)
    external
    payable
    override
    checkDeadline(params.deadline)
    returns (uint256 amountOut)
{
    address payer = msg.sender; // msg.sender pays for the first hop

    while (true) {
        bool hasMultiplePools = params.path.hasMultiplePools();

        // the outputs of prior swaps become the inputs to subsequent ones
        params.amountIn = exactInputInternal(
            params.amountIn,
            hasMultiplePools ? address(this) : params.recipient, // for intermediate swaps, this contract custodies
            0,
            SwapCallbackData({
                path: params.path.getFirstPool(), // only the first pool in the path is necessary
                payer: payer
            })
        );

        // decide whether to continue or terminate
        if (hasMultiplePools) {
            payer = address(this); // at this point, the caller has paid
            params.path = params.path.skipToken();
        } else {
            amountOut = params.amountIn;
            break;
        }
    }

    require(amountOut >= params.amountOutMinimum, 'Too little received');
}

在多步交换中,需要按照交换路径,拆成多个单步交换,循环进行,直到路径结束。

如果是第一步交换,则payer为合约调用方,否则,payer为当前SwapRouter合约。

在循环中首先根据hasMultiplePools判断路径path中是否剩余2个及以上的池子。如果有,则中间交换步骤的收款地址设置为当前SwapRouter合约,否则设置为入口参数recipient

每一步交换后,将当前交换路径path的前20+3个字节删除,即弹出(pop)最前面的token+fee信息,进入下一次交换,并将每一步交换的输出作为下一次交换的输入。

每一步交换调用exactInputInternal进行。

多步交换后,确认最后的amountOut满足最小输出代币要求amountOutMinimum

exactOutputSingle

单步交换,指定输出代币数量,尽可能少地提供输入代币。

参数如下:

  • tokenIn:输入代币地址
  • tokenOut:输出代币地址
  • fee:手续费等级
  • recipient:输出代币收款人
  • deadline:请求截止时间
  • amountOut:输出代币数量
  • amountInMaximum:最大输入代币数量
  • sqrtPriceLimitX96:最大或最小代币价格

返回:

  • amountIn:实际输入代币数量
/// @inheritdoc ISwapRouter
function exactOutputSingle(ExactOutputSingleParams calldata params)
    external
    payable
    override
    checkDeadline(params.deadline)
    returns (uint256 amountIn)
{
    // avoid an SLOAD by using the swap return data
    amountIn = exactOutputInternal(
        params.amountOut,
        params.recipient,
        params.sqrtPriceLimitX96,
        SwapCallbackData({path: abi.encodePacked(params.tokenOut, params.fee, params.tokenIn), payer: msg.sender})
    );

    require(amountIn <= params.amountInMaximum, 'Too much requested');
    // has to be reset even though we don't use it in the single hop case
    amountInCached = DEFAULT_AMOUNT_IN_CACHED;
}

调用exactOutputInternal完成单步交换,并确认实际输入代币数量amountIn小于等于最大输入代币数量amountInMaximum

exactOutput

多步交换,指定输出代币数量,尽可能少地提供输入代币。

参数如下:

  • path:交换路径,格式请参考:Path.sol
  • recipient:输出代币收款人
  • deadline:请求截止时间
  • amountOut:指定输出代币数量
  • amountInMaximum:最大输入代币数量
/// @inheritdoc ISwapRouter
function exactOutput(ExactOutputParams calldata params)
    external
    payable
    override
    checkDeadline(params.deadline)
    returns (uint256 amountIn)
{
    // it's okay that the payer is fixed to msg.sender here, as they're only paying for the "final" exact output
    // swap, which happens first, and subsequent swaps are paid for within nested callback frames
    exactOutputInternal(
        params.amountOut,
        params.recipient,
        0,
        SwapCallbackData({path: params.path, payer: msg.sender})
    );

    amountIn = amountInCached;
    require(amountIn <= params.amountInMaximum, 'Too much requested');
    amountInCached = DEFAULT_AMOUNT_IN_CACHED;
}

调用exactOutputInternal完成交换,注意,该方法会在回调方法中继续完成下一步交换,因此不需要像exactInput使用循环交易。

最后确认实际输入代币数量amountIn小于等于最大输入代币数量amountInMaximum

exactInputInternal

单步交换,内部方法,指定输入代币数量,尽可能多地获得输出代币。

/// @dev Performs a single exact input swap
function exactInputInternal(
    uint256 amountIn,
    address recipient,
    uint160 sqrtPriceLimitX96,
    SwapCallbackData memory data
) private returns (uint256 amountOut) {
    // allow swapping to the router address with address 0
    if (recipient == address(0)) recipient = address(this);

如果没有指定recipient,则默认为当前SwapRouter合约地址。这是因为在多步交换时,需要将中间代币保存在当前SwapRouter合约。

    (address tokenIn, address tokenOut, uint24 fee) = data.path.decodeFirstPool();

根据decodeFirstPool解析path中第一个池子的信息。

    bool zeroForOne = tokenIn < tokenOut;

因为Uniswap v3池子token0地址小于token1,根据两个代币地址判断当前是否由token0交换到token1。注意,tokenIn可以是token0token1

    (int256 amount0, int256 amount1) =
        getPool(tokenIn, tokenOut, fee).swap(
            recipient,
            zeroForOne,
            amountIn.toInt256(),
            sqrtPriceLimitX96 == 0
                ? (zeroForOne ? TickMath.MIN_SQRT_RATIO + 1 : TickMath.MAX_SQRT_RATIO - 1)
                : sqrtPriceLimitX96,
            abi.encode(data)
        );

调用swap方法,获得完成本次交换所需的amount0amount1。如果是从token0交换token1,则amount1是负数;反之,amount0是负数。

如果没有指定sqrtPriceLimitX96,则默认为最低或最高价格,因为在多步交换中,无法指定每一步的价格。

    return uint256(-(zeroForOne ? amount1 : amount0));

返回amountOut

exactOutputInternal

单步交换,内部方法,指定输出代币数量,尽可能少地提供输入代币。

/// @dev Performs a single exact output swap
function exactOutputInternal(
    uint256 amountOut,
    address recipient,
    uint160 sqrtPriceLimitX96,
    SwapCallbackData memory data
) private returns (uint256 amountIn) {
    // allow swapping to the router address with address 0
    if (recipient == address(0)) recipient = address(this);

    (address tokenOut, address tokenIn, uint24 fee) = data.path.decodeFirstPool();

    bool zeroForOne = tokenIn < tokenOut;

这部分代码与exactInputInternal类似。

    (int256 amount0Delta, int256 amount1Delta) =
        getPool(tokenIn, tokenOut, fee).swap(
            recipient,
            zeroForOne,
            -amountOut.toInt256(),
            sqrtPriceLimitX96 == 0
                ? (zeroForOne ? TickMath.MIN_SQRT_RATIO + 1 : TickMath.MAX_SQRT_RATIO - 1)
                : sqrtPriceLimitX96,
            abi.encode(data)
        );

调用Uniswap-v3-core的swap方法完成单步交换,注意,因为是指定输出代币数量,此处需要使用-amountOut.toInt256()。 返回的amount0Deltaamount1Delta为完成本次交换所需的token0数量和实际输出的token1数量。

    uint256 amountOutReceived;
    (amountIn, amountOutReceived) = zeroForOne
        ? (uint256(amount0Delta), uint256(-amount1Delta))
        : (uint256(amount1Delta), uint256(-amount0Delta));
    // it's technically possible to not receive the full output amount,
    // so if no price limit has been specified, require this possibility away
    if (sqrtPriceLimitX96 == 0) require(amountOutReceived == amountOut);
}

uniswapV3SwapCallback

swap的回调方法,实现IUniswapV3SwapCallback.uniswapV3SwapCallback接口。

参数如下:

  • amount0Delta:本次交换产生的amount0(对应代币为token0);对于合约而言,如果大于0,则表示应输入代币;如果小于0,则表示应收到代币
  • amount1Delta:本次交换产生的amount1(对应代币为token1);对于合约而言,如果大于0,则表示应输入代币;如果小于0,则表示应收到代币
  • _data:回调参数,这里为SwapCallbackData类型
/// @inheritdoc IUniswapV3SwapCallback
function uniswapV3SwapCallback(
    int256 amount0Delta,
    int256 amount1Delta,
    bytes calldata _data
) external override {
    require(amount0Delta > 0 || amount1Delta > 0); // swaps entirely within 0-liquidity regions are not supported
    SwapCallbackData memory data = abi.decode(_data, (SwapCallbackData));
    (address tokenIn, address tokenOut, uint24 fee) = data.path.decodeFirstPool();
    CallbackValidation.verifyCallback(factory, tokenIn, tokenOut, fee);

解析回调参数_data,根据decodeFirstPool获得交易路径上的第一个交易对信息。

    (bool isExactInput, uint256 amountToPay) =
        amount0Delta > 0
            ? (tokenIn < tokenOut, uint256(amount0Delta))
            : (tokenOut < tokenIn, uint256(amount1Delta));

根据不同输入,有以下几种交易组合:

场景 说明 amount0Delta > 0 amount1Delta > 0 tokenIn < tokenOut isExactInput amountToPay
1 输入指定数量token0,输出尽可能多token1 true false true true amount0Delta
2 输入尽可能少token0,输出指定数量token1 true false true false amount0Delta
3 输入指定数量token1,输出尽可能多token0 false true false true amount1Delta
4 输入尽可能少token1,输出指定数量token0 false true false false amount1Delta
    if (isExactInput) {
        pay(tokenIn, data.payer, msg.sender, amountToPay);
    } else {
        // either initiate the next swap or pay
        if (data.path.hasMultiplePools()) {
            data.path = data.path.skipToken();
            exactOutputInternal(amountToPay, msg.sender, 0, data);
        } else {
            amountInCached = amountToPay;
            tokenIn = tokenOut; // swap in/out because exact output swaps are reversed
            pay(tokenIn, data.payer, msg.sender, amountToPay);
        }
    }
  • 如果isExactInput,即指定输入代币的场景,上表中的场景1和场景3,则直接向SwapRouter合约转账amount0Delta(场景1)或amount1Delta(场景3)(都是正数)。
  • 如果是指定输出代币的场景
    • 如果是多步交换,则移除前23的字符(pop最前面的token+fee),将需要的输入作为下一步的输出,进入下一步交换
    • 如果是单步交换(或最后一步),则tokenIntokenOut交换,并向SwapRouter合约转账

LiquidityManagement.sol

uniswapV3MintCallback

添加流动性的回调方法。

参数如下:

  • amount0Owed:应转账的token0数量
  • amount1Owed:应转账的token1数量
  • data:在mint方法中传入的回调参数
/// @inheritdoc IUniswapV3MintCallback
function uniswapV3MintCallback(
    uint256 amount0Owed,
    uint256 amount1Owed,
    bytes calldata data
) external override {
    MintCallbackData memory decoded = abi.decode(data, (MintCallbackData));
    CallbackValidation.verifyCallback(factory, decoded.poolKey);

    if (amount0Owed > 0) pay(decoded.poolKey.token0, decoded.payer, msg.sender, amount0Owed);
    if (amount1Owed > 0) pay(decoded.poolKey.token1, decoded.payer, msg.sender, amount1Owed);
}

首先反向解析回调参数MintCallbackData,并确认该方法是被指定的交易对合约调用,因为该方法是一个external方法,可以被外部调用,因此需要确认调用方。

最后,向调用方转入指定的代币数量。

addLiquidity

给已初始化的交易对(池子)添加流动性。

/// @notice Add liquidity to an initialized pool
function addLiquidity(AddLiquidityParams memory params)
    internal
    returns (
        uint128 liquidity,
        uint256 amount0,
        uint256 amount1,
        IUniswapV3Pool pool
    )
{
    PoolAddress.PoolKey memory poolKey =
        PoolAddress.PoolKey({token0: params.token0, token1: params.token1, fee: params.fee});

    pool = IUniswapV3Pool(PoolAddress.computeAddress(factory, poolKey));

根据factorytoken0token1fee获取交易对pool

    // compute the liquidity amount
    {
        (uint160 sqrtPriceX96, , , , , , ) = pool.slot0();
        uint160 sqrtRatioAX96 = TickMath.getSqrtRatioAtTick(params.tickLower);
        uint160 sqrtRatioBX96 = TickMath.getSqrtRatioAtTick(params.tickUpper);

        liquidity = LiquidityAmounts.getLiquidityForAmounts(
            sqrtPriceX96,
            sqrtRatioAX96,
            sqrtRatioBX96,
            params.amount0Desired,
            params.amount1Desired
        );
    }

slot0获取当前价格sqrtPriceX96,根据tickLowertickUpper计算区间的最低价格sqrtRatioAX96和最高价格sqrtRatioBX96

根据getLiquidityForAmounts计算能够获得的最大流动性。

    (amount0, amount1) = pool.mint(
        params.recipient,
        params.tickLower,
        params.tickUpper,
        liquidity,
        abi.encode(MintCallbackData({poolKey: poolKey, payer: msg.sender}))
    );

使用Uniswap-v3-core的mint方法添加流动性,并返回实际消耗的amount0amount1

我们在Uniswap-v3-core的mint方法中提到,调用方需实现uniswapV3MintCallback接口。这里传入MintCallbackData作为回调参数,在uniswapV3MintCallback方法中可以反向解析出来,以便获取交易对和用户信息。

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

最后,确认实际消耗的amount0amount1满足amount0Minamount1Min的最低要求。

LiquidityAmounts.sol

getLiquidityForAmount0

根据amount0和价格区间计算流动性。

根据Uniswap-v3-core的getAmount0Delta中的公式:

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

可得:

$$ L = amount0 \cdot (\frac{\sqrt{P_a} \cdot \sqrt{P_b}}{\sqrt{P_a} - \sqrt{P_b}}) $$

/// @notice Computes the amount of liquidity received for a given amount of token0 and price range
/// @dev Calculates amount0 * (sqrt(upper) * sqrt(lower)) / (sqrt(upper) - sqrt(lower))
/// @param sqrtRatioAX96 A sqrt price representing the first tick boundary
/// @param sqrtRatioBX96 A sqrt price representing the second tick boundary
/// @param amount0 The amount0 being sent in
/// @return liquidity The amount of returned liquidity
function getLiquidityForAmount0(
    uint160 sqrtRatioAX96,
    uint160 sqrtRatioBX96,
    uint256 amount0
) internal pure returns (uint128 liquidity) {
    if (sqrtRatioAX96 > sqrtRatioBX96) (sqrtRatioAX96, sqrtRatioBX96) = (sqrtRatioBX96, sqrtRatioAX96);
    uint256 intermediate = FullMath.mulDiv(sqrtRatioAX96, sqrtRatioBX96, FixedPoint96.Q96);
    return toUint128(FullMath.mulDiv(amount0, intermediate, sqrtRatioBX96 - sqrtRatioAX96));
}

getLiquidityForAmount1

根据amount1和价格区间计算流动性。

根据Uniswap-v3-core的getAmount1Delta公式:

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

可得:

$$ L = \frac{amount1}{\sqrt{P_b} - \sqrt{P_a}} $$

/// @notice Computes the amount of liquidity received for a given amount of token1 and price range
/// @dev Calculates amount1 / (sqrt(upper) - sqrt(lower)).
/// @param sqrtRatioAX96 A sqrt price representing the first tick boundary
/// @param sqrtRatioBX96 A sqrt price representing the second tick boundary
/// @param amount1 The amount1 being sent in
/// @return liquidity The amount of returned liquidity
function getLiquidityForAmount1(
    uint160 sqrtRatioAX96,
    uint160 sqrtRatioBX96,
    uint256 amount1
) internal pure returns (uint128 liquidity) {
    if (sqrtRatioAX96 > sqrtRatioBX96) (sqrtRatioAX96, sqrtRatioBX96) = (sqrtRatioBX96, sqrtRatioAX96);
    return toUint128(FullMath.mulDiv(amount1, FixedPoint96.Q96, sqrtRatioBX96 - sqrtRatioAX96));
}

getLiquidityForAmounts

根据当前价格,计算能够返回的最大流动性。

  • 因为当 $\sqrt{P}$ 增大时,需要消耗 $x$ ,因此如果当前价格低于价格区间低点时,需要完全根据 $x$amount0计算流动性
  • 反之,如果当前价格高于价格区间高点,需要根据 $y$amount1计算流动性

如下图所示:

$$ p,...,\overbrace{p_a,...,p_b}^{amount0} $$

$$ \overbrace{p_a,...}^{amount1},p,\overbrace{...,p_b}^{amount0} $$

$$ \overbrace{p_a,...,p_b}^{amount1},...,p $$

其中, $p$ 表示当前价格, $p_a$ 表示区间低点, $p_b$ 表示区间高点。

/// @notice Computes the maximum amount of liquidity received for a given amount of token0, token1, the current
/// pool prices and the prices at the tick boundaries
/// @param sqrtRatioX96 A sqrt price representing the current pool prices
/// @param sqrtRatioAX96 A sqrt price representing the first tick boundary
/// @param sqrtRatioBX96 A sqrt price representing the second tick boundary
/// @param amount0 The amount of token0 being sent in
/// @param amount1 The amount of token1 being sent in
/// @return liquidity The maximum amount of liquidity received
function getLiquidityForAmounts(
    uint160 sqrtRatioX96,
    uint160 sqrtRatioAX96,
    uint160 sqrtRatioBX96,
    uint256 amount0,
    uint256 amount1
) internal pure returns (uint128 liquidity) {
    if (sqrtRatioAX96 > sqrtRatioBX96) (sqrtRatioAX96, sqrtRatioBX96) = (sqrtRatioBX96, sqrtRatioAX96);

    if (sqrtRatioX96 <= sqrtRatioAX96) {
        liquidity = getLiquidityForAmount0(sqrtRatioAX96, sqrtRatioBX96, amount0);
    } else if (sqrtRatioX96 < sqrtRatioBX96) {
        uint128 liquidity0 = getLiquidityForAmount0(sqrtRatioX96, sqrtRatioBX96, amount0);
        uint128 liquidity1 = getLiquidityForAmount1(sqrtRatioAX96, sqrtRatioX96, amount1);

        liquidity = liquidity0 < liquidity1 ? liquidity0 : liquidity1;
    } else {
        liquidity = getLiquidityForAmount1(sqrtRatioAX96, sqrtRatioBX96, amount1);
    }
}

Path.sol

在Uniswap v3 SwapRouter中,交易路径被编码为一个bytes类型字符串,其格式为:

$$ \overbrace{token_0}^{20}\overbrace{fee_0}^{3}\overbrace{token_1}^{20}\overbrace{fee_1}^{3}\overbrace{token_2}^{20}... $$

其中, $token_n$ 的长度为20个字节(bytes), $fee_n$ 的长度为3个字节,上述路径表示:从token0交换到token1,使用手续费等级为fee0的池子(token0token1fee0),继续交换到token2,使用手续费等级为fee1的池子(token1token2fee1)。

交易路径path示例如下:

$$ 0x\overbrace{ca90cf0734d6ccf5ef52e9ec0a515921a67d6013}^{token0,20bytes}\overbrace{0001f4}^{fee,3bytes}\overbrace{68b3465833fb72a70ecdf485e0e4c7bd8665fc45}^{token1,20bytes} $$

hasMultiplePools

判断交易路径是否经过多个池子(2个及以上)。

/// @notice Returns true iff the path contains two or more pools
/// @param path The encoded swap path
/// @return True if path contains two or more pools, otherwise false
function hasMultiplePools(bytes memory path) internal pure returns (bool) {
    return path.length >= MULTIPLE_POOLS_MIN_LENGTH;
}

我们从上述路径编码可知,如果经过2个池子,至少包含3个代币,则路径长度至少需要 $20+3+20+3+20=66$ 个字节。代码中MULTIPLE_POOLS_MIN_LENGTH即等于66。

numPools

计算路径中的池子数量。

算法为:

$$ num = \frac{length - 20}{20 + 3} $$

/// @notice Returns the number of pools in the path
/// @param path The encoded swap path
/// @return The number of pools in the path
function numPools(bytes memory path) internal pure returns (uint256) {
    // Ignore the first token address. From then on every fee and token offset indicates a pool.
    return ((path.length - ADDR_SIZE) / NEXT_OFFSET);
}

decodeFirstPool

解析第一个path的信息,包括token0token1fee

分别返回字符串中0-19子串(token0,转address类型),20-22子串(fee,转uint24类型),和23-42子串(token1,转address类型)。请参考BytesLib.soltoAddresstoUint24方法。

/// @notice Decodes the first pool in path
/// @param path The bytes encoded swap path
/// @return tokenA The first token of the given pool
/// @return tokenB The second token of the given pool
/// @return fee The fee level of the pool
function decodeFirstPool(bytes memory path)
    internal
    pure
    returns (
        address tokenA,
        address tokenB,
        uint24 fee
    )
{
    tokenA = path.toAddress(0);
    fee = path.toUint24(ADDR_SIZE);
    tokenB = path.toAddress(NEXT_OFFSET);
}

getFirstPool

返回第一个池子的路径,即返回前43(即20+3+20)个字符组成的子字符串。

/// @notice Gets the segment corresponding to the first pool in the path
/// @param path The bytes encoded swap path
/// @return The segment containing all data necessary to target the first pool in the path
function getFirstPool(bytes memory path) internal pure returns (bytes memory) {
    return path.slice(0, POP_OFFSET);
}

skipToken

跳过当前路径上的第一个token+fee,即跳过前20+3个字符。

/// @notice Skips a token + fee element from the buffer and returns the remainder
/// @param path The swap path
/// @return The remaining token + fee elements in the path
function skipToken(bytes memory path) internal pure returns (bytes memory) {
    return path.slice(NEXT_OFFSET, path.length - NEXT_OFFSET);
}

BytesLib.sol

toAddress

从字符串的指定序号起,读取一个地址(20个字符):

function toAddress(bytes memory _bytes, uint256 _start) internal pure returns (address) {
    require(_start + 20 >= _start, 'toAddress_overflow');
    require(_bytes.length >= _start + 20, 'toAddress_outOfBounds');
    address tempAddress;

    assembly {
        tempAddress := div(mload(add(add(_bytes, 0x20), _start)), 0x1000000000000000000000000)
    }

    return tempAddress;
}

因为变量_bytes类型为bytes,根据ABI定义bytes的第一个32字节存储字符串的长度(length),因此需要先跳过前面32字节,即add(_bytes, 0x20)add(add(_bytes, 0x20), _start)表示定位到字符串指定序号_startmload读取从该序号起的32个字节,因为address类型只有20字节,因此需要div 0x1000000000000000000000000,即右移12字节。

假设_strat = 0_bytes的分布如下图所示:

$$ 0x\overbrace{0000000...2b}^{length,32bytes}\underbrace{\overbrace{ca90cf0734d6ccf5ef52e9ec0a515921a67d6013}^{address, 20 bytes}\overbrace{0001f468b3465833fb72a70e}^{div,12 bytes}}_{mload, 32bytes}cdf485e0e4c7bd8665fc45 $$

toUint24

从字符串的指定序号起,读取一个uint24(24位,即3个字符):

function toUint24(bytes memory _bytes, uint256 _start) internal pure returns (uint24) {
    require(_start + 3 >= _start, 'toUint24_overflow');
    require(_bytes.length >= _start + 3, 'toUint24_outOfBounds');
    uint24 tempUint;

    assembly {
        tempUint := mload(add(add(_bytes, 0x3), _start))
    }

    return tempUint;
}

因为_bytes前32个字符表示字符串长度;mload读取32字节,可以确保从_start开始的3个字节在读取出来的32字节的最低位,赋值给类型为uint24的变量将只保留最低位的3个字节。

假设_strat = 0_bytes的分布如下图所示:

$$ 0x\overbrace{000000}^{0x3+_start}\underbrace{0...2b\overbrace{ca90cf0734d6ccf5ef52e9ec0a515921a67d6013}^{address1,20bytes}\overbrace{0001f4}^{fee,3bytes}}_{mload,32bytes}\overbrace{68b3465833fb72a70ecdf485e0e4c7bd8665fc45}^{address2,20bytes} $$

OracleLibrary.sol

根据白皮书公式5.3-5.5,计算 $t_1$$t_2$ 时间内的几何平均价格如下:

$$ \log_{1.0001}(P_{t_1,t_2}) = \frac{\sum_{i=t_1}^{t_2} \log_{1.0001}(P_i)}{t_2 - t_1} \tag{5.3} $$

$$ \log_{1.0001}(P_{t_1,t_2}) = \frac{a_{t_2} - a_{t_1}}{t_2 - t_1} \tag{5.4} $$

$$ P_{t_1,t_2} = 1.0001^{\frac{a_{t_2} - a_{t_1}}{t_2 - t_1}} \tag{5.5} $$

本合约提供价格预言机相关方法,包括如下方法:

  • consult:查询从一段时间前到现在的几何平均价格(以tick形式)
  • getQuoteAtTick:根据tick计算代币价格

consult

查询从一段时间前到现在的几何平均价格(以tick形式)。

参数如下:

  • pool: 交易对池子地址
  • period:以秒计数的区间

返回:

  • timeWeightedAverageTick:时间加权平均价格
/// @notice Fetches time-weighted average tick using Uniswap V3 oracle
/// @param pool Address of Uniswap V3 pool that we want to observe
/// @param period Number of seconds in the past to start calculating time-weighted average
/// @return timeWeightedAverageTick The time-weighted average tick from (block.timestamp - period) to block.timestamp
function consult(address pool, uint32 period) internal view returns (int24 timeWeightedAverageTick) {
    require(period != 0, 'BP');

    uint32[] memory secondAgos = new uint32[](2);
    secondAgos[0] = period;
    secondAgos[1] = 0;

构造两个监测点,第一个为period时间之前,第二个为现在。

    (int56[] memory tickCumulatives, ) = IUniswapV3Pool(pool).observe(secondAgos);
    int56 tickCumulativesDelta = tickCumulatives[1] - tickCumulatives[0];

根据IUniswapV3Pool.observe方法获取累积tick,即公式5.4中的 $a_{t_2}$$a_{t_1}$

$$ tickCumulativesDelta = a_{t_2} - a_{t_1} $$

    timeWeightedAverageTick = int24(tickCumulativesDelta / period);

$$ timeWeightedAverageTick = \frac{tickCumulativesDelta}{t_2 - t_1} $$

    // Always round to negative infinity
    if (tickCumulativesDelta < 0 && (tickCumulativesDelta % period != 0)) timeWeightedAverageTick--;

如果tickCumulativesDelta为负数,并且无法被period整除,则将平均价格-1。

getQuoteAtTick

/// @notice Given a tick and a token amount, calculates the amount of token received in exchange
/// @param tick Tick value used to calculate the quote
/// @param baseAmount Amount of token to be converted
/// @param baseToken Address of an ERC20 token contract used as the baseAmount denomination
/// @param quoteToken Address of an ERC20 token contract used as the quoteAmount denomination
/// @return quoteAmount Amount of quoteToken received for baseAmount of baseToken
function getQuoteAtTick(
    int24 tick,
    uint128 baseAmount,
    address baseToken,
    address quoteToken
) internal pure returns (uint256 quoteAmount) {
    uint160 sqrtRatioX96 = TickMath.getSqrtRatioAtTick(tick);

    // Calculate quoteAmount with better precision if it doesn't overflow when multiplied by itself
    if (sqrtRatioX96 <= type(uint128).max) {
        uint256 ratioX192 = uint256(sqrtRatioX96) * sqrtRatioX96;
        quoteAmount = baseToken < quoteToken
            ? FullMath.mulDiv(ratioX192, baseAmount, 1 << 192)
            : FullMath.mulDiv(1 << 192, baseAmount, ratioX192);
    } else {
        uint256 ratioX128 = FullMath.mulDiv(sqrtRatioX96, sqrtRatioX96, 1 << 64);
        quoteAmount = baseToken < quoteToken
            ? FullMath.mulDiv(ratioX128, baseAmount, 1 << 128)
            : FullMath.mulDiv(1 << 128, baseAmount, ratioX128);
    }
}

根据Uniswap-v3-core的getSqrtRatioAtTick方法计算tick对应的 $\sqrt{P}$ ,即 $\sqrt{\frac{token1}{token0}}$

如果baseToken < quoteToken,则baseTokentoken0quoteTokentoken1

$$ quoteAmount = baseAmount \cdot (\sqrt{P})^2 $$

反之,baseTokentoken1quoteTokentoken0

$$ quoteAmount = \frac{baseAmount}{(\sqrt{P})^2} $$