# 剖析DeFi交易产品之UniswapV3：交易路由合约

By [Keegan小钢](https://paragraph.com/@keeganlee) · 2023-11-12

---

**SwapRouter** 合约封装了面向用户的交易接口，但不再像 **UniswapV2Router** 一样根据不同交易场景拆分为了那么多函数，UniswapV3 的 SwapRouter 核心就只有 4 个交易函数：

*   `exactInputSingle`：指定输入数量的单池内交易
    
*   `exactOutputSingle`：指定输出数量的单池内交易
    
*   `exactInput`：指定输入数量和交易路径的交易
    
*   `exactOutput`：指定输出数量和交易路径的交易
    

带 `Single` 的只支持单池内的交易，而不带 `Single` 的则支持跨不同池子的互换交易。

### exactInputSingle

先来看简单的单池交易，以 `exactInputSingle` 为始，其代码实现如下：

    struct ExactInputSingleParams {
        address tokenIn;   //输入token
        address tokenOut;  //输出token
        uint24 fee;        //手续费率
        address recipient; //收款地址
        uint256 deadline;  //过期时间
        uint256 amountIn;  //指定的输入token数量
        uint256 amountOutMinimum;  //输出token的最小数量
        uint160 sqrtPriceLimitX96; //限定的价格
    }
    
    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');
    }
    

其入参有 9 个参数，返回值就一个 `amountOut`，即输出的 token 数量。

从代码上可看出，实际的逻辑实现是在内部函数 `exactInputInternal`。查看该内部函数之前，我们先来了解下 `SwapCallbackData`。我们从上面代码可以看到，调用 `exactInputInternal` 时，最后一个传入的参数就是 `SwapCallbackData`，这其实是一个结构体，定义了两个属性：

    struct SwapCallbackData {
        bytes path;
        address payer;
    }
    

`path` 表示交易路径，在以上代码中，就是由 `tokenIn`、`fee`、`tokenOut` 这三个变量拼接而成。`payer` 表示支付输入 token 的地址，上面的就是 `msg.sender`。

接着，来看看内部函数 `exactInputInternal` 的代码实现：

    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();
        //当tokenIn<tokenOUt时，则说明tokenIn为token0，所以是要将token0兑换成token1
        bool zeroForOne = tokenIn < tokenOut;
        //调用底层池子的swap函数执行交易
        (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)
            );
        //返回amountOut
        return uint256(-(zeroForOne ? amount1 : amount0));
    }
    

首先，如果 recipient 地址为零地址的话，那会把 recipient 重置为当前合约地址。

接着，通过 `data.path.decodeFirstPool()` 从路径中解码得出 `tokenIn`、`tokenOut` 和 `fee`。`decodeFirstPool` 函数是在库合约 **Path** 里实现的。

布尔类型的 `zeroForOne` 表示底层 `token0` 和 `token1` 的兑换方向，为 `true` 表示用 `token0` 兑换 `token1`，`false` 则反之。因为底层的 `token0` 是小于 `token1` 的，所以，当 `tokenIn` 也小于 `tokenOut` 的时候，说明 `tokenIn == token0`，所以 `zeroForOne` 为 `true`。

然后，通过 `getPool` 函数可得到池子地址，再调用底层池子的 `swap` 函数来执行实际的交易逻辑。

最后，我们要得到的是 `amountOut`，这是 amount0 和 amount1 中的其中一个。我们已经知道，`zeroForOne` 为 `true` 的时候，`tokenIn` 等于 `token0`，所以 `tokenOut` 就是 `token1`，因此 `amountOut` 就是 `amount1`。另外，对底层池子来说，属于输出的时候，返回的数值是负数，即 `amount1` 其实是一个负数，因此需要再加个负号转为正数的 `uint256` 类型。

在这个函数里，我们可以看出并没有支付 token 的功能，但前面讲解 **UniswapV3Pool** 时已经了解到，支付是在回调函数 `uniswapV3SwapCallback` 里完成的。因为这个回调函数会涉及到所有 4 种交易类型，所以我们留到最后再来讲解。

### exactOutputSingle

接着，来看 `exactOutputSingle` 函数的实现，其代码如下：

    struct ExactOutputSingleParams {
        address tokenIn;   //输入token
        address tokenOut;  //输出token
        uint24 fee;        //手续费率
        address recipient; //收款地址
        uint256 deadline;  //过期时间
        uint256 amountOut; //指定的输出token数量
        uint256 amountInMaximum;   //输入token的最大数量
        uint160 sqrtPriceLimitX96; //限定的价格
    }
    
    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;
    }
    

可看出，`exactOutputSingle` 函数的实现与 `exactInputSingle` 函数大同小异。首先，参数上，只有两个不同，`exactInputSingle` 函数指定的是 `amountIn` 和 `amountOutMinimum`；而 `exactOutputSingle` 函数改为了 `amountOut` 和 `amountInMaximum`，即输出是指定的，而输入则限制了最大值。其次，实际逻辑封装在了 `exactOutputInternal` 内部函数，而且传给该内部函数的最后一个参数的 `path` 组装顺序也不一样了，排在第一位的是 `tokenOut`。

核心实现还是在 `exactOutputInternal` 内部函数，其代码实现如下：

    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();
        //是否token0兑换token1
        bool zeroForOne = tokenIn < tokenOut;
        //调用底层池子的swap函数执行交易
        (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)
            );
            
        //确定amountIn和amountOut
        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);
    }
    

可见和 `exactInputInternal` 的实现也是大同小异。不过，有一个细节需要补充一下。因为是指定的输出数额，所以调用底层的 swap 函数时，第三个传参转为了负数，这也是前面讲解 UniswapV3Pool 的 swap 函数时讲过的，当指定的交易数额是输出的数额时，则需传负数。

和 `exactInputInternal` 一样，在当前函数里没有支付 token 的逻辑，也是统一在 `uniswapV3SwapCallback` 回调函数里去完成支付。

### exactInput

`exactInput` 函数则用于处理跨多个池子的指定输入数量的交易，相比单池交易会复杂一些，而且这里面的逻辑还有点绕，我们来进行一一剖析。其实现代码如下：

    struct ExactInputParams {
        bytes path;         //交易路径
        address recipient;  //收款地址
        uint256 deadline;   //过期时间
        uint256 amountIn;   //指定输入token数量
        uint256 amountOutMinimum; //输出token的最小数量
    }
    
    function exactInput(ExactInputParams memory params)
        external
        payable
        override
        checkDeadline(params.deadline)
        returns (uint256 amountOut)
    {
        //调用者需支付路径中的第一个代币
        address payer = msg.sender;
        //遍历路径
        while (true) {
            //路径中是否还存在多个池子
            bool hasMultiplePools = params.path.hasMultiplePools();
            //先前交换的输出成为后续交换的输入
            params.amountIn = exactInputInternal(
                params.amountIn,
                hasMultiplePools ? address(this) : params.recipient,
                0,
                SwapCallbackData({
                    path: params.path.getFirstPool(), // 只需要路径里的第一个池子
                    payer: payer
                })
            );
            //当路径依然由多个池子组成时，则继续循环，否则退出循环
            if (hasMultiplePools) {
                payer = address(this);
                //跳过第一个token，作为下一轮的路径
                params.path = params.path.skipToken();
            } else {
                //最后一次兑换，把前面设为了amountIn的重新赋值给amountOut
                amountOut = params.amountIn;
                break;
            }
        }
    
        require(amountOut >= params.amountOutMinimum, 'Too little received');
    }
    

其中，需要跨多个池子的路径编码方式如下图：

![](https://storage.googleapis.com/papyrus_images/9fee3ca69488caa0ebe0a4a186c1f69e6998699138c4c28530e0f47dc0702fb4.webp)

和 UniswapV2 一样，这个路径是由前端计算出来再传给合约的。寻找最优路径的算法也是和 UniswapV2 一样的思路。

`exactInput` 函数的核心实现逻辑是，循环处理路径中的每一个配对池，每处理完一个池子的交易，就从路径中移除第一个 token 和 fee，直到路径只剩下最后一个池子就结束循环。期间，每一次执行 `exactInputInternal` 后，将返回的 `amounOut` 作为下一轮的 `amountIn`。第一轮兑换时，`payer` 是合约的调用者，即 `msg.sender`，而输出代币的 `recipient` 则是当前合约地址。中间的每一次兑换，`payer` 和 `recipient` 都是当前合约地址。到最后一次兑换时，`recipient` 才转为用户传入的地址。

### exactOutput

剩下最后一个函数 `exactOutput` 了，也是用于处理跨多个池子的的交易，而指定的是输出的数量。以下是其代码实现：

    struct ExactOutputParams {
        bytes path;        //交易路径
        address recipient; //收款地址
        uint256 deadline;  //过期时间
        uint256 amountOut; //指定输出token数量
        uint256 amountInMaximum; //输入token的最大数量
    }
    
    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` 一样的循环处理。但在整个流程中，其实还是进行了遍历路径的多次交易的，只是这个流程完成得比较隐晦。其关键其实是在 `uniswapV3SwapCallback` 回调函数里，后面我们会说到。

### uniswapV3SwapCallback

以下就是回调函数的实现：

    function uniswapV3SwapCallback(
        int256 amount0Delta,
        int256 amount1Delta,
        bytes calldata _data
    ) external override {
        require(amount0Delta > 0 || amount1Delta > 0);
        //解码出_data数据
        SwapCallbackData memory data = abi.decode(_data, (SwapCallbackData));
        //解码出路径的第一个池子
        (address tokenIn, address tokenOut, uint24 fee) = data.path.decodeFirstPool();
        //校验callback的调用者
        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()) {
                // 路径里有多个池子时，则跳过路径的第一个token，使用下一个配对的池子进行交易
                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);
            }
        }
    }
    

另外，这个是 swap 时的回调函数。而之前的文章我们还讲了另一个回调函数 `uniswapV3MintCallback` 是添加流动性时的回调函数，两者是不同的，不要搞混了。

其逻辑实现并不复杂。首先，先把 `_data` 解码成 `SwapCallbackData` 结构体类型数据。接着，解码出路径的第一个池子。然后，通过 `verifyCallback` 校验调用当前回调函数的是否为底层 pool 合约，非底层 pool 合约是不允许调起回调函数的。

`isExactInput` 和 `amountToPay` 的赋值需要拆解一下才好理解。首先需知道，`amount0Delta` 和 `amount1Delta` 其实是一正一负的，正数是输入的，负数是输出的。因此，`amount0Delta` 大于 0 的话则 `amountToPay` 就是 `amount0Delta`，否则就是 `amount1Delta` 了。 `amount0Delta` 大于 0 也说明了输入的是 `token0`，因此，当 `tokenIn < tokenOut` 的时候，说明 `tokenIn` 就是 `token0`，也即是说用户指定的是输入数量，所以这时候的 `isExactInput` 即为 `true`。

当指定金额为输出的时候，也就是处理 `exactOutput` 和 `exactOutputSingle` 函数的时候。我们前面看到 `exactOutput` 的代码逻辑里并没有对路径进行遍历处理，这个遍历其实就是在这个回调函数里完成的。仔细看这段代码：

    if (data.path.hasMultiplePools()) {
        // 路径里有多个池子时，则跳过路径的第一个token，使用下一个配对的池子进行交易
        data.path = data.path.skipToken();
        exactOutputInternal(amountToPay, msg.sender, 0, data);
    }
    

这不就是遍历路径多次执行 `exactOutputInternal` 了吗。

至此，SwapRouter 合约也讲解完了。

---

*Originally published on [Keegan小钢](https://paragraph.com/@keeganlee/defi-uniswapv3-3)*
