Uniswap V2 Core 和 Periphery 合约核心接口文档
Uniswap V2 核心接口文档
源码基于 Uniswap V2 官方仓库 v2-core / v2-periphery,Solidity 0.5.16。
文档结构:先架构总览,再逐合约展开,含完整函数签名、Events、关键参数说明及安全注意事项。
目录
- 架构总览
- Core:UniswapV2Factory
- Core:UniswapV2Pair
- Core:闪电兑换接口 IUniswapV2Callee
- Periphery:UniswapV2Library
- Periphery:UniswapV2Router02
- 重要常量速查
- 安全注意事项
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 状态变量
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 合约。
| 参数 | 类型 | 说明 |
|---|---|---|
tokenA | address | 代币 A 地址 |
tokenB | address | 代币 B 地址 |
约束条件:
tokenA != tokenBtokenA != 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 * reserve13.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 / reserve1与token0 / 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。
| 参数 | 类型 | 说明 |
|---|---|---|
to | address | LP 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 并转回两种代币。
| 参数 | 类型 | 说明 |
|---|---|---|
to | address | 收回代币的接收地址 |
计算逻辑:
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,需外部做安全检查)。
| 参数 | 类型 | 说明 |
|---|---|---|
amount0Out | uint | 要输出的 token0 数量(可为 0) |
amount1Out | uint | 要输出的 token1 数量(可为 0) |
to | address | 接收代币的地址(不能是 token0/token1 本身) |
data | bytes | 非空则回调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 != tokenB,tokenA != 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 / reserveA5.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 | 最小注入量(防止滑点过高) |
to | LP 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 后注入)。
| 参数 | 说明 |
|---|---|
token | ERC20 代币地址 |
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] 必须是 WETHswapETHForExactTokens(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] 必须是 WETHswapExactTokensForETH(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] 必须是 WETHswapTokensForExactETH(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_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修饰符检查:
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 余额同步为 reserveskim:用于提取超出 reserve 的意外余额,可用于紧急救援
源码仓库:v2-core / v2-periphery