Uniswap V3 第2章 现货价:slot0、sqrtPriceX96 与 tick

讲清 Uniswap V3 如何存储价格:slot0 里的 sqrtPriceX96 和 tick,为什么存价格的平方根,以及三者之间的换算。

6 分钟阅读
Uniswap V3 第2章 现货价:slot0、sqrtPriceX96 与 tick

Uniswap V3 第 2 章:现货价(sqrtPriceX96 与 tick)

V3 不直接存”价格”,而是存价格的平方根,并用一种叫 sqrtPriceX96 的定点数格式。这一章讲清为什么这么设计、slot0 里有什么、以及 sqrtPriceX96tick、真实价格三者怎么互相换算。这是读懂 V3 一切数学的前提。


目录


1. 价格的定义:P = token1 / token0

V3 池子里两种代币按地址排序:地址小的是 token0(记作 X),大的是 token1(记作 Y)。价格定义为:

P = token1 数量 / token0 数量 = Y / X

“一个 token0 值多少个 token1”。例如 USDC/WETH 池里 token0=USDC、token1=WETH,那么 P = WETH/USDC,是”1 个 USDC 值多少 WETH”。要得到”1 个 WETH 值多少 USDC”,取倒数 1/P

这个方向很容易搞反,务必先确认谁是 token0。下面第 6 节的案例会演示。


2. slot0 里存了什么

slot0 是池子最常被读取的状态,打包在一个存储槽里:

struct Slot0 {
    uint160 sqrtPriceX96;   // 当前价格的平方根(Q96 定点)
    int24   tick;            // 当前 tick
    uint16  observationIndex;        // 预言机相关
    uint16  observationCardinality;
    uint16  observationCardinalityNext;
    uint8   feeProtocol;
    bool    unlocked;        // 重入锁
}

最重要的是前两个:sqrtPriceX96(当前价的平方根)tick(当前刻度)。两者描述同一个价格,只是精度不同:sqrtPriceX96 是精确值,tick 是离散近似。


3. 为什么存”平方根”而不是价格本身

这是 V3 一个聪明的设计。回忆 V3 的流动性公式(下一章细讲):

x = L / √P        y = L · √P

交易时价格变化,需要反复计算 √P。如果存的是 P,每次都要开方(昂贵且有精度损失)。直接存 √P,让最频繁的运算变成加减乘除,避免开方。

具体好处:

  • swap 时,Δy = L · Δ(√P)Δ(1/√P) = Δx / L —— 都是 √P 的线性运算,无需开方。
  • 价格永远为正,平方根定义良好。

所以 V3 全程以 √P 为”一等公民”,价格 P 反而是派生量。


4. sqrtPriceX96 的 Q96 定点格式

Solidity 没有小数。V3 用 Q96 定点数表示 √P

sqrtPriceX96 = √P · 2^96

即把 √P 乘以 2^96(记作 Q96)放大成整数。“X96”就是”乘了 2^96”的意思。

  • 整数部分和小数部分各占一段位,2^96 提供约 28 位十进制的小数精度。
  • 用 uint160 存储(96 位小数 + 64 位整数足够覆盖所有现实价格)。

要还原 √P√P = sqrtPriceX96 / 2^96。要还原 PP = (sqrtPriceX96 / 2^96)^2


5. 从 sqrtPriceX96 还原真实价格

P = (sqrtPriceX96 / 2^96)^2 = sqrtPriceX96² / 2^192

工程实现要小心溢出:sqrtPriceX96² 可能超过 uint256。用 FullMath.mulDiv(a, b, denominator) 做”先乘后除、中间用 512 位”避免溢出:

// price = sqrtPriceX96 * sqrtPriceX96 / 2^96  (得到 P · 2^96)
uint256 priceX96 = FullMath.mulDiv(sqrtPriceX96, sqrtPriceX96, Q96);
// 此时 priceX96 = P · 2^96,再按需要除以 Q96 / 调整 decimals

6. 案例:算 WETH 以 USDC 计的价格

USDC/WETH 0.05% 池:token0 = USDC(6 位小数),token1 = WETH(18 位小数)。

P = Y/X = WETH/USDC (原始单位,含各自精度)

我们想要”1 WETH = ? USDC”,即 1/P,并希望结果带 18 位小数。

步骤(对应练习代码):

// 1. price = P · 2^96 = sqrtPriceX96² / 2^96
price = FullMath.mulDiv(sqrtPriceX96, sqrtPriceX96, Q96);

// 2. 取倒数并调整精度:
//    P 的原始 decimals = 1e18/1e6 = 1e12
//    我们要 1/P 带 1e18 精度
price = 1e12 * 1e18 * Q96 / price;

最终 price 就是”1 WETH 值多少 USDC,带 18 位小数”,比如 3000e18 表示 3000 USDC/WETH。

关键点:别忘了 token0/token1 的方向(决定 P 还是 1/P)和两者的 decimals 差(这里 1e12)。这两处是 V3 价格计算最容易出错的地方。


7. tick 与 sqrtPrice 的关系

tick 和 sqrtPrice 描述同一价格:

P = 1.0001^tick
√P = 1.0001^(tick/2)
sqrtPriceX96 = 1.0001^(tick/2) · 2^96

换算:

  • tick → sqrtPriceX96TickMath.getSqrtRatioAtTick(tick)
  • sqrtPriceX96 → tickTickMath.getTickAtSqrtRatio(sqrtPriceX96)(向下取整到最近 tick)。

slot0 同时存两者:sqrtPriceX96 是精确价格(swap 在 tick 内部连续移动它),tick 是当前所在的离散刻度(用于定位流动性区间)。


8. 精度陷阱:token0/token1 与 decimals

V3 价格计算最常见的两个 bug:

  1. 方向搞反:忘了 P = token1/token0,把要算的价格取反了。永远先确认谁是 token0(地址小的)。
  2. decimals 没调:原始价格 P 隐含了两种代币的精度比(如 USDC 6 位、WETH 18 位,差 1e12)。不调整就会差好几个数量级。

记住口诀:先定方向(token0/token1),再调精度(decimals 差),最后才看数值。


9. 本章小结

  1. 价格 P = token1/token0 = Y/X(“1 个 token0 值多少 token1”),反方向取倒数 1/P
  2. slot0 存当前 sqrtPriceX96(精确)tick(离散近似)
  3. V3 存价格的平方根,因为流动性/交易公式都是 √P 的线性运算,避免开方。
  4. sqrtPriceX96 = √P · 2^96(Q96 定点);P = sqrtPriceX96² / 2^192,用 FullMath.mulDiv 防溢出。
  5. tick 关系:P=1.0001^ticksqrtPriceX96 = 1.0001^(tick/2)·2^96,用 TickMath 互转。
  6. 两大陷阱:token0/token1 方向decimals 差

10. 动手练习

对应课程的 Spot Price 练习:从 sqrtPriceX96 算出 WETH 以 USDC 计的价格。

练习:spot price from sqrtPriceX96

主网分叉,USDC/WETH 0.05% 池:

interface IUniswapV3Pool {
    function slot0() external view returns (
        uint160 sqrtPriceX96, int24 tick, uint16,uint16,uint16,uint8,bool
    );
}
// 用 FullMath.mulDiv 防溢出;Q96 = 1 << 96

思路:

  1. slot0().sqrtPriceX96
  2. priceX96 = FullMath.mulDiv(sqrtPriceX96, sqrtPriceX96, Q96)(= P·2^96)。
  3. 因为要 “WETH 价(USDC 计)” 且带 18 位小数:price = 1e12 * 1e18 * Q96 / priceX96
  4. 打印 price,断言 > 0,并除以 1e18 看是否接近当前 ETH 市价(如 ~3000)。

进阶

  • 也算一遍 1/price(USDC 以 WETH 计的价),体会方向取倒数。
  • slot0().tick1.0001^tick 换算成价格,和 sqrtPriceX96 路线的结果对比,应非常接近(tick 是离散近似)。

运行

forge test --evm-version cancun --fork-url $FORK_URL \
  --match-path test/UniswapV3SpotPrice.t.sol -vvv

下一章(第 3 章 数学)推导集中流动性的核心方程:流动性 L 与 token 数量、价格的关系,以及”真实储备曲线”。

💬 评论