Uniswap V2 Core 和 Periphery 合约核心接口文档

Uniswap V2 核心接口文档

源码基于 Uniswap V2 官方仓库 v2-core / v2-periphery,Solidity 0.5.16。

文档结构:先架构总览,再逐合约展开,含完整函数签名、Events、关键参数说明及安全注意事项。


目录

  1. 架构总览
  2. Core:UniswapV2Factory
  3. Core:UniswapV2Pair
  4. Core:闪电兑换接口 IUniswapV2Callee
  5. Periphery:UniswapV2Library
  6. Periphery:UniswapV2Router02
  7. 重要常量速查
  8. 安全注意事项

1. 架构总览

┌─────────────────────────────────────────────────────────────┐ │ 架构分层 │ │ │ │ ┌──────────────┐ ┌──────────────┐ │ │ │ Periphery │ │ Interface │ DApp / Frontend │ │ │ UniswapV2Router02 │ IUniswapV2Router02 │ │ │ └──────┬───────┘ └──────────────┘ │ │ │ │ │ ┌──────▼───────────────────────────────────────┐ │ │ │ Core │ │ │ │ UniswapV2Factory ← 创建/管理 Pair │ │ │ │ UniswapV2Pair ← 核心 AMM 逻辑 │ │ │ │ UniswapV2ERC20 ← LP Token (ERC20) │ │ │ └─────────────────────────────────────────────┘ │ │ │ │ ┌─────────────────────────────────────────────┐ │ │ │ UniswapV2Library (Periphery) │ │ │ │ pairFor / getAmountOut / getAmountIn 等 │ │ │ └─────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────┘
层级合约职责
CoreUniswapV2Factory创建交易对、管理 Pair 注册表
CoreUniswapV2PairAMM 核心逻辑:mint / burn / swap / 预言机
CoreUniswapV2ERC20LP Token(ERC20 + EIP-2612 permit)
PeripheryUniswapV2Router02用户入口:添加流动性 / 移除流动性 / 交易
PeripheryUniswapV2Library纯函数计算库:价格 / 金额 / pair 地址

主线网常用地址

合约地址
Factory0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f
Router020x7a250d5630B4cF539739dF2C5dAcb4c659F2488D

2. Core:UniswapV2Factory

源码:Uniswap/v2-core/contracts/UniswapV2Factory.sol
角色:交易对工厂,通过create2部署 Pair 合约并维护注册表。

2.1 状态变量

address public feeTo; // 协议手续费接收地址(非0则开启,抽取 1/6 的流动性增长) address public feeToSetter; // 有权修改 feeTo 的地址(部署时构造函数传入) mapping(address => mapping(address => address)) public getPair; // getPair[tokenA][tokenB] = Pair 合约地址,按 token 地址排序索引 address[] public allPairs; // 所有创建的 Pair 地址数组

2.2 核心函数

createPair
function createPair(address tokenA, address tokenB) external returns (address pair)

创建新的交易对 Pair 合约。

参数类型说明
tokenAaddress代币 A 地址
tokenBaddress代币 B 地址

约束条件:

  • tokenA != tokenB
  • tokenA != address(0)tokenB != address(0)
  • getPair[token0][token1] == address(0)(已存在则 revert)

实现细节:

  • 内部自动按地址排序确定token0 / token1
  • 使用CREATE2+keccak256(token0, token1)作为 salt,确保 Pair 地址可预测
  • Pair 内调用initialize(token0, token1)完成初始化
  • 触发事件PairCreated(token0, token1, pair, allPairs.length)

返回:新部署的 Pair 合约地址


getPair
function getPair(address tokenA, address tokenB) external view returns (address pair)

查询两个代币对应的 Pair 地址(按地址排序后查找)。


allPairs/allPairsLength
function allPairs(uint index) external view returns (address pair) function allPairsLength() external view returns (uint)

枚举所有已创建的 Pair。


setFeeTo
function setFeeTo(address _feeTo) external

设置协议手续费接收地址。

权限:feeToSetter可调用。

feeTo != address(0)时,Pair 的_mintFee()会将 sqrt(k) 增长量的 1/6 铸造为协议手续费,等效抽取 LP 总手续费的0.05%(总手续费的 1/6 = 0.05%)。


setFeeToSetter
function setFeeToSetter(address _feeToSetter) external

转移feeToSetter权限。

权限:仅当前feeToSetter可调用。


2.3 Events

event PairCreated( address indexed token0, // 按地址排序后的较小者 address indexed token1, // 按地址排序后的较大者 address pair, // Pair 合约地址 uint // allPairs.length,即该交易对的序号(从 1 开始) );

3. Core:UniswapV2Pair

源码:Uniswap/v2-core/contracts/UniswapV2Pair.sol
角色:AMM 流动性池核心,存储两种代币的储备,执行 mint / burn / swap / 预言机更新。

3.1 状态变量

address public factory; // 创建此 Pair 的 Factory 地址 address public token0; // 按地址排序较小的代币 address public token1; // 按地址排序较大的代币 uint112 private reserve0; // token0 的储备量 uint112 private reserve1; // token1 的储备量 uint32 private blockTimestampLast; // 上次更新价格的区块时间戳 uint public price0CumulativeLast; // token0 的累计价格(用于 TWAP 预言机) uint public price1CumulativeLast; // token1 的累计价格 uint public kLast; // 上次手续费计算时的 reserve0 * reserve1

3.2 核心常量

uint public constant MINIMUM_LIQUIDITY = 10**3; // 首次添加流动性时永久锁入 address(0) 的 LP Token 数量,防止稀释攻击

3.3 只读函数(Read-Only)

getReserves
function getReserves() public view returns ( uint112 _reserve0, uint112 _reserve1, uint32 _blockTimestampLast )

返回当前储备量及上次价格更新的区块时间戳。

注意:两个 token 的reserve0 / reserve1token0 / token1一一对应,需结合token0()确定各代币的储备。


mint / burn / swap / skim / sync权限说明

这些函数均为external,但通过lock修饰符(unlocked == 1检查)防止重入。

3.4 流动性操作

mint
function mint(address to) external lock returns (uint liquidity)

添加流动性。调用者需先已将两种代币转入 Pair 合约(ERC20transfer),mint根据转入量的增量铸造 LP Token。

参数类型说明
toaddressLP Token 接收地址

计算逻辑:

// 首次添加(totalSupply == 0) liquidity = sqrt(amount0 * amount1) - MINIMUM_LIQUIDITY // 后续添加 liquidity = min( amount0 * totalSupply / reserve0, amount1 * totalSupply / reserve1 )

事件:

event Mint(address indexed sender, uint amount0, uint amount1); // sender = msg.sender,amount0/amount1 = 本次实际注入量

burn
function burn(address to) external lock returns (uint amount0, uint amount1)

移除流动性。调用者需先将 LP Token 转入 Pair 合约(ERC20transfer),burn按比例销毁 LP Token 并转回两种代币。

参数类型说明
toaddress收回代币的接收地址

计算逻辑:

amount0 = liquidity * balance0 / totalSupply amount1 = liquidity * balance1 / totalSupply

事件:

event Burn(address indexed sender, uint amount0, uint amount1, address indexed to); // sender = msg.sender,to = 代币接收方

3.5 交易操作

swap
function swap( uint amount0Out, uint amount1Out, address to, bytes calldata data ) external lock

代币交换(低-level,需外部做安全检查)。

参数类型说明
amount0Outuint要输出的 token0 数量(可为 0)
amount1Outuint要输出的 token1 数量(可为 0)
toaddress接收代币的地址(不能是 token0/token1 本身)
databytes非空则回调IUniswapV2Callee(to).uniswapV2Call(...)(闪电兑换)

约束条件:

require(amount0Out > 0 || amount1Out > 0, 'INSUFFICIENT_OUTPUT_AMOUNT'); require(amount0Out < reserve0 && amount1Out < reserve1, 'INSUFFICIENT_LIQUIDITY'); require(to != token0 && to != token1, 'INVALID_TO');

K 值校验(防止手续费提取攻击):

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% 手续费 → 实际余额增量比理论少 0.3% → 乘以 997/1000 修正,K 值校验保证交易后k' >= k * 1000²(即 0.3% 手续费被合理扣留)。

事件:

event Swap( address indexed sender, uint amount0In, // token0 输入量(正向交易) uint amount1In, // token1 输入量 uint amount0Out, // token0 输出量 uint amount1Out, // token1 输出量 address indexed to );

3.6 辅助操作

skim
function skim(address to) external lock

将池合约中超出reserve的代币余额(通常是意外转入的代币)转出给to


sync
function sync() external lock

将池内实际 ERC20 余额强制同步为reserve,用于恢复因外部转账导致的储备不一致。


3.7 预言机相关

price0CumulativeLast/price1CumulativeLast
uint public price0CumulativeLast; // token1/token0 的累计价格 × 时间 uint public price1CumulativeLast; // token0/token1 的累计价格 × 时间

更新时机:_update()在每个区块(首次调用时)累加:

price0CumulativeLast += uint(UQ112x112.encode(_reserve1).uqdiv(_reserve0)) * timeElapsed; price1CumulativeLast += uint(UQ112x112.encode(_reserve0).uqdiv(_reserve1)) * timeElapsed;

用途:计算 TWAP(时间加权平均价格):

// 时间段 [t1, t2] 内的平均价格 price_avg = (price0CumulativeLast[t2] - price0CumulativeLast[t1]) / (t2 - t1)

4. Core:IUniswapV2Callee

源码:Uniswap/v2-core/contracts/interfaces/IUniswapV2Callee.sol
用途:**闪电兑换(Flash Swap)**回调接口。

接口签名

pragma solidity >=0.5.0; interface IUniswapV2Callee { function uniswapV2Call( address sender, // 调用者(即调用 swap 的 msg.sender) uint amount0, // 输出的 token0 数量 uint amount1, // 输出的 token1 数量 bytes calldata data // swap 调用时传入的 data ) external; }

闪电兑换工作流程

1. 任意合约调用 Pair.swap(amount0Out, amount1Out, to, data) ↓ 2. Pair 先将代币转给 to(乐观转账) ↓ 3. Pair 调用 IUniswapV2Callee(to).uniswapV2Call(...) ↓ 4. to 收到回调:在 uniswapV2Call 中执行任意逻辑 ↓ 5. to 必须确保 swap 结束后 balance >= 扣除手续费后的 reserve (即:to 必须"还上"从 Pair 借出的代币 + 手续费,或用另一种代币偿还) ↓ 6. Pair 执行 K 值校验,确认 balance 满足要求

注意:闪电兑换可以用一种代币借出,偿还时用另一种代币(不等额),这是套利的基础。


5. Periphery:UniswapV2Library

源码:Uniswap/v2-periphery/contracts/libraries/UniswapV2Library.sol
性质:纯函数库(library),无状态,仅用于计算。

5.1sortTokens

function sortTokens(address tokenA, address tokenB) internal pure returns (address token0, address token1)

按地址排序两个代币,确保与 Pair 的token0 / token1顺序一致。

约束:tokenA != tokenBtokenA != address(0)


5.2pairFor

function pairFor( address factory, address tokenA, address tokenB ) internal pure returns (address pair)

通过 factory 地址 + 两个代币地址,用 CREATE2 反推 Pair 地址,无需做外部调用。

内部用到的init code hash(主网):

0x96e8ac4277198ff8b6f785478aa9a39f403cb768dd02cbee326c3e7da348845f

⚠️ 不同链/不同部署的 init code hash 可能不同,需用实际部署的 Pair bytecode 重新计算。


5.3getReserves

function getReserves( address factory, address tokenA, address tokenB ) internal view returns (uint reserveA, uint reserveB)

获取指定两个代币的当前储备量,自动处理 token0/token1 的排序映射。


5.4quote

function quote( uint amountA, uint reserveA, uint reserveB ) internal pure returns (uint amountB)

已知输入量amountA,估算可获得的另一种代币数量(不考虑手续费,等效 1:1 定价)。

amountB = amountA * reserveB / reserveA

5.5getAmountOut

function getAmountOut( uint amountIn, uint reserveIn, uint reserveOut ) internal pure returns (uint amountOut)

已知输入量,计算最大输出量(含 0.3% 手续费)。

// 扣除 0.3% 手续费:amountIn * 997 / 1000 uint amountInWithFee = amountIn * 997; // 恒定乘积公式 amountOut = amountInWithFee * reserveOut / (reserveIn * 1000 + amountInWithFee)

5.6getAmountIn

function getAmountIn( uint amountOut, uint reserveIn, uint reserveOut ) internal pure returns (uint amountIn)

已知目标输出量,计算最小输入量(含 0.3% 手续费)。

uint numerator = reserveIn * amountOut * 1000; uint denominator = (reserveOut - amountOut) * 997; amountIn = numerator / denominator + 1; // +1 防止四舍五入导致实际不足

5.7getAmountsOut

function getAmountsOut( address factory, uint amountIn, address[] memory path ) internal view returns (uint[] memory amounts)

多跳路径的输出量计算,从 path[0] → path[1] → … → path[last]。

参数说明
factory工厂地址
amountIn输入总量
path交易路径,如[WETH, USDC, DAI]

返回:amounts[i]= path[i] 的输出量(amounts[0] == amountIn)。


5.8getAmountsIn

function getAmountsIn( address factory, uint amountOut, address[] memory path ) internal view returns (uint[] memory amounts)

多跳路径的反向输入量计算(已知最终目标量,推算初始输入量)。


6. Periphery:UniswapV2Router02

源码:Uniswap/v2-periphery/contracts/UniswapV2Router02.sol
角色:用户入口合约,封装 Core 的低-level 操作,提供友好的添加/移除流动性、交易接口。

⚠️ Router02 仅用于与 Core 交互,内部不持有用户资产(无回调陷阱风险)。

6.1 构造函数与状态

address public immutable override factory; // Factory 地址 address public immutable override WETH; // WETH 地址(如 Mainnet: 0xC02aa...) constructor(address _factory, address _WETH) public { factory = _factory; WETH = _WETH; } receive() external payable { // 仅接受来自 WETH 合约的 ETH 退还(removeLiquidityETH 退款路径) assert(msg.sender == WETH); }

6.2 内部辅助

_addLiquidity(internal)
function _addLiquidity( address tokenA, address tokenB, uint amountADesired, uint amountBDesired, uint amountAMin, uint amountBMin ) internal virtual returns (uint amountA, uint amountB)

计算最优的两种代币注入量(维持池内当前价格比例):

  • 若池不存在 → 创建 Pair,按 desired 全量注入
  • 若池存在 → 用quote计算维持当前比例的最优量

6.3 添加流动性

addLiquidity
function addLiquidity( address tokenA, address tokenB, uint amountADesired, uint amountBDesired, uint amountAMin, uint amountBMin, address to, uint deadline ) external virtual override ensure(deadline) returns (uint amountA, uint amountB, uint liquidity)

添加 ERC20-ERC20 流动性。

参数说明
tokenA / tokenB两种代币地址
amountADesired / amountBDesired希望注入的数量上限
amountAMin / amountBMin最小注入量(防止滑点过高)
toLP Token 接收地址
deadline交易截止时间戳(防止重放)

addLiquidityETH
function addLiquidityETH( address token, uint amountTokenDesired, uint amountTokenMin, uint amountETHMin, address to, uint deadline ) external virtual override payable ensure(deadline) returns (uint amountToken, uint amountETH, uint liquidity)

添加 ETH-ERC20 流动性(Router 将 ETH 包裹为 WETH 后注入)。

参数说明
tokenERC20 代币地址
amountTokenDesired希望注入的 Token 数量
msg.value随调用一起发送的 ETH 数量上限
amountTokenMin / amountETHMin最小注入量

6.4 移除流动性

removeLiquidity
function removeLiquidity( address tokenA, address tokenB, uint liquidity, // 要销毁的 LP Token 数量 uint amountAMin, uint amountBMin, address to, uint deadline ) public virtual override ensure(deadline) returns (uint amountA, uint amountB)

移除 ERC20-ERC20 流动性,将代币发送至to


removeLiquidityETH
function removeLiquidityETH( address token, uint liquidity, uint amountTokenMin, uint amountETHMin, address to, uint deadline ) public virtual override ensure(deadline) returns (uint amountToken, uint amountETH)

移除 ETH-ERC20 流动性,ETH 自动从 WETH 解除包裹后发送给to


带 Permit 的变体
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 virtual override returns (uint amountA, uint amountB) function removeLiquidityETHWithPermit( address token, uint liquidity, uint amountTokenMin, uint amountETHMin, address to, uint deadline, bool approveMax, uint8 v, bytes32 r, bytes32 s ) external virtual override returns (uint amountToken, uint amountETH)

允许离线签名(EIP-2612),无需预先 approve,直接用 permit 授权。


支持 Fee-on-Transfer 代币的移除
function removeLiquidityETHSupportingFeeOnTransferTokens( address token, uint liquidity, uint amountTokenMin, uint amountETHMin, address to, uint deadline ) public virtual override ensure(deadline) returns (uint amountETH)

适用于转出时扣除手续费的代币(余额变化不是线性的),通过转出后查余额而非固定计算量来确认。


6.5 代币交换

swapExactTokensForTokens(ERC20 → ERC20,固定输入量)
function swapExactTokensForTokens( uint amountIn, uint amountOutMin, // 最小输出量(防滑点) address[] calldata path, // 交易路径 address to, uint deadline ) external virtual override ensure(deadline) returns (uint[] memory amounts)

固定输入量amountIn全部由path[0]换出,amountOutMin控制最低输出。


swapTokensForExactTokens(ERC20 → ERC20,固定输出量)
function swapTokensForExactTokens( uint amountOut, // 期望输出的目标数量 uint amountInMax, // 最大愿意支付的输入量 address[] calldata path, address to, uint deadline ) external virtual override ensure(deadline) returns (uint[] memory amounts)

固定输出量:精确控制换出的目标数量,超出amountInMax则 revert。


swapExactETHForTokens(ETH → ERC20)
function swapExactETHForTokens( uint amountOutMin, address[] calldata path, address to, uint deadline ) external virtual override payable ensure(deadline) returns (uint[] memory amounts) // path[0] 必须是 WETH

swapETHForExactTokens(ETH → ERC20,固定输出)
function swapETHForExactTokens( uint amountOut, address[] calldata path, address to, uint deadline ) external virtual override payable ensure(deadline) returns (uint[] memory amounts) // path[0] 必须是 WETH

swapExactTokensForETH(ERC20 → ETH)
function swapExactTokensForETH( uint amountIn, uint amountOutMin, address[] calldata path, address to, uint deadline ) external virtual override ensure(deadline) returns (uint[] memory amounts) // path[last] 必须是 WETH

swapTokensForExactETH(ERC20 → ETH,固定输出)
function swapTokensForExactETH( uint amountOut, uint amountInMax, address[] calldata path, address to, uint deadline ) external virtual override ensure(deadline) returns (uint[] memory amounts) // path[last] 必须是 WETH

支持 Fee-on-Transfer 代币的交换
// ERC20 → ERC20(输入方代币含 fee-on-transfer) function swapExactTokensForTokensSupportingFeeOnTransferTokens( uint amountIn, uint amountOutMin, address[] calldata path, address to, uint deadline ) external virtual override ensure(deadline) // ETH → ERC20(输入方含 fee-on-transfer) function swapExactETHForTokensSupportingFeeOnTransferTokens( uint amountOutMin, address[] calldata path, address to, uint deadline ) external virtual override payable ensure(deadline) // ERC20 → ETH(输出方含 fee-on-transfer) function swapExactTokensForETHSupportingFeeOnTransferTokens( uint amountIn, uint amountOutMin, address[] calldata path, address to, uint deadline ) external virtual override ensure(deadline)

关键区别:因为无法事前计算实际输出量,改用交易后查余额差值来验证:

uint balanceBefore = IERC20(path[last]).balanceOf(to); _swapSupportingFeeOnTransferTokens(path, to); require( IERC20(path[last]).balanceOf(to).sub(balanceBefore) >= amountOutMin, 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT' );

6.6 计算函数(Library 代理)

function quote( uint amountA, uint reserveA, uint reserveB ) public pure virtual override returns (uint amountB) function getAmountOut( uint amountIn, uint reserveIn, uint reserveOut ) public pure virtual override returns (uint amountOut) function getAmountIn( uint amountOut, uint reserveIn, uint reserveOut ) public pure virtual override returns (uint amountIn) function getAmountsOut( uint amountIn, address[] memory path ) public view virtual override returns (uint[] memory amounts) function getAmountsIn( uint amountOut, address[] memory path ) public view virtual override returns (uint[] memory amounts)

这些函数直接代理UniswapV2Library的同名方法,前端/合约可调用以预计算交易价格。


7. 重要常量速查

常量所在合约说明
MINIMUM_LIQUIDITY10**3Pair首次添加流动性时永久锁入address(0)的 LP Token
手续费率0.3%Pair (swap)每笔交易扣 0.3% → 全归 LP
协议手续费1/6 × 手续费增长Pair (_mintFee)feeTo != 0时触发,约占 LP 总手续费的0.05%
手续费精度因子1000Library计算时使用 997/1000(= 1 - 0.003)
K 校验精度1000²Pair (swap)交易后需满足balance0Adj × balance1Adj >= reserve0 × reserve1 × 1000²
Pair init code hash(主网)0x96e8ac42...LibrarypairFor 反推地址用,不同链需重新计算

8. 安全注意事项

8.1 时间戳攻击(deadline)

所有交易必须传deadline参数,Router 用ensure修饰符检查:

require(deadline >= block.timestamp, 'UniswapV2Router: EXPIRED');

→ 防止交易在签署后被矿工延迟打包导致不利价格执行。

8.2 K 值校验(防止手续费提取攻击)

Pair 的swap中强制校验:

balance0Adj × balance1Adj ≥ reserve0 × reserve1 × 1000²

→ 确保用户支付的 0.3% 手续费被合理扣留,防止通过异常余额绕过的攻击。

8.3 重入保护

Pair 的mint / burn / swap / skim / sync均使用lock修饰符:

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

→ 单合约重入被阻止,但跨合约调用(如 multicall 场景)仍需额外注意。

8.4 闪电兑换安全(IUniswapV2Callee)

实现uniswapV2Call时必须:

  • 在回调结束时(或通过其他路径)偿还从 Pair 借出的代币
  • 偿还量必须 ≥amount0Out + amount0Out × 3/1000(或 token1 等效)
  • 建议使用等量偿还(即借 A 还 A),避免汇率风险

8.5approve+transferFrom的两步骤陷阱

  • Router 的addLiquidity系列函数不调用 approve,而是要求用户预先 approveRouter 可以支配的代币额度
  • 对于支持permit的代币(EIP-2612),应优先使用 permit 签名代替 approve,避免 approve 授权导致的潜在风险

8.6 Pair 地址的跨链差异

UniswapV2Library.pairFor中硬编码的init code hash仅适用于以太坊主网

hex'96e8ac4277198ff8b6f785478aa9a39f403cb768dd02cbee326c3e7da348845f'

部署到其他 EVM 链时需用对应链的 Pair bytecode 重新计算。

8.7 滑点与amountOutMin/amountInMax

  • 输出保护swapExact*系列必须设amountOutMin,防止价格剧烈波动下的糟糕成交
  • 输入保护swap*ForExact*系列必须设amountInMax,防止过度消耗预算
  • 建议值:普通交易 0.5% 以内,高波动资产 ≤ 1%

8.8skim/sync的使用场景

  • sync:用于当外部直接向 Pair 合约转账(如错误转账)后,强制将 ERC20 余额同步为 reserve
  • skim:用于提取超出 reserve 的意外余额,可用于紧急救援

源码仓库:v2-core / v2-periphery