Uniswap V3 Periphery 提供了用户交互接口,最大的特点是使用 NFT 来表示流动性头寸,每个头寸都是独一无二的。
1. 概述
V3 Periphery 与 V2 的主要区别:
| 特性 |
V2 |
V3 |
| 流动性凭证 |
ERC-20 LP Token |
ERC-721 NFT |
| 价格范围 |
全范围 (0, ∞) |
自定义范围 [tickLower, tickUpper] |
| 头寸管理 |
同质化,可合并 |
非同质化,每个独立 |
| 合约 |
功能 |
| SwapRouter |
交换路由,支持单池和多池路径交换 |
| NonfungiblePositionManager |
NFT 头寸管理器,创建/管理流动性头寸 |
| Quoter |
报价器,模拟交换计算输出数量 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| ┌─────────────────────────────────────────────────────────┐ │ 用户 │ └─────────────────────────────────────────────────────────┘ │ │ ▼ ▼ ┌─────────────────────┐ ┌─────────────────────────────┐ │ SwapRouter │ │ NonfungiblePositionManager │ │ (交换路由) │ │ (NFT 头寸管理) │ └─────────────────────┘ └─────────────────────────────┘ │ │ └──────────────┬───────────────┘ ▼ ┌─────────────────────────────────────────────────────────┐ │ UniswapV3Pool │ │ (Core - 核心逻辑层) │ └─────────────────────────────────────────────────────────┘
|
2. SwapRouter 合约解析
SwapRouter 封装了 Pool 的 swap 函数,提供更友好的交换接口。
2.1 单池交换
指定输入数量,单池交换(Exact Input)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| struct ExactInputSingleParams { address tokenIn; // 输入代币 address tokenOut; // 输出代币 uint24 fee; // 池子费率(500/3000/10000) address recipient; // 接收地址 uint256 deadline; // 截止时间 uint256 amountIn; // 输入数量 uint256 amountOutMinimum; // 最小输出(滑点保护) uint160 sqrtPriceLimitX96;// 价格限制(0 表示不限制) }
function exactInputSingle( ExactInputSingleParams calldata params ) external payable returns (uint256 amountOut);
|
示例:用 1 ETH 换 USDC
1 2 3 4 5 6
| tokenIn = WETH tokenOut = USDC fee = 3000 amountIn = 1 ETH amountOutMinimum = 1900 USDC sqrtPriceLimitX96 = 0
|
exactOutputSingle
指定输出数量,单池交换(Exact Output)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| struct ExactOutputSingleParams { address tokenIn; address tokenOut; uint24 fee; address recipient; uint256 deadline; uint256 amountOut; // 期望输出数量 uint256 amountInMaximum; // 最大输入(滑点保护) uint160 sqrtPriceLimitX96; }
function exactOutputSingle( ExactOutputSingleParams calldata params ) external payable returns (uint256 amountIn);
|
示例:想获得正好 2000 USDC
1 2 3 4 5
| tokenIn = WETH tokenOut = USDC fee = 3000 amountOut = 2000 USDC amountInMaximum = 1.1 ETH
|
2.2 多池路径交换
V3 的路径编码格式:tokenIn + fee + tokenOut + fee + tokenOut + ...
1 2
| 路径示例:USDC → ETH → DAI 编码:USDC(20bytes) + 3000(3bytes) + WETH(20bytes) + 500(3bytes) + DAI(20bytes)
|
指定输入,多池路径交换。
1 2 3 4 5 6 7 8 9 10 11
| struct ExactInputParams { bytes path; // 编码后的路径 address recipient; uint256 deadline; uint256 amountIn; uint256 amountOutMinimum; }
function exactInput( ExactInputParams calldata params ) external payable returns (uint256 amountOut);
|
示例:100 USDC → WETH → DAI
1 2 3 4 5 6 7 8 9 10 11 12 13
| ExactInputParams({ path: abi.encodePacked( USDC, // tokenIn uint24(3000), // USDC-WETH 池费率 WETH, // 中间代币 uint24(500), // WETH-DAI 池费率 DAI // tokenOut ), recipient: msg.sender, deadline: block.timestamp + 300, amountIn: 100e6, // 100 USDC amountOutMinimum: 99e18 // 至少 99 DAI })
|
exactOutput
指定输出,多池路径交换。
1 2 3 4 5 6 7 8 9 10 11
| struct ExactOutputParams { bytes path; // 路径(注意:从 tokenOut 到 tokenIn 反向编码) address recipient; uint256 deadline; uint256 amountOut; uint256 amountInMaximum; }
function exactOutput( ExactOutputParams calldata params ) external payable returns (uint256 amountIn);
|
注意:exactOutput 的 path 是反向的,从输出代币到输入代币。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| // 想获得 100 DAI,路径 USDC → WETH → DAI // path 编码为:DAI + fee + WETH + fee + USDC(反向) ExactOutputParams({ path: abi.encodePacked( DAI, // 从 tokenOut 开始 uint24(500), WETH, uint24(3000), USDC // 到 tokenIn 结束 ), recipient: msg.sender, deadline: block.timestamp + 300, amountOut: 100e18, // 100 DAI amountInMaximum: 102e6 // 最多 102 USDC })
|
3. NonfungiblePositionManager 合约解析
NonfungiblePositionManager 将流动性头寸封装为 ERC-721 NFT,每个 NFT 代表一个独立的流动性头寸。
3.1 头寸管理
mint - 创建头寸
创建新的流动性头寸,铸造 NFT。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| struct MintParams { address token0; // 代币0(地址较小) address token1; // 代币1(地址较大) uint24 fee; // 费率 int24 tickLower; // 价格下界 tick int24 tickUpper; // 价格上界 tick uint256 amount0Desired; // 期望存入的 token0 uint256 amount1Desired; // 期望存入的 token1 uint256 amount0Min; // 最小 token0(滑点保护) uint256 amount1Min; // 最小 token1(滑点保护) address recipient; // NFT 接收地址 uint256 deadline; }
function mint( MintParams calldata params ) external payable returns ( uint256 tokenId, // NFT ID uint128 liquidity, // 流动性数量 uint256 amount0, // 实际存入的 token0 uint256 amount1 // 实际存入的 token1 );
|
示例:在 ETH/USDC 池创建头寸,价格范围 1500-2500 USDC/ETH
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| // 首先计算 tick 范围 // tick = log1.0001(price) // tickLower ≈ 73136 (对应价格 ~1500) // tickUpper ≈ 78245 (对应价格 ~2500)
MintParams({ token0: USDC, // 地址较小 token1: WETH, // 地址较大 fee: 3000, tickLower: 73136, tickUpper: 78245, amount0Desired: 3000e6, // 3000 USDC amount1Desired: 1e18, // 1 ETH amount0Min: 2970e6, amount1Min: 0.99e18, recipient: msg.sender, deadline: block.timestamp + 300 })
|
increaseLiquidity
向已有头寸添加流动性。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| struct IncreaseLiquidityParams { uint256 tokenId; // NFT ID uint256 amount0Desired; uint256 amount1Desired; uint256 amount0Min; uint256 amount1Min; uint256 deadline; }
function increaseLiquidity( IncreaseLiquidityParams calldata params ) external payable returns ( uint128 liquidity, uint256 amount0, uint256 amount1 );
|
decreaseLiquidity
从头寸移除流动性(不提取代币,只减少流动性)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| struct DecreaseLiquidityParams { uint256 tokenId; uint128 liquidity; // 要移除的流动性数量 uint256 amount0Min; uint256 amount1Min; uint256 deadline; }
function decreaseLiquidity( DecreaseLiquidityParams calldata params ) external payable returns ( uint256 amount0, // 可提取的 token0 uint256 amount1 // 可提取的 token1 );
|
注意:decreaseLiquidity 只是将流动性转换为待提取的代币,实际提取需要调用 collect。
3.2 收益提取
collect
提取头寸的代币(包括移除的流动性 + 累积的手续费)。
1 2 3 4 5 6 7 8 9 10 11 12 13
| struct CollectParams { uint256 tokenId; address recipient; uint128 amount0Max; // 最大提取 token0(type(uint128).max 提取全部) uint128 amount1Max; // 最大提取 token1 }
function collect( CollectParams calldata params ) external payable returns ( uint256 amount0, uint256 amount1 );
|
完整退出流程:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| // 1. 移除全部流动性 decreaseLiquidity({ tokenId: 12345, liquidity: position.liquidity, // 全部流动性 amount0Min: 0, amount1Min: 0, deadline: block.timestamp + 300 });
// 2. 提取代币 + 手续费 collect({ tokenId: 12345, recipient: msg.sender, amount0Max: type(uint128).max, // 提取全部 amount1Max: type(uint128).max });
// 3. 销毁 NFT(可选) burn(12345);
|
4. Quoter 合约解析
Quoter 用于模拟交换,返回预期输出数量,常用于前端显示报价。
4.1 交换报价
单池交换报价。
1 2 3 4 5 6 7
| function quoteExactInputSingle( address tokenIn, address tokenOut, uint24 fee, uint256 amountIn, uint160 sqrtPriceLimitX96 ) external returns (uint256 amountOut);
|
注意:Quoter 函数会 revert 并在 revert data 中返回结果,调用时需要用 try/catch 或 staticcall 捕获。
1 2 3 4 5 6 7 8
| // 前端调用示例(ethers.js) const quotedAmountOut = await quoter.callStatic.quoteExactInputSingle( WETH_ADDRESS, USDC_ADDRESS, 3000, ethers.utils.parseEther("1"), 0 );
|
多池路径交换报价。
1 2 3 4
| function quoteExactInput( bytes memory path, uint256 amountIn ) external returns (uint256 amountOut);
|
quoteExactOutputSingle / quoteExactOutput
反向报价,计算达到指定输出需要的输入数量。
1 2 3 4 5 6 7 8 9 10 11 12
| function quoteExactOutputSingle( address tokenIn, address tokenOut, uint24 fee, uint256 amountOut, uint160 sqrtPriceLimitX96 ) external returns (uint256 amountIn);
function quoteExactOutput( bytes memory path, // 反向路径 uint256 amountOut ) external returns (uint256 amountIn);
|
5. 常用操作流程
5.1 添加流动性
1 2 3 4 5
| 1. 选择代币对和费率 2. 设置价格范围(tickLower, tickUpper) 3. approve 代币给 NonfungiblePositionManager 4. 调用 mint() 创建头寸 5. 获得 NFT(tokenId)
|
5.2 移除流动性
1 2 3
| 1. 调用 decreaseLiquidity() 减少流动性 2. 调用 collect() 提取代币 3. (可选)调用 burn() 销毁空头寸 NFT
|
5.3 交换代币
1 2 3 4
| 1. 使用 Quoter 获取报价 2. approve 代币给 SwapRouter 3. 调用 exactInputSingle() 或 exactInput() 4. 获得输出代币
|