Uniswap v2-Swap

2025-08-02T15:24:00

Uniswap v2

一、Swap

单个币种之间swap的流程

多个币种之间swap的流程

1.swapExactTokensForTokens 和 swapTokensForExactTokens

这个函数是 Uniswap V2 路由器合约中的一个重要功能,用于将确切数量的某种代币交换为另一种代币(可能经过多个交易对)。

// v2-periphery/contracts/UniswapV2Router02.sol
// swapExactTokensForTokens (精确输入,不确定输出)
// 功能:用确切数量的输入代币换取尽可能多的输出代币
function swapExactTokensForTokens(
        uint amountIn,
        uint amountOutMin,
        address[] calldata path,
        address to,
        uint deadline
    ) external virtual override ensure(deadline) returns (uint[] memory amounts) {
        amounts = UniswapV2Library.getAmountsOut(factory, amountIn, path);
        require(amounts[amounts.length - 1] >= amountOutMin, 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT');
        TransferHelper.safeTransferFrom(
            path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]
        );
        _swap(amounts, path, to);
    }

// swapTokensForExactTokens 精确输出,不确定输入
// 用尽可能少的输入代币换取确切数量的输出代币
function swapTokensForExactTokens(
        uint amountOut,
        uint amountInMax,
        address[] calldata path,
        address to,
        uint deadline
    ) external virtual override ensure(deadline) returns (uint[] memory amounts) {
        amounts = UniswapV2Library.getAmountsIn(factory, amountOut, path);
        require(amounts[0] <= amountInMax, 'UniswapV2Router: EXCESSIVE_INPUT_AMOUNT');
        TransferHelper.safeTransferFrom(
            path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]
        );
        _swap(amounts, path, to);
    }
参数解释
  • amountIn: 你想交换的输入代币数量
  • amountOutMin: 你愿意接受的最小输出代币数量(防止高滑点)
  • path: 交换路径(代币地址数组)
  • to: 接收输出代币的地址
  • deadline: 交易有效期限(Unix 时间戳)
path 参数

path 数组表示从代币 A 到代币 B 可能经过的所有中间代币。例如:

  • 直接交换:[代币A, 代币B]
  • 通过中间代币交换:[代币A, 代币WETH, 代币B]

函数会按照路径顺序一一交换,直到最后一个目标币种。

factory 参数

factory 确实是指 UniswapV2Factory 合约(核心合约之一),它负责创建和管理交易对。

amounts 数组

amounts 是通过 UniswapV2Library.getAmountsOut() 计算得到的数组,它包含:

  1. 沿交换路径每个阶段预期的代币数量
  2. 数组的第一个元素是 amountIn(输入数量)
  3. 最后一个元素是预期的输出代币数量
  4. 中间元素是每个中间交换步骤的预期数量

例如,对于路径 [A, B, C]amounts 可能看起来像 [100 A, 50 B, 25 C],表示:

  • 用 100 A 换 50 B
  • 再用 50 B 换 25 C
函数执行流程
  1. 计算沿路径的预期输出量 (getAmountsOut)
  2. 检查最终输出是否满足用户的最小要求 (amountOutMin)
  3. 将输入代币从用户转移到第一个交易对
  4. 执行沿路径的交换 (_swap 内部函数)
  5. 返回各阶段的实际交换数量

这种设计允许复杂的多跳交换,同时确保用户获得至少他们指定的最小输出量。

分叉主网测试getAmountsOut和getAmountsIn
forge test --fork-url $FORK_URL -vvv
// SPDX-License-Identifier: MIT
pragma solidity 0.8.24;

import {Test, console2} from "forge-std/Test.sol";
import {IERC20} from "../../src/interfaces/IERC20.sol";
import {IWETH} from "../../src/interfaces/IWETH.sol";
import {IUniswapV2Router02} from "../../src/interfaces/uniswap-v2/IUniswapV2Router02.sol";
import {DAI, WETH, MKR, UNISWAP_V2_ROUTER_02} from "../../src/Constants.sol";

contract UniswapV2SwapAmountsTest is Test {
    IWETH private constant weth = IWETH(WETH);
    IERC20 private constant dai = IERC20(DAI);
    IERC20 private constant mkr = IERC20(MKR);

    IUniswapV2Router02 private constant router = IUniswapV2Router02(UNISWAP_V2_ROUTER_02);

    function test_getAmountsOut() public {
        address[] memory path = new address[](3);
        path[0] = WETH;
        path[1] = DAI;
        path[2] = MKR;

        uint256 amountIn = 1e18;
        uint256[] memory amounts = router.getAmountsOut(amountIn, path);

        console2.log("WETH", amounts[0]);
        console2.log("DAI", amounts[1]);
        console2.log("MKR", amounts[2]);
    }
    
    function test_getAmountsIn() public {
        address[] memory path = new address[](3);
        path[0] = WETH;
        path[1] = DAI;
        path[2] = MKR;

        uint256 amountOut = 1e16;
        uint256[] memory amounts = router.getAmountsIn(amountOut, path);

        console2.log("WETH", amounts[0]);
        console2.log("DAI", amounts[1]);
        console2.log("MKR", amounts[2]);
    }
}

2._swap

最终执行两个token之间转换的核心函数

 // v2-periphery/contracts/UniswapV2Router02.sol
 function _swap(uint[] memory amounts, address[] memory path, address _to) internal virtual {
        for (uint i; i < path.length - 1; i++) {
            (address input, address output) = (path[i], path[i + 1]);
            (address token0,) = UniswapV2Library.sortTokens(input, output);
            uint amountOut = amounts[i + 1];
            (uint amount0Out, uint amount1Out) = input == token0 ? (uint(0), amountOut) : (amountOut, uint(0));
            address to = i < path.length - 2 ? UniswapV2Library.pairFor(factory, output, path[i + 2]) : _to;
            IUniswapV2Pair(UniswapV2Library.pairFor(factory, input, output)).swap(
                amount0Out, amount1Out, to, new bytes(0)
            );
        }
    }

函数参数

  • amounts:交易路径上每个代币的预期数量数组(由 getAmountsOut 计算得出)
  • path:代币交换路径(如 [代币A, 代币B, 代币C]
  • _to:最终接收代币的地址

核心逻辑分步解析

步骤1:遍历交易路径
for (uint i; i < path.length - 1; i++) {
    (address input, address output) = (path[i], path[i + 1]);
  • 循环处理路径中的每一跳(如 A→B→C 需要处理 A→B 和 B→C 两跳)
  • inputoutput 分别表示当前交易的输入代币和输出代币
步骤2:排序代币确定交易对
(address token0,) = UniswapV2Library.sortTokens(input, output);
  • Uniswap 交易对中的代币按地址排序存储(token0 < token1
  • 通过排序确定代币在交易对中的顺序(影响 amount0Outamount1Out 的赋值)
步骤3:确定输出量
uint amountOut = amounts[i + 1];
(uint amount0Out, uint amount1Out) = input == token0 ? (uint(0), amountOut) : (amountOut, uint(0));
  • amountOut 是当前交易对的预期输出量(来自 amounts 数组)
  • 关键规则

    • 如果 inputtoken0,则 amount0Out = 0amount1Out = amountOut(因为输入是 token0,输出是 token1)
    • 如果 inputtoken1,则 amount0Out = amountOutamount1Out = 0(输入是 token1,输出是 token0)
步骤4:确定接收地址
address to = i < path.length - 2 ? UniswapV2Library.pairFor(factory, output, path[i + 2]) : _to;
  • 如果是中间交易(如 A→B→C 中的 A→B):

    • to 是下一个交易对的地址(B/C 的交易对)
  • 如果是最后一跳(如 B→C):

    • to 是用户指定的最终地址 _to
步骤5:执行交易
IUniswapV2Pair(UniswapV2Library.pairFor(factory, input, output)).swap(
    amount0Out, amount1Out, to, new bytes(0)
);
  • 通过 pairFor 计算当前交易对的地址
  • 调用交易对的 swap 方法,传递输出量和接收地址
  • new bytes(0) 表示无回调数据(非闪电贷场景)

_swap调用了IUniswapV2Pair的pairFor.swap函数

// v2-core/contracts/UniswapV2Pair.sol
// this low-level function should be called from a contract which performs important safety checks
    function swap(uint amount0Out, uint amount1Out, address to, bytes calldata data) external lock {
        require(amount0Out > 0 || amount1Out > 0, 'UniswapV2: INSUFFICIENT_OUTPUT_AMOUNT');
        (uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings
        require(amount0Out < _reserve0 && amount1Out < _reserve1, 'UniswapV2: INSUFFICIENT_LIQUIDITY');

        uint balance0;
        uint balance1;
        { // scope for _token{0,1}, avoids stack too deep errors
        address _token0 = token0;
        address _token1 = token1;
        require(to != _token0 && to != _token1, 'UniswapV2: INVALID_TO');
        if (amount0Out > 0) _safeTransfer(_token0, to, amount0Out); // optimistically transfer tokens
        if (amount1Out > 0) _safeTransfer(_token1, to, amount1Out); // optimistically transfer tokens
        if (data.length > 0) IUniswapV2Callee(to).uniswapV2Call(msg.sender, amount0Out, amount1Out, data);
        balance0 = IERC20(_token0).balanceOf(address(this));
        balance1 = IERC20(_token1).balanceOf(address(this));
        }
        uint amount0In = balance0 > _reserve0 - amount0Out ? balance0 - (_reserve0 - amount0Out) : 0;
        uint amount1In = balance1 > _reserve1 - amount1Out ? balance1 - (_reserve1 - amount1Out) : 0;
        require(amount0In > 0 || amount1In > 0, 'UniswapV2: INSUFFICIENT_INPUT_AMOUNT');
        { // scope for reserve{0,1}Adjusted, avoids stack too deep errors
        uint balance0Adjusted = balance0.mul(1000).sub(amount0In.mul(3));
        uint balance1Adjusted = balance1.mul(1000).sub(amount1In.mul(3));
        require(balance0Adjusted.mul(balance1Adjusted) >= uint(_reserve0).mul(_reserve1).mul(1000**2), 'UniswapV2: K');
        }

        _update(balance0, balance1, _reserve0, _reserve1);
        emit Swap(msg.sender, amount0In, amount1In, amount0Out, amount1Out, to);
    }

函数参数

  • amount0Out:从储备中提取的 token0 数量
  • amount1Out:从储备中提取的 token1 数量
  • to:接收输出代币的地址
  • data:回调数据(用于闪电贷等高级操作)

主要逻辑步骤

初步检查
require(amount0Out > 0 || amount1Out > 0, 'UniswapV2: INSUFFICIENT_OUTPUT_AMOUNT');
  • 确保至少有一个输出量大于0(不能两个都为0)

使用getReserves来获取状态变量 在同一个快照时间获取数据保持数据一致性 同时这三个变量由于在一个存储槽内会被打包成一个sload操作节省gas

uint112 private reserve0;           // uses single storage slot, accessible via getReserves
uint112 private reserve1;           // uses single storage slot, accessible via getReserves
uint32  private blockTimestampLast;

为了安全会提前检查此时需要swap的两个token目前的余量

require(amount0Out < _reserve0 && amount1Out < _reserve1, 'UniswapV2: INSUFFICIENT_LIQUIDITY');
执行转账

这块代码中使用了很多花括号 创建一个新的作用域(Scope),它可以用来 临时限制变量的生命周期,从而帮助解决 "Stack Too Deep"(堆栈过深) 的错误。Solidity 的 EVM 使用 栈(Stack) 来存储局部变量,但 栈的最大深度限制是 16 个槽位(即最多同时存储 16 个局部变量)。{ ... } 内部定义的变量只在块内有效,退出块后,这些变量 不再占用栈空间。这样,编译器可以 复用栈槽位,而不是持续占用新的位置。

除了 { ... } 作用域隔离,还可以:

  1. 使用 memorycalldata 存储临时数据(减少栈使用)。
  2. 拆分成多个函数(减少单个函数的变量数量)。
  3. 使用结构体(struct)或数组 打包变量(但可能增加 Gas 成本)。
  4. 内联汇编(assembly 手动管理栈(高级用法)。
{ // scope for _token{0,1}, avoids stack too deep errors
        address _token0 = token0;
        address _token1 = token1;
        require(to != _token0 && to != _token1, 'UniswapV2: INVALID_TO');
        if (amount0Out > 0) _safeTransfer(_token0, to, amount0Out); // optimistically transfer tokens
        if (amount1Out > 0) _safeTransfer(_token1, to, amount1Out); // optimistically transfer tokens
        if (data.length > 0) IUniswapV2Callee(to).uniswapV2Call(msg.sender, amount0Out, amount1Out, data);
        balance0 = IERC20(_token0).balanceOf(address(this));
        balance1 = IERC20(_token1).balanceOf(address(this));
}
  • 获取代币地址
  • 确保接收地址不是代币合约本身
  • 执行输出代币的转账(乐观转账)
处理回调(闪电贷)
if (data.length > 0) IUniswapV2Callee(to).uniswapV2Call(msg.sender, amount0Out, amount1Out, data);
  • 如果有回调数据,通知接收者(用于闪电贷等场景)
计算实际输入量
balance0 = IERC20(_token0).balanceOf(address(this));
balance1 = IERC20(_token1).balanceOf(address(this));

uint amount0In = balance0 > _reserve0 - amount0Out ? balance0 - (_reserve0 - amount0Out) : 0;
uint amount1In = balance1 > _reserve1 - amount1Out ? balance1 - (_reserve1 - amount1Out) : 0;
  • 获取新的余额
  • 计算实际输入量:

    • 如果新余额 > (旧储备 - 输出量),差额就是输入量
    • 否则输入量为0
  • 先转账后检测,swap之前先讲要转换的token转账到币对合约

    • 兼容 ERC-20 标准:
      ERC-20 的 transferFrom 需要单独授权和转账操作,无法在单次调用中同时完成转入和交换。
    • 安全性
      强制用户先转账可以防止重入攻击(转账和交换分离)。
检查恒定乘积公式
uint balance0Adjusted = balance0.mul(1000).sub(amount0In.mul(3));
uint balance1Adjusted = balance1.mul(1000).sub(amount1In.mul(3));
require(balance0Adjusted.mul(balance1Adjusted) >= uint(_reserve0).mul(_reserve1).mul(1000**2), 'UniswapV2: K');
  • 计算调整后的余额(考虑0.3%手续费)
  • 确保 (adjustedBalance0 × adjustedBalance1) ≥ (旧储备0 × 旧储备1)
  • 这是Uniswap恒定乘积公式的核心检查
更新状态
_update(balance0, balance1, _reserve0, _reserve1);
emit Swap(msg.sender, amount0In, amount1In, amount0Out, amount1Out, to);
  • 更新储备量
  • 触发Swap事件
function _update(uint balance0, uint balance1, uint112 _reserve0, uint112 _reserve1) private {
        require(balance0 <= uint112(-1) && balance1 <= uint112(-1), 'UniswapV2: OVERFLOW');
        uint32 blockTimestamp = uint32(block.timestamp % 2**32);
        uint32 timeElapsed = blockTimestamp - blockTimestampLast; // overflow is desired
        if (timeElapsed > 0 && _reserve0 != 0 && _reserve1 != 0) {
            // * never overflows, and + overflow is desired
            price0CumulativeLast += uint(UQ112x112.encode(_reserve1).uqdiv(_reserve0)) * timeElapsed;
            price1CumulativeLast += uint(UQ112x112.encode(_reserve0).uqdiv(_reserve1)) * timeElapsed;
        }
        reserve0 = uint112(balance0);
        reserve1 = uint112(balance1);
        blockTimestampLast = blockTimestamp;
        emit Sync(reserve0, reserve1);
    }
(1) 溢出检查
require(balance0 <= uint112(-1) && balance1 <= uint112(-1), 'UniswapV2: OVERFLOW');
  • 作用:确保余额不超过 uint112 的最大值($2^{112}-1$)。
  • uint112(-1) 是 Solidity 中获取该类型最大值的方式(等价于 type(uint112).max)。
(2) 时间处理
uint32 blockTimestamp = uint32(block.timestamp % 2**32);
uint32 timeElapsed = blockTimestamp - blockTimestampLast; // overflow is desired
  • 时间截断block.timestamp % 2**32 确保时间戳在 uint32 范围内(约 136 年循环一次)。
  • 时间差计算

    • timeElapsed 允许溢出(如时间戳回绕时),因为 Uniswap 依赖时间差模运算的正确性。
    • 例如:若 blockTimestampLast = 2^32-1blockTimestamp = 0,则 timeElapsed = 1(符合预期)。
(3) 累计价格更新
if (timeElapsed > 0 && _reserve0 != 0 && _reserve1 != 0) {
    price0CumulativeLast += uint(UQ112x112.encode(_reserve1).uqdiv(_reserve0)) * timeElapsed;
    price1CumulativeLast += uint(UQ112x112.encode(_reserve0).uqdiv(_reserve1)) * timeElapsed;
}
  • 触发条件

    • 时间已流逝(timeElapsed > 0)。
    • 储备金非零(防止除零错误)。
  • 价格计算

    • price0CumulativeLast:记录 token1/token0 的累计价格(单位为秒)。
      使用 UQ112x112 定点数库保持精度(112 位整数 + 112 位小数)。
    • price1CumulativeLast:同理记录 token0/token1 的价格。
    • 为什么需要时间加权?

      抗操纵性

      • 市场价格可能被瞬时操纵(如闪电贷攻击)。
      • TWAP 通过长时间累积平滑价格波动,外部合约可查询两个时间点的累计价差,再除以时间差得到平均价格:

        TWAP = priceCumulative(t2)−priceCumulative(t1) / (t2−t1)

  • 设计目的

    • 支持链下价格预言机(如 TWAP 时间加权平均价格)。
    • 累计价格随时间线性增长,外部合约可通过差值计算时间段内的平均价格。
(4) 储备金和时间戳更新
reserve0 = uint112(balance0);
reserve1 = uint112(balance1);
blockTimestampLast = blockTimestamp;
emit Sync(reserve0, reserve1);
  • 更新状态

    • 将实时余额写入储备金(reserve0/reserve1)。
    • 记录当前时间戳(blockTimestampLast)。
  • 事件:触发 Sync 事件,通知外部监听者储备金变化。
分叉主网测试swapExactTokensForTokens和swapTokensForExactTokens
// SPDX-License-Identifier: MIT
pragma solidity 0.8.24;

import {Test, console2} from "forge-std/Test.sol";
import {IERC20} from "../src/interfaces/IERC20.sol";
import {IWETH} from "../src/interfaces/IWETH.sol";
import {IUniswapV2Router02} from "../src/interfaces/uniswap-v2/IUniswapV2Router02.sol";
import {IUniswapV2Pair} from "../src/interfaces/uniswap-v2/IUniswapV2Pair.sol";
import {DAI, WETH, MKR, UNISWAP_V2_PAIR_DAI_MKR, UNISWAP_V2_ROUTER_02} from "../../../src/Constants.sol";

contract UniswapV2SwapTest is Test {
    IWETH private constant weth = IWETH(WETH);
    IERC20 private constant dai = IERC20(DAI);
    IERC20 private constant mkr = IERC20(MKR);

    IUniswapV2Router02 private constant router = IUniswapV2Router02(UNISWAP_V2_ROUTER_02);
    IUniswapV2Pair private constant pair = IUniswapV2Pair(UNISWAP_V2_PAIR_DAI_MKR);

    address private constant user = address(100);

    function setUp() public {
        deal(user, 100 * 1e18);
        vm.startPrank(user);
        weth.deposit{value: 100 * 1e18}();
        weth.approve(address(router), type(uint256).max);
        vm.stopPrank();

        // Add MKR liquidity to DAI/MKR pool
        deal(DAI, address(pair), 1e6 * 1e18);
        deal(MKR, address(pair), 1e5 * 1e18);
        pair.sync();
    }

    // Swap all input tokens for as many output tokens as possible
    function test_swapExactTokensForTokens() public {
        address[] memory path = new address[](3);
        path[0] = WETH;
        path[1] = DAI;
        path[2] = MKR;

        uint256 amountIn = 1e18;
        uint256 amountOutMin = 1;

        vm.prank(user);
        uint256[] memory amounts = router.swapExactTokensForTokens({
            amountIn: amountIn,
            amountOutMin: amountOutMin,
            path: path,
            to: user,
            deadline: block.timestamp
        });

        console2.log("WETH", amounts[0]);
        console2.log("DAI", amounts[1]);
        console2.log("MKR", amounts[2]);
        assertGe(mkr.balanceOf(user), amountOutMin, "MKR balance of user");
    }

    // Receive an exact amount of output tokens for as few input tokens
    // as possible
    function test_swapTokensForExactTokens() public {
        address[] memory path = new address[](3);
        path[0] = WETH;
        path[1] = DAI;
        path[2] = MKR;

        uint256 amountOut = 0.1 * 1e18;
        uint256 amountInMax = 1e18;

        vm.prank(user);
        uint256[] memory amounts = router.swapTokensForExactTokens({
            amountOut: amountOut,
            amountInMax: amountInMax,
            path: path,
            to: user,
            deadline: block.timestamp
        });

        console2.log("WETH", amounts[0]);
        console2.log("DAI", amounts[1]);
        console2.log("MKR", amounts[2]);

        assertEq(mkr.balanceOf(user), amountOut, "MKR balance of user");
    }
}
当前页面是本站的「Baidu MIP」版。发表评论请点击:完整版 »