Uniswap V3 第 6 章:流动性(Liquidity)
这一章讲 LP 怎么实操:用
NonfungiblePositionManager铸造一个 NFT 头寸、增加/减少流动性、领取累积的手续费。底层对应 Pool 的mint/burn/collect。
目录
- 1. 头寸的生命周期
- 2. mint:铸造一个 NFT 头寸
- 3. tick 必须是 tickSpacing 的整数倍
- 4. 底层三件套:pool.mint / burn / collect
- 5. 关键:burn 不退币,collect 才退
- 6. increaseLiquidity / decreaseLiquidity
- 7. 案例:完整的 LP 流程
- 8. 本章小结
- 9. 动手练习
1. 头寸的生命周期
一个 V3 流动性头寸的一生:
mint(铸造 NFT,存入初始流动性)
│
├─ increaseLiquidity(往同一个头寸追加)
├─ decreaseLiquidity(抽出部分流动性 → 变成"欠你的代币")
├─ collect(领取手续费 + decrease 出来的代币)
│
└─ burn(流动性清零后,销毁这个 NFT)
NonfungiblePositionManager(简称 NPM)是 LP 的主要入口,它把这些操作包装好,并用一个 NFT(tokenId)代表你的整个头寸。
2. mint:铸造一个 NFT 头寸
manager.mint(MintParams({
token0: DAI, token1: WETH, fee: 3000,
tickLower: ..., tickUpper: ..., // 你选的价格区间
amount0Desired: 1000e18, amount1Desired: 1e18, // 想投入的量
amount0Min: 0, amount1Min: 0, // 滑点保护
recipient: address(this), // NFT 给谁
deadline: block.timestamp
}));
// 返回 (tokenId, liquidity, amount0, amount1)
- 你给
amount0Desired/amount1Desired,NPM 按你选的区间和当前价格,算出实际能用多少(回忆第 3 章:区间内两种币的比例是固定的,多的那种不会全用上)。 - 返回
tokenId(你的 NFT 编号)、实际投入的amount0/amount1、以及获得的流动性liquidity(L)。 - 区间用
tickLower/tickUpper表示。想做”全区间”(类似 V2)就用最大/最小 tick。
3. tick 必须是 tickSpacing 的整数倍
tickLower 和 tickUpper 必须能被池子的 tickSpacing 整除,否则 revert。
例如 0.3% 池的 tickSpacing=60,要做”全区间”头寸,不能直接用 MIN_TICK = -887272(不是 60 的倍数),而要对齐:
int24 tickLower = MIN_TICK / TICK_SPACING * TICK_SPACING; // 向 0 取整到 60 的倍数
int24 tickUpper = MAX_TICK / TICK_SPACING * TICK_SPACING;
这是练习里最容易踩的坑——任何区间端点都要先对齐到 tickSpacing。
4. 底层三件套:pool.mint / burn / collect
NPM 最终调用 Pool 的三个函数:
| Pool 函数 | 作用 |
|---|---|
mint(recipient, tickLower, tickUpper, amount, data) | 在区间增加流动性 L,通过回调收取两种代币 |
burn(tickLower, tickUpper, amount) | 减少流动性 L,把对应代币 + 已赚手续费记为”欠你的”(tokensOwed),但不转账 |
collect(recipient, tickLower, tickUpper, amount0Req, amount1Req) | 把”欠你的”代币真正转给你 |
mint 用回调模式(类似 flash):Pool 先记账,再回调要求调用者把代币转进来,检查到账才算数。
5. 关键:burn 不退币,collect 才退
这是 V3 一个反直觉但重要的设计:
burn不会把代币转给你,它只是把”该退的本金 + 已赚的手续费”累加到你头寸的tokensOwed0/tokensOwed1。要真正拿到币,必须再调collect。
为什么分两步?
- 让”减少流动性”和”提取代币”解耦,更灵活(你可以多次 burn 再一次性 collect)。
- 手续费的领取也走
collect:哪怕你不 burn,只要调collect,就能把累积的手续费取走(手续费在每次 swap 经过你区间时累计到 tokensOwed,详见第 8 章)。
所以”领手续费” = collect;“撤本金” = decreaseLiquidity(底层 burn) + collect。
6. increaseLiquidity / decreaseLiquidity
对一个已存在的头寸(tokenId):
manager.increaseLiquidity(IncreaseLiquidityParams({
tokenId: id, amount0Desired: ..., amount1Desired: ...,
amount0Min: ..., amount1Min: ..., deadline: ...
})); // 往同一区间追加流动性
manager.decreaseLiquidity(DecreaseLiquidityParams({
tokenId: id, liquidity: 减少多少L,
amount0Min: ..., amount1Min: ..., deadline: ...
})); // 抽出部分流动性 → 记为 tokensOwed(还没到手)
manager.collect(CollectParams({
tokenId: id, recipient: 你, amount0Max: max, amount1Max: max
})); // 把 tokensOwed(本金+手续费)真正取出来
完全撤出并销毁 NFT 的顺序:decreaseLiquidity(全部L) → collect(全部) → burn(tokenId)(NPM 的 burn,要求流动性和 owed 都已清零)。
7. 案例:完整的 LP 流程
小明给 DAI/WETH 0.3% 池做 LP:
1. mint:选区间 [tickLower, tickUpper](对齐到 60),投入 1000 DAI + 0.5 WETH
→ 得到 NFT #123,流动性 L,实际用了 1000 DAI + 0.33 WETH(多的 WETH 退回)
2. 一段时间内,交易者经过他的区间 swap,手续费累积
3. collect(#123):领取累积的手续费(比如 5 DAI + 0.002 WETH)
4. 想退出:
a. decreaseLiquidity(#123, 全部 L) → 本金记为 tokensOwed
b. collect(#123) → 拿回本金 + 剩余手续费
c. burn(#123) → 销毁 NFT
8. 本章小结
- V3 头寸是 NFT,由
NonfungiblePositionManager管理;生命周期:mint → increase/decrease/collect → burn。 mint选区间 [tickLower, tickUpper] 和投入量,返回 tokenId、实际投入、流动性 L。- 区间端点必须是 tickSpacing 的整数倍(要先对齐,否则 revert)。
- 底层 Pool 三件套:
mint(加 L,回调收币)、burn(减 L,记 tokensOwed,不转账)、collect(真正转账)。 - 领手续费 = collect;撤本金 = decreaseLiquidity + collect;销毁 = burn(NFT)。
9. 动手练习
对应课程的 Liquidity 练习:mint / increase / decrease / collect 全流程。
练习:NFT 头寸的完整操作
主网分叉,NPM = 0xC36442b4a4522E871399CD717aBDD847Ab11FE88,DAI/WETH 0.3% 池:
interface INonfungiblePositionManager {
function mint(MintParams calldata) external payable returns (uint256 tokenId, uint128 liquidity, uint256 amount0, uint256 amount1);
function increaseLiquidity(...) external payable returns (...);
function decreaseLiquidity(...) external payable returns (uint256 amount0, uint256 amount1);
function collect(...) external payable returns (uint256 amount0, uint256 amount1);
}
思路:
setUp:deal给自己 3000 DAI + 3 WETH,approve给 NPM。- mint:tickLower/tickUpper 用
MIN_TICK/60*60和MAX_TICK/60*60(全区间,已对齐),amount0Desired=1000e18、amount1Desired=1e18。断言返回liquidity > 0,拿到tokenId。 - increaseLiquidity:对同一 tokenId 再追加一些,断言流动性增加。
- swap 制造手续费(可选):用 Router 做几笔 DAI↔WETH 兑换,让你的区间累积手续费。
- decreaseLiquidity:减少全部 L。
- collect:
amount0Max/amount1Max = type(uint128).max,断言收回了本金 + 手续费(两种币余额增加)。
运行
forge test --evm-version cancun --fork-url $FORK_URL \
--match-path test/UniswapV3Liquidity.t.sol -vvv
下一章(第 7 章 Tick Bitmap)讲:swap 跨 tick 时,如何用位图高效找到”下一个有流动性的 tick”。