Uniswap v3分析(三)

Uniswapv3分析(三)Uniswap-v3-core中是实现V3的底层核心逻辑,而Uniswap-v3-periphery才是用户直接交互的地方。NonfungiblePositionManager.sol此合约继承了ERC721,可以铸造NFT表示头寸f(由onwer,tickLow

Uniswap v3分析(三)

Uniswap-v3-core中是实现V3的底层核心逻辑,而Uniswap-v3-periphery才是用户直接交互的地方。

NonfungiblePositionManager.sol

此合约继承了ERC721,可以铸造NFT表示头寸f(由onwer,tickLower,tickUpper唯一确定),用于管理用户的头寸。主要包括以下几个方法:

mint:创建头寸

increaseLiquidity:添加流动性

decreaseLiquidity:减少流动性

burn:销毁头寸

collect:取回代币

mint

mint接收一个结构体参数,结构如下:

token0:代币0
token1:代币1
fee:手续费等级
recipient:头寸接收者
tickLower:价格区间低点
tickUpper:价格区间高点
deadline:截止时间(防止重放攻击)
amount0Desired:希望存入的代币0数量
amount1Desired:希望存入的代币1数量
amount0Min:最少存入的token0数量(防止被frontrun)
amount1Min:最少存入的token1数量(防止被frontrun)

函数里首先添加流动性,获得实际添加的流动性liquidity,消耗的amount0、amount1,以及交易对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
        })
    );

然后为receipient铸造NFT

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

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

        // 计算positionKey
        bytes32 positionKey = PositionKey.compute(address(this), params.tickLower, params.tickUpper);
        // 通过positionKey获取每单位token0和token1的流动性的手续费数量
        (, uint256 feeGrowthInside0LastX128, uint256 feeGrowthInside1LastX128, , ) = pool.positions(positionKey);
        // 生成PoolId
        uint80 poolId =
            cachePoolKey(
                address(pool),
                PoolAddress.PoolKey({token0: params.token0, token1: params.token1, fee: params.fee})
            );
        // 将用户的流动性头寸,存入positions
        _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
        });

increaseLiquidity

为头寸添加流动性(只能修改头寸的代币数量,无法修改价格区间。)。接收如下参数:

tokenId:NFT的tokenId
deadline:截止时间
amount0Desired:希望添加的token0数量
amount1Desired:希望添加的token1数量
amount0Min:最少添加的token0数量
amount1Min:最少添加的token1数量

首先获取头寸信息和key

与mint相同,然后通过addLiquidity添加流动性

最后更新头寸信息


   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;

decreaseLiquidity

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

tokenId:NFT的tokenId
deadline:截止时间
liquidity:希望移除的流动性数量
amount0Min:最少移除的token0数量
amount1Min:最少移除的token1数量

首先检查头寸流动性大于等于要除流动性。

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

调用pool.burn方法销毁流动性,返回该流动性对应的token0和token1的代币数量amount0和amount1,然后确认其符合amount0Min和amount1Min的限制。

    (amount0, amount1) = pool.burn(position.tickLower, position.tickUpper, params.liquidity);
    require(amount0 >= params.amount0Min && amount1 >= params.amount1Min, 'Price slippage check');

最后更新头寸状态。

  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;

        position.liquidity = positionLiquidity - params.liquidity;

burn

销毁头寸,当该头寸的流动性与待取回代币数量都是0,才能调用。

function burn(uint256 tokenId) external payable override isAuthorizedForToken(tokenId) {
    Position storage position = _positions[tokenId];
    // 检查position的 liquidity tokensOwed0 tokensOwed1 必须为0
    // 否则不能销毁position
    require(position.liquidity == 0 && position.tokensOwed0 == 0 && position.tokensOwed1 == 0, 'Not cleared');
    // 删除position数据
    delete _positions[tokenId];
    // 销毁NFT
    _burn(tokenId);
}

collect

取出待领取代币(移除的流动性,积累的收益) 接收以下参数:

tokenId:NFT的tokenId
recipient:代币接收者
amount0Max:最多领取的token0代币数量
amount1Max:最多领取的token1代币数量

首先获取取出代币数量。

    require(params.amount0Max > 0 || params.amount1Max > 0);
    // 当入参recipient为0,设为本Manager合约地址
    address recipient = params.recipient == address(0) ? address(this) : params.recipient;
    // 根据tokenId获取用户的position
    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);

然后判断该头寸是否含有流动性,如果有,则使用burn(0)来触发一次头寸状态的更新。

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

最后取回代币

        // 计算实际要取数量
        (uint128 amount0Collect, uint128 amount1Collect) =
            (
                params.amount0Max > tokensOwed0 ? tokensOwed0 : params.amount0Max,
                params.amount1Max > tokensOwed1 ? tokensOwed1 : params.amount1Max
            );
        // 取出代币,得到实际取出的代币数量
        (amount0, amount1) = pool.collect(
            recipient,
            position.tickLower,
            position.tickUpper,
            amount0Collect,
            amount1Collect
        );
        the full amount expected
       // 更新头寸状态
       (position.tokensOwed0, position.tokensOwed1) = (tokensOwed0 - amount0Collect, tokensOwed1 - amount1Collect);

SwapRouter.sol

SwapRouter是swap路由的管理。提供代币交易的接口,它是对 UniswapV3Pool 合约中交易相关接口的进一步封装,前端界面主要与这个合约来进行对接。主要包括以下函数:

exactInputSingle()
exactInput()
exactInputInternal()
exactOutputSingle()
exactOutput()
exactOutputInternal()
uniswapV3SwapCallback()

exactInputSingle

单步交换,指定输入代币数量,尽可能多地获得输出代币。接收如下参数:

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

函数逻辑十分简单,使用exactInputInternal进行交换,然后检查amountOut是否大于amountOutMinimum。

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

exactInput

多步交换,指定输入代币数量,尽可能多地获得输出代币。与单步交易tokenA -> tokenB不同的是,多步交换可以设置Basetoken,即 tokenA -> Basetoken -> tokenB。可以让用户选择最优的交易路径(注:要交多次手续费)。

接收参数如下:

path:交换路径
recipient:输出代币收款人
deadline:交易截止时间
amountIn:输入代币数量
amountOutMinimum:最少输出代币数量

逻辑如下:

    function exactInput(ExactInputParams memory params)
        external
        payable
        override
        checkDeadline(params.deadline)
        returns (uint256 amountOut)
    {
        address payer = msg.sender;

        while (true) {
            // 判断路径中交易的token地址是否大于等于2
            bool hasMultiplePools = params.path.hasMultiplePools();
            // 进行swap
            params.amountIn = exactInputInternal(
                params.amountIn,
                hasMultiplePools ? address(this) : params.recipient, // 中间代币由此合约托管
                0, // 传入价格 0 代表以市价交易
                SwapCallbackData({
                    path: params.path.getFirstPool(), 
                    payer: payer
                })
            );

            // 判断是否需要继续交易
            if (hasMultiplePools) {
                payer = address(this); 
                // 将当前交换路径path的前20+3个字节删除,即pop最前面的token+fee
                params.path = params.path.skipToken();
            } else {
                amountOut = params.amountIn;
                break;
            }
        }
        // 检查交易输出
        require(amountOut >= params.amountOutMinimum, 'Too little received');
    }

exactOutputInternal

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

逻辑如下:

function exactInputInternal(
        uint256 amountIn,
        address recipient,
        uint160 sqrtPriceLimitX96,
        SwapCallbackData memory data
    ) private returns (uint256 amountIn) {
        // 如果接受者地址为0,就转为本合约地址
        if (recipient == address(0)) recipient = address(this);
        // 从path中解析出Pool的关键信息
        (address tokenOut, address tokenIn, uint24 fee) = data.path.decodeFirstPool();
        // 判断交易顺序
        // 之所以要判断,是因为Pool中price始终以 y/x 表示 (tokenx < tokeny)
        bool zeroForOne = tokenIn < tokenOut;
        // 调用Pool.swap进行交易
        //返回完成本次交换所需的token0数量amount0Delta和实际输出的token1数量amount1Delta
        (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)
            );
        return uint256(-(zeroForOne ? amount1 : amount0));
    }

uniswapV3SwapCallback

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

接收参数如下:

amount0Delta:本次交换产生的amount0(对应代币为token0);对于合约而言,如果大于0,则表示应输入代币;如果小于0,则表示应收到代币
amount1Delta:本次交换产生的amount1(对应代币为token1);对于合约而言,如果大于0,则表示应输入代币;如果小于0,则表示应收到代币
_data:回调参数,这里为SwapCallbackData类型

逻辑如下:

 function uniswapV3SwapCallback(
        int256 amount0Delta,
        int256 amount1Delta,
        bytes calldata _data
    ) external override {
        require(amount0Delta > 0 || amount1Delta > 0);
        SwapCallbackData memory data = abi.decode(_data, (SwapCallbackData));
        // 解析回调参数_data
        (address tokenIn, address tokenOut, uint24 fee) = data.path.decodeFirstPool();
        CallbackValidation.verifyCallback(factory, tokenIn, tokenOut, fee);
        // 根据不同输入,判断交易场景
        (bool isExactInput, uint256 amountToPay) =
            amount0Delta > 0
                ? (tokenIn < tokenOut, uint256(amount0Delta))
                : (tokenOut < tokenIn, uint256(amount1Delta));

        //输入代币的场景
        //则直接向pool合约转amountToPay

        //指定输出代币的场景
        //如果是多步交换,则移除前23的字符,将需要的输入作为下一步的输出,进入下一步交换
        //如果是单步交换(或最后一步),则tokenIn与tokenOut交换,并向pool合约转账
        if (isExactInput) {
            pay(tokenIn, data.payer, msg.sender, amountToPay);
        } else {
            if (data.path.hasMultiplePools()) {
                data.path = data.path.skipToken();
                exactOutputInternal(amountToPay, msg.sender, 0, data);
            } else {
                amountInCached = amountToPay;
                tokenIn = tokenOut; 
                pay(tokenIn, data.payer, msg.sender, amountToPay);
            }
        }
    }
点赞 0
收藏 1
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

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