Skip to main content

第四篇:Uniswap V3 流动性管理核心代码解析

深入剖析mint、burn、collect的完整实现流程


📋 目录

  1. 流动性管理概述
  2. Position数据结构
  3. mint函数完整解析
  4. burn函数详解
  5. collect函数实现
  6. 手续费计算机制
  7. 实战案例分析
  8. 总结与思考

1. 流动性管理概述

1.1 V3与V2的流动性差异

V2流动性

// 全价格范围
x * y = k
LP代币代表在整个范围内的流动性份额

V3流动性

// 指定价格区间 [tickLower, tickUpper]
每个仓位独立管理
使用NFT代表仓位(在Periphery中)

1.2 核心函数三剑客

mint()    :添加流动性到指定价格区间
burn() :移除流动性
collect() :提取手续费和/或移除的代币

设计哲学

  • mint和burn只改变流动性状态
  • 代币的实际转移通过回调和collect完成
  • 分离状态变更和资金转移,提高灵活性

2. Position数据结构

2.1 Position.Info定义

library Position {
struct Info {
// 流动性数量
uint128 liquidity;

// 上次更新时的内部手续费增长率
uint256 feeGrowthInside0LastX128;
uint256 feeGrowthInside1LastX128;

// 累计的未提取手续费
uint128 tokensOwed0;
uint128 tokensOwed1;
}
}

2.2 Position的唯一标识

// 仓位key的计算
function _positionKey(
address owner,
int24 tickLower,
int24 tickUpper
) private pure returns (bytes32) {
return keccak256(abi.encodePacked(owner, tickLower, tickUpper));
}

// 存储映射
mapping(bytes32 => Position.Info) public override positions;

特点

  • 同一owner可以有多个仓位(不同价格区间)
  • 同一价格区间,同一owner只能有一个仓位
  • 后续添加流动性会累加到现有仓位

2.3 Position.update函数

function update(
Info storage self,
int128 liquidityDelta,
uint256 feeGrowthInside0X128,
uint256 feeGrowthInside1X128
) internal {
Info memory _self = self;

uint128 liquidityNext;
if (liquidityDelta == 0) {
// 仅更新手续费,不改变流动性
require(_self.liquidity > 0, 'NP');
liquidityNext = _self.liquidity;
} else {
// 更新流动性
liquidityNext = LiquidityMath.addDelta(_self.liquidity, liquidityDelta);
}

// 计算新增手续费
uint128 tokensOwed0 = FullMath.mulDiv(
feeGrowthInside0X128 - _self.feeGrowthInside0LastX128,
_self.liquidity,
FixedPoint128.Q128
).toUint128();

uint128 tokensOwed1 = FullMath.mulDiv(
feeGrowthInside1X128 - _self.feeGrowthInside1LastX128,
_self.liquidity,
FixedPoint128.Q128
).toUint128();

// 更新状态
if (liquidityDelta != 0) self.liquidity = liquidityNext;
self.feeGrowthInside0LastX128 = feeGrowthInside0X128;
self.feeGrowthInside1X128 = feeGrowthInside1X128;
if (tokensOwed0 > 0 || tokensOwed1 > 0) {
self.tokensOwed0 += tokensOwed0;
self.tokensOwed1 += tokensOwed1;
}
}

3. mint函数完整解析

3.1 函数签名

function mint(
address recipient, // 仓位接收者
int24 tickLower, // 下边界Tick
int24 tickUpper, // 上边界Tick
uint128 amount, // 流动性数量
bytes calldata data // 回调数据
) external override lock returns (uint256 amount0, uint256 amount1) {
// 实现...
}

3.2 完整流程

function mint(...) external override lock returns (uint256 amount0, uint256 amount1) {
// ═══════════════════════════════════════════
// 步骤1:参数验证
// ═══════════════════════════════════════════
require(amount > 0);

// ═══════════════════════════════════════════
// 步骤2:修改仓位(核心逻辑)
// ═══════════════════════════════════════════
(, int256 amount0Int, int256 amount1Int) = _modifyPosition(
ModifyPositionParams({
owner: recipient,
tickLower: tickLower,
tickUpper: tickUpper,
liquidityDelta: int256(amount).toInt128()
})
);

amount0 = uint256(amount0Int);
amount1 = uint256(amount1Int);

// ═══════════════════════════════════════════
// 步骤3:记录余额(用于后续验证)
// ═══════════════════════════════════════════
uint256 balance0Before;
uint256 balance1Before;
if (amount0 > 0) balance0Before = balance0();
if (amount1 > 0) balance1Before = balance1();

// ═══════════════════════════════════════════
// 步骤4:回调要求转账
// ═══════════════════════════════════════════
IUniswapV3MintCallback(msg.sender).uniswapV3MintCallback(
amount0,
amount1,
data
);

// ═══════════════════════════════════════════
// 步骤5:验证余额变化
// ═══════════════════════════════════════════
if (amount0 > 0) require(balance0Before.add(amount0) <= balance0(), 'M0');
if (amount1 > 0) require(balance1Before.add(amount1) <= balance1(), 'M1');

// ═══════════════════════════════════════════
// 步骤6:触发事件
// ═══════════════════════════════════════════
emit Mint(msg.sender, recipient, tickLower, tickUpper, amount, amount0, amount1);
}

3.3 _modifyPosition核心逻辑

struct ModifyPositionParams {
address owner;
int24 tickLower;
int24 tickUpper;
int128 liquidityDelta;
}

function _modifyPosition(ModifyPositionParams memory params)
private
returns (Position.Info storage position, int256 amount0, int256 amount1)
{
// ────────────────────────────────────
// 步骤1:验证Tick有效性
// ────────────────────────────────────
checkTicks(params.tickLower, params.tickUpper);

Slot0 memory _slot0 = slot0;

// ────────────────────────────────────
// 步骤2:获取仓位
// ────────────────────────────────────
position = _updatePosition(
params.owner,
params.tickLower,
params.tickUpper,
params.liquidityDelta,
_slot0.tick
);

// ────────────────────────────────────
// 步骤3:计算需要的代币数量
// ────────────────────────────────────
if (params.liquidityDelta != 0) {
if (_slot0.tick < params.tickLower) {
// 当前价格在范围下方:只需要token0
amount0 = SqrtPriceMath.getAmount0Delta(
TickMath.getSqrtRatioAtTick(params.tickLower),
TickMath.getSqrtRatioAtTick(params.tickUpper),
params.liquidityDelta
);

} else if (_slot0.tick < params.tickUpper) {
// 当前价格在范围内:需要两种token
uint128 liquidityBefore = liquidity;

// 更新预言机
(slot0.observationIndex, slot0.observationCardinality) = observations.write(
_slot0.observationIndex,
_blockTimestamp(),
_slot0.tick,
liquidityBefore,
_slot0.observationCardinality,
_slot0.observationCardinalityNext
);

amount0 = SqrtPriceMath.getAmount0Delta(
_slot0.sqrtPriceX96,
TickMath.getSqrtRatioAtTick(params.tickUpper),
params.liquidityDelta
);

amount1 = SqrtPriceMath.getAmount1Delta(
TickMath.getSqrtRatioAtTick(params.tickLower),
_slot0.sqrtPriceX96,
params.liquidityDelta
);

// 更新全局流动性
liquidity = LiquidityMath.addDelta(liquidityBefore, params.liquidityDelta);

} else {
// 当前价格在范围上方:只需要token1
amount1 = SqrtPriceMath.getAmount1Delta(
TickMath.getSqrtRatioAtTick(params.tickLower),
TickMath.getSqrtRatioAtTick(params.tickUpper),
params.liquidityDelta
);
}
}
}

3.4 _updatePosition函数

function _updatePosition(
address owner,
int24 tickLower,
int24 tickUpper,
int128 liquidityDelta,
int24 tick
) private returns (Position.Info storage position) {
// ────────────────────────────────────
// 步骤1:更新Tick状态
// ────────────────────────────────────
bool flippedLower;
bool flippedUpper;

if (liquidityDelta != 0) {
uint32 time = _blockTimestamp();
(int56 tickCumulative, uint160 secondsPerLiquidityCumulativeX128) =
observations.observeSingle(
time,
0,
slot0.tick,
slot0.observationIndex,
liquidity,
slot0.observationCardinality
);

// 更新下边界Tick
flippedLower = ticks.update(
tickLower,
tick,
liquidityDelta,
feeGrowthGlobal0X128,
feeGrowthGlobal1X128,
secondsPerLiquidityCumulativeX128,
tickCumulative,
time,
false, // lower tick
maxLiquidityPerTick
);

// 更新上边界Tick
flippedUpper = ticks.update(
tickUpper,
tick,
liquidityDelta,
feeGrowthGlobal0X128,
feeGrowthGlobal1X128,
secondsPerLiquidityCumulativeX128,
tickCumulative,
time,
true, // upper tick
maxLiquidityPerTick
);

// ────────────────────────────────────
// 步骤2:更新TickBitmap
// ────────────────────────────────────
if (flippedLower) {
tickBitmap.flipTick(tickLower, tickSpacing);
}
if (flippedUpper) {
tickBitmap.flipTick(tickUpper, tickSpacing);
}
}

// ────────────────────────────────────
// 步骤3:计算手续费增长
// ────────────────────────────────────
(uint256 feeGrowthInside0X128, uint256 feeGrowthInside1X128) =
ticks.getFeeGrowthInside(tickLower, tickUpper, tick, feeGrowthGlobal0X128, feeGrowthGlobal1X128);

// ────────────────────────────────────
// 步骤4:更新仓位
// ────────────────────────────────────
position = positions.get(owner, tickLower, tickUpper);
position.update(liquidityDelta, feeGrowthInside0X128, feeGrowthInside1X128);

// ────────────────────────────────────
// 步骤5:清理空仓位(gas优化)
// ────────────────────────────────────
if (liquidityDelta < 0) {
if (flippedLower) {
ticks.clear(tickLower);
}
if (flippedUpper) {
ticks.clear(tickUpper);
}
}
}

3.5 代币数量计算详解

场景1:当前价格在范围下方

价格线:                     current

──────────────────────────┼──────────────────
[position]
↑ ↑
tickLower tickUpper

此时仓位完全由token0组成
amount0 = L * (1/√P_lower - 1/√P_upper)
amount1 = 0

场景2:当前价格在范围内

价格线:                          current

────────────────────[position]───┼───────────
↑ ↑
tickLower tickUpper

此时仓位由两种token组成
amount0 = L * (1/√P_current - 1/√P_upper)
amount1 = L * (√P_current - √P_lower)

场景3:当前价格在范围上方

价格线:   current

─────────┼────────────[position]──────────────
↑ ↑
tickLower tickUpper

此时仓位完全由token1组成
amount0 = 0
amount1 = L * (√P_upper - √P_lower)

4. burn函数详解

4.1 函数签名与流程

function burn(
int24 tickLower,
int24 tickUpper,
uint128 amount
) external override lock returns (uint256 amount0, uint256 amount1) {
// ═══════════════════════════════════════════
// 步骤1:修改仓位(liquidityDelta为负)
// ═══════════════════════════════════════════
(Position.Info storage position, int256 amount0Int, int256 amount1Int) =
_modifyPosition(
ModifyPositionParams({
owner: msg.sender,
tickLower: tickLower,
tickUpper: tickUpper,
liquidityDelta: -int256(amount).toInt128()
})
);

amount0 = uint256(-amount0Int);
amount1 = uint256(-amount1Int);

// ═══════════════════════════════════════════
// 步骤2:更新tokensOwed(记录可提取金额)
// ═══════════════════════════════════════════
if (amount0 > 0 || amount1 > 0) {
(position.tokensOwed0, position.tokensOwed1) = (
position.tokensOwed0 + uint128(amount0),
position.tokensOwed1 + uint128(amount1)
);
}

// ═══════════════════════════════════════════
// 步骤3:触发事件
// ═══════════════════════════════════════════
emit Burn(msg.sender, tickLower, tickUpper, amount, amount0, amount1);
}

4.2 burn vs mint的区别

特性mintburn
liquidityDelta正值负值
代币流向用户→池子池子→用户(稍后)
立即转账是(通过回调)否(记录tokensOwed)
需要collect

为什么burn不立即转账?

  1. Gas优化:用户可能多次burn后一次性collect
  2. 手续费一起提取:burn的代币和手续费一起提取
  3. 灵活性:用户可以选择提取时机

5. collect函数实现

5.1 函数定义

function collect(
address recipient, // 接收地址
int24 tickLower,
int24 tickUpper,
uint128 amount0Requested, // 请求的token0数量
uint128 amount1Requested // 请求的token1数量
) external override lock returns (uint128 amount0, uint128 amount1) {
// ═══════════════════════════════════════════
// 步骤1:获取仓位
// ═══════════════════════════════════════════
Position.Info storage position = positions.get(msg.sender, tickLower, tickUpper);

// ═══════════════════════════════════════════
// 步骤2:计算实际可提取数量
// ═══════════════════════════════════════════
amount0 = amount0Requested > position.tokensOwed0
? position.tokensOwed0
: amount0Requested;
amount1 = amount1Requested > position.tokensOwed1
? position.tokensOwed1
: amount1Requested;

// ═══════════════════════════════════════════
// 步骤3:更新tokensOwed
// ═══════════════════════════════════════════
if (amount0 > 0) {
position.tokensOwed0 -= amount0;
TransferHelper.safeTransfer(token0, recipient, amount0);
}
if (amount1 > 0) {
position.tokensOwed1 -= amount1;
TransferHelper.safeTransfer(token1, recipient, amount1);
}

// ═══════════════════════════════════════════
// 步骤4:触发事件
// ═══════════════════════════════════════════
emit Collect(msg.sender, recipient, tickLower, tickUpper, amount0, amount1);
}

5.2 使用模式

模式1:提取全部

// 提取所有可提取的代币和手续费
pool.collect(
recipient,
tickLower,
tickUpper,
type(uint128).max, // 最大值表示全部
type(uint128).max
);

模式2:部分提取

// 只提取部分(例如只提取手续费)
pool.collect(
recipient,
tickLower,
tickUpper,
1000 * 1e6, // 只提取1000 USDC
0 // 不提取token1
);

模式3:先burn再collect

// 1. 移除流动性
(uint256 amount0, uint256 amount1) = pool.burn(tickLower, tickUpper, liquidity);

// 2. 提取代币(包括手续费)
pool.collect(
recipient,
tickLower,
tickUpper,
type(uint128).max,
type(uint128).max
);

6. 手续费计算机制

6.1 全局手续费追踪

// 每次swap后更新
if (state.liquidity > 0) {
feeGrowthGlobal0X128 += FullMath.mulDiv(
feeAmount,
FixedPoint128.Q128,
state.liquidity
);
}

6.2 仓位手续费计算

// 在Position.update中
uint256 tokensOwed0 = FullMath.mulDiv(
feeGrowthInside0X128 - _self.feeGrowthInside0LastX128, // 增量
_self.liquidity, // 仓位流动性
FixedPoint128.Q128 // 定点数转换
).toUint128();

// 累加到tokensOwed
self.tokensOwed0 += tokensOwed0;

// 更新last值
self.feeGrowthInside0LastX128 = feeGrowthInside0X128;

6.3 手续费计算时机

手续费在以下操作时计算并累加到tokensOwed:

1. mint():添加流动性时
- 计算上次操作以来累积的手续费
- 添加到tokensOwed

2. burn():移除流动性时
- 计算累积的手续费
- 添加到tokensOwed

3. collect():仅更新手续费(liquidityDelta=0)
- 不改变流动性
- 只计算和累加手续费

6.4 手续费计算示例

// 初始状态
position.liquidity = 1000000
position.feeGrowthInside0LastX128 = 100 * 2^128

// 一段时间后
feeGrowthInside0X128 = 150 * 2^128

// 计算应得手续费
deltaGrowth = (150 - 100) * 2^128 = 50 * 2^128
tokensOwed0 = (50 * 2^128) * 1000000 / 2^128
= 50 * 1000000
= 50,000,000

// 更新状态
position.tokensOwed0 += 50,000,000
position.feeGrowthInside0LastX128 = 150 * 2^128

7. 实战案例分析

7.1 案例:完整的流动性生命周期

// ═══════════════════════════════════════════
// 阶段1:添加流动性
// ═══════════════════════════════════════════
// 初始状态
currentTick = 1000
currentPrice = 1.0001^10001.1052

// 用户操作
pool.mint(
recipient = 0xAlice,
tickLower = 900, // 价格 ≈ 1.0942
tickUpper = 1100, // 价格 ≈ 1.1163
amount = 1000000 // 流动性
)

// 计算需要的代币
sqrtP_current =1.10521.0513
sqrtP_lower =1.09421.0460
sqrtP_upper =1.11631.0566

amount0 = 1000000 * (1/1.0513 - 1/1.0566)4,780
amount1 = 1000000 * (1.0513 - 1.0460)5,300

// 状态更新
positions[keccak256(Alice, 900, 1100)] = {
liquidity: 1000000,
feeGrowthInside0LastX128: currentFeeGrowth0,
feeGrowthInside1LastX128: currentFeeGrowth1,
tokensOwed0: 0,
tokensOwed1: 0
}

ticks[900].liquidityGross += 1000000
ticks[900].liquidityNet += 1000000
ticks[1100].liquidityGross += 1000000
ticks[1100].liquidityNet -= 1000000

liquidity += 1000000 // 全局流动性增加

// ═══════════════════════════════════════════
// 阶段2:产生交易手续费
// ═══════════════════════════════════════════
// 假设发生多笔交易,累计手续费
totalFees0 = 1000
totalFees1 = 1200

// 更新全局手续费增长率
feeGrowthGlobal0X128 += (1000 * 2^128) / liquidity
feeGrowthGlobal1X128 += (1200 * 2^128) / liquidity

// ═══════════════════════════════════════════
// 阶段3:部分移除流动性
// ═══════════════════════════════════════════
pool.burn(
tickLower = 900,
tickUpper = 1100,
amount = 500000 // 移除一半
)

// 计算手续费(mint到burn期间)
(feeGrowthInside0, feeGrowthInside1) = getFeeGrowthInside(900, 1100)
earnedFees0 = (feeGrowthInside0 - feeGrowthInside0Last) * 1000000 / 2^128
earnedFees1 = (feeGrowthInside1 - feeGrowthInside1Last) * 1000000 / 2^128

// 计算返还的代币
returnAmount0 = 500000 * (1/sqrtP_current - 1/sqrtP_upper)
returnAmount1 = 500000 * (sqrtP_current - sqrtP_lower)

// 更新状态
position.liquidity = 500000
position.tokensOwed0 += earnedFees0 + returnAmount0
position.tokensOwed1 += earnedFees1 + returnAmount1
position.feeGrowthInside0LastX128 = feeGrowthInside0
position.feeGrowthInside1LastX128 = feeGrowthInside1

// ═══════════════════════════════════════════
// 阶段4:提取代币和手续费
// ═══════════════════════════════════════════
pool.collect(
recipient = 0xAlice,
tickLower = 900,
tickUpper = 1100,
amount0Requested = type(uint128).max, // 全部提取
amount1Requested = type(uint128).max
)

// 转账
token0.transfer(Alice, position.tokensOwed0)
token1.transfer(Alice, position.tokensOwed1)

// 更新状态
position.tokensOwed0 = 0
position.tokensOwed1 = 0

7.2 案例:手续费只提取不移除流动性

// 仓位继续工作,只提取手续费
pool.collect(
recipient,
tickLower,
tickUpper,
type(uint128).max,
type(uint128).max
);

// 内部流程
Position.update(liquidityDelta = 0) // 不改变流动性
├─> 计算新增手续费
├─> 累加到tokensOwed
└─> 更新feeGrowthInsideLast

collect()
└─> 转账tokensOwed到recipient

8. 总结与思考

8.1 核心要点

  1. 三步分离:mint(状态) → burn(状态) → collect(转账)
  2. 手续费追踪:使用增长率差值计算,避免遍历
  3. tokensOwed机制:延迟转账优化Gas
  4. 价格区间管理:根据当前价格计算所需代币比例
  5. Tick联动更新:流动性变化时同步更新Tick和TickBitmap

8.2 思考题

  1. 为什么burn不立即转账而是记录tokensOwed?
  2. 如果仓位跨越当前价格,mint需要两种token的比例如何计算?
  3. 手续费在什么时候计算?计算后立即转账吗?
  4. collect可以部分提取吗?这样设计有什么好处?
  5. 为什么要在_modifyPosition中更新预言机?

8.3 延伸阅读


本文是"Uniswap V3源码赏析系列"的第四篇

📢 Share this article