Uniswap V2 源码执行流程解读(10 ETH 换 USDT 举例)
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 ETHreserveUSDT= 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);
}
执行过程:
- 按地址大小排序 →
token0 = WETH,token1 = USDT - 使用
CREATE2部署新的UniswapV2Pair合约 - 调用
Pair.initialize(WETH, USDT)设置 token0/token1 - 写入
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.addLiquidityETH→Factory.createPair→Pair.mint→ERC20._mint - 移除流动性:
Router.removeLiquidityETH→Pair.burn→ERC20._burn - 闪电贷:
Pair.swap(data.length > 0)→ 回调IUniswapV2Callee.uniswapV2Call