死磕Uniswap V3(五):流动性管理与头寸

本文是「死磕Uniswap V3」系列的第五篇,深入剖析V3的流动性管理机制、Position数据结构以及mint/burn操作的完整实现。

系列导航

序号标题核心内容
01概述与集中流动性AMM演进、集中流动性原理
02Tick机制与价格数学Tick设计、价格转换算法
03架构与合约设计Factory、Pool合约结构
04交换机制深度解析swap函数、价格发现
05流动性管理与头寸Position、mint/burn
06费用系统与预言机费用分配、TWAP
07MEV与套利策略JIT、三明治攻击

1. 流动性管理概述

1.1 V3流动性的独特性

与V2不同,V3的流动性是非同质化的。每个流动性头寸都是独特的:

flowchart TB
    subgraph V2Group["V2 流动性 (ERC20)"]
        V2LP[LP Token]
        V2A[LP A: 100 LP]
        V2B[LP B: 100 LP]
        V2C[LP C: 100 LP]
        V2NOTE[所有LP权益相同<br/>可相互替代]
        V2LP --> V2A
        V2LP --> V2B
        V2LP --> V2C
    end

    subgraph V3Group["V3 流动性 (ERC721)"]
        V3NFT[NFT Position]
        V3A["LP A: [1800,2200]<br/>L=1000"]
        V3B["LP B: [1900,2100]<br/>L=500"]
        V3C["LP C: [2000,2500]<br/>L=2000"]
        V3NOTE[每个头寸独特<br/>不可替代]
        V3NFT --> V3A
        V3NFT --> V3B
        V3NFT --> V3C
    end

    style V2Group fill:#ffcdd2
    style V3Group fill:#c8e6c9

1.2 核心操作流程

flowchart LR
    subgraph AddGroup["添加流动性"]
        M1[选择价格区间]
        M2[计算所需代币]
        M3[调用mint]
        M4[获得NFT头寸]
        M1 --> M2
        M2 --> M3
        M3 --> M4
    end

    subgraph ManageGroup["管理流动性"]
        P1[增加流动性]
        P2[收取费用]
        P3[减少流动性]
        P1 --> P2
        P2 --> P3
    end

    subgraph RemoveGroup["移除流动性"]
        B1[调用burn]
        B2[获得代币]
        B3[调用collect]
        B4[取回费用]
        B1 --> B2
        B2 --> B3
        B3 --> B4
    end

    AddGroup --> ManageGroup
    ManageGroup --> RemoveGroup

    style AddGroup fill:#e3f2fd
    style ManageGroup fill:#fff3e0
    style RemoveGroup fill:#fce4ec

2. Position数据结构

2.1 Position.Info详解

每个流动性头寸由唯一的三元组标识:(owner, tickLower, tickUpper)

library Position {
    struct Info {
        // 此头寸的流动性数量
        uint128 liquidity;
 
        // 上次更新时的内部费用增长率
        // 用于计算自上次操作以来累积的费用
        uint256 feeGrowthInside0LastX128;
        uint256 feeGrowthInside1LastX128;
 
        // 待领取的费用(已结算但未提取)
        uint128 tokensOwed0;
        uint128 tokensOwed1;
    }
}
graph TB
    subgraph PosGroup["Position.Info 结构"]
        L[liquidity<br/>uint128<br/>流动性数量]
        FG0[feeGrowthInside0LastX128<br/>uint256<br/>token0费用增长快照]
        FG1[feeGrowthInside1LastX128<br/>uint256<br/>token1费用增长快照]
        TO0[tokensOwed0<br/>uint128<br/>待领取token0]
        TO1[tokensOwed1<br/>uint128<br/>待领取token1]
    end

    subgraph FuncGroup["功能分类"]
        流动性管理[流动性管理]
        费用追踪[费用追踪]
        待领取[待领取费用]
    end

    L --> 流动性管理
    FG0 --> 费用追踪
    FG1 --> 费用追踪
    TO0 --> 待领取
    TO1 --> 待领取

    style PosGroup fill:#e8f5e9

2.2 Position的唯一标识

/// @notice 获取头寸信息
function get(
    mapping(bytes32 => Info) storage self,
    address owner,
    int24 tickLower,
    int24 tickUpper
) internal view returns (Position.Info storage position) {
    // 使用keccak256哈希三元组作为键
    position = self[keccak256(abi.encodePacked(owner, tickLower, tickUpper))];
}
flowchart LR
    subgraph InputGroup["输入"]
        O[owner地址]
        TL[tickLower]
        TU[tickUpper]
    end

    subgraph HashGroup["哈希计算"]
        PACK["abi.encodePacked(owner, tickLower, tickUpper)"]
        HASH["keccak256(...)"]
    end

    subgraph StoreGroup["存储"]
        KEY["bytes32 key"]
        POS["positions[key]"]
    end

    O --> PACK
    TL --> PACK
    TU --> PACK
    PACK --> HASH
    HASH --> KEY
    KEY --> POS

    style HashGroup fill:#fff3e0

2.3 费用快照机制

sequenceDiagram
    participant LP as 流动性提供者
    participant Pool as 池子合约
    participant Position as Position结构

    LP->>Pool: mint(tickLower, tickUpper, amount)
    Pool->>Position: 记录当前feeGrowthInside
    Note over Position: feeGrowthInside0LastX128 = 当前值

    Note over Pool: ... 交易发生,费用累积 ...

    LP->>Pool: burn(tickLower, tickUpper, 0)
    Pool->>Position: 计算费用差值
    Note over Position: 应得费用 = <br/>(当前feeGrowthInside - Last) × liquidity
    Pool->>Position: 更新tokensOwed
    Pool-->>LP: 返回可领取金额

3. mint函数:添加流动性

3.1 函数签名与参数

function mint(
    address recipient,       // 头寸所有者
    int24 tickLower,        // 价格区间下界
    int24 tickUpper,        // 价格区间上界
    uint128 amount,         // 流动性数量
    bytes calldata data     // 回调数据
) external override lock returns (
    uint256 amount0,        // 需要的token0数量
    uint256 amount1         // 需要的token1数量
);

3.2 完整执行流程

flowchart TB
    START[mint调用] --> VALIDATE[参数验证]
    VALIDATE --> |amount > 0| MODIFY[_modifyPosition]

    subgraph _modifyPosition
        MP1[检查tick有效性]
        MP2[_updatePosition]
        MP3[计算所需代币数量]
    end

    MODIFY --> MP1 --> MP2 --> MP3

    MP3 --> CALC{当前价格位置?}

    CALC -->|P < Pa| ONLY0[只需token0]
    CALC -->|Pa ≤ P ≤ Pb| BOTH[需要两种token]
    CALC -->|P > Pb| ONLY1[只需token1]

    ONLY0 & BOTH & ONLY1 --> BALANCE[记录余额before]
    BALANCE --> CALLBACK[调用mint回调]
    CALLBACK --> VERIFY[验证余额增加]
    VERIFY --> EMIT[发出Mint事件]
    EMIT --> END[返回代币数量]

    style _modifyPosition fill:#e3f2fd
    style CALC fill:#fff3e0

3.3 核心代码实现

function mint(
    address recipient,
    int24 tickLower,
    int24 tickUpper,
    uint128 amount,
    bytes calldata data
) external override lock returns (uint256 amount0, uint256 amount1) {
    require(amount > 0);
 
    // 修改头寸并获取所需代币数量
    (, int256 amount0Int, int256 amount1Int) = _modifyPosition(
        ModifyPositionParams({
            owner: recipient,
            tickLower: tickLower,
            tickUpper: tickUpper,
            liquidityDelta: int256(amount).toInt128()
        })
    );
 
    amount0 = uint256(amount0Int);
    amount1 = uint256(amount1Int);
 
    uint256 balance0Before;
    uint256 balance1Before;
 
    // 记录余额快照
    if (amount0 > 0) balance0Before = balance0();
    if (amount1 > 0) balance1Before = balance1();
 
    // 调用回调函数,由调用者转入代币
    IUniswapV3MintCallback(msg.sender).uniswapV3MintCallback(
        amount0, amount1, data
    );
 
    // 验证代币已转入
    if (amount0 > 0) require(balance0Before.add(amount0) <= balance0(), 'M0');
    if (amount1 > 0) require(balance1Before.add(amount1) <= balance1(), 'M1');
 
    emit Mint(msg.sender, recipient, tickLower, tickUpper, amount, amount0, amount1);
}

4. _modifyPosition:核心位置修改

4.1 功能概述

_modifyPosition是流动性管理的核心函数,负责:

flowchart TB
    subgraph _modifyPosition职责
        R1[验证tick参数]
        R2[更新头寸信息]
        R3[更新tick数据]
        R4[管理tick位图]
        R5[计算代币数量]
    end

    R1 --> R2 --> R3 --> R4 --> R5

    style _modifyPosition职责 fill:#e8f5e9

4.2 参数结构

struct ModifyPositionParams {
    // 头寸所有者
    address owner;
    // 价格区间下界tick
    int24 tickLower;
    // 价格区间上界tick
    int24 tickUpper;
    // 流动性变化量(正=添加,负=移除)
    int128 liquidityDelta;
}

4.3 完整代码实现

function _modifyPosition(ModifyPositionParams memory params)
    private
    noDelegateCall
    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) {
            // 当前价格在区间内:需要两种代币
            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(liquidity, params.liquidityDelta);
        } else {
            // 当前价格在区间上方:只需要token1
            amount1 = SqrtPriceMath.getAmount1Delta(
                TickMath.getSqrtRatioAtTick(params.tickLower),
                TickMath.getSqrtRatioAtTick(params.tickUpper),
                params.liquidityDelta
            );
        }
    }
}

4.4 三种价格位置的代币需求

flowchart LR
    subgraph LowerGroup["价格 < tickLower"]
        A1[只需token0]
        A2["amount0 = L × (1/√Pa - 1/√Pb)"]
        A3["amount1 = 0"]
        A1 --> A2
        A2 --> A3
    end

    subgraph InsideGroup["tickLower ≤ 价格 < tickUpper"]
        B1[需要两种token]
        B2["amount0 = L × (1/√P - 1/√Pb)"]
        B3["amount1 = L × (√P - √Pa)"]
        B1 --> B2
        B2 --> B3
    end

    subgraph UpperGroup["价格 ≥ tickUpper"]
        C1[只需token1]
        C2["amount0 = 0"]
        C3["amount1 = L × (√Pb - √Pa)"]
        C1 --> C2
        C2 --> C3
    end

    style LowerGroup fill:#e3f2fd
    style InsideGroup fill:#fff3e0
    style UpperGroup fill:#fce4ec

5. _updatePosition:头寸更新机制

5.1 更新流程

flowchart TB
    START[_updatePosition] --> GET[获取现有头寸]
    GET --> READ[读取全局费用增长率]

    READ --> CHECK{liquidityDelta != 0?}

    CHECK -->|是| ORACLE[更新预言机观察值]
    ORACLE --> UPDATE_LOWER[更新tickLower的Tick信息]
    UPDATE_LOWER --> UPDATE_UPPER[更新tickUpper的Tick信息]
    UPDATE_UPPER --> FLIP_CHECK{tick状态翻转?}

    FLIP_CHECK -->|是| FLIP_BITMAP[更新TickBitmap]
    FLIP_CHECK -->|否| CALC_FEE[计算内部费用增长率]
    FLIP_BITMAP --> CALC_FEE

    CHECK -->|否| CALC_FEE

    CALC_FEE --> UPDATE_POS[更新Position信息]
    UPDATE_POS --> CLEANUP{liquidityDelta < 0<br/>且tick已翻转?}

    CLEANUP -->|是| CLEAR[清理tick数据]
    CLEANUP -->|否| END[返回position]
    CLEAR --> END

    style FLIP_CHECK fill:#fff3e0
    style UPDATE_POS fill:#c8e6c9

5.2 完整代码实现

function _updatePosition(
    address owner,
    int24 tickLower,
    int24 tickUpper,
    int128 liquidityDelta,
    int24 tick
) private returns (Position.Info storage position) {
    // 获取头寸引用
    position = positions.get(owner, tickLower, tickUpper);
 
    // 缓存全局费用增长率
    uint256 _feeGrowthGlobal0X128 = feeGrowthGlobal0X128;
    uint256 _feeGrowthGlobal1X128 = feeGrowthGlobal1X128;
 
    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,  // 下界
            maxLiquidityPerTick
        );
 
        // 更新上界tick
        flippedUpper = ticks.update(
            tickUpper,
            tick,
            liquidityDelta,
            _feeGrowthGlobal0X128,
            _feeGrowthGlobal1X128,
            secondsPerLiquidityCumulativeX128,
            tickCumulative,
            time,
            true,   // 上界
            maxLiquidityPerTick
        );
 
        // 更新位图
        if (flippedLower) {
            tickBitmap.flipTick(tickLower, tickSpacing);
        }
        if (flippedUpper) {
            tickBitmap.flipTick(tickUpper, tickSpacing);
        }
    }
 
    // 计算区间内的费用增长率
    (uint256 feeGrowthInside0X128, uint256 feeGrowthInside1X128) =
        ticks.getFeeGrowthInside(
            tickLower,
            tickUpper,
            tick,
            _feeGrowthGlobal0X128,
            _feeGrowthGlobal1X128
        );
 
    // 更新头寸信息
    position.update(liquidityDelta, feeGrowthInside0X128, feeGrowthInside1X128);
 
    // 清理不再需要的tick数据
    if (liquidityDelta < 0) {
        if (flippedLower) {
            ticks.clear(tickLower);
        }
        if (flippedUpper) {
            ticks.clear(tickUpper);
        }
    }
}

6. Tick更新机制

6.1 Tick.update函数

function update(
    mapping(int24 => Tick.Info) storage self,
    int24 tick,
    int24 tickCurrent,
    int128 liquidityDelta,
    uint256 feeGrowthGlobal0X128,
    uint256 feeGrowthGlobal1X128,
    uint160 secondsPerLiquidityCumulativeX128,
    int56 tickCumulative,
    uint32 time,
    bool upper,                    // true=上界false=下界
    uint128 maxLiquidity
) internal returns (bool flipped) {
    Tick.Info storage info = self[tick];
 
    uint128 liquidityGrossBefore = info.liquidityGross;
    uint128 liquidityGrossAfter = LiquidityMath.addDelta(
        liquidityGrossBefore,
        liquidityDelta
    );
 
    require(liquidityGrossAfter <= maxLiquidity, 'LO');
 
    // 判断是否翻转(从0变为非0,或从非0变为0)
    flipped = (liquidityGrossAfter == 0) != (liquidityGrossBefore == 0);
 
    if (liquidityGrossBefore == 0) {
        // 首次初始化该tick
        if (tick <= tickCurrent) {
            // 初始化outside值
            info.feeGrowthOutside0X128 = feeGrowthGlobal0X128;
            info.feeGrowthOutside1X128 = feeGrowthGlobal1X128;
            info.secondsPerLiquidityOutsideX128 = secondsPerLiquidityCumulativeX128;
            info.tickCumulativeOutside = tickCumulative;
            info.secondsOutside = time;
        }
        info.initialized = true;
    }
 
    info.liquidityGross = liquidityGrossAfter;
 
    // 更新liquidityNet
    // 下界:正向添加(从左向右跨越时增加流动性)
    // 上界:反向添加(从左向右跨越时减少流动性)
    info.liquidityNet = upper
        ? int256(info.liquidityNet).sub(liquidityDelta).toInt128()
        : int256(info.liquidityNet).add(liquidityDelta).toInt128();
}

6.2 liquidityNet的符号约定

flowchart TB
    subgraph AddGroup["添加流动性示例"]
        ADD[添加区间[100, 200]<br/>流动性L=1000]
    end

    subgraph TickGroup["tick更新"]
        T100["tick 100 (下界)<br/>liquidityNet += 1000"]
        T200["tick 200 (上界)<br/>liquidityNet -= 1000"]
    end

    subgraph PriceGroup["价格从左向右移动"]
        CROSS100[跨越tick 100<br/>活跃流动性 += 1000]
        CROSS200[跨越tick 200<br/>活跃流动性 -= 1000]
    end

    ADD --> T100
    ADD --> T200
    T100 -.-> CROSS100
    T200 -.-> CROSS200

    style AddGroup fill:#e3f2fd
    style PriceGroup fill:#c8e6c9

6.3 Tick翻转与位图管理

flowchart TD
    subgraph tick翻转条件
        C1["liquidityGross: 0 → 非0<br/>(首次有LP进入)"]
        C2["liquidityGross: 非0 → 0<br/>(最后LP离开)"]
    end

    subgraph 位图操作
        F1[flipTick翻转位]
        F2["0→1: tick变为活跃"]
        F3["1→0: tick变为非活跃"]
    end

    C1 --> F1 --> F2
    C2 --> F1 --> F3

    style tick翻转条件 fill:#fff3e0

7. Position.update:费用结算

7.1 费用计算原理

flowchart TB
    subgraph 费用增长率概念
        G[全局费用增长率<br/>feeGrowthGlobalX128]
        I[内部费用增长率<br/>feeGrowthInsideX128]
        L[上次记录值<br/>feeGrowthInsideLastX128]
    end

    subgraph 计算公式
        F["应得费用 = (Inside当前 - Inside上次) × liquidity"]
    end

    G --> I
    I & L --> F

    style 计算公式 fill:#c8e6c9

7.2 完整实现

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);
    }
 
    // 计算应得费用
    // 使用unchecked因为溢出是期望行为(环绕算术)
    uint128 tokensOwed0 = uint128(
        FullMath.mulDiv(
            feeGrowthInside0X128 - _self.feeGrowthInside0LastX128,
            _self.liquidity,
            FixedPoint128.Q128
        )
    );
    uint128 tokensOwed1 = uint128(
        FullMath.mulDiv(
            feeGrowthInside1X128 - _self.feeGrowthInside1LastX128,
            _self.liquidity,
            FixedPoint128.Q128
        )
    );
 
    // 更新流动性
    if (liquidityDelta != 0) self.liquidity = liquidityNext;
 
    // 更新费用增长快照
    self.feeGrowthInside0LastX128 = feeGrowthInside0X128;
    self.feeGrowthInside1LastX128 = feeGrowthInside1X128;
 
    // 累积待领取费用
    if (tokensOwed0 > 0 || tokensOwed1 > 0) {
        self.tokensOwed0 += tokensOwed0;
        self.tokensOwed1 += tokensOwed1;
    }
}

7.3 费用计算示例

flowchart LR
    subgraph InitGroup["初始状态"]
        S1["liquidity = 1000"]
        S2["feeGrowthInsideLast = 100"]
    end

    subgraph CurrGroup["当前状态"]
        C1["feeGrowthInside = 150"]
    end

    subgraph CalcGroup["计算"]
        CALC["tokensOwed = (150 - 100) × 1000 / 2^128"]
    end

    subgraph ResultGroup["结果"]
        R["tokensOwed ≈ 50 (归一化后)"]
    end

    S1 --> CALC
    S2 --> CALC
    C1 --> CALC
    CALC --> R

    style CalcGroup fill:#fff3e0

8. burn函数:移除流动性

8.1 函数签名

function burn(
    int24 tickLower,
    int24 tickUpper,
    uint128 amount
) external override lock returns (
    uint256 amount0,
    uint256 amount1
);

8.2 执行流程

flowchart TB
    START[burn调用] --> MODIFY[_modifyPosition<br/>liquidityDelta为负]

    MODIFY --> UPDATE[更新头寸]
    UPDATE --> CALC[计算可取回代币]

    CALC --> ADD_OWED[累加到tokensOwed]
    ADD_OWED --> EMIT[发出Burn事件]
    EMIT --> END[返回代币数量]

    NOTE[注意:burn不直接转账<br/>需要调用collect取回]

    style NOTE fill:#ffcdd2

8.3 代码实现

function burn(
    int24 tickLower,
    int24 tickUpper,
    uint128 amount
) external override lock returns (uint256 amount0, uint256 amount1) {
    // 以负的liquidityDelta调用_modifyPosition
    (Position.Info storage position, int256 amount0Int, int256 amount1Int) =
        _modifyPosition(
            ModifyPositionParams({
                owner: msg.sender,
                tickLower: tickLower,
                tickUpper: tickUpper,
                liquidityDelta: -int256(amount).toInt128()
            })
        );
 
    // 转换为正数(burn返回的是应得的代币)
    amount0 = uint256(-amount0Int);
    amount1 = uint256(-amount1Int);
 
    // 累加到待领取(position.update已经处理了费用)
    if (amount0 > 0 || amount1 > 0) {
        (position.tokensOwed0, position.tokensOwed1) = (
            position.tokensOwed0 + uint128(amount0),
            position.tokensOwed1 + uint128(amount1)
        );
    }
 
    emit Burn(msg.sender, tickLower, tickUpper, amount, amount0, amount1);
}

9. collect函数:领取代币

9.1 函数签名

function collect(
    address recipient,
    int24 tickLower,
    int24 tickUpper,
    uint128 amount0Requested,
    uint128 amount1Requested
) external override lock returns (
    uint128 amount0,
    uint128 amount1
);

9.2 执行流程

flowchart TB
    START[collect调用] --> GET[获取头寸]
    GET --> CALC0[计算实际可领取token0]
    CALC0 --> CALC1[计算实际可领取token1]

    CALC1 --> CHECK0{amount0 > 0?}
    CHECK0 -->|是| UPDATE0[更新tokensOwed0]
    CHECK0 -->|否| CHECK1

    UPDATE0 --> TRANSFER0[转账token0]
    TRANSFER0 --> CHECK1{amount1 > 0?}

    CHECK1 -->|是| UPDATE1[更新tokensOwed1]
    CHECK1 -->|否| EMIT

    UPDATE1 --> TRANSFER1[转账token1]
    TRANSFER1 --> EMIT[发出Collect事件]

    EMIT --> END[返回实际领取数量]

    style CHECK0 fill:#fff3e0
    style CHECK1 fill:#fff3e0

9.3 代码实现

function collect(
    address recipient,
    int24 tickLower,
    int24 tickUpper,
    uint128 amount0Requested,
    uint128 amount1Requested
) external override lock returns (uint128 amount0, uint128 amount1) {
    Position.Info storage position = positions.get(msg.sender, tickLower, tickUpper);
 
    // 计算实际可领取数量(取请求和可用的最小值)
    amount0 = amount0Requested > position.tokensOwed0
        ? position.tokensOwed0
        : amount0Requested;
    amount1 = amount1Requested > position.tokensOwed1
        ? position.tokensOwed1
        : amount1Requested;
 
    // 更新待领取余额
    if (amount0 > 0) {
        position.tokensOwed0 -= amount0;
        TransferHelper.safeTransfer(token0, recipient, amount0);
    }
    if (amount1 > 0) {
        position.tokensOwed1 -= amount1;
        TransferHelper.safeTransfer(token1, recipient, amount1);
    }
 
    emit Collect(msg.sender, recipient, tickLower, tickUpper, amount0, amount1);
}

10. NonfungiblePositionManager:NFT封装

10.1 架构关系

flowchart TB
    subgraph 用户层
        USER[用户]
    end

    subgraph 外围合约
        NPM[NonfungiblePositionManager<br/>ERC721]
    end

    subgraph 核心合约
        POOL[UniswapV3Pool]
    end

    USER -->|mint/burn NFT| NPM
    NPM -->|mint/burn 流动性| POOL

    style 外围合约 fill:#e3f2fd
    style 核心合约 fill:#fff3e0

10.2 Position结构(NFT)

struct Position {
    // 用于计算费用的临时变量
    uint96 nonce;
 
    // 允许操作此头寸的地址
    address operator;
 
    // 池子标识
    address token0;
    address token1;
    uint24 fee;
 
    // 价格区间
    int24 tickLower;
    int24 tickUpper;
 
    // 流动性数量
    uint128 liquidity;
 
    // 费用增长快照
    uint256 feeGrowthInside0LastX128;
    uint256 feeGrowthInside1LastX128;
 
    // 待领取费用
    uint128 tokensOwed0;
    uint128 tokensOwed1;
}

10.3 NFT铸造流程

sequenceDiagram
    participant User as 用户
    participant NPM as PositionManager
    participant Pool as Pool合约
    participant NFT as ERC721

    User->>NPM: mint(params)
    NPM->>Pool: mint(tickLower, tickUpper, amount)
    Pool-->>NPM: 回调uniswapV3MintCallback
    NPM->>Pool: 转入所需代币
    Pool-->>NPM: 返回(amount0, amount1)
    NPM->>NFT: _mint(tokenId)
    NFT-->>User: NFT头寸代币

10.4 关键函数

// 铸造新头寸
function mint(MintParams calldata params)
    external
    payable
    override
    checkDeadline(params.deadline)
    returns (
        uint256 tokenId,
        uint128 liquidity,
        uint256 amount0,
        uint256 amount1
    );
 
// 增加流动性
function increaseLiquidity(IncreaseLiquidityParams calldata params)
    external
    payable
    override
    checkDeadline(params.deadline)
    returns (
        uint128 liquidity,
        uint256 amount0,
        uint256 amount1
    );
 
// 减少流动性
function decreaseLiquidity(DecreaseLiquidityParams calldata params)
    external
    payable
    override
    isAuthorizedForToken(params.tokenId)
    checkDeadline(params.deadline)
    returns (uint256 amount0, uint256 amount1);
 
// 收取费用
function collect(CollectParams calldata params)
    external
    payable
    override
    isAuthorizedForToken(params.tokenId)
    returns (uint256 amount0, uint256 amount1);

11. 流动性数学计算

11.1 流动性与代币数量的关系

graph TB
    subgraph 核心公式
        F1["token0数量变化:<br/>Δx = L × (1/√Pa - 1/√Pb)"]
        F2["token1数量变化:<br/>Δy = L × (√Pb - √Pa)"]
    end

    subgraph 等价形式
        E1["Δx = L × (√Pb - √Pa) / (√Pa × √Pb)"]
        E2["L = Δy / (√Pb - √Pa)"]
        E3["L = Δx × √Pa × √Pb / (√Pb - √Pa)"]
    end

    F1 --> E1
    F2 --> E2 & E3

    style 核心公式 fill:#e8f5e9

11.2 SqrtPriceMath实现

/// @notice 计算价格变化所需的token0数量
function getAmount0Delta(
    uint160 sqrtRatioAX96,
    uint160 sqrtRatioBX96,
    uint128 liquidity,
    bool roundUp
) internal pure returns (uint256 amount0) {
    // 确保 sqrtRatioA < sqrtRatioB
    if (sqrtRatioAX96 > sqrtRatioBX96)
        (sqrtRatioAX96, sqrtRatioBX96) = (sqrtRatioBX96, sqrtRatioAX96);
 
    // amount0 = L × (√Pb - √Pa) / (√Pa × √Pb)
    // = L × (sqrtRatioB - sqrtRatioA) / (sqrtRatioA × sqrtRatioB / 2^96)
    uint256 numerator1 = uint256(liquidity) << FixedPoint96.RESOLUTION;
    uint256 numerator2 = sqrtRatioBX96 - sqrtRatioAX96;
 
    require(sqrtRatioAX96 > 0);
 
    return roundUp
        ? UnsafeMath.divRoundingUp(
            FullMath.mulDivRoundingUp(numerator1, numerator2, sqrtRatioBX96),
            sqrtRatioAX96
        )
        : FullMath.mulDiv(numerator1, numerator2, sqrtRatioBX96) / sqrtRatioAX96;
}
 
/// @notice 计算价格变化所需的token1数量
function getAmount1Delta(
    uint160 sqrtRatioAX96,
    uint160 sqrtRatioBX96,
    uint128 liquidity,
    bool roundUp
) internal pure returns (uint256 amount1) {
    // 确保 sqrtRatioA < sqrtRatioB
    if (sqrtRatioAX96 > sqrtRatioBX96)
        (sqrtRatioAX96, sqrtRatioBX96) = (sqrtRatioBX96, sqrtRatioAX96);
 
    // amount1 = L × (√Pb - √Pa)
    return roundUp
        ? FullMath.mulDivRoundingUp(
            liquidity,
            sqrtRatioBX96 - sqrtRatioAX96,
            FixedPoint96.Q96
        )
        : FullMath.mulDiv(
            liquidity,
            sqrtRatioBX96 - sqrtRatioAX96,
            FixedPoint96.Q96
        );
}

12. 本章小结

12.1 核心概念回顾

mindmap
  root((流动性管理))
    Position
      唯一标识三元组
      流动性数量
      费用快照机制
      待领取费用
    mint操作
      参数验证
      _modifyPosition
      代币需求计算
      回调验证
    burn操作
      负liquidityDelta
      费用结算
      累加tokensOwed
    collect操作
      领取待结算代币
      支持部分领取
    NFT封装
      NonfungiblePositionManager
      ERC721标准
      头寸管理

12.2 关键设计要点

设计要点实现方式效果
非同质化Position三元组每个头寸独特可追踪
费用跟踪增长率快照O(1)费用计算
安全转账回调+余额验证防止代币丢失
NFT封装PositionManager用户友好的接口
Tick管理位图+翻转检测高效状态管理

12.3 操作流程对比

flowchart LR
    subgraph AddLiqGroup["添加流动性"]
        A1[mint]
        A2[_modifyPosition]
        A3[回调转入代币]
        A1 --> A2
        A2 --> A3
    end

    subgraph RemoveLiqGroup["移除流动性"]
        B1[burn]
        B2[_modifyPosition]
        B3[累加tokensOwed]
        B4[collect取回]
        B1 --> B2
        B2 --> B3
        B3 --> B4
    end

    subgraph CollectOnlyGroup["仅收费用"]
        C1[burn amount=0]
        C2[结算费用]
        C3[collect取回]
        C1 --> C2
        C2 --> C3
    end

    style AddLiqGroup fill:#c8e6c9
    style RemoveLiqGroup fill:#ffcdd2
    style CollectOnlyGroup fill:#fff3e0

下一篇预告

在下一篇文章中,我们将深入探讨费用系统与预言机,包括:

  • 费用增长率的完整计算机制
  • 协议费用的分配与提取
  • TWAP预言机的实现原理
  • 观察者数组的管理与扩容

参考资料