Uniswap V2 —— Periphery 解析

Uniswap V2 Periphery 提供了用户友好的交互接口,封装了 Core 合约的底层操作,简化流动性管理和代币交换。

1. 概述

Periphery 合约是用户与 Uniswap 交互的主要入口,Core 合约负责核心逻辑,Periphery 负责:

  • 处理 ETH ↔ WETH 转换
  • 计算最优交易路径
  • 提供滑点保护
  • 支持 permit 签名授权
合约 功能
UniswapV2Router02 路由合约,提供添加/移除流动性、代币交换等接口
UniswapV2Library 辅助库,计算价格、数量、交易对地址等
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
┌─────────────────────────────────────────────────────────┐
│ 用户 │
└─────────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────┐
│ UniswapV2Router02 │
│ (Periphery - 用户交互层) │
└─────────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────┐
│ UniswapV2Pair │
│ (Core - 核心逻辑层) │
└─────────────────────────────────────────────────────────┘

2. UniswapV2Router02 合约解析

UniswapV2Router02 是 V2 Router 的升级版,修复了 V1 的一些问题,是目前主要使用的路由合约。

2.1 添加流动性

addLiquidity

为 ERC-20/ERC-20 交易对添加流动性。

1
2
3
4
5
6
7
8
9
10
function addLiquidity(
address tokenA,
address tokenB,
uint amountADesired, // 期望存入的 tokenA 数量
uint amountBDesired, // 期望存入的 tokenB 数量
uint amountAMin, // 最少接受的 tokenA 数量(滑点保护)
uint amountBMin, // 最少接受的 tokenB 数量(滑点保护)
address to, // LP Token 接收地址
uint deadline // 交易截止时间
) external returns (uint amountA, uint amountB, uint liquidity);

示例场景

Alice 想为 ETH/USDC 池添加流动性,期望存入 1 ETH 和 2000 USDC:

1
2
3
4
amountADesired = 1 ETH
amountBDesired = 2000 USDC
amountAMin = 0.99 ETH # 允许 1% 滑点
amountBMin = 1980 USDC

Router 会根据当前池子比例计算实际需要的数量,确保不超过期望值且不低于最小值。

addLiquidityETH

为 ERC-20/ETH 交易对添加流动性,自动处理 ETH → WETH 转换。

1
2
3
4
5
6
7
8
function addLiquidityETH(
address token,
uint amountTokenDesired,
uint amountTokenMin,
uint amountETHMin,
address to,
uint deadline
) external payable returns (uint amountToken, uint amountETH, uint liquidity);

2.2 移除流动性

removeLiquidity

销毁 LP Token,按比例取回两种代币。

1
2
3
4
5
6
7
8
9
function removeLiquidity(
address tokenA,
address tokenB,
uint liquidity, // 要销毁的 LP Token 数量
uint amountAMin, // 最少获得的 tokenA(滑点保护)
uint amountBMin, // 最少获得的 tokenB(滑点保护)
address to,
uint deadline
) external returns (uint amountA, uint amountB);

removeLiquidityETH

移除流动性并自动将 WETH 转换为 ETH。

1
2
3
4
5
6
7
8
function removeLiquidityETH(
address token,
uint liquidity,
uint amountTokenMin,
uint amountETHMin,
address to,
uint deadline
) external returns (uint amountToken, uint amountETH);

removeLiquidityWithPermit

通过 EIP-2612 permit 签名移除流动性,无需预先 approve。

1
2
3
4
5
6
7
8
9
10
11
function removeLiquidityWithPermit(
address tokenA,
address tokenB,
uint liquidity,
uint amountAMin,
uint amountBMin,
address to,
uint deadline,
bool approveMax, // 是否授权最大值
uint8 v, bytes32 r, bytes32 s // 签名参数
) external returns (uint amountA, uint amountB);

2.3 交换函数

swapExactTokensForTokens

指定输入数量,交换代币(Exact Input)。

1
2
3
4
5
6
7
function swapExactTokensForTokens(
uint amountIn, // 输入的代币数量
uint amountOutMin, // 最少获得的输出数量(滑点保护)
address[] calldata path, // 交换路径 [tokenIn, ..., tokenOut]
address to,
uint deadline
) external returns (uint[] memory amounts);

示例:用 100 USDC 换 ETH

1
2
3
amountIn = 100 USDC
amountOutMin = 0.049 ETH # 期望至少得到 0.049 ETH
path = [USDC, WETH] # 直接路径

多跳路径示例:USDC → DAI → ETH

1
path = [USDC, DAI, WETH]    # 先 USDC→DAI,再 DAI→WETH

swapTokensForExactTokens

指定输出数量,交换代币(Exact Output)。

1
2
3
4
5
6
7
function swapTokensForExactTokens(
uint amountOut, // 期望获得的输出数量
uint amountInMax, // 最多支付的输入数量(滑点保护)
address[] calldata path,
address to,
uint deadline
) external returns (uint[] memory amounts);

示例:想获得正好 1 ETH

1
2
3
amountOut = 1 ETH
amountInMax = 2100 USDC # 最多支付 2100 USDC
path = [USDC, WETH]

swapExactETHForTokens

用 ETH 交换代币。

1
2
3
4
5
6
function swapExactETHForTokens(
uint amountOutMin,
address[] calldata path, // path[0] 必须是 WETH
address to,
uint deadline
) external payable returns (uint[] memory amounts);

swapTokensForExactETH

用代币交换指定数量的 ETH。

1
2
3
4
5
6
7
function swapTokensForExactETH(
uint amountOut, // 期望获得的 ETH 数量
uint amountInMax,
address[] calldata path, // path[path.length-1] 必须是 WETH
address to,
uint deadline
) external returns (uint[] memory amounts);

3. UniswapV2Library 库解析

UniswapV2Library 提供纯函数计算,不涉及状态修改。

3.1 价格与数量计算

getAmountOut

根据输入数量计算输出数量(含 0.3% 手续费)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function getAmountOut(
uint amountIn,
uint reserveIn,
uint reserveOut
) internal pure returns (uint amountOut) {
require(amountIn > 0, 'UniswapV2Library: INSUFFICIENT_INPUT_AMOUNT');
require(reserveIn > 0 && reserveOut > 0, 'UniswapV2Library: INSUFFICIENT_LIQUIDITY');

// 扣除 0.3% 手续费
uint amountInWithFee = amountIn.mul(997);
uint numerator = amountInWithFee.mul(reserveOut);
uint denominator = reserveIn.mul(1000).add(amountInWithFee);
amountOut = numerator / denominator;
}

计算示例

1
2
3
4
5
6
7
池子状态:reserve0 = 10 ETH, reserve1 = 20000 USDC
输入:1 ETH

amountInWithFee = 1 * 997 = 997
numerator = 997 * 20000 = 19,940,000
denominator = 10 * 1000 + 997 = 10,997
amountOut = 19,940,000 / 10,9971814 USDC

getAmountIn

根据期望输出计算所需输入。

1
2
3
4
5
6
7
8
9
10
11
12
function getAmountIn(
uint amountOut,
uint reserveIn,
uint reserveOut
) internal pure returns (uint amountIn) {
require(amountOut > 0, 'UniswapV2Library: INSUFFICIENT_OUTPUT_AMOUNT');
require(reserveIn > 0 && reserveOut > 0, 'UniswapV2Library: INSUFFICIENT_LIQUIDITY');

uint numerator = reserveIn.mul(amountOut).mul(1000);
uint denominator = reserveOut.sub(amountOut).mul(997);
amountIn = (numerator / denominator).add(1);
}

getAmountsOut / getAmountsIn

沿路径计算每一跳的数量。

1
2
3
4
5
6
7
8
9
10
11
12
13
// 计算路径上每个池的输出
function getAmountsOut(
address factory,
uint amountIn,
address[] memory path
) internal view returns (uint[] memory amounts);

// 计算路径上每个池的输入
function getAmountsIn(
address factory,
uint amountOut,
address[] memory path
) internal view returns (uint[] memory amounts);

3.2 辅助函数

pairFor

使用 CREATE2 计算交易对地址(无需链上查询)。

1
2
3
4
5
6
7
8
9
10
11
12
13
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
))));
}

getReserves

获取交易对的储备量。

1
2
3
4
5
6
7
8
9
function getReserves(
address factory,
address tokenA,
address tokenB
) internal view returns (uint reserveA, uint reserveB) {
(address token0,) = sortTokens(tokenA, tokenB);
(uint reserve0, uint reserve1,) = IUniswapV2Pair(pairFor(factory, tokenA, tokenB)).getReserves();
(reserveA, reserveB) = tokenA == token0 ? (reserve0, reserve1) : (reserve1, reserve0);
}