12.Uniswap V3 添加流动性案例

核心摘要 (Key Takeaways)

  • Uniswap V3 的核心是集中流动性:与 V2 的无限价格范围不同,V3 允许流动性提供者(LP)将资金集中在特定的价格区间 [Pa, Pb] 内,其底层数学模型可以看作是对 V2 的 x * y = k 模型进行了平移和截断。
  • 价格变动消耗特定 Token:在设定的价格区间内,当价格从当前价 P 下降至 Pa 时,池子会消耗 Y Token;当价格从 P 上升至 Pb 时,池子会消耗 X Token。理解这一点是计算所需流动性的关键。
  • 流动性计算基于虚拟曲线:尽管 V3 的真实曲线是分段的,但其流动性数量 ΔX\Delta XΔY\Delta Y 的计算可以基于 V2 的 xy=L2x \cdot y = L^2P=y/xP = y/x 这两个基础公式,应用在一条“虚拟”的、经过平移的曲线上进行推导。
  • 工程实现中的核心逻辑:在实际添加流动性时,智能合约会根据用户提供的两种 Token (ΔX\Delta XΔY\Delta Y) 分别计算出两个可能的流动性值 L,并最终选择较小的那个作为最终的流动性。这可能导致实际使用的某一种 Token 数量略少于用户最初提供的数量。

一、 Uniswap V3 核心公式与概念回顾

1. V3 流动性公式与曲线

Uniswap V3 引入了集中流动性的概念,允许将流动性提供在指定的价格区间 [Pa, Pb]。其公式可以理解为对 V2 公式的平移。

13.Uniswap V3 单价格区间内 Swap 机制

Uniswap V3:单价格区间内 Swap 机制与数学实现

核心摘要 (Key Takeaways)
  • 两步核心算法:在 Uniswap V3 的单个价格区间内执行 swap 的核心逻辑分为两步:首先,根据输入的代币数量和当前流动性,计算出交易完成后的新价格;然后,利用新旧两个价格点和流动性,计算出可以兑换出的输出代币数量。
  • 基石数学关系:所有计算都基于 流动性 (L)代币 X 数量代币 Y 数量价格 (P) 之间的恒定关系。具体来说,X=L/PX = L / \sqrt{P}Y=LPY = L \cdot \sqrt{P} 是推导所有 swap 公式的基石。
  • 价格方向决定公式形态:无论是用 Y 买 X(价格上升)还是用 X 卖 Y(价格下降),其底层的数学原理是统一的。公式的具体形态(谁减谁)取决于价格的高低(PupperP_{upper} vs PlowerP_{lower}),最终可以抽象为统一的表达式。

1. 基础回顾:价格、Tick 与流动性

在深入 swap 逻辑之前,我们首先要回顾 Uniswap V3 的基本概念。协议通过 Tick 来离散化价格,并将 Tick 转换为实际计算中使用的 P\sqrt{P} (square root price)。在一个给定的价格点 P,流动性 L 与两种代币(X 和 Y)的数量关系如下:

14.Uniswap V3 Tick Bitmap

核心摘要 (Key Takeaways)

  • Tick BitMap 的核心目的: 为了高效、低成本地跟踪 Uniswap V3 池中哪些价格点(Ticks)被用作了流动性区间的边界,从而知道哪些价格范围是活跃的。
  • 精巧的数据压缩: Tick BitMap 抛弃了为每个 Tick 单独存储状态的朴素方案(成本过高),而是通过位运算将 256 个连续的 Tick 状态压缩存储在一个 uint256 变量中,极大地节省了 Gas 费和存储空间。
  • 关键转换公式: 任何一个 tick (类型为 int24) 都可以通过数学运算定位到它在 BitMap 中的具体位置。其核心是两个坐标:wordPosition (确定在哪一个 uint256 组里) 和 bitPosition (确定在该组的第几位)。
    • wordPosition = tick / 256
    • bitPosition = tick % 256
  • 数据结构: Tick BitMap 在合约中实现为一个 mapping(int16 => uint256)。其中 key (int16)wordPositionvalue (uint256) 是一个 256 位的位图,每一位代表一个 tick 的活跃状态(1 表示活跃,0 表示不活跃)。
  • 状态更新机制: 添加或移除流动性时,通过生成一个特定的掩码 (Mask),并与 BitMap 中对应的 uint256 值进行按位或 (OR)按位与 (AND) 运算,来精确地将某一位从 0 改为 1 或从 1 改为 0,而不影响其他位。

1. Tick 的回顾与作用

  • 定义: Tick (也称 Tick Index) 是 Uniswap V3 中用来标记离散化价格点的单位。
  • 价格公式: 价格 Ptick T 的关系通过以下公式定义: P=1.0001T P = 1.0001^T 其中 T 就是 tick 的索引。当 T 增大时,价格 P 上升;当 T 减小时,价格 P 下降。
  • Tick 范围:
    • 合约中 tick 的类型为 int24,其理论范围是从 -887272+887272
    • 案例: 这个范围足以表示极其宽广的价格区间。
      • tick-887272 时,价格约为 2.938×10392.938 \times 10^{-39}
      • tick+887272 时,价格约为 3.4×10383.4 \times 10^{38}
      • 这个范围远超比特币等主流资产的价格波动范围,因此在设计上是足够用的。

2. 核心问题:如何跟踪活跃的流动性区间?

  • 背景: 在 Uniswap V3 中,用户在自定义的价格区间 [PA, PB] (对应 [Tick_A, Tick_B]) 内添加流动性。
  • 挑战: 一个活跃的交易对(如 BTC/USDT)可能有成千上万的用户在各种不同的、甚至重叠的价格区间添加流动性。合约需要一种高效的方式来跟踪哪些 tick 成为了这些流动性区间的边界(即被激活)。
    • 案例: 假设一个用户在 tick 范围 [-10, -5] 添加了流动性,而另一个用户在 [-4, 1] 添加了流动性。此外,还可能存在重叠区间,例如用户 A 在 [6, 10] 添加流动性,用户 B 在 [7, 12] 添加流动性,那么在 [7, 10] 区间内流动性是重叠的。合约必须有效管理这一切。
  • 从 ERC20 到 ERC721:
    • V2: 所有流动性提供者的 LP 凭证是同质化的 ERC20 代币。
    • V3: 由于每个人的流动性价格区间和数量都可能不同,LP 凭证变成了非同质化的 ERC721 代币(NFT)。
    • 案例: 一个 V3 LP NFT (通过 NonfungiblePositionManager 合约管理) 记录了如 tickLowertickUpperliquidity (uint128) 等唯一信息,代表一个独一无二的流动性仓位。
  • 根本问题: 尽管 NFT 记录了每个仓位的具体信息,但 Pool 合约本身如何宏观地、低成本地知道在任意一个 tick 上,是否有流动性边界存在?

3. 一种被否决的朴素方案

  • 思路: 设计一个简单的键值对(Key-Value)结构来记录每个 tick 的状态。
    • mapping(int24 => bool)
    • Key: tick 索引 (例如: -10000, 20000)
    • Value: true (被用作边界) 或 false (未被用作边界)
  • 致命缺陷:
    1. 存储量巨大: tick 的总数约有 177 万个。为每一个 tick 都创建一个存储槽位在区块链上是无法承受的,成本极高。
    2. 计算量巨大 (高 Gas 费): 当一笔交易(swap)需要跨越成千上万个 tick 时,如果需要逐一查询这些 tick 的状态,将会导致交易的 Gas 费极高,用户无法接受。

4. Uniswap V3 的精巧解决方案:Tick BitMap

Tick indexes in tick bitmap

15.Uniswap V3 Tick Bitmap 寻找下一个 Tick

核心摘要 (Key Takeaways)

  • Tick Bitmap 的核心作用:它是一个 uint256 的位图,用于高效追踪 Uniswap V3 池中哪些价格点(Ticks)被用作了流动性区间的边界。1 代表该 Tick 活跃(有流动性),0 代表不活跃。
  • 寻找下一个 Tick 的目的:在进行交易(Swap)时,价格会发生变动。当价格穿过一个活跃的 Tick,意味着当前流动性区间的流动性(L)将被耗尽,合约必须找到下一个有流动性的价格区间才能继续完成交易。
  • 通过位运算实现高效查找:寻找下一个活跃 Tick 的过程利用了**掩码(Mask)按位与(Bitwise AND)**运算。通过构建特定的掩码,可以快速地隔离出目标方向(价格升高或降低)上所有活跃的 Ticks。
  • 交易方向决定搜索方向
    • 当交易导致价格上涨时(例如用 USDC 买 ETH),需要在 Tick Bitmap 中向搜索(寻找更大的 Tick)。
    • 当交易导致价格下跌时(例如卖出 ETH 换 USDC),需要在 Tick Bitmap 中向搜索(寻找更小的 Tick)。
  • Bitmap 内部方向与全局 Tick 轴方向的区别:在 Bitmap(一个 uint256)内部,索引位(bit position)从右到左增大,因此向查找意味着寻找更大的 Tick 值。这与全局价格数轴上数值从左到右增大的直觉相反,需要注意区分。

I. Tick Bitmap 核心概念回顾

本节内容对应 TickBitmap.sol 库合约中的核心逻辑。

16.Uniswap V3 Cross Tick Swap

核心摘要 (Key Takeaways)

  • 跨Tick交易的核心机制:当一笔交易的规模大到足以耗尽当前价格区间(Tick Range)内的流动性时,交易会“穿越”到一个新的、相邻的活跃价格区间,并利用新区间的流动性继续完成剩余部分的兑换。
  • 迭代循环过程:跨Tick交易并非一次性计算,而是一个循环过程。系统在当前区间内完成部分交易,计算剩余未兑换量,然后寻找下一个活跃Tick,更新流动性,再对剩余量进行计算,如此往复,直至交易完成或流动性耗尽。
  • 两大基础工具:整个跨Tick交易的实现依赖于两个核心数据结构:Tick Bitmap 用于快速查找下一个有流动性的活跃Tick;Liquidity Net 用于在穿越Tick边界时,高效地计算出新价格区间的总流动性。
  • V3与V2的复杂度对比:Uniswap V3由于引入了集中流动性,其swap(交易)的数学逻辑和代码实现复杂度远超V2。V2的交易遵循简单的 x×y=kx \times y = k 恒定乘积公式,而V3则需要处理离散的、分段的流动性,进行复杂的迭代计算。

1. 交易机制回顾与铺垫

1.1 基础概念回顾
  • 横坐标 (价格/Tick):智能合约使用 Tick Bitmap 来记录哪些价格点(Ticks)是活跃的(即作为某个流动性区间的边界)。
  • 纵坐标 (流动性):使用 Liquidity Net 记录在每个Tick点上流动性的净变化量。当价格穿越一个Tick时,总流动性会根据该Tick的liquidityNet值进行更新。
  • 流动性叠加:在某个价格区间,多个用户提供的流动性会叠加起来,形成一个总的、更高的流动性池。
1.2 单一价格区间内的交易 (未跨Tick)

这是跨Tick交易的基础。在一个固定的价格区间 [PA, PB] 内,流动性 L 是恒定的。

16.Uniswap V3 LiquidityNet

核心摘要 (Key Takeaways)

  • 智能合约的效率限制:Uniswap v3 智能合约因存储和计算成本的限制,不会记录每个价格区间的具体流动性值。它采用一种更高效的动态计算方法,只在流动性发生变化的 tick 边界点记录关键信息。
  • 核心概念 Liquidity Net:合约追踪流动性的核心是记录在每个被初始化的 tick 上的 Liquidity Net (流动性净变化量)。这是一个有符号的值 (ΔL),表示当价格穿越该 tick 时,池中活跃流动性的变化量
  • 边界点的符号规则:当用户在 [tick_lower, tick_upper] 区间添加流动性 L 时,合约会在 tick_lower 处记录一个正的净变化量 (+L),在 tick_upper 处记录一个负的净变化量 (-L)。移除流动性则符号相反。
  • 流动性的动态计算:在交易(swap)过程中,当价格从一个区间穿越 tick 进入下一个区间时,合约通过一个简单的累加公式实时更新当前的活跃流动性:L_{新区间} = L_{旧区间} + \Delta L_{tick}。这使得合约只需追踪当前区间的流动性,而无需预存所有区间的流动性信息。

1. 背景问题:Uniswap v3 如何高效追踪流动性?

在 Uniswap v3 中,流动性提供者可以在任意自定义的价格区间(由 tick 界定)内提供流动性。这导致了池子的总流动性在不同价格段是阶梯式变化的。