Uniswap V2 源码执行流程解读(10 ETH 换 USDT 举例)

zhangwjr 发布于 2026-07-13 10:50 阅读 30

Uniswap V2 源码执行流程解读

本文以 用 10 ETH 交换 USDT 为例,逐步追踪 Uniswap V2 四个核心源码文件的代码执行路径:

文件 职责
UniswapV2Factory.sol 创建交易对(Pair)
UniswapV2Pair.sol 核心 AMM 逻辑(swap、mint、burn)
UniswapV2ERC20.sol LP Token 的 ERC20 实现
UniswapV2Router02.sol 用户入口,封装 ETH/WETH 转换与路由

场景设定

假设已部署以下合约:

合约 作用
UniswapV2Factory 创建并注册交易对
UniswapV2Pair (WETH/USDT) 实际执行 swap 的流动性池
UniswapV2Router02 用户调用的路由合约
WETH 将 ETH 包装为 ERC20 代币

用户调用:

router.swapExactETHForTokens{value: 10 ether}(
    amountOutMin,           // 最少要收到多少 USDT(滑点保护)
    [WETH, USDT],           // 交易路径(单跳)
    msg.sender,             // USDT 接收地址
    deadline                // 交易截止时间
);

假设 WETH/USDT 池子当前储备:

  • reserveWETH = 1000 ETH
  • reserveUSDT = 3,000,000 USDT

整体调用链

sequenceDiagram
    participant User
    participant Router as UniswapV2Router02
    participant Library as UniswapV2Library
    participant WETH
    participant Pair as UniswapV2Pair(WETH/USDT)
    participant USDT

    User->>Router: swapExactETHForTokens (10 ETH)
    Router->>Router: ensure(deadline)
    Router->>Library: getAmountsOut(10 ETH, [WETH, USDT])
    Library->>Pair: getReserves()
    Pair-->>Library: reserve0, reserve1
    Library-->>Router: amounts = [10 ETH, ~29910 USDT]
    Router->>WETH: deposit(10 ETH)
    Router->>WETH: transfer(10 WETH → Pair)
    Router->>Router: _swap()
    Router->>Pair: swap(0, amountOut, User)
    Pair->>USDT: transfer(User, amountOut)
    Pair->>Pair: 验证 K 值 + _update 储备

注意:本次 swap 过程中 不会调用 Factory。Factory 仅在「创建交易对」时使用;Router 通过 UniswapV2Library.pairFor() 用 CREATE2 公式 离线计算 Pair 地址,无需链上查询。


阶段 0:前置条件(Factory + Pair 已存在)

在 swap 之前,池子必须已经创建并有流动性。

Step 0.1 — Factory 创建 Pair

有人(通常是 Router 在 addLiquidity 时)调用 Factory.createPair(WETH, USDT)

// UniswapV2Factory.sol
function createPair(address tokenA, address tokenB) external returns (address pair) {
    require(tokenA != tokenB, 'UniswapV2: IDENTICAL_ADDRESSES');
    (address token0, address token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA);
    require(token0 != address(0), 'UniswapV2: ZERO_ADDRESS');
    require(getPair[token0][token1] == address(0), 'UniswapV2: PAIR_EXISTS');
    // CREATE2 部署 UniswapV2Pair 合约
    IUniswapV2Pair(pair).initialize(token0, token1);
    getPair[token0][token1] = pair;
    getPair[token1][token0] = pair;
    allPairs.push(pair);
    emit PairCreated(token0, token1, pair, allPairs.length);
}

执行过程:

  1. 按地址大小排序 → token0 = WETH, token1 = USDT
  2. 使用 CREATE2 部署新的 UniswapV2Pair 合约
  3. 调用 Pair.initialize(WETH, USDT) 设置 token0/token1
  4. 写入 getPair 双向映射,并推入 allPairs 数组

Step 0.2 — Pair 继承 ERC20(LP Token)

UniswapV2Pair 继承 UniswapV2ERC20,每个 Pair 同时是一个 LP Token(符号 UNI-V2):

// UniswapV2Pair.sol
contract UniswapV2Pair is IUniswapV2Pair, UniswapV2ERC20 {

添加流动性时会调用 UniswapV2ERC20._mint

// UniswapV2ERC20.sol
function _mint(address to, uint value) internal {
    totalSupply = totalSupply.add(value);
    balanceOf[to] = balanceOf[to].add(value);
    emit Transfer(address(0), to, value);
}

swap 本身不 mint/burn LP Token,但 Pair 的 swap() 会通过 _safeTransfer 转出 WETH/USDT。


阶段 1:Router 入口 — swapExactETHForTokens

// UniswapV2Router02.sol
function swapExactETHForTokens(uint amountOutMin, address[] calldata path, address to, uint deadline)
    external
    payable
    ensure(deadline)
    returns (uint[] memory amounts)
{
    require(path[0] == WETH, 'UniswapV2Router: INVALID_PATH');
    amounts = UniswapV2Library.getAmountsOut(factory, msg.value, path);
    require(amounts[amounts.length - 1] >= amountOutMin, 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT');
    IWETH(WETH).deposit{value: amounts[0]}();
    assert(IWETH(WETH).transfer(UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]));
    _swap(amounts, path, to);
}

Step 1.1 — ensure(deadline) 修饰符

modifier ensure(uint deadline) {
    require(deadline >= block.timestamp, 'UniswapV2Router: EXPIRED');
    _;
}

检查交易未过期。

Step 1.2 — 校验 path

require(path[0] == WETH, 'UniswapV2Router: INVALID_PATH');

ETH 换 Token,路径第一个必须是 WETH。

Step 1.3 — 计算输出量 getAmountsOut

调用 UniswapV2Library.getAmountsOut(factory, 10 ETH, [WETH, USDT])

// UniswapV2Library.sol
function getAmountsOut(address factory, uint amountIn, address[] memory path)
    internal view returns (uint[] memory amounts)
{
    amounts = new uint[](path.length);
    amounts[0] = amountIn;  // = 10 ETH
    for (uint i; i < path.length - 1; i++) {
        (uint reserveIn, uint reserveOut) = getReserves(factory, path[i], path[i + 1]);
        amounts[i + 1] = getAmountOut(amounts[i], reserveIn, reserveOut);
    }
}

子步骤 a > pairFor — 离线计算 Pair 地址

function pairFor(address factory, address tokenA, address tokenB) internal pure returns (address pair) {
    (address token0, address token1) = sortTokens(tokenA, tokenB);
    pair = address(uint(keccak256(abi.encodePacked(
        hex'ff',
        factory,
        keccak256(abi.encodePacked(token0, token1)),
        hex'96e8ac4277198ff8b6f785478aa9a39f403cb768dd02cbee326c3e7da348845f' // init code hash
    ))));
}

纯计算,不发链上调用

子步骤 b > getReserves — 读取 Pair 储备

// UniswapV2Pair.sol
function getReserves() public view returns (uint112 _reserve0, uint112 _reserve1, uint32 _blockTimestampLast) {
    _reserve0 = reserve0;   // WETH 储备
    _reserve1 = reserve1;   // USDT 储备
    _blockTimestampLast = blockTimestampLast;
}

子步骤 c > getAmountOut — 恒定乘积 + 0.3% 手续费

function getAmountOut(uint amountIn, uint reserveIn, uint reserveOut) internal pure returns (uint amountOut) {
    uint amountInWithFee = amountIn.mul(997);           // 扣 0.3% 手续费
    uint numerator = amountInWithFee.mul(reserveOut);
    uint denominator = reserveIn.mul(1000).add(amountInWithFee);
    amountOut = numerator / denominator;
}

代入数字(10 ETH → USDT):

amountInWithFee = 10 ETH × 997
numerator       = 10 × 997 × 3,000,000 USDT
denominator     = 1000 ETH × 1000 + 10 ETH × 997

amountOut ≈ 29,910 USDT

返回:amounts = [10 ETH, ~29,910 USDT]

Step 1.4 — 滑点保护

require(amounts[1] >= amountOutMin, 'INSUFFICIENT_OUTPUT_AMOUNT');

若计算出的 USDT 少于用户设定的最小值,交易 revert。

Step 1.5 — ETH → WETH

IWETH(WETH).deposit{value: 10 ether}();

Router 收到 10 ETH,WETH 合约 mint 10 WETH 给 Router。

Step 1.6 — WETH 转入 Pair

IWETH(WETH).transfer(pairAddress, 10 WETH);

关键设计:Pair 的 swap() 要求「输入代币已经到 Pair 里」,Router 先转 WETH,再调 swap


阶段 2:Router 内部 — _swap

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]);  // WETH, USDT
        (address token0,) = UniswapV2Library.sortTokens(input, output);
        uint amountOut = amounts[i + 1];  // ~29,910 USDT
        (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)
        );
    }
}

单跳路径 [WETH, USDT] 时,循环只执行 1 次

变量
input WETH
output USDT
amountOut ~29,910 USDT
amount0Out / amount1Out 若 WETH 是 token0 → (0, amountOut)
to msg.sender(用户地址)

最终调用:

Pair.swap(0, 29910e6, msg.sender, "")

阶段 3:核心 — UniswapV2Pair.swap()

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();
    require(amount0Out < _reserve0 && amount1Out < _reserve1, 'UniswapV2: INSUFFICIENT_LIQUIDITY');

    // ① 乐观转出 USDT 给用户
    if (amount1Out > 0) _safeTransfer(_token1, to, amount1Out);

    // ② 读取 Pair 当前余额
    balance0 = IERC20(_token0).balanceOf(address(this));  // WETH 余额已增加 10
    balance1 = IERC20(_token1).balanceOf(address(this));  // USDT 余额已减少

    // ③ 计算实际输入量
    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');

    // ④ K 值校验(含 0.3% 手续费)
    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);
}

Step 3.1 — lock 防重入

modifier lock() {
    require(unlocked == 1, 'UniswapV2: LOCKED');
    unlocked = 0;
    _;
    unlocked = 1;
}

Step 3.2 — 读取旧储备

_reserve0 = 1000 ETH (WETH)
_reserve1 = 3,000,000 USDT

Step 3.3 — 乐观转出(Optimistic Transfer)

先把 USDT 转给用户,再验证输入是否足够。这是 Uniswap V2 的经典设计。

function _safeTransfer(address token, address to, uint value) private {
    (bool success, bytes memory data) = token.call(abi.encodeWithSelector(SELECTOR, to, value));
    require(success && (data.length == 0 || abi.decode(data, (bool))), 'UniswapV2: TRANSFER_FAILED');
}

Step 3.4 — 计算实际输入

Router 已转入 10 WETH,因此:

balance0 (WETH) = _reserve0 + 10 ETH = 1010 ETH
balance1 (USDT) = _reserve1 - amount1Out

amount0In = balance0 - (_reserve0 - 0) = 10 ETH  ✓
amount1In = 0

Step 3.5 — K 值不变量(含手续费)

公式含义:输入量的 0.3% 作为手续费,不参与 K 计算:

balance0Adjusted = balance0 × 1000 - amount0In × 3
balance1Adjusted = balance1 × 1000 - amount1In × 3

require: balance0Adjusted × balance1Adjusted >= reserve0 × reserve1 × 1000²

等价于恒定乘积公式 (x + Δx)(y - Δy) ≥ k,且 Δx 只有 99.7% 进入池子。

Step 3.6 — _update 更新储备

function _update(uint balance0, uint balance1, uint112 _reserve0, uint112 _reserve1) private {
    reserve0 = uint112(balance0);  // 1010 ETH
    reserve1 = uint112(balance1);  // ~2,970,090 USDT
    blockTimestampLast = blockTimestamp;
    emit Sync(reserve0, reserve1);
}

同时更新 TWAP 价格累加器(price0CumulativeLast / price1CumulativeLast)。

Step 3.7 — 发出 Swap 事件

emit Swap(Router地址, 10 ETH, 0, 0, 29910 USDT, 用户地址);

四个文件各自的角色

文件 在 10 ETH → USDT swap 中的角色
UniswapV2Router02 入口:wrap ETH、计算输出、转 WETH 到 Pair、调用 _swap
UniswapV2Library 纯计算:Pair 地址、储备查询、输出量(getAmountOut
UniswapV2Pair 核心:转出 USDT、验证 K 值、更新 reserve0/reserve1
UniswapV2Factory swap 时 不参与;仅在创建 Pair 时使用
UniswapV2ERC20 Pair 的 LP Token 基类;swap 时通过 _safeTransfer 转 WETH/USDT

多跳路径(WETH → USDC → USDT)

若路径为 [WETH, USDC, USDT]_swap 循环执行 2 次

第 1 跳: WETH/USDC Pair
  - Router 转 10 WETH → WETH/USDC Pair
  - swap 输出 USDC → 直接转到 USDC/USDT Pair 地址(不是用户地址)

第 2 跳: USDC/USDT Pair
  - USDC 已在 Pair 里(上一跳转入)
  - swap 输出 USDT → 转给用户

关键代码:

// 中间跳的 to 是下一个 Pair 的地址,只有最后一跳 to = 用户地址
address to = i < path.length - 2
    ? UniswapV2Library.pairFor(factory, output, path[i + 2])
    : _to;

完整状态变化

时刻 用户 ETH 用户 USDT Pair WETH Pair USDT
swap 前 -10 ETH 0 1000 3,000,000
swap 后 0 +~29,910 1010 ~2,970,090

手续费约 0.3%:10 ETH 中约 0.03 ETH 留在池子里,作为 LP 提供者收益。


相关源码文件

uniswapv2/
├── UniswapV2Factory.sol      # 创建交易对
├── UniswapV2Pair.sol         # AMM 核心逻辑
├── UniswapV2ERC20.sol        # LP Token ERC20
├── UniswapV2Router02.sol     # 用户路由入口
└── libraries/
    └── UniswapV2Library.sol  # 价格计算与 Pair 地址推导

延伸阅读

  • 添加流动性Router.addLiquidityETHFactory.createPairPair.mintERC20._mint
  • 移除流动性Router.removeLiquidityETHPair.burnERC20._burn
  • 闪电贷Pair.swap(data.length > 0) → 回调 IUniswapV2Callee.uniswapV2Call

相关文章

0 条评论