11.Uniswap V3 Tick 与价格表示
目录
核心摘要 (Key Takeaways)
tick引入目的与作用: Uniswap V3 引入了tick机制,将连续的价格范围离散化,以解决集中流动性中用户随意设置价格区间导致的高 Gas 消耗问题,从而大幅节省 Gas 费用。- 价格与
tick的数学关系: Uniswap V3 中的价格 与tick索引 之间通过公式 建立精确的指数关系。 - 合约价格存储: Uniswap V3 合约的
slot0结构中,价格信息并非直接存储为 ,而是以 的形式(即sqrtPriceX96)存储,并采用 Q64.96 定点数格式,以适应 Solidity 对浮点数处理的限制并优化 Gas 消耗。 - 实际价格计算的复杂性: 从合约中读取的价格需要考虑 Token 精度差异 和
token0/token1顺序(可能导致价格倒数)进行修正,才能得到真实的市场价格。 tick范围的由来:tick的范围被限制在 -887272 到 +887272,这一范围是根据Q64.96定点数格式所能表示的 的上下限,通过价格与tick的数学关系推导而来,并硬编码在合约中。
1. tick 的引入与作用
1.1 问题背景:集中流动性的 Gas 消耗
- Uniswap V3 的核心创新: 引入了集中流动性 (Concentrated Liquidity),允许用户在任意价格区间 和 内提供流动性。
- 潜在问题: 如果用户可以随意设置 和 (例如 到 ),会带来以下问题:
- 每个用户设置的价格范围都是独一无二且随意的。
- 维护和记录每个用户添加的 和 范围会消耗巨量的 Gas 费用,因为合约状态需要存储大量不规则的数据。
1.2 解决方案:引入 tick 进行离散化

- 核心思想: 将连续的价格空间离散化。用户添加流动性的最小单位是
tick。 tick的定义:tick可以理解为价格轴上的一个“刻度点”。用户只能在两个tick之间选择 和 来添加流动性,即 必须是lowerTick, 必须是upperTick。- 类比: 就像高速公路的里程表,你可以连续地报出 公里,但更常见和高效的方式是离散地报出 公里、 公里等整数里程。
tick机制就是将连续的价格报数离散化。 - 好处:
- 节省 Gas 费用: 通过将连续的价格范围转换为离散的
tick范围,大大减少了需要维护的状态数量和计算复杂度。 - 标准化: 统一了流动性添加的边界,使得不同用户的流动性可以更有效地聚合和管理。
- 节省 Gas 费用: 通过将连续的价格范围转换为离散的
- 价格表示: 在 Uniswap V3 的图示中,
tick之间的斜率表示价格。

1.3 Uniswap V2 与 V3 合约记录逻辑对比
Uniswap V2 和 V3 在合约中记录流动性和价格的方式存在根本性差异,以下流程图展示了它们的逻辑对比:
graph TD
subgraph Uniswap V2
A[记录 Token X 数量] --> B[推导价格 P]
A --> C[推导流动性 L]
D[记录 Token Y 数量] --> B
D --> C
end
subgraph Uniswap V3
E[记录价格 P] --> F[推导特定 tick 范围内的 Token X 数量]
E --> G[推导特定 tick 范围内的 Token Y 数量]
H[记录流动性 L]
end
graph TD
subgraph Uniswap V2
A[记录 Token X 数量] --> B[推导价格 P]
A --> C[推导流动性 L]
D[记录 Token Y 数量] --> B
D --> C
end
subgraph Uniswap V3
E[记录价格 P] --> F[推导特定 tick 范围内的 Token X 数量]
E --> G[推导特定 tick 范围内的 Token Y 数量]
H[记录流动性 L]
end
graph TD
subgraph Uniswap V2
A[记录 Token X 数量] --> B[推导价格 P]
A --> C[推导流动性 L]
D[记录 Token Y 数量] --> B
D --> C
end
subgraph Uniswap V3
E[记录价格 P] --> F[推导特定 tick 范围内的 Token X 数量]
E --> G[推导特定 tick 范围内的 Token Y 数量]
H[记录流动性 L]
endgraph TD
subgraph Uniswap V2
A[记录 Token X 数量] --> B[推导价格 P]
A --> C[推导流动性 L]
D[记录 Token Y 数量] --> B
D --> C
end
subgraph Uniswap V3
E[记录价格 P] --> F[推导特定 tick 范围内的 Token X 数量]
E --> G[推导特定 tick 范围内的 Token Y 数量]
H[记录流动性 L]
end
解释:
- Uniswap V2: 合约直接记录
Token X和Token Y的数量,然后通过这些数量推导出当前价格 和总流动性 。 - Uniswap V3: 合约直接记录当前价格 (通过
tick和sqrtPriceX96表示) 以及总流动性 。然后,反过来根据这些信息推导出在每个特定tick范围内的Token X和Token Y数量。这意味着直接查看 V3 合约的Token X和Token Y余额并不能直接计算出当前价格,因为流动性是分散在不同价格区间内的。
2. tick 与价格的数学关系
2.1 核心价格公式
Uniswap V3 使用以下公式将 tick 索引 转换为价格 :
- : 表示
tick索引,可以是正数、负数或零。 - : 表示价格。
2.2 tick 变化对价格的影响
- : 任何非零数的零次方都是 1。因此,当 时,。
- : 随着 增大,价格 会不断增大。
- : 随着 减小(变为负数),价格 会不断减小,可以表示非常小的价格(例如 )。
tick边界: 当价格在tick之间移动并越过一个tick边界时,合约会记录当前价格向下取整的tick索引。
2.3 tick 的有效范围
Uniswap V3 将 tick 的有效范围限制在:
- 最小
tick: -887272 - 最大
tick: +887272
这个范围的由来将在后面的章节详细解释。
3. Uniswap V3 合约中的价格存储
3.1 slot0 结构
Uniswap V3 合约(例如 UniswapV3Pool)内部定义了一个 STRUCT 叫 slot0 来存储当前池子的关键状态信息,其中包括:
uint160 sqrtPriceX96: 当前价格的平方根,经过 缩放后的值。int24 tick: 当前的tick索引。
struct Slot0 {
// the current price
uint160 sqrtPriceX96;
// the current tick
int24 tick;
// the most-recently updated index of the observations array
uint16 observationIndex;
// the current maximum number of observations that are being stored
uint16 observationCardinality;
// the next maximum number of observations to store, triggered in observations.write
uint16 observationCardinalityNext;
// the current protocol fee as a percentage of the swap fee taken on withdrawal
// represented as an integer denominator (1/x)%
uint8 feeProtocol;
// whether the pool is locked
bool unlocked;
}3.2 为什么存储 sqrtPriceX96 而不是 ?
- 原因: Uniswap V3 的流动性计算公式中大量使用了价格的平方根 (),例如: 如果合约中存储的是 ,每次计算都需要进行开平方根运算,这在 EVM (以太坊虚拟机) 上是非常消耗 Gas 的操作。
- 优化: 为了节省 Gas,合约直接存储 的值。
- Solidity 浮点数限制: Solidity 语言不支持浮点数运算。为了在整数运算中保留浮点数的精度,Uniswap V3 采用了 Q64.96 定点数格式。
- 存储方式:
sqrtPriceX96存储的是 乘以 后的整数值。 - 公式:
- 逆运算(获取 ):
- 最终价格 :
- 存储方式:
- 精度: 乘以 (一个非常大的数) 后,小数部分被“提升”为整数,从而在整数运算中保留了足够的精度,避免了直接存储小数可能导致的精度损失。
3.3 案例:从 sqrtPriceX96 计算价格 (ETH/DAI)
假设从区块链浏览器查询到 ETH/DAI 交易对的 sqrtPriceX96 值为 3364239860641323386927909307。
- 计算 :
- 计算 :
(这与 ETH 的市场价格 接近)
- 注意: ETH 和 DAI 的精度都是 18,因此无需额外精度修正。
4. 实际价格计算中的精度与方向问题
在从 Uniswap V3 合约中读取 tick 或 sqrtPriceX96 并计算实际价格时,需要注意以下两个常见问题:
4.1 Token 精度差异
- 问题: 如果交易对中的两个 Token (例如 ETH 和 USDC) 具有不同的
decimals(精度),直接通过tick或sqrtPriceX96计算出的价格将不正确。- 案例: ETH (18 位精度) / USDC (6 位精度)
- 从合约
slot0读取tick= -201344。 - 直接计算 。这个价格显然是错误的,因为 ETH 价格远高于此。
- 从合约
- 案例: ETH (18 位精度) / USDC (6 位精度)
- 解决方案: 需要根据 Token 的精度差异进行修正。
- 修正公式: 真实价格 = 计算价格
- 案例修正: 假设 ETH 是
token0(18 精度),USDC 是token1(6 精度)。 真实价格 = 真实价格 = 真实价格 = (这与 ETH 的市场价格相符)
- 无精度差异: 如果两个 Token 精度相同 (例如 ETH/DAI 都是 18 位精度),则无需进行此修正。
4.2 token0/token1 顺序导致的价格倒数问题
- 问题: Uniswap V3 合约内部定义了
token0和token1,但这两个 Token 在合约中的顺序可能与我们预期计算价格时使用的 X 轴和 Y 轴的 Token 顺序不一致。例如,如果合约将 USDC 视为token0(X 轴),ETH 视为token1(Y 轴),那么通过 计算出的价格可能是 USDC/ETH 的价格,而不是我们通常期望的 ETH/USDC 价格。 - 表现: 通过
sqrtPriceX96计算出的价格与实际市场价格互为倒数。 - 解决方案: 如果发现通过
sqrtPriceX96计算出的价格与预期价格互为倒数,则需要对计算结果再取一次倒数。- 修正步骤:
- 从
sqrtPriceX96计算出 。 - 如果 与预期价格互为倒数,则 。
- 再应用精度修正 (如果需要)。
- 从
- 案例: 假设 ETH/USDC 交易对,通过
sqrtPriceX96计算出的 经过精度修正后是 (一个非常大的错误数字)。这表明合约内部的token0和token1顺序与我们期望的 ETH/USDC 顺序相反。- 取倒数: 。
- 再应用精度修正 (假设 ETH 是 18 精度,USDC 是 6 精度): 。这个价格与 ETH 实际价格相符。
- 修正步骤:
5. tick 范围的由来
Uniswap V3 tick 范围 (-887272 到 +887272) 并非随意设置,而是由 sqrtPriceX96 采用的 Q64.96 定点数格式所能表示的价格范围推导而来。
5.1 Q64.96 格式对 的限制
uint160类型用于存储sqrtPriceX96,意味着它可以存储一个 160 位的无符号整数。Q64.96格式表示这个 160 位整数中,有 64 位用于整数部分,96 位用于小数部分。- 为了保证价格的对称性 (即 和 都能被表示),Uniswap V3 将 的有效范围限制在:
- 最小值:
- 最大值:
5.2 价格 的有效范围
基于 的范围,价格 的有效范围为 :
- 最小值:
- 最大值:
5.3 从价格范围推导 tick 范围
我们已知价格与 tick 的关系公式为 。为了求出 ,我们可以对两边取以 为底的对数:
现在,我们将 的最大值和最小值代入此公式:
-
最大
tick():使用换底公式 :
计算结果约为 。向下取整,得到 +887272。
-
最小
tick():计算结果约为 。向上取整,得到 -887272。
5.4 合约硬编码
这些计算出的 tick 范围值被硬编码在 Uniswap V3 Core 合约中,例如 MIN_TICK 和 MAX_TICK 常量。这确保了所有 Uniswap V3 池子的 tick 都在一个预定义的、数学上合理的范围内运行。