死磕Uniswap V3(三):架构与合约设计

本文是「死磕Uniswap V3」系列的第三篇,深入剖析V3的合约架构和核心数据结构设计。

系列导航

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

1. 整体架构概览

1.1 合约层次结构

Uniswap V3采用了模块化的架构设计,将功能划分为多个层次:

flowchart TB
    subgraph UserLayer["用户层"]
        U1[交易者]
        U2[流动性提供者]
    end

    subgraph PeripheryContracts["外围合约 Periphery Contracts"]
        R[SwapRouter]
        PM[NonfungiblePositionManager]
        Q[Quoter]
    end

    subgraph CoreContracts["核心合约 Core Contracts"]
        F[UniswapV3Factory]
        P1[Pool A]
        P2[Pool B]
        P3[Pool C]
    end

    subgraph Libraries["库合约 Libraries"]
        TM[TickMath]
        SM[SwapMath]
        POS[Position]
        TK[Tick]
        TB[TickBitmap]
        OR[Oracle]
    end

    U1 --> R
    U2 --> PM
    R --> P1 & P2 & P3
    PM --> P1 & P2 & P3
    F -->|创建| P1 & P2 & P3
    P1 & P2 & P3 --> TM & SM & POS & TK & TB & OR

    style CoreContracts fill:#e3f2fd
    style Libraries fill:#e8f5e9

1.2 核心设计原则

mindmap
  root((V3架构设计))
    模块化
      接口分离
      库复用
      单一职责
    Gas优化
      存储槽打包
      位运算优化
      内联库函数
    安全性
      重入保护
      余额验证
      权限控制
    可扩展性
      工厂模式
      多费率支持
      预言机扩容

2. Factory合约:单例工厂模式

2.1 工厂合约的职责

Factory合约是整个协议的入口点,负责池子的创建和参数管理:

flowchart LR
    subgraph FactoryDuties["Factory职责"]
        C1[创建Pool]
        C2[管理费率]
        C3[设置协议费]
        C4[所有权管理]
    end

    subgraph KeyMappings["关键映射"]
        M1["getPool[token0][token1][fee]"]
        M2["feeAmountTickSpacing[fee]"]
    end

    C1 --> M1
    C2 --> M2

    style FactoryDuties fill:#fff3e0

2.2 核心数据结构

contract UniswapV3Factory is IUniswapV3Factory, UniswapV3PoolDeployer, NoDelegateCall {
    address public override owner;
 
    // 三层嵌套映射:确保每个代币对+费率只有一个池子
    mapping(address => mapping(address => mapping(uint24 => address))) public override getPool;
 
    // 费率与Tick间距的绑定关系
    mapping(uint24 => int24) public override feeAmountTickSpacing;
 
    constructor() {
        owner = msg.sender;
        emit OwnerChanged(address(0), msg.sender);
 
        // 预设标准费率等级
        feeAmountTickSpacing[500] = 10;    // 0.05% fee
        feeAmountTickSpacing[3000] = 60;   // 0.30% fee
        feeAmountTickSpacing[10000] = 200; // 1.00% fee
    }
}

2.3 地址排序机制

代币地址排序是确保池子唯一性的关键设计:

flowchart TD
    subgraph InputTokens["输入"]
        A[tokenA]
        B[tokenB]
    end

    subgraph SortingLogic["排序逻辑"]
        CMP{tokenA < tokenB?}
        R1["token0 = tokenA<br/>token1 = tokenB"]
        R2["token0 = tokenB<br/>token1 = tokenA"]
    end

    subgraph ResultPool["结果"]
        POOL["唯一池子地址<br/>getPool[token0][token1][fee]"]
    end

    A & B --> CMP
    CMP -->|是| R1
    CMP -->|否| R2
    R1 & R2 --> POOL

    style SortingLogic fill:#e8f5e9

地址排序的实现

function createPool(
    address tokenA,
    address tokenB,
    uint24 fee
) external override noDelegateCall returns (address pool) {
    require(tokenA != tokenB);
 
    // 关键:地址排序确保唯一性
    (address token0, address token1) = tokenA < tokenB
        ? (tokenA, tokenB)
        : (tokenB, tokenA);
 
    require(token0 != address(0));
 
    int24 tickSpacing = feeAmountTickSpacing[fee];
    require(tickSpacing != 0);
    require(getPool[token0][token1][fee] == address(0));
 
    pool = deploy(address(this), token0, token1, fee, tickSpacing);
 
    // 双向映射优化查询
    getPool[token0][token1][fee] = pool;
    getPool[token1][token0][fee] = pool;
 
    emit PoolCreated(token0, token1, fee, tickSpacing, pool);
}

设计优势

特性说明
唯一性保证相同代币对在特定费率下只能有一个池子
查询优化无论查询顺序如何都能找到正确池子
存储效率避免重复存储相同的池子
前端友好简化前端的池子查找逻辑

3. Pool合约:状态管理精髓

3.1 接口的模块化组合

V3将Pool接口拆分为多个子接口,体现了接口隔离原则:

flowchart TB
    subgraph IUniswapV3Pool
        direction TB
        I1[IUniswapV3PoolImmutables]
        I2[IUniswapV3PoolState]
        I3[IUniswapV3PoolDerivedState]
        I4[IUniswapV3PoolActions]
        I5[IUniswapV3PoolOwnerActions]
        I6[IUniswapV3PoolEvents]
    end

    subgraph ResponsibilityDesc["职责说明"]
        D1["不可变属性<br/>factory, token0, token1, fee"]
        D2["状态变量<br/>slot0, liquidity, ticks"]
        D3["派生状态<br/>observe, snapshotCumulatives"]
        D4["用户操作<br/>swap, mint, burn, collect"]
        D5["管理操作<br/>setFeeProtocol, collectProtocol"]
        D6["事件定义<br/>Swap, Mint, Burn, Flash"]
    end

    I1 --- D1
    I2 --- D2
    I3 --- D3
    I4 --- D4
    I5 --- D5
    I6 --- D6

    style IUniswapV3Pool fill:#e3f2fd
// 接口的模块化组合
interface IUniswapV3Pool is
    IUniswapV3PoolImmutables,    // 不可变属性
    IUniswapV3PoolState,         // 状态变量
    IUniswapV3PoolDerivedState,  // 派生状态
    IUniswapV3PoolActions,       // 用户操作
    IUniswapV3PoolOwnerActions,  // 管理员操作
    IUniswapV3PoolEvents         // 事件定义
{
    // 空接口,纯组合
}

3.2 Slot0:极致的存储优化

Slot0是V3最精妙的存储优化设计,将多个状态变量打包到单个存储槽中:

graph TB
    subgraph "Slot0 结构 (32字节存储槽)"
        direction LR
        S1["sqrtPriceX96<br/>20字节<br/>(uint160)"]
        S2["tick<br/>3字节<br/>(int24)"]
        S3["observationIndex<br/>2字节<br/>(uint16)"]
        S4["observationCardinality<br/>2字节<br/>(uint16)"]
        S5["observationCardinalityNext<br/>2字节<br/>(uint16)"]
        S6["feeProtocol<br/>1字节<br/>(uint8)"]
        S7["unlocked<br/>1字节<br/>(bool)"]
    end

    subgraph Total["总计"]
        T["20+3+2+2+2+1+1 = 31字节<br/>完美契合32字节存储槽"]
    end

    S1 --> S2 --> S3 --> S4 --> S5 --> S6 --> S7
    S7 --> T

    style T fill:#c8e6c9
struct Slot0 {
    // 当前价格√P (Q64.96格式)
    uint160 sqrtPriceX96;              // 20字节
 
    // 当前tick
    int24 tick;                        // 3字节
 
    // 预言机观察者数组当前索引
    uint16 observationIndex;           // 2字节
 
    // 预言机数组当前容量
    uint16 observationCardinality;     // 2字节
 
    // 预言机数组目标容量
    uint16 observationCardinalityNext; // 2字节
 
    // 协议费率 (4位token0 + 4位token1)
    uint8 feeProtocol;                 // 1字节
 
    // 重入保护锁
    bool unlocked;                     // 1字节
}
// 总计:31字节,完美契合32字节存储槽

Gas优化效果

操作传统设计Slot0设计节省
读取价格+tick4200 gas2100 gas50%
更新价格+tick10000+ gas5000 gas50%+
swap单次读取多次SLOAD单次SLOAD显著

3.3 Pool合约完整状态

contract UniswapV3Pool {
    // 核心状态变量
    Slot0 public override slot0;
 
    // 全局费用增长累积器
    uint256 public override feeGrowthGlobal0X128;
    uint256 public override feeGrowthGlobal1X128;
 
    // 协议费用累积
    struct ProtocolFees {
        uint128 token0;
        uint128 token1;
    }
    ProtocolFees public override protocolFees;
 
    // 当前有效流动性
    uint128 public override liquidity;
 
    // 核心数据结构映射
    mapping(int24 => Tick.Info) public override ticks;
    mapping(int16 => uint256) public override tickBitmap;
    mapping(bytes32 => Position.Info) public override positions;
 
    // 预言机观察者数组(固定大小)
    Oracle.Observation[65535] public override observations;
}
flowchart TB
    subgraph PoolStateVars["Pool状态变量"]
        direction TB
        S0[Slot0<br/>打包状态]
        FG[feeGrowthGlobal<br/>费用累积器]
        PF[protocolFees<br/>协议费用]
        LQ[liquidity<br/>当前流动性]
    end

    subgraph MappingStructure["映射结构"]
        TK["ticks<br/>mapping(int24 => Tick.Info)"]
        TB["tickBitmap<br/>mapping(int16 => uint256)"]
        PS["positions<br/>mapping(bytes32 => Position.Info)"]
    end

    subgraph ArrayStructure["数组结构"]
        OB["observations[65535]<br/>固定大小预言机数组"]
    end

    S0 --> TK & TB
    LQ --> PS
    S0 --> OB

    style PoolStateVars fill:#e3f2fd
    style MappingStructure fill:#fff3e0

4. Library模式:代码复用与Gas优化

4.1 核心库概览

V3大量使用library实现代码复用和gas优化:

flowchart LR
    subgraph MathLibs["数学库"]
        TM[TickMath<br/>Tick↔价格转换]
        SM[SwapMath<br/>交换计算]
        SPM[SqrtPriceMath<br/>价格变化计算]
        FM[FullMath<br/>高精度乘除]
    end

    subgraph DataStructLibs["数据结构库"]
        TK[Tick<br/>Tick信息管理]
        PS[Position<br/>头寸管理]
        TB[TickBitmap<br/>位图操作]
        OR[Oracle<br/>预言机管理]
    end

    subgraph UtilLibs["工具库"]
        LM[LiquidityMath<br/>流动性计算]
        BM[BitMath<br/>位运算]
        TH[TransferHelper<br/>安全转账]
    end

    P[Pool合约] --> MathLibs & DataStructLibs & UtilLibs

    style MathLibs fill:#e8f5e9
    style DataStructLibs fill:#fff3e0
    style UtilLibs fill:#fce4ec

4.2 TickMath库

library TickMath {
    int24 internal constant MIN_TICK = -887272;
    int24 internal constant MAX_TICK = -MIN_TICK;
 
    uint160 internal constant MIN_SQRT_RATIO = 4295128739;
    uint160 internal constant MAX_SQRT_RATIO =
        1461446703485210103287273052203988822378723970342;
 
    // Tick → √Price 转换
    function getSqrtRatioAtTick(int24 tick)
        internal pure returns (uint160 sqrtPriceX96);
 
    // √Price → Tick 转换
    function getTickAtSqrtRatio(uint160 sqrtPriceX96)
        internal pure returns (int24 tick);
}

4.3 SwapMath库

SwapMath处理交换过程中的核心数学计算:

library SwapMath {
    /// @notice 计算单步交换的结果
    function computeSwapStep(
        uint160 sqrtRatioCurrentX96,    // 当前价格
        uint160 sqrtRatioTargetX96,     // 目标价格
        uint128 liquidity,              // 当前流动性
        int256 amountRemaining,         // 剩余数量
        uint24 feePips                  // 费率
    ) internal pure returns (
        uint160 sqrtRatioNextX96,       // 新价格
        uint256 amountIn,               // 输入数量
        uint256 amountOut,              // 输出数量
        uint256 feeAmount               // 费用
    );
}

4.4 Position库

library Position {
    struct Info {
        // 流动性数量
        uint128 liquidity;
 
        // 上次更新时的内部费用增长率
        uint256 feeGrowthInside0LastX128;
        uint256 feeGrowthInside1LastX128;
 
        // 待领取的费用
        uint128 tokensOwed0;
        uint128 tokensOwed1;
    }
 
    /// @notice 获取头寸
    function get(
        mapping(bytes32 => Info) storage self,
        address owner,
        int24 tickLower,
        int24 tickUpper
    ) internal view returns (Position.Info storage position) {
        position = self[keccak256(abi.encodePacked(owner, tickLower, tickUpper))];
    }
 
    /// @notice 更新头寸
    function update(
        Info storage self,
        int128 liquidityDelta,
        uint256 feeGrowthInside0X128,
        uint256 feeGrowthInside1X128
    ) internal;
}

Library设计优势

优势说明
代码复用同样的数学逻辑被多个合约使用
Gas优化库函数在编译时内联,避免外部调用开销
安全性核心数学逻辑集中维护,降低错误风险
模块化功能分离便于测试和审计
升级性可通过代理模式实现逻辑升级

5. 核心数据结构设计

5.1 Tick数据结构

Tick.Info存储了每个初始化tick的完整信息:

graph TB
    subgraph "Tick.Info 结构"
        L1["liquidityGross<br/>uint128<br/>此tick的总流动性"]
        L2["liquidityNet<br/>int128<br/>跨越时的流动性变化"]
        F1["feeGrowthOutside0X128<br/>uint256"]
        F2["feeGrowthOutside1X128<br/>uint256"]
        T1["tickCumulativeOutside<br/>int56"]
        S1["secondsPerLiquidityOutsideX128<br/>uint160"]
        S2["secondsOutside<br/>uint32"]
        I["initialized<br/>bool"]
    end

    subgraph UsageCategory["用途分类"]
        Liquidity["流动性管理"]
        Fee["费用追踪"]
        Oracle["预言机数据"]
        Status["状态标记"]
    end

    L1 & L2 --> Liquidity
    F1 & F2 --> Fee
    T1 & S1 & S2 --> Oracle
    I --> Status

    style UsageCategory fill:#e8f5e9
library Tick {
    struct Info {
        // 此tick的总流动性(用于限制单tick最大流动性)
        uint128 liquidityGross;
 
        // 从左向右跨越此tick时的流动性变化
        // 正值:增加流动性;负值:减少流动性
        int128 liquidityNet;
 
        // tick外部的费用增长(用于计算区间内费用)
        uint256 feeGrowthOutside0X128;
        uint256 feeGrowthOutside1X128;
 
        // tick外部的累积tick值(预言机用)
        int56 tickCumulativeOutside;
 
        // tick外部的每流动性秒数(预言机用)
        uint160 secondsPerLiquidityOutsideX128;
 
        // tick外部的累积秒数
        uint32 secondsOutside;
 
        // 是否已初始化
        bool initialized;
    }
}

5.2 liquidityNet的精妙设计

flowchart LR
    subgraph PriceMovement["价格从左向右移动"]
        T1["Tick A<br/>liquidityNet = +100"]
        T2["Tick B<br/>liquidityNet = -50"]
        T3["Tick C<br/>liquidityNet = -50"]
    end

    subgraph LiquidityChange["流动性变化"]
        L1["L = 0"]
        L2["L = 100"]
        L3["L = 50"]
        L4["L = 0"]
    end

    L1 -->|"跨越A<br/>+100"| L2
    L2 -->|"跨越B<br/>-50"| L3
    L3 -->|"跨越C<br/>-50"| L4

    T1 -.-> L2
    T2 -.-> L3
    T3 -.-> L4

liquidityNet符号约定

  • 正值:从左向右跨越时增加流动性(进入某个LP的区间下界)
  • 负值:从左向右跨越时减少流动性(离开某个LP的区间上界)

5.3 Tick跨越的”翻转”技巧

function cross(
    mapping(int24 => Tick.Info) storage self,
    int24 tick,
    uint256 feeGrowthGlobal0X128,
    uint256 feeGrowthGlobal1X128,
    uint160 secondsPerLiquidityCumulativeX128,
    int56 tickCumulative,
    uint32 time
) internal returns (int128 liquidityNet) {
    Tick.Info storage info = self[tick];
 
    // "翻转"技巧:将outside值变为新的outside值
    info.feeGrowthOutside0X128 = feeGrowthGlobal0X128 - info.feeGrowthOutside0X128;
    info.feeGrowthOutside1X128 = feeGrowthGlobal1X128 - info.feeGrowthOutside1X128;
    info.secondsPerLiquidityOutsideX128 =
        secondsPerLiquidityCumulativeX128 - info.secondsPerLiquidityOutsideX128;
    info.tickCumulativeOutside = tickCumulative - info.tickCumulativeOutside;
    info.secondsOutside = time - info.secondsOutside;
 
    liquidityNet = info.liquidityNet;
}
flowchart TB
    subgraph BeforeCross["跨越前"]
        B1["outside = 累积到tick左侧的值"]
        B2["inside = global - outside"]
    end

    subgraph CrossOperation["跨越操作"]
        OP["outside_new = global - outside_old"]
    end

    subgraph AfterCross["跨越后"]
        A1["outside = 累积到tick右侧的值"]
        A2["inside = global - outside"]
    end

    B1 --> OP --> A1

    style CrossOperation fill:#fff3e0

数学原理:当价格跨越tick时,“外部”和”内部”的概念发生翻转。通过简单的减法操作,原来的outside值就变成了新的outside值。

5.4 Position数据结构

struct Position.Info {
    // 此位置的流动性数量
    uint128 liquidity;
 
    // 上次更新时的内部费用增长率
    uint256 feeGrowthInside0LastX128;
    uint256 feeGrowthInside1LastX128;
 
    // 累积的未领取费用
    uint128 tokensOwed0;
    uint128 tokensOwed1;
}

5.5 费用计算机制

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

    subgraph CalculationFormula["计算公式"]
        F["应得费用 = <br/>(当前内部增长率 - 上次内部增长率) × 流动性"]
    end

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

    style CalculationFormula fill:#c8e6c9
function update(
    Info storage self,
    int128 liquidityDelta,
    uint256 feeGrowthInside0X128,
    uint256 feeGrowthInside1X128
) internal {
    Info memory _self = self;
 
    // 计算自上次更新以来的费用增长
    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 = LiquidityMath.addDelta(_self.liquidity, liquidityDelta);
    }
 
    // 更新费用增长记录点
    self.feeGrowthInside0LastX128 = feeGrowthInside0X128;
    self.feeGrowthInside1LastX128 = feeGrowthInside1X128;
 
    // 累积欠付费用
    if (tokensOwed0 > 0 || tokensOwed1 > 0) {
        self.tokensOwed0 += tokensOwed0;
        self.tokensOwed1 += tokensOwed1;
    }
}

费用计算设计优势

特性说明
O(1)复杂度无论时间间隔多长,计算复杂度都是常数
精确累积避免复合计算中的精度损失
存储高效只存储差值而非绝对值
自动更新每次流动性变化时自动更新费用

6. 安全机制设计

6.1 重入保护

modifier lock() {
    require(slot0.unlocked, 'LOK');
    slot0.unlocked = false;
    _;
    slot0.unlocked = true;
}
sequenceDiagram
    participant User as 用户
    participant Pool as Pool合约
    participant External as 外部合约

    User->>Pool: swap()
    Pool->>Pool: require(unlocked)
    Pool->>Pool: unlocked = false
    Pool->>External: callback
    External-->>Pool: 尝试重入
    Pool-->>External: revert('LOK')
    Pool->>Pool: unlocked = true
    Pool-->>User: 返回结果

6.2 NoDelegateCall保护

abstract contract NoDelegateCall {
    address private immutable original;
 
    constructor() {
        original = address(this);
    }
 
    modifier noDelegateCall() {
        require(address(this) == original);
        _;
    }
}

6.3 回调验证机制

flowchart TD
    subgraph SwapCallbackFlow["swap回调流程"]
        S1[记录balance0Before]
        S2[调用callback]
        S3[验证balance0变化]
        S4{balance增加足够?}
        S5[交易成功]
        S6[revert IIA]
    end

    S1 --> S2 --> S3 --> S4
    S4 -->|是| S5
    S4 -->|否| S6

    style S6 fill:#ffcdd2
// 在swap函数中
uint256 balance0Before = balance0();
IUniswapV3SwapCallback(msg.sender).uniswapV3SwapCallback(amount0, amount1, data);
require(balance0Before.add(uint256(amount0)) <= balance0(), 'IIA');

7. 本章小结

7.1 架构设计要点

mindmap
  root((V3架构精髓))
    工厂模式
      统一池子管理
      地址排序确保唯一
      费率参数化
    存储优化
      Slot0打包设计
      节省50% Gas
      单次SLOAD读取
    Library模式
      代码复用
      编译时内联
      集中维护
    数据结构
      Tick的liquidityNet
      翻转技巧
      费用增长率
    安全机制
      重入保护
      余额验证
      权限控制

7.2 关键设计模式总结

模式应用场景效果
单例工厂Pool创建统一管理,确保唯一性
接口隔离IUniswapV3Pool职责分离,易于维护
存储槽打包Slot050%+ Gas节省
库内联所有Library消除外部调用开销
增量累积费用计算O(1)时间复杂度

下一篇预告

在下一篇文章中,我们将深入探讨交换机制深度解析,包括:

  • swap函数的完整执行流程
  • 跨Tick交换的实现细节
  • 价格发现机制
  • 滑点控制与保护

参考资料