Uniswap V3 第 2 章:现货价(sqrtPriceX96 与 tick)
V3 不直接存”价格”,而是存价格的平方根,并用一种叫
sqrtPriceX96的定点数格式。这一章讲清为什么这么设计、slot0里有什么、以及sqrtPriceX96、tick、真实价格三者怎么互相换算。这是读懂 V3 一切数学的前提。
目录
- 1. 价格的定义:P = token1 / token0
- 2. slot0 里存了什么
- 3. 为什么存”平方根”而不是价格本身
- 4. sqrtPriceX96 的 Q96 定点格式
- 5. 从 sqrtPriceX96 还原真实价格
- 6. 案例:算 WETH 以 USDC 计的价格
- 7. tick 与 sqrtPrice 的关系
- 8. 精度陷阱:token0/token1 与 decimals
- 9. 本章小结
- 10. 动手练习
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。要还原 P:P = (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 → sqrtPriceX96:
TickMath.getSqrtRatioAtTick(tick)。 - sqrtPriceX96 → tick:
TickMath.getTickAtSqrtRatio(sqrtPriceX96)(向下取整到最近 tick)。
slot0 同时存两者:sqrtPriceX96 是精确价格(swap 在 tick 内部连续移动它),tick 是当前所在的离散刻度(用于定位流动性区间)。
8. 精度陷阱:token0/token1 与 decimals
V3 价格计算最常见的两个 bug:
- 方向搞反:忘了
P = token1/token0,把要算的价格取反了。永远先确认谁是 token0(地址小的)。 - decimals 没调:原始价格
P隐含了两种代币的精度比(如 USDC 6 位、WETH 18 位,差 1e12)。不调整就会差好几个数量级。
记住口诀:先定方向(token0/token1),再调精度(decimals 差),最后才看数值。
9. 本章小结
- 价格
P = token1/token0 = Y/X(“1 个 token0 值多少 token1”),反方向取倒数1/P。 slot0存当前sqrtPriceX96(精确) 和tick(离散近似)。- V3 存价格的平方根,因为流动性/交易公式都是
√P的线性运算,避免开方。 sqrtPriceX96 = √P · 2^96(Q96 定点);P = sqrtPriceX96² / 2^192,用FullMath.mulDiv防溢出。- tick 关系:
P=1.0001^tick,sqrtPriceX96 = 1.0001^(tick/2)·2^96,用TickMath互转。 - 两大陷阱: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
思路:
- 读
slot0().sqrtPriceX96。 priceX96 = FullMath.mulDiv(sqrtPriceX96, sqrtPriceX96, Q96)(= P·2^96)。- 因为要 “WETH 价(USDC 计)” 且带 18 位小数:
price = 1e12 * 1e18 * Q96 / priceX96。 - 打印
price,断言> 0,并除以 1e18 看是否接近当前 ETH 市价(如 ~3000)。
进阶
- 也算一遍
1/price(USDC 以 WETH 计的价),体会方向取倒数。 - 把
slot0().tick用1.0001^tick换算成价格,和 sqrtPriceX96 路线的结果对比,应非常接近(tick 是离散近似)。
运行
forge test --evm-version cancun --fork-url $FORK_URL \
--match-path test/UniswapV3SpotPrice.t.sol -vvv
下一章(第 3 章 数学)推导集中流动性的核心方程:流动性 L 与 token 数量、价格的关系,以及”真实储备曲线”。