« 上一篇下一篇 »

UniswapV3简析(四)

SwapRouter合约是直接与链下前端进行交互的外围合约, 主要对外交易兑换功能, 此合约可以看成用户的交易代理合约, 可以协助用户完成一些相对复杂的兑换操作, 用户<==>SwapRouter合约<==>UniswapV3Pool合约

SwapRouter合约主要封装了四个与交易相关的API接口对外提供服务, 分别是: 

exactInputSingle: 两个token之间进行兑换, 已知输入token的数量, 输出token的数量函数内部会自动进行计算

exactInput两个token之间进行兑换, 已知输入token的数量, 支持复杂路径, 比如ETH->DAI, 可以直接在ETH/DAI流动池兑换, 也可以利用ETH->USDC->DAI这个路径, 通过ETH/USDC, USDC/DAI两个流动池接力完成兑换, 至于采取哪个路径, 由链下前端决定, 链下前端会根据情况计算出最优路径.

exactOutputSingle两个token之间进行兑换, 已知输出token的数量, 输入token的数量函数内部会自动进行计算

exactOutput两个token之间进行兑换, 已知输出token的数量, 支持复杂路径(实际并没有实现)

代码位于contracts\SwapRouter.sol, 参考代码如下:
contract SwapRouter is
    ISwapRouter,
    PeripheryImmutableState,
    PeripheryValidation,
    PeripheryPaymentsWithFee,
    Multicall,
    SelfPermit
{
        ...

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

        (bool isExactInput, uint256 amountToPay) =
            amount0Delta > 0
                ? (tokenIn < tokenOut, uint256(amount0Delta))
                : (tokenOut < tokenIn, uint256(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);
            }
        }
    }


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

        (address tokenIn, address tokenOut, uint24 fee) = data.path.decodeFirstPool(); #路径解码

        bool zeroForOne = tokenIn < tokenOut; #根据地址大小决定池内是token0->token1还是token1->token0

        (int256 amount0, int256 amount1) =
            getPool(tokenIn, tokenOut, fee).swap(
                recipient, #收款地址
                zeroForOne,
                amountIn.toInt256(), #输入token的数量
                sqrtPriceLimitX96 == 0
                    ? (zeroForOne ? TickMath.MIN_SQRT_RATIO + 1 : TickMath.MAX_SQRT_RATIO - 1)
                    : sqrtPriceLimitX96, #价格限制,卖设置最低价,买设置最高价
                abi.encode(data)
            );

        return uint256(-(zeroForOne ? amount1 : amount0)); #输出token数量用负数表示
    }


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


    /// @inheritdoc ISwapRouter
    function exactInput(ExactInputParams memory params)
        external
        payable
        override
        checkDeadline(params.deadline)
        returns (uint256 amountOut)
    {
        address payer = msg.sender; // 第一个交易对肯定需要用户支付token

        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, // 如果是复杂路径交易,中间token由合约自己先代收
                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); // 如果是复杂路径交易,中间token由合约自己先代收,所以也需要合约自己支付到下一个流动池
                params.path = params.path.skipToken();
            } else {
                amountOut = params.amountIn;
                break;
            }
        }

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


    /// @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; #根据地址大小决定池内是token0->token1还是token1->token0

        (int256 amount0Delta, int256 amount1Delta) =
            getPool(tokenIn, tokenOut, fee).swap(
                recipient,
                zeroForOne,
                -amountOut.toInt256(), #负数表示的是输出token数量
                sqrtPriceLimitX96 == 0
                    ? (zeroForOne ? TickMath.MIN_SQRT_RATIO + 1 : TickMath.MAX_SQRT_RATIO - 1)
                    : sqrtPriceLimitX96,
                abi.encode(data)
            );

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


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


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

这里有几个细节要注意一下:

1.当创建流动池的时候就已经确保了池内的token0地址小于token1地址, 所以当用户想用tokenIn兑换tokenOut的时候, 也就是tokenIn=>tokenOut, 那么需要判断它俩地址的大小, 当tokenIn<tokenOut的时候, tokenIn等于池内的token0, tokenOut等于池内的token1, 也就是token0=>token1. tokenIn>tokenOut的时候, tokenIn等于池内的token1, tokenOut等于池内的token0, 也就是token1=>token0

2.对于每一个流动池, 池内的token0是基础货币, token1是计价货币, 池内的价格指的是token0的价格, 以token1计价, 所以用token0兑换token1(token0=>token1), 可以理解为卖出操作, 对于卖出而言, sqrtPriceLimitX96指的是可以接受的最低卖出价格. 而用token1兑换token0(token1=>token0), 可以理解为买入操作, 对于买入而言, sqrtPriceLimitX96指的是可以接受的最高买入价格. 最后需要再次强调的是一个交易对在流动池里面只会以交易对中两个token合约地址大小来确定顺序, 而不会考虑日常的习惯, 比如交易对LINK/USDT, 假如LINK合约地址大于USDT合约地址, 那么它们在流动池里面实际是USDT/LINK, 也就是说token0是USDT, token1是LINK.

3.可以用checkDeadline修改器来实现一笔订单的有效期检查, 代码位于contracts\base\PeripheryValidation.sol, 参考代码如下:
abstract contract PeripheryValidation is BlockTimestamp {
    modifier checkDeadline(uint256 deadline) {
        require(_blockTimestamp() <= deadline, 'Transaction too old'); #_blockTimestamp()内部就是block.timestamp
        _;
    }
}
代码其实很简单, 就是通过检查当前区块时间来判断订单是否过期


前面有提到复杂路径问题, 比如ETH->DAI, 可以直接在ETH/DAI流动池兑换, 也可以利用ETH->USDC->DAI这个路径, 通过ETH/USDC, USDC/DAI两个流动池接力完成兑换, 之所以需要复杂路径一般是出于两方面考虑:

1.简单路径下可能没有对应的流动池可以交易, 比如你想用A兑换B, 可能Uniswap并没有支持A/B这个交易对的流动池, 但是有A/C和B/C, 那么你可以通过这两个流动池组合完成整个兑换动作

2.为了优化成本, 假如A/B这个池流动性不好, 但是A/C和B/C两个池流动性很好, 那么大概率利用这两个池的组合完成的交易比单独用A/B这个池完成的交易费用要更低

关于优化交易成本的问题这里可以稍微展开说说, 其实SwapRouter合约只实现了纵向路径, 比如前面说的ETH->DAI, 可以直接在ETH/DAI流动池兑换, 也可以利用ETH->USDC->DAI这个路径, 这种是属于纵向路径, 其实还可以采用横向路径, 也有机会能节约交易成本, Uniswap V3中同一个交易对是可能会对应多个不同手续费的流动池的, 比如ETH/USDT, 可能就会有一个千三手续费的流动池和一个万五手续费的流动池, 所以你在做一笔大额交易的时候, 如果将订单进行横向拆分, 在两个池分别进行交易, 如果滑点损失小到一定程度, 那这个横向路径的交易可能比单池交易会更优.

Uniswap交易成本可以拆成三部分:
1.链上gas费
2.固定的交易手续费, 比如千三, 万五等
3.滑点, 受流动性大小的影响

不管是横向路径的拆分还是纵向路径的拆分, 亦或者横向纵向相结合, 他们相对于单路径单流动池的交易成本的比较如下表:
路径\成本
gas费
交易手续费
滑点
总成本
纵向路径拆分
上升(代码更多)取决于选择的池子
取决于池子流动性
三者相加
横向路径拆分
上升(代码更多)
取决于选择的池子
取决于池子流动性
三者相加
首先, gas费肯定会上升, 因为代码更多更复杂, 但是如果选择得当, 交易手续费和滑点是可能下降的, 特别是滑点在大额交易的时候比较明显, 这个领域做的最好的项目就是1inch, 以后有时间可以单独拿出来分析.


从前面的代码可以看出, Uniswap V3主要是针对ERC20标准的交易对进行操作, 那么如果交易对中包括以太坊上的原生代币ETH的话, 比如ETH/USDT, 这种情况下系统内部其实会隐式的先把ETH一比一转换成ERC20标准的包装代币WETH, 变成WETH/USDT, 操作完后再将WETH兑换回ETH, 相关代码位于contracts\base\PeripheryPayments.sol, 参考代码如下:
abstract contract PeripheryPayments is IPeripheryPayments, PeripheryImmutableState {
    receive() external payable {
        require(msg.sender == WETH9, 'Not WETH9');
    }

    /// @inheritdoc IPeripheryPayments
    function unwrapWETH9(uint256 amountMinimum, address recipient) external payable override {
        uint256 balanceWETH9 = IWETH9(WETH9).balanceOf(address(this));
        require(balanceWETH9 >= amountMinimum, 'Insufficient WETH9');
        if (balanceWETH9 > 0) {
            IWETH9(WETH9).withdraw(balanceWETH9); #WETH转换成ETH
            TransferHelper.safeTransferETH(recipient, balanceWETH9);
        }
    }

    /// @inheritdoc IPeripheryPayments
    function sweepToken(
        address token,
        uint256 amountMinimum,
        address recipient
    ) external payable override {
        uint256 balanceToken = IERC20(token).balanceOf(address(this));
        require(balanceToken >= amountMinimum, 'Insufficient token');
        if (balanceToken > 0) {
            TransferHelper.safeTransfer(token, recipient, balanceToken);
        }
    }

    /// @inheritdoc IPeripheryPayments
    function refundETH() external payable override {
        if (address(this).balance > 0) TransferHelper.safeTransferETH(msg.sender, address(this).balance);
    }

    /// @param token The token to pay
    /// @param payer The entity that must pay
    /// @param recipient The entity that will receive payment
    /// @param value The amount to pay
    function pay(
        address token,
        address payer,
        address recipient,
        uint256 value
    ) internal {
        if (token == WETH9 && address(this).balance >= value) {
            // pay with WETH9
            IWETH9(WETH9).deposit{value: value}(); // ETH转换成WETH
            IWETH9(WETH9).transfer(recipient, value);
        } else if (payer == address(this)) {
            // pay with tokens already in the contract (for the exact input multihop case)
            TransferHelper.safeTransfer(token, recipient, value);
        } else {
            // pull payment
            TransferHelper.safeTransferFrom(token, payer, recipient, value);
        }
    }
}

其中WETH9合约的参考代码如下:
contract WETH9 {
    string public name     = "Wrapped Ether";
    string public symbol   = "WETH";
    uint8  public decimals = 18;

    event  Approval(address indexed src, address indexed guy, uint wad);
    event  Transfer(address indexed src, address indexed dst, uint wad);
    event  Deposit(address indexed dst, uint wad);
    event  Withdrawal(address indexed src, uint wad);

    mapping (address => uint)                       public  balanceOf;
    mapping (address => mapping (address => uint))  public  allowance;

    function() public payable { #当仅仅接收到ETH转账时此函数默认会被调用
        deposit();
    }

    function deposit() public payable {
        balanceOf[msg.sender] += msg.value; #ETH一比一兑换成WETH
        Deposit(msg.sender, msg.value);
    }

    function withdraw(uint wad) public {
        require(balanceOf[msg.sender] >= wad);
        balanceOf[msg.sender] -= wad; #减少该用户一定量的WETH
        msg.sender.transfer(wad); #将等量的ETH发还给用户
        Withdrawal(msg.sender, wad);
    }

    function totalSupply() public view returns (uint) {
        return this.balance;
    }

    function approve(address guy, uint wad) public returns (bool) {
        allowance[msg.sender][guy] = wad;
        Approval(msg.sender, guy, wad);
        return true;
    }

    function transfer(address dst, uint wad) public returns (bool) {
        return transferFrom(msg.sender, dst, wad);
    }

    function transferFrom(address src, address dst, uint wad)
        public
        returns (bool)
    {
        require(balanceOf[src] >= wad);
        if (src != msg.sender && allowance[src][msg.sender] != uint(-1)) {
            require(allowance[src][msg.sender] >= wad);
            allowance[src][msg.sender] -= wad;
        }
        balanceOf[src] -= wad;
        balanceOf[dst] += wad;
        Transfer(src, dst, wad);
        return true;
    }
}

最后看一下链下前端如何调用交易接口, 这个和之前调用NonfungiblePositionManager合约一样, 也需要通过multicall, 相关代码位于:v3-sdk\src\swapRouter.ts, 参考代码如下:
public static swapCallParameters(
    trades: Trade<Currency, Currency, TradeType> | Trade<Currency, Currency, TradeType>[],
    options: SwapOptions
): MethodParameters {
    ...
    const calldatas: string[] = []
    ...
    // encode permit if necessary
    if (options.inputTokenPermit) {
        invariant(sampleTrade.inputAmount.currency.isToken, 'NON_TOKEN_PERMIT')
        calldatas.push(SelfPermit.encodePermit(sampleTrade.inputAmount.currency, options.inputTokenPermit))
    }
    ...
    for (const trade of trades) {
        for (const { route, inputAmount, outputAmount } of trade.swaps) {
            ...
            // flag for whether the trade is single hop or not
            const singleHop = route.pools.length === 1
            if (singleHop) {
                if (trade.tradeType === TradeType.EXACT_INPUT) {
                    ...
                    calldatas.push(SwapRouter.INTERFACE.encodeFunctionData('exactInputSingle', [exactInputSingleParams]))
                } else {
                    ...
                    calldatas.push(SwapRouter.INTERFACE.encodeFunctionData('exactOutputSingle', [exactOutputSingleParams]))
                }
            } else {
                ...
                if (trade.tradeType === TradeType.EXACT_INPUT) {
                    ...
                    calldatas.push(SwapRouter.INTERFACE.encodeFunctionData('exactInput', [exactInputParams]))
                } else {
                    ...
                    calldatas.push(SwapRouter.INTERFACE.encodeFunctionData('exactOutput', [exactOutputParams]))
                }
            }
        }
    }

    // unwrap
    if (routerMustCustody) {
        if (!!options.fee) {
            if (outputIsNative) {
                calldatas.push(Payments.encodeUnwrapWETH9(totalAmountOut.quotient, recipient, options.fee))
            } else {
              calldatas.push(
                  Payments.encodeSweepToken(
                  sampleTrade.outputAmount.currency.wrapped,
                  totalAmountOut.quotient,
                  recipient,
                  options.fee))
            }
        } else {
            calldatas.push(Payments.encodeUnwrapWETH9(totalAmountOut.quotient, recipient))
        }
    }

    // refund
    if (mustRefund) {
        calldatas.push(Payments.encodeRefundETH())
    }

    return {
        calldata: Multicall.encodeMulticall(calldatas),
        value: toHex(totalValue.quotient)
    }
}
从上面代码可以看到, 其实就是根据业务逻辑将 [token授权转账权限, 交易兑换, WETH/ETH转换] 等操作, 整体打包成一次multicall调用, 要么全部成功, 要么全部失败