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](#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 等    │            │
│  └─────────────────────────────────────────────┘            │
└─────────────────────────────────────────────────────────────┘
层级 合约 职责
Core UniswapV2Factory 创建交易对、管理 Pair 注册表
Core UniswapV2Pair AMM 核心逻辑:mint / burn / swap / 预言机
Core UniswapV2ERC20 LP Token(ERC20 + EIP-2612 permit)
Periphery UniswapV2Router02 用户入口:添加流动性 / 移除流动性 / 交易
Periphery UniswapV2Library 纯函数计算库:价格 / 金额 / pair 地址

主线网常用地址

合约 地址
Factory 0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f
Router02 0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D

2. Core:UniswapV2Factory

源码:Uniswap/v2-core/contracts/UniswapV2Factory.sol

角色:交易对工厂 ,通过 create2 部署 Pair 合约并维护注册表。

2.1 状态变量

solidity 复制代码
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
solidity 复制代码
function createPair(address tokenA, address tokenB) external returns (address pair)

创建新的交易对 Pair 合约。

参数 类型 说明
tokenA address 代币 A 地址
tokenB address 代币 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
solidity 复制代码
function getPair(address tokenA, address tokenB) external view returns (address pair)

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


allPairs / allPairsLength
solidity 复制代码
function allPairs(uint index) external view returns (address pair)
function allPairsLength() external view returns (uint)

枚举所有已创建的 Pair。


setFeeTo
solidity 复制代码
function setFeeTo(address _feeTo) external

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

权限:feeToSetter 可调用。

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


setFeeToSetter
solidity 复制代码
function setFeeToSetter(address _feeToSetter) external

转移 feeToSetter 权限。

权限: 仅当前 feeToSetter 可调用。


2.3 Events

solidity 复制代码
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 状态变量

solidity 复制代码
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 核心常量

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

3.3 只读函数(Read-Only)

getReserves
solidity 复制代码
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
solidity 复制代码
function mint(address to) external lock returns (uint liquidity)

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

参数 类型 说明
to address LP Token 接收地址

计算逻辑:

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

事件:

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

burn
solidity 复制代码
function burn(address to) external lock returns (uint amount0, uint amount1)

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

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

计算逻辑:

复制代码
amount0 = liquidity * balance0 / totalSupply
amount1 = liquidity * balance1 / totalSupply

事件:

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

3.5 交易操作

swap
solidity 复制代码
function swap(
    uint amount0Out,
    uint amount1Out,
    address to,
    bytes calldata data
) external lock

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

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

约束条件:

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

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

solidity 复制代码
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% 手续费被合理扣留)。

事件:

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

3.6 辅助操作

skim
solidity 复制代码
function skim(address to) external lock

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


sync
solidity 复制代码
function sync() external lock

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


3.7 预言机相关

price0CumulativeLast / price1CumulativeLast
solidity 复制代码
uint public price0CumulativeLast;  // token1/token0 的累计价格 × 时间
uint public price1CumulativeLast;  // token0/token1 的累计价格 × 时间

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

solidity 复制代码
price0CumulativeLast += uint(UQ112x112.encode(_reserve1).uqdiv(_reserve0)) * timeElapsed;
price1CumulativeLast += uint(UQ112x112.encode(_reserve0).uqdiv(_reserve1)) * timeElapsed;

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

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

4. Core:IUniswapV2Callee

源码:Uniswap/v2-core/contracts/interfaces/IUniswapV2Callee.sol

用途:**闪电兑换(Flash Swap)**回调接口。

接口签名

solidity 复制代码
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.1 sortTokens

solidity 复制代码
function sortTokens(address tokenA, address tokenB)
    internal pure returns (address token0, address token1)

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

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


5.2 pairFor

solidity 复制代码
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.3 getReserves

solidity 复制代码
function getReserves(
    address factory,
    address tokenA,
    address tokenB
) internal view returns (uint reserveA, uint reserveB)

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


5.4 quote

solidity 复制代码
function quote(
    uint amountA,
    uint reserveA,
    uint reserveB
) internal pure returns (uint amountB)

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

solidity 复制代码
amountB = amountA * reserveB / reserveA

5.5 getAmountOut

solidity 复制代码
function getAmountOut(
    uint amountIn,
    uint reserveIn,
    uint reserveOut
) internal pure returns (uint amountOut)

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

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

5.6 getAmountIn

solidity 复制代码
function getAmountIn(
    uint amountOut,
    uint reserveIn,
    uint reserveOut
) internal pure returns (uint amountIn)

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

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

5.7 getAmountsOut

solidity 复制代码
function getAmountsOut(
    address factory,
    uint amountIn,
    address[] memory path
) internal view returns (uint[] memory amounts)

多跳路径的输出量计算,从 path0 → path1 → ... → pathlast

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

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


5.8 getAmountsIn

solidity 复制代码
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 构造函数与状态

solidity 复制代码
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)
solidity 复制代码
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
solidity 复制代码
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 最小注入量(防止滑点过高)
to LP Token 接收地址
deadline 交易截止时间戳(防止重放)

addLiquidityETH
solidity 复制代码
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 后注入)。

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

6.4 移除流动性

removeLiquidity
solidity 复制代码
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
solidity 复制代码
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 的变体
solidity 复制代码
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 代币的移除
solidity 复制代码
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,固定输入量)
solidity 复制代码
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,固定输出量)
solidity 复制代码
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)
solidity 复制代码
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,固定输出)
solidity 复制代码
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)
solidity 复制代码
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,固定输出)
solidity 复制代码
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 代币的交换
solidity 复制代码
// 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)

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

solidity 复制代码
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 代理)

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

8. 安全注意事项

8.1 时间戳攻击(deadline)

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

solidity 复制代码
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 修饰符:

solidity 复制代码
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.5 approve + transferFrom 的两步骤陷阱

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

8.6 Pair 地址的跨链差异

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

solidity 复制代码
hex'96e8ac4277198ff8b6f785478aa9a39f403cb768dd02cbee326c3e7da348845f'

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

8.7 滑点与 amountOutMin / amountInMax

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

8.8 skim / sync 的使用场景

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

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