死磕Uniswap V2(六):Router与路由

本文是「死磕Uniswap V2」系列的第六篇,深入剖析V2的Router合约与路由机制。

系列导航

序号标题核心内容
01V2概述与核心原理恒定乘积AMM、核心公式
02Factory与Pair合约合约结构、创建流程
03流动性与LP代币mint/burn、份额计算
04交换机制深度解析swap函数、滑点、Flash Swap
05价格预言机TWAP、价格计算
06Router与路由最佳路径、多跳交易
07安全实践与最佳实践漏洞防护、开发建议

1. Router合约概览

1.1 Router的职责

graph TB
    subgraph Router[\"Router合约职责\"]
        R1[\"封装Pair交互\"]
        R2[\"多跳路由\"]
        R3[\"流动性管理\"]
        R4[\"滑点保护\"]
    end

    subgraph Benefits[\"用户收益\"]
        B1[\"简化交易\"]
        B2[\"最佳路径\"]
        B3[\"原子化执行\"]
    end

    Router --> Benefits

    style Benefits fill:#c8e6c9

1.2 Router vs 直接调用Pair

特性直接调用Pair通过Router
代码复杂度
多跳支持需手动实现内置支持
流动性添加复杂简化API
滑点保护需自己实现内置支持
WETH处理需手动包装/解包自动处理

1.3 Router合约架构

classDiagram
    class UniswapV2Router02 {
        +address factory
        +address WETH
        +swapExactTokensForTokens()
        +swapTokensForExactTokens()
        +swapExactETHForTokens()
        +swapTokensForExactETH()
        +swapExactTokensForETH()
        +swapETHForExactTokens()
        +addLiquidity()
        +addLiquidityETH()
        +removeLiquidity()
        +removeLiquidityETH()
    }

    class IWETH {
        +deposit()
        +withdraw()
    }

    class IUniswapV2Factory {
        +getPair()
        +createPair()
    }

    class IUniswapV2Pair {
        +swap()
        +getReserves()
    }

    UniswapV2Router02 --> IWETH
    UniswapV2Router02 --> IUniswapV2Factory
    UniswapV2Router02 --> IUniswapV2Pair

2. Router核心功能

2.1 Router合约变量

contract UniswapV2Router02 is IUniswapV2Router02 {
    /// @notice Factory合约地址
    address public immutable factory;
 
    /// @notice WETH地址
    address public immutable WETH;
 
    constructor(address _factory, address _WETH) {
        factory = _factory;
        WETH = _WETH;
    }
}

2.2 swapExactTokensForTokens

function swapExactTokensForTokens(
    uint256 amountIn,           // 精确输入数量
    uint256 amountOutMin,       // 最小输出数量(滑点保护)
    address[] calldata path,     // 交易路径
    address to,                 // 接收地址
    uint256 deadline            // 截止时间
) external returns (uint256[] memory amounts) {
    // 1. 检查截止时间
    require(deadline >= block.timestamp, "UniswapV2Router: EXPIRED");
 
    // 2. 验证路径
    require(path.length >= 2, "Invalid path");
 
    // 3. 计算各步输出
    amounts = getAmountsOut(amountIn, path);
 
    // 4. 验证最小输出
    require(
        amounts[amounts.length - 1] >= amountOutMin,
        "UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT"
    );
 
    // 5. 执行交易
    _safeSwap(
        amounts,
        path,
        to
    );
}

2.3 swapTokensForExactTokens

function swapTokensForExactTokens(
    uint256 amountOut,          // 精确输出数量
    uint256 amountInMax,        // 最大输入数量
    address[] calldata path,
    address to,
    uint256 deadline
) external returns (uint256[] memory amounts) {
    require(deadline >= block.timestamp, "UniswapV2Router: EXPIRED");
 
    // 从输出反推输入
    amounts = getAmountsIn(amountOut, path);
 
    // 验证输入不超过最大值
    require(
        amounts[0] <= amountInMax,
        "UniswapV2Router: EXCESSIVE_INPUT_AMOUNT"
    );
 
    _safeSwap(amounts, path, to);
}

2.4 ETH相关交换函数

// ETH -> Tokens
function swapExactETHForTokens(
    uint256 amountOutMin,
    address[] calldata path,
    address to,
    uint256 deadline
) external payable returns (uint256[] memory amounts) {
    require(deadline >= block.timestamp, "UniswapV2Router: EXPIRED");
    require(path[0] == WETH, "UniswapV2Router: INVALID_PATH");
 
    amounts = getAmountsOut(msg.value, path);
    require(
        amounts[amounts.length - 1] >= amountOutMin,
        "UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT"
    );
 
    // 将ETH包装为WETH
    IWETH(WETH).deposit{value: amounts[0]}();
    assert(IWETH(WETH).transfer(UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]));
 
    _swap(amounts, path, to);
}
 
// Tokens -> ETH
function swapExactTokensForETH(
    uint256 amountIn,
    uint256 amountOutMin,
    address[] calldata path,
    address to,
    uint256 deadline
) external returns (uint256[] memory amounts) {
    require(deadline >= block.timestamp, "UniswapV2Router: EXPIRED");
    require(path[path.length - 1] == WETH, "UniswapV2Router: INVALID_PATH");
 
    amounts = getAmountsOut(amountIn, path);
    require(
        amounts[amounts.length - 1] >= amountOutMin,
        "UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT"
    );
 
    _safeSwap(amounts, path, address(this));
 
    // 将WETH解包为ETH
    IWETH(WETH).withdraw(amounts[amounts.length - 1]);
    TransferHelper.safeTransferETH(to, amounts[amounts.length - 1]);
}

3. 多跳交易机制

3.1 多跳交易流程

sequenceDiagram
    participant U as User
    participant R as Router
    participant P1 as Pair USDC/DAI
    participant P2 as Pair DAI/USDT
    participant P3 as Pair USDT/WETH

    U->>R: swapExactTokensForTokens(1000 USDC, [USDC, DAI, USDT, WETH])
    R->>R: getAmountsOut() 计算各步输出

    Note over R: amounts = [1000, 998, 997, 0.5]

    R->>U: transferFrom(1000 USDC)
    R->>P1: swap(0, 998 DAI, P2, data)
    P1->>P2: transfer(998 DAI)

    P2->>P2: swap(0, 997 USDT, P3, data)
    P2->>P3: transfer(997 USDT)

    P3->>P3: swap(0, 0.5 WETH, user, data)
    P3->>U: transfer(0.5 WETH)

3.2 _swap内部函数

function _swap(
    uint256[] memory amounts,
    address[] memory path,
    address _to
) internal virtual {
    for (uint256 i; i < path.length - 1; i++) {
        (address input, address output) = (path[i], path[i + 1]);
 
        // 确定token0和token1顺序
        (address token0, ) = UniswapV2Library.sortTokens(input, output);
        uint256 amountOut = amounts[i + 1];
 
        // 确定输出地址:最后一跳输出给用户,中间跳输出给下一个Pair
        (uint256 amount0Out, uint256 amount1Out) = input == token0
            ? (uint256(0), amountOut)
            : (amountOut, uint256(0));
 
        // 确定接收地址
        address to = i < path.length - 2
            ? UniswapV2Library.pairFor(factory, output, path[i + 2])
            : _to;
 
        // 获取Pair地址并执行swap
        IUniswapV2Pair(UniswapV2Library.pairFor(factory, input, output)).swap(
            amount0Out,
            amount1Out,
            to,
            new bytes(0)
        );
    }
}
 
function _safeSwap(
    uint256[] memory amounts,
    address[] memory path,
    address _to
) internal virtual {
    // 先转入代币到第一个Pair
    IERC20(path[0]).safeTransferFrom(
        msg.sender,
        UniswapV2Library.pairFor(factory, path[0], path[1]),
        amounts[0]
    );
 
    _swap(amounts, path, _to);
}

3.3 getAmountsOut函数

function getAmountsOut(
    uint256 amountIn,
    address[] memory path
) public view returns (uint256[] memory amounts) {
    require(path.length >= 2, "UniswapV2Library: INVALID_PATH");
 
    amounts = new uint256[](path.length);
    amounts[0] = amountIn;
 
    for (uint256 i; i < path.length - 1; i++) {
        // 获取Pair储备量
        (uint256 reserveIn, uint256 reserveOut) = UniswapV2Library.getReserves(
            factory,
            path[i],
            path[i + 1]
        );
 
        // 计算输出
        amounts[i + 1] = getAmountOut(amounts[i], reserveIn, reserveOut);
    }
}

3.4 getAmountsIn函数

function getAmountsIn(
    uint256 amountOut,
    address[] memory path
) public view returns (uint256[] memory amounts) {
    require(path.length >= 2, "UniswapV2Library: INVALID_PATH");
 
    amounts = new uint256[](path.length);
    amounts[amounts.length - 1] = amountOut;
 
    for (uint256 i = path.length - 1; i > 0; i--) {
        (uint256 reserveIn, uint256 reserveOut) = UniswapV2Library.getReserves(
            factory,
            path[i - 1],
            path[i]
        );
 
        amounts[i - 1] = getAmountIn(amounts[i], reserveIn, reserveOut);
    }
}

4. 路径查找算法

4.1 路径搜索问题

graph TB
    subgraph Graph[\"交易路径图\"]
        USDC[USDC]
        DAI[DAI]
        USDT[USDT]
        WETH[WETH]
        WBTC[WBTC]

        USDC --> DAI
        USDC --> USDT
        USDC --> WETH
        DAI --> USDT
        DAI --> WETH
        USDT --> WETH
        WETH --> WBTC
    end

    subgraph Paths[\"USDC → WBTC 可能路径\"]
        P1[\"USDC → WETH → WBTC<br/>2跳\"]
        P2[\"USDC → DAI → WETH → WBTC<br/>3跳\"]
        P3[\"USDC → USDT → WETH → WBTC<br/>3跳\"]
    end

    Graph --> Paths

    style P1 fill:#c8e6c9

4.2 简单路径查找

contract PathFinder {
    address public immutable factory;
 
    struct Route {
        address[] path;
        uint256 amountOut;
    }
 
    constructor(address _factory) {
        factory = _factory;
    }
 
    /// @notice 查找最佳路径(简化版)
    function findBestPath(
        address tokenIn,
        address tokenOut,
        uint256 amountIn
    ) external view returns (address[] memory bestPath, uint256 bestAmount) {
        // 1. 尝试直接交易对
        address directPair = IUniswapV2Factory(factory).getPair(tokenIn, tokenOut);
        if (directPair != address(0)) {
            (uint256 reserveIn, uint256 reserveOut, ) = IUniswapV2Pair(directPair).getReserves();
            if (reserveIn > 0 && reserveOut > 0) {
                bestPath = new address[](2);
                bestPath[0] = tokenIn;
                bestPath[1] = tokenOut;
                bestAmount = getAmountOut(amountIn, reserveIn, reserveOut);
            }
        }
 
        // 2. 尝试通过WETH中转
        address weth = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; // Mainnet WETH
 
        if (tokenIn != weth && tokenOut != weth) {
            uint256 amountViaWeth = getAmountViaWeth(tokenIn, tokenOut, amountIn);
 
            if (amountViaWeth > bestAmount) {
                bestPath = new address[](3);
                bestPath[0] = tokenIn;
                bestPath[1] = weth;
                bestPath[2] = tokenOut;
                bestAmount = amountViaWeth;
            }
        }
 
        return (bestPath, bestAmount);
    }
 
    /// @notice 通过WETH中转计算输出
    function getAmountViaWeth(
        address tokenIn,
        address tokenOut,
        uint256 amountIn
    ) internal view returns (uint256 amountOut) {
        address weth = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;
 
        // 第一步: tokenIn -> WETH
        address pair1 = IUniswapV2Factory(factory).getPair(tokenIn, weth);
        if (pair1 == address(0)) return 0;
 
        (uint256 reserveIn1, uint256 reserveOut1, ) = IUniswapV2Pair(pair1).getReserves();
        // 确定储备顺序
        if (IERC20(tokenIn) > IERC20(weth)) {
            (reserveIn1, reserveOut1) = (reserveOut1, reserveIn1);
        }
        uint256 amountWeth = getAmountOut(amountIn, reserveIn1, reserveOut1);
 
        // 第二步: WETH -> tokenOut
        address pair2 = IUniswapV2Factory(factory).getPair(weth, tokenOut);
        if (pair2 == address(0)) return 0;
 
        (uint256 reserveIn2, uint256 reserveOut2, ) = IUniswapV2Pair(pair2).getReserves();
        if (IERC20(weth) > IERC20(tokenOut)) {
            (reserveIn2, reserveOut2) = (reserveOut2, reserveIn2);
        }
        amountOut = getAmountOut(amountWeth, reserveIn2, reserveOut2);
    }
 
    function getAmountOut(
        uint256 amountIn,
        uint256 reserveIn,
        uint256 reserveOut
    ) internal pure returns (uint256) {
        uint256 amountInWithFee = amountIn * 997;
        uint256 numerator = amountInWithFee * reserveOut;
        uint256 denominator = reserveIn * 1000 + amountInWithFee;
        return numerator / denominator;
    }
}

4.3 广度优先搜索

contract BFSPathFinder {
    address public immutable factory;
    address public immutable weth;
 
    uint256 public constant MAX_HOPS = 3;
 
    constructor(address _factory, address _weth) {
        factory = _factory;
        weth = _weth;
    }
 
    /// @notice BFS查找最佳路径
    function findBestPathBFS(
        address tokenIn,
        address tokenOut,
        uint256 amountIn,
        address[] memory baseTokens
    ) external view returns (address[] memory bestPath, uint256 bestAmount) {
        bestAmount = 0;
 
        // 尝试直接路径
        (bestPath, bestAmount) = _tryDirectPath(tokenIn, tokenOut, amountIn);
 
        // 尝试通过baseToken中转
        for (uint256 i = 0; i < baseTokens.length; i++) {
            address base = baseTokens[i];
 
            // 避免无效路径
            if (base == tokenIn || base == tokenOut) continue;
 
            (address[] memory path, uint256 amount) = _tryTwoHopPath(
                tokenIn,
                base,
                tokenOut,
                amountIn
            );
 
            if (amount > bestAmount) {
                bestPath = path;
                bestAmount = amount;
            }
        }
 
        return (bestPath, bestAmount);
    }
 
    function _tryDirectPath(
        address tokenIn,
        address tokenOut,
        uint256 amountIn
    ) internal view returns (address[] memory path, uint256 amount) {
        address pair = IUniswapV2Factory(factory).getPair(tokenIn, tokenOut);
        if (pair == address(0)) return (new address[](0), 0);
 
        (uint112 reserve0, uint112 reserve1, ) = IUniswapV2Pair(pair).getReserves();
        if (reserve0 == 0 || reserve1 == 0) return (new address[](0), 0);
 
        path = new address[](2);
        path[0] = tokenIn;
        path[1] = tokenOut;
 
        (uint256 reserveIn, uint256 reserveOut) = tokenIn < tokenOut
            ? (reserve0, reserve1)
            : (reserve1, reserve0);
 
        amount = _getAmountOut(amountIn, reserveIn, reserveOut);
    }
 
    function _tryTwoHopPath(
        address tokenIn,
        address midToken,
        address tokenOut,
        uint256 amountIn
    ) internal view returns (address[] memory path, uint256 amount) {
        // 第一段: tokenIn -> midToken
        address pair1 = IUniswapV2Factory(factory).getPair(tokenIn, midToken);
        if (pair1 == address(0)) return (new address[](0), 0);
 
        (uint112 reserve0a, uint112 reserve1a, ) = IUniswapV2Pair(pair1).getReserves();
        if (reserve0a == 0 || reserve1a == 0) return (new address[](0), 0);
 
        (uint256 reserveIn1, uint256 reserveOut1) = tokenIn < midToken
            ? (reserve0a, reserve1a)
            : (reserve1a, reserve0a);
 
        uint256 midAmount = _getAmountOut(amountIn, reserveIn1, reserveOut1);
        if (midAmount == 0) return (new address[](0), 0);
 
        // 第二段: midToken -> tokenOut
        address pair2 = IUniswapV2Factory(factory).getPair(midToken, tokenOut);
        if (pair2 == address(0)) return (new address[](0), 0);
 
        (uint112 reserve0b, uint112 reserve1b, ) = IUniswapV2Pair(pair2).getReserves();
        if (reserve0b == 0 || reserve1b == 0) return (new address[](0), 0);
 
        (uint256 reserveIn2, uint256 reserveOut2) = midToken < tokenOut
            ? (reserve0b, reserve1b)
            : (reserve1b, reserve0b);
 
        amount = _getAmountOut(midAmount, reserveIn2, reserveOut2);
 
        path = new address[](3);
        path[0] = tokenIn;
        path[1] = midToken;
        path[2] = tokenOut;
    }
 
    function _getAmountOut(
        uint256 amountIn,
        uint256 reserveIn,
        uint256 reserveOut
    ) internal pure returns (uint256) {
        uint256 amountInWithFee = amountIn * 997;
        uint256 numerator = amountInWithFee * reserveOut;
        uint256 denominator = reserveIn * 1000 + amountInWithFee;
        return numerator / denominator;
    }
}

5. 流动性管理功能

5.1 addLiquidity

function addLiquidity(
    address tokenA,
    address tokenB,
    uint256 amountADesired,
    uint256 amountBDesired,
    uint256 amountAMin,
    uint256 amountBMin,
    address to,
    uint256 deadline
) external returns (uint256 amountA, uint256 amountB, uint256 liquidity) {
    require(deadline >= block.timestamp, "UniswapV2Router: EXPIRED");
 
    // 1. 确保token顺序
    (tokenA, tokenB) = UniswapV2Library.sortTokens(tokenA, tokenB);
 
    // 2. 获取Pair储备量
    (uint256 reserveA, uint256 reserveB) = UniswapV2Library.getReserves(
        factory,
        tokenA,
        tokenB
    );
 
    // 3. 如果Pair不存在,创建
    if (IUniswapV2Factory(factory).getPair(tokenA, tokenB) == address(0)) {
        IUniswapV2Factory(factory).createPair(tokenA, tokenB);
    }
 
    // 4. 计算最优添加数量(按比例)
    uint256 amountAOptimal = UniswapV2Library.quote(
        amountADesired,
        reserveA,
        reserveB
    );
 
    if (amountAOptimal <= amountADesired) {
        require(
            amountAOptimal >= amountAMin,
            "UniswapV2Router: INSUFFICIENT_A_AMOUNT"
        );
        (amountA, amountB) = (amountAOptimal, amountBDesired);
    } else {
        uint256 amountBOptimal = UniswapV2Library.quote(
            amountBDesired,
            reserveB,
            reserveA
        );
        require(
            amountBOptimal >= amountBMin,
            "UniswapV2Router: INSUFFICIENT_B_AMOUNT"
        );
        (amountA, amountB) = (amountADesired, amountBOptimal);
    }
 
    // 5. 转入代币
    TransferHelper.safeTransferFrom(tokenA, msg.sender, pair, amountA);
    TransferHelper.safeTransferFrom(tokenB, msg.sender, pair, amountB);
 
    // 6. 调用Pair的mint
    liquidity = IUniswapV2Pair(pair).mint(to);
}

5.2 addLiquidityETH

function addLiquidityETH(
    address token,
    uint256 amountTokenDesired,
    uint256 amountTokenMin,
    uint256 amountETHMin,
    address to,
    uint256 deadline
) external payable returns (uint256 amountToken, uint256 amountETH, uint256 liquidity) {
    require(deadline >= block.timestamp, "UniswapV2Router: EXPIRED");
 
    // 1. 包装ETH为WETH
    IWETH(WETH).deposit{value: msg.value}();
    assert(IWETH(WETH).transfer(UniswapV2Library.pairFor(factory, token, WETH), msg.value));
 
    // 2. 获取储备量
    (uint256 reserveToken, uint256 reserveETH) = UniswapV2Library.getReserves(
        factory,
        token,
        WETH
    );
 
    // 3. 计算最优token数量
    uint256 amountTokenOptimal = UniswapV2Library.quote(
        msg.value,
        reserveETH,
        reserveToken
    );
 
    if (amountTokenOptimal <= amountTokenDesired) {
        require(
            amountTokenOptimal >= amountTokenMin,
            "UniswapV2Router: INSUFFICIENT_TOKEN_AMOUNT"
        );
        (amountToken, amountETH) = (amountTokenOptimal, msg.value);
    } else {
        uint256 amountETHOptimal = UniswapV2Library.quote(
            amountTokenDesired,
            reserveToken,
            reserveETH
        );
        require(
            amountETHOptimal >= amountETHMin,
            "UniswapV2Router: INSUFFICIENT_ETH_AMOUNT"
        );
        (amountToken, amountETH) = (amountTokenDesired, amountETHOptimal);
 
        // 退还多余ETH
        IWETH(WETH).withdraw(msg.value - amountETHOptimal);
        TransferHelper.safeTransferETH(msg.sender, msg.value - amountETHOptimal);
    }
 
    // 4. 转入token
    TransferHelper.safeTransferFrom(
        token,
        msg.sender,
        UniswapV2Library.pairFor(factory, token, WETH),
        amountToken
    );
 
    // 5. Mint LP代币
    liquidity = IUniswapV2Pair(UniswapV2Library.pairFor(factory, token, WETH)).mint(to);
 
    // 退还剩余WETH(如果有)
    if (msg.value > amountETH) {
        IWETH(WETH).withdraw(msg.value - amountETH);
        TransferHelper.safeTransferETH(msg.sender, msg.value - amountETH);
    }
}

5.3 removeLiquidity

function removeLiquidity(
    address tokenA,
    address tokenB,
    uint256 liquidity,
    uint256 amountAMin,
    uint256 amountBMin,
    address to,
    uint256 deadline
) public returns (uint256 amountA, uint256 amountB) {
    require(deadline >= block.timestamp, "UniswapV2Router: EXPIRED");
 
    // 1. 获取Pair地址
    address pair = UniswapV2Library.pairFor(factory, tokenA, tokenB);
 
    // 2. 授权Router
    IUniswapV2Pair(pair).approve(address(this), liquidity);
 
    // 3. 转入LP代币
    TransferHelper.safeTransferFrom(
        msg.sender,
        pair,
        pair, // 转入Pair合约
        liquidity
    );
 
    // 4. 调用Pair的burn
    (uint256 amount0, uint256 amount1) = IUniswapV2Pair(pair).burn(to);
 
    // 5. 确定tokenA和tokenB对应的amount
    (amountA, amountB) = tokenA < tokenB
        ? (amount0, amount1)
        : (amount1, amount0);
 
    // 6. 验证最小输出
    require(amountA >= amountAMin, "UniswapV2Router: INSUFFICIENT_A_AMOUNT");
    require(amountB >= amountBMin, "UniswapV2Router: INSUFFICIENT_B_AMOUNT");
}

6. UniswapV2Library工具函数

6.1 sortTokens

library UniswapV2Library {
    /// @notice 排序两个token地址
    function sortTokens(address tokenA, address tokenB)
        internal
        pure
        returns (address token0, address token1)
    {
        require(tokenA != tokenB, "UniswapV2Library: IDENTICAL_ADDRESSES");
        (token0, token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA);
        require(token0 != address(0), "UniswapV2Library: ZERO_ADDRESS");
    }
}

6.2 pairFor

/// @notice 计算Pair地址(确定性计算)
function pairFor(
    address factory,
    address tokenA,
    address tokenB
) internal pure returns (address pair) {
    (address token0, address token1) = sortTokens(tokenA, tokenB);
    pair = address(
        uint160(
            uint256(
                keccak256(
                    abi.encodePacked(
                        hex"ff",
                        factory,
                        keccak256(abi.encodePacked(token0, token1)),
                        hex"96e8ac4277198ff8b6f785478aa9a39f403cb768dd02cbee326c3e7da348845f" // init code hash
                    )
                )
            )
        )
    );
}

6.3 getReserves

/// @notice 获取Pair储备量
function getReserves(
    address factory,
    address tokenA,
    address tokenB
) internal view returns (uint256 reserveA, uint256 reserveB) {
    (address token0, ) = sortTokens(tokenA, tokenB);
    (uint112 reserve0, uint112 reserve1, ) = IUniswapV2Pair(pairFor(factory, tokenA, tokenB)).getReserves();
    (reserveA, reserveB) = tokenA == token0 ? (reserve0, reserve1) : (reserve1, reserve0);
}

6.4 quote

/// @notice 按比例计算数量
function quote(
    uint256 amountA,
    uint256 reserveA,
    uint256 reserveB
) internal pure returns (uint256 amountB) {
    require(amountA > 0, "UniswapV2Library: INSUFFICIENT_AMOUNT");
    require(reserveA > 0 && reserveB > 0, "UniswapV2Library: INSUFFICIENT_LIQUIDITY");
    amountB = (amountA * reserveB) / reserveA;
}

7. Router使用示例

7.1 完整交易合约

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
 
import "@uniswap/v2-periphery/contracts/interfaces/IUniswapV2Router02.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
 
contract UniswapTrader {
    IUniswapV2Router02 public immutable router;
    address public immutable weth;
 
    event Swapped(
        address indexed tokenIn,
        address indexed tokenOut,
        uint256 amountIn,
        uint256 amountOut,
        address indexed recipient
    );
 
    constructor(address _router) {
        router = IUniswapV2Router02(_router);
        weth = router.WETH();
    }
 
    /// @notice 精确输入交易
    function swapExactTokensForTokens(
        uint256 amountIn,
        uint256 amountOutMin,
        address[] calldata path,
        address recipient
    ) external returns (uint256 amountOut) {
        // 授权Router
        IERC20(path[0]).approve(address(router), amountIn);
 
        // 转入代币
        IERC20(path[0]).transferFrom(msg.sender, address(this), amountIn);
 
        // 执行交易
        uint256[] memory amounts = router.swapExactTokensForTokens(
            amountIn,
            amountOutMin,
            path,
            recipient,
            block.timestamp
        );
 
        amountOut = amounts[amounts.length - 1];
 
        emit Swapped(path[0], path[path.length - 1], amountIn, amountOut, recipient);
    }
 
    /// @notice ETH交易
    function swapExactETHForTokens(
        uint256 amountOutMin,
        address[] calldata path,
        address recipient
    ) external payable returns (uint256 amountOut) {
        require(path[0] == weth, "Invalid path");
 
        uint256[] memory amounts = router.swapExactETHForTokens{value: msg.value}(
            amountOutMin,
            path,
            recipient,
            block.timestamp
        );
 
        amountOut = amounts[amounts.length - 1];
        emit Swapped(weth, path[path.length - 1], msg.value, amountOut, recipient);
    }
 
    /// @notice 代币换ETH
    function swapExactTokensForETH(
        uint256 amountIn,
        uint256 amountOutMin,
        address[] calldata path,
        address recipient
    ) external returns (uint256 amountOut) {
        require(path[path.length - 1] == weth, "Invalid path");
 
        IERC20(path[0]).approve(address(router), amountIn);
        IERC20(path[0]).transferFrom(msg.sender, address(this), amountIn);
 
        uint256[] memory amounts = router.swapExactTokensForETH(
            amountIn,
            amountOutMin,
            path,
            recipient,
            block.timestamp
        );
 
        amountOut = amounts[amounts.length - 1];
        emit Swapped(path[0], weth, amountIn, amountOut, recipient);
    }
 
    /// @notice 查询输出数量
    function getAmountsOut(
        uint256 amountIn,
        address[] calldata path
    ) external view returns (uint256[] memory) {
        return router.getAmountsOut(amountIn, path);
    }
 
    // 接收ETH
    receive() external payable {}
}

7.2 最佳路径查找器

contract BestPathFinder {
    IUniswapV2Router02 public immutable router;
    address public immutable factory;
 
    address[] public baseTokens;
 
    event BestPathFound(
        address indexed tokenIn,
        address indexed tokenOut,
        address[] path,
        uint256 amountOut
    );
 
    constructor(address _router, address[] memory _baseTokens) {
        router = IUniswapV2Router02(_router);
        factory = router.factory();
        baseTokens = _baseTokens;
    }
 
    /// @notice 查找最佳路径并执行交易
    function findBestPathAndSwap(
        uint256 amountIn,
        uint256 amountOutMin,
        address tokenIn,
        address tokenOut,
        address recipient
    ) external returns (uint256 amountOut) {
        (address[] memory bestPath, uint256 bestAmount) = _findBestPath(
            tokenIn,
            tokenOut,
            amountIn
        );
 
        require(bestPath.length > 0, "No valid path found");
        require(bestAmount >= amountOutMin, "Insufficient output amount");
 
        // 授权并转入代币
        IERC20(tokenIn).approve(address(router), amountIn);
        IERC20(tokenIn).transferFrom(msg.sender, address(this), amountIn);
 
        // 执行交易
        uint256[] memory amounts = router.swapExactTokensForTokens(
            amountIn,
            amountOutMin,
            bestPath,
            recipient,
            block.timestamp
        );
 
        amountOut = amounts[amounts.length - 1];
 
        emit BestPathFound(tokenIn, tokenOut, bestPath, amountOut);
    }
 
    /// @notice 查找最佳路径
    function _findBestPath(
        address tokenIn,
        address tokenOut,
        uint256 amountIn
    ) internal view returns (address[] memory bestPath, uint256 bestAmount) {
        // 尝试直接路径
        address directPair = IUniswapV2Factory(factory).getPair(tokenIn, tokenOut);
        if (directPair != address(0)) {
            bestPath = new address[](2);
            bestPath[0] = tokenIn;
            bestPath[1] = tokenOut;
 
            uint256[] memory amounts = router.getAmountsOut(amountIn, bestPath);
            bestAmount = amounts[1];
        }
 
        // 尝试通过baseToken中转
        for (uint256 i = 0; i < baseTokens.length; i++) {
            address base = baseTokens[i];
            if (base == tokenIn || base == tokenOut) continue;
 
            address[] memory path = new address[](3);
            path[0] = tokenIn;
            path[1] = base;
            path[2] = tokenOut;
 
            // 检查两个Pair是否都存在
            if (IUniswapV2Factory(factory).getPair(tokenIn, base) == address(0)) continue;
            if (IUniswapV2Factory(factory).getPair(base, tokenOut) == address(0)) continue;
 
            uint256[] memory amounts = router.getAmountsOut(amountIn, path);
 
            if (amounts[2] > bestAmount) {
                bestPath = path;
                bestAmount = amounts[2];
            }
        }
    }
}

8. 本章小结

8.1 Router总结

mindmap
  root((Router与路由))
    Router功能
      封装Pair交互
      多跳交易
      WETH处理
      流动性管理
    核心函数
      swapExactTokensForTokens
      swapTokensForExactTokens
      addLiquidity
      removeLiquidity
    路径查找
      直接路径
      单跳中转
      多跳路径
      最佳路径
    Library工具
      sortTokens
      pairFor
      getReserves
      quote

8.2 关键函数回顾

函数功能特点
swapExactTokensForTokens()精确输入指定输入、最小输出
swapTokensForExactTokens()精确输出指定输出、最大输入
getAmountsOut()查询输出计算路径输出
addLiquidity()添加流动性按比例添加
removeLiquidity()移除流动性销毁LP赎回

下一篇预告

在下一篇文章中,我们将深入探讨安全实践与最佳实践,包括:

  • 常见安全漏洞
  • 滑点保护策略
  • MEV防护
  • 审计检查清单

参考资料