- 示例工程
- 区块链
【免费下载链接】Dapp-Learning
Dapp learning project for developers at all stages. Becoming and cultivating sovereign individuals. Nonprofit organization.
本文聚焦 Perpetual Protocol V2(Curie)在 defi/Perpetual-V2/contracts/Tick.md 中定义的Tick库:它复用了 Uniswap V3 手续费分配中「tick 上记录 outside 累计值、价格穿过时翻转」的核心思想,将其推广到基于区块的 funding payment(资金费率)场景,从而在 O(1) 时间内算出任意做市价格区间内的 funding growth。读完本文,你将掌握getAllFundingGrowth与cross两个函数的完整实现逻辑、与Funding.Growth全局累计变量、OrderBook.replaySwap、Exchange._getFundingGrowthGlobalAndTwaps之间的调用关系,以及 maker 端 funding payment 公式liquidity * (G' − G / sqrt(upper))的底层数据来源。
一、背景:为什么 Perp V2 需要一套「tick 级」的 funding 累计机制
Perp V2 与 V1 最大的架构差异,是用 Uniswap V3 替代了原有的 vAMM:每个交易市场都会初始化一个 Uniswap V3 pool,并用系统铸造的 v-token(如 vUSDC/vETH)创建真实池子,v-token 在 pool 中的价格即 Perp V2 的Mark price,开仓多空本质上是对 pool 的 swap 操作,提供流动性即向 pool 添加区间流动性(range order)。整体设计可见 defi/Perpetual-V2/README.md。
在资金费率上,V2 引入了比 V1「每小时收取」更激进的基于区块(block-based)的 funding 机制:每当交易发生就结算 funding payment,公式为:
Funding Rate = (Premium / Index Price) * Δtime / 1 day Premium = Mark Price - Index Price由于不能每笔交易都实时重算,链上采用累计值(growth)方案,在 defi/Perpetual-V2/Block-basedFundingPayments.md 中定义了全局累计变量:
G = twPremiumX96 = Σ(Δtime * premium),其中premium = markTwap − indexTwap;G' = twPremiumDivBySqrtPriceX96 = Σ(Δtime * premium / sqrt(MarkPrice))。
maker(做市商 / range order)的 funding payment 最终可化简为:
maker's funding payment = liquidity * (G' − G / sqrt(upper))问题在于:maker 的流动性按价格区间分布(Uniswap V3 的区间做市机制),每个 range order 的lowerTick/upperTick各不相同。若每次结算都遍历区间内所有 tick 累加,gas 将不可接受。这正是Tick库的用武之地——参照 Uniswap V3 手续费分配中「区间外手续费」的思路,把累计值记录在 tick 上,让区间内数值可以通过 O(1) 的加减法算出。
二、数据结构:tick 上的 GrowthInfo 与查询结果 FundingGrowthRangeInfo
Tick库操作的核心数据结构是GrowthInfo(原文档中的self即mapping(int24 => GrowthInfo) storage)。结合 defi/Perpetual-V2/contracts/OrderBook.md 中的cross调用可知,它包含三个字段:
feeX128:Uniswap V3 池中每单位流动性累计赚取的手续费(Q128 定点数,沿用 Uniswap 的feeGrowthGlobalX128);twPremiumX96:全局累计变量G = Σ(Δtime * premium),X96 定点数;twPremiumDivBySqrtPriceX96:全局累计变量G' = Σ(Δtime * premium / sqrt(MarkPrice)),X96 定点数。
其中后两个字段与Funding.Growth结构体(定义于 Funding.sol,见 defi/Perpetual-V2/Block-basedFundingPayments.md)一一对应。也就是说,每个被初始化的 tick 上都缓存了一份「价格穿过该 tick 时刻」的全局累计值快照。
getAllFundingGrowth的返回类型FundingGrowthRangeInfo则是对某一段价格区间(lowerTick, upperTick)的查询结果,包含四个成员:
twPremiumGrowthBelowX96:区间下方(≤ lowerTick)累计的G;twPremiumGrowthAboveX96:区间上方(≥ upperTick)累计的G;twPremiumGrowthInsideX96:区间内累计的G;twPremiumDivBySqrtPriceGrowthInsideX96:区间内累计的G'。
在 OrderBook 中,这些 tick 数据按 base token 隔离存储,避免不同市场互相污染:
// first key: base token, second key: tick index // value: the accumulator of **Tick.GrowthInfo** outside each tick of each pool mapping(address => mapping(int24 => Tick.GrowthInfo)) internal _growthOutsideTickMap;三、核心思想:outside 翻转机制(源自 Uniswap V3 手续费)
原文档明确指出,Tick库的原理与 Uniswap V3 fees 的计算 完全相同。回顾 Uniswap V3 的手续费分配方案(详见 defi/Uniswap-V3/whitepaperGuide/understandV3Witepaper.md):
feeGrowthInside = feeGrowthGlobal - feeGrowthOutside_below - feeGrowthOutside_above其精巧之处在于:不需要遍历区间内的每个 tick,而是把「区间外」的数据记录在边界 tick 上,用全局总量减去区间外两侧,反推出区间内:
- 每个已初始化 tick 上保存
feeGrowthOutside(该 tick 作为边界时、价格另一侧的累计手续费); - 当
currentTick穿过某个 tick 时,由于该 tick 与currentTick的位置关系发生翻转,其 outside 数值也要翻转:GrowthOutside = GrowthGlobal - GrowthOutside; - 查询任意区间时,根据
currentTick与下边界、上边界的相对位置,决定两侧 outside 是直接取 tick 缓存值、还是用GrowthGlobal减去缓存值。
Perp V2 的Tick库把这一套机制从「手续费(feeGrowth)」平移到「资金费率增长(twPremium 系列)」:区别只是累计的量从feeGrowthGlobalX128变成了twPremiumX96与twPremiumDivBySqrtPriceX96,翻转变量的个数从 1 个变成了 2 个(外加复用同一库维护feeX128)。
四、getAllFundingGrowth:O(1) 查询任意价格区间外的 funding growth
getAllFundingGrowth是Tick库的 View 函数,根据当前价格与 price range 的位置关系,计算 price range之外的FundingGrowth数值。其完整实现如下:
/// @return all values returned can underflow per feeGrowthOutside specs; /// see https://www.notion.so/32990980ba8b43859f6d2541722a739b function getAllFundingGrowth( mapping(int24 => GrowthInfo) storage self, int24 lowerTick, int24 upperTick, int24 currentTick, int256 twPremiumGrowthGlobalX96, int256 twPremiumDivBySqrtPriceGrowthGlobalX96 ) internal view returns (FundingGrowthRangeInfo memory) { GrowthInfo storage lowerTickGrowthInfo = self[lowerTick]; GrowthInfo storage upperTickGrowthInfo = self[upperTick]; int256 lowerTwPremiumGrowthOutsideX96 = lowerTickGrowthInfo.twPremiumX96; int256 upperTwPremiumGrowthOutsideX96 = upperTickGrowthInfo.twPremiumX96; FundingGrowthRangeInfo memory fundingGrowthRangeInfo; fundingGrowthRangeInfo.twPremiumGrowthBelowX96 = currentTick >= lowerTick ? lowerTwPremiumGrowthOutsideX96 : twPremiumGrowthGlobalX96 - lowerTwPremiumGrowthOutsideX96; int256 twPremiumGrowthAboveX96 = currentTick < upperTick ? upperTwPremiumGrowthOutsideX96 : twPremiumGrowthGlobalX96 - upperTwPremiumGrowthOutsideX96; int256 lowerTwPremiumDivBySqrtPriceGrowthOutsideX96 = lowerTickGrowthInfo.twPremiumDivBySqrtPriceX96; int256 upperTwPremiumDivBySqrtPriceGrowthOutsideX96 = upperTickGrowthInfo.twPremiumDivBySqrtPriceX96; int256 twPremiumDivBySqrtPriceGrowthBelowX96 = currentTick >= lowerTick ? lowerTwPremiumDivBySqrtPriceGrowthOutsideX96 : twPremiumDivBySqrtPriceGrowthGlobalX96 - lowerTwPremiumDivBySqrtPriceGrowthOutsideX96; int256 twPremiumDivBySqrtPriceGrowthAboveX96 = currentTick < upperTick ? upperTwPremiumDivBySqrtPriceGrowthOutsideX96 : twPremiumDivBySqrtPriceGrowthGlobalX96 - upperTwPremiumDivBySqrtPriceGrowthOutsideX96; fundingGrowthRangeInfo.twPremiumGrowthInsideX96 = twPremiumGrowthGlobalX96 - fundingGrowthRangeInfo.twPremiumGrowthBelowX96 - twPremiumGrowthAboveX96; fundingGrowthRangeInfo.twPremiumDivBySqrtPriceGrowthInsideX96 = twPremiumDivBySqrtPriceGrowthGlobalX96 - twPremiumDivBySqrtPriceGrowthBelowX96 - twPremiumDivBySqrtPriceGrowthAboveX96; return fundingGrowthRangeInfo; }4.1 算法流程拆解
取边界缓存:直接读取
lowerTick、upperTick两个 tick 上的GrowthInfo,得到各自的twPremiumX96(即 outside 累计值G)与twPremiumDivBySqrtPriceX96(即 outside 累计值G')。代码注释提醒:这些返回值允许 underflow,语义与 Uniswap V3feeGrowthOutside的规格一致(X96 定点数在超过2^255后回绕,属于可接受的精度损失)。判断下方(below)数值:对下边界
lowerTick,当currentTick >= lowerTick时,价格位于边界上方,tick 上缓存的 outside 记录的就是「下方区间」的累计值,直接取用;反之(价格还低于下边界),tick 上缓存的是「上方区间」的累计值,需要用全局值反推:twPremiumGrowthBelowX96 = twPremiumGrowthGlobalX96 - lowerTwPremiumGrowthOutsideX96判断上方(above)数值:对上边界
upperTick,当currentTick < upperTick时,价格位于边界下方,tick 上缓存的 outside 记录的是「上方区间」的累计值,直接取用;反之(价格已越过上边界)用全局值反推:twPremiumGrowthAboveX96 = twPremiumGrowthGlobalX96 - upperTwPremiumGrowthOutsideX96反推区间内:全局总量减去区间下方、区间上方,即得区间内累计值:
twPremiumGrowthInsideX96 = Global - Below - Above对
G与G'两个维度分别执行上述 1–4,最终返回FundingGrowthRangeInfo。
4.2 四种位置关系一目了然
把currentTick与(lowerTick, upperTick)的关系分为三种情形理解:
| currentTick 位置 | below 取值 | above 取值 |
|---|---|---|
currentTick < lowerTick(价格在区间下方) | Global − lowerOutside(lower 缓存的是上方值,需翻转) | upperOutside(upper 缓存的就是上方值) |
lowerTick <= currentTick < upperTick(价格在区间内) | lowerOutside | upperOutside |
currentTick >= upperTick(价格在区间上方) | lowerOutside | Global − upperOutside(upper 缓存的是下方值,需翻转) |
这正是原文档强调的要点:每当currentTick穿过某个 tick,该 tick 上的 outside 数值就会因位置关系翻转而翻转,所以查询时也必须依据位置关系决定「直接取缓存」还是「全局减缓存」。
五、cross:穿过 tick 时触发 outside 翻转
cross是Tick库的非 View 函数,每当currentTick穿过某个已初始化的 tick 时被调用,翻转该 tick 上的三个累计字段:
function cross( mapping(int24 => GrowthInfo) storage self, int24 tick, GrowthInfo memory globalGrowthInfo ) internal { GrowthInfo storage growthInfo = self[tick]; growthInfo.feeX128 = globalGrowthInfo.feeX128 - growthInfo.feeX128; growthInfo.twPremiumX96 = globalGrowthInfo.twPremiumX96 - growthInfo.twPremiumX96; growthInfo.twPremiumDivBySqrtPriceX96 = globalGrowthInfo.twPremiumDivBySqrtPriceX96 - growthInfo.twPremiumDivBySqrtPriceX96; }翻转的本质是GrowthOutside = GrowthGlobal - GrowthOutside:传入当前最新的全局累计值globalGrowthInfo(包含feeX128、twPremiumX96、twPremiumDivBySqrtPriceX96三个维度),用「全局总量 − 旧的 outside 缓存」得到新的 outside 缓存,覆盖写回。由于价格已经换边,旧的「一侧累计值」在数学上恰好等于全局总量减去另一侧的累计值,从而保证任何时刻、任何位置查询都能自洽。
5.1 调用时机:OrderBook.replaySwap
cross由 OrderBook 在模拟 swap(replaySwap)过程中触发,见 defi/Perpetual-V2/contracts/OrderBook.md。replaySwap内部逻辑与 Uniswap V3 Pool 的 swap 基本一致:当sqrtPriceX96 == step.nextSqrtPriceX96(价格到达某个已初始化 tick 的边界)且shouldUpdateState == true时,构造一个包含最新全局累计值的GrowthInfo并调用tickMap.cross:
if (params.shouldUpdateState) { // update the tick if it has been initialized mapping(int24 => Tick.GrowthInfo) storage tickMap = _growthOutsideTickMap[params.baseToken]; tickMap.cross( step.nextTick, Tick.GrowthInfo({ feeX128: swapState.feeGrowthGlobalX128, twPremiumX96: params.globalFundingGrowth.twPremiumX96, twPremiumDivBySqrtPriceX96: params.globalFundingGrowth.twPremiumDivBySqrtPriceX96 }) ); }这里的params.globalFundingGrowth正是 Exchange 侧最新算出的全局累计值(见下节)。可以推断,Perp V2 让replaySwap在模拟交易阶段就同步更新 tick 状态,保证真实 swap 前后 tick 缓存与全局累计值的一致性,而不会额外消耗一次单独的状态更新交易。
六、完整调用链:从全局累计到单个 maker 的 funding payment
Tick库不是孤立存在的,它处于一条完整的数据链路中。三个环节环环相扣:
6.1 上游:Exchange 维护全局累计变量 G 与 G'
defi/Perpetual-V2/contracts/Exchange.md 的_getFundingGrowthGlobalAndTwaps负责计算最新全局累计值:
// deltaTwPremium = (markTwap - indexTwap) * (now - lastSettledTimestamp) int256 deltaTwPremiumX96 = _getDeltaTwapX96(markTwapX96, indexTwap.formatX10_18ToX96()).mul( timestamp.sub(lastSettledTimestamp).toInt256() ); fundingGrowthGlobal.twPremiumX96 = lastFundingGrowthGlobal.twPremiumX96.add(deltaTwPremiumX96); // twPremiumDivBySqrtPrice += deltaTwPremium / getSqrtMarkTwap(baseToken) fundingGrowthGlobal.twPremiumDivBySqrtPriceX96 = lastFundingGrowthGlobal.twPremiumDivBySqrtPriceX96.add( PerpMath.mulDiv(deltaTwPremiumX96, PerpFixedPoint96._IQ96, getSqrtMarkTwapX96(baseToken, 0)) );即G累加Δtime * (markTwap − indexTwap),G'额外除以sqrt(MarkPrice)。同一区块内多笔交易只取第一笔的数据更新_globalFundingGrowthX96Map与_lastSettledTimestampMap(settleFunding中的once per block逻辑),从而保证全局累计值单调、可复现。
6.2 中游:OrderBook 遍历用户订单,调用 getAllFundingGrowth
defi/Perpetual-V2/contracts/OrderBook.md 的updateFundingGrowthAndLiquidityCoefficientInFundingPayment(以及 View 版本getLiquidityCoefficientInFundingPayment)遍历 trader 的所有 range order:
- 从 Uniswap V3 pool 的
getSlot0读取当前tick; - 对每个 order,以
order.lowerTick、order.upperTick、当前tick和全局累计值调用tickMap.getAllFundingGrowth(...),得到该价格区间的FundingGrowthRangeInfo; - 用
Funding.calcLiquidityCoefficientInFundingPaymentByOrder汇总每个 order 的未结算 funding; - 用最新的
FundingGrowthRangeInfo覆盖写回 order 上的缓存字段(lastTwPremiumGrowthInsideX96、lastTwPremiumGrowthBelowX96、lastTwPremiumDivBySqrtPriceGrowthInsideX96),供下次结算时计算增量。
6.3 下游:Funding.sol 把 tick 数据转化为资金数值
calcLiquidityCoefficientInFundingPaymentByOrder(见 defi/Perpetual-V2/Block-basedFundingPayments.md)将FundingGrowthRangeInfo拆成两部分:
- fundingBelowX96:价格小于 range lower 时累计的 funding growth——用
LiquidityAmounts.getAmount0ForLiquidity(lowerTick, upperTick, liquidity)算出区间下方全部 base token 数量,再乘以ΔtwPremiumGrowthBelowX96; - fundingInsideX96:即
equation A − equation B,对应公式liquidity * ((G' − E') − (G − E) / sqrt(upper)),实现为:liquidity * (ΔtwPremiumDivBySqrtPriceGrowthInsideX96 - ΔtwPremiumGrowthInsideX96 / sqrtPriceAtUpperTick)
不需要考虑价格高于区间的情况——因为此时 base token 已全部被换成 quote token,没有 base 头寸自然没有 funding payment。最终fundingBelow + fundingInside除以PerpFixedPoint96._IQ96还原为真实数值,即一个 range order 的未结算 funding payment。taker 部分则用baseBalance * ΔtwPremiumGrowthGlobalX96计算,两者相加即用户总 funding payment(calcPendingFundingPaymentWithLiquidityCoefficient)。
可以看到,Tick.getAllFundingGrowth提供的twPremiumGrowthInsideX96与twPremiumDivBySqrtPriceGrowthInsideX96正是 maker funding 公式liquidity * (G' − G / sqrt(upper))中G'、G的区间内增量来源,是整个资金费率结算体系的「数据中枢」。
七、精度与溢出:X96 定点数带来的边界约束
Tick库全程使用int256配合 X96 定点数,有几个工程细节值得注意:
- 允许 underflow:
getAllFundingGrowth的注释明确说明返回值可能 underflow,语义上继承 Uniswap V3feeGrowthOutside的规格(累计值超过2^255后自然回绕,只要增量计算采用「最新值 − 缓存值」的方式,回绕不影响正确性)。 - X96 定点数:
twPremiumX96与twPremiumDivBySqrtPriceX96均以 X96 精度存储。Funding.Growth的溢出估算注释(见 defi/Perpetual-V2/Block-basedFundingPayments.md)给出:twPremiumX96最大约2^159 ≈ 7.3e47,即使假设 premium 高达 10000、持续 10 年,twPremium ≈ 3.15e12,远在安全范围内。 - 同一 tick 上的多维度同步维护:
cross同时翻转手续费与两个 funding 字段,保证 Uniswap V3 手续费与 Perp funding 两套账本在价格穿越 tick 的瞬间同时完成换边,避免账目割裂。
八、总结
Tick库是 Perp V2 将 Uniswap V3 的 tick 数据结构移植到资金费率场景的关键桥梁:通过在每个已初始化 tick 上缓存全局 funding growth 快照、并在价格穿越时执行cross翻转,getAllFundingGrowth得以在 O(1) 时间内给出任意做市区间外的twPremium累计值,进而反推出区间内数值,支撑起 defi/Perpetual-V2/Block-basedFundingPayments.md 中 maker 端liquidity * (G' − G / sqrt(upper))的资金费率公式。这种「全局累计 + 边界缓存 + 穿越翻转」的模式与 Uniswap V3 手续费分配同源(原理见 Uniswap V3 白皮书解读:手续费),是理解集中流动性 AMM 之上叠加衍生品记账逻辑的绝佳范例。建议进一步结合 OrderBook.md、Exchange.md 与 ClearningHouse.md 串读,以还原完整的资金费率结算闭环。
- 示例工程
- 区块链
【免费下载链接】Dapp-Learning
Dapp learning project for developers at all stages. Becoming and cultivating sovereign individuals. Nonprofit organization.
相关推荐
Perp V2 Exchange 合约深度解析:资金费率结算、Uniswap V3 交易执行与 PnL 结算机制
Perp V2 Exchange 合约深度解析:资金费率结算、Uniswap V3 交易执行与 PnL 结算机制 Perpetual Protocol V2(代
示例工程区块链Perpetual Protocol V2 基于区块的资金费率(Block-based Funding)机制深度解析
Perpetual Protocol V2 基于区块的资金费率(Block based Funding)机制深度解析 导读 本篇以 Block basedFun
示例工程区块链Perpetual Protocol V2(Curie)架构解析:基于 Uniswap V3 的永续合约与基于区块的 Funding 机制
Perpetual Protocol V2(Curie)架构解析:基于 Uniswap V3 的永续合约与基于区块的 Funding 机制 永续合约市场长期存在
示例工程区块链
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考