文章前言
本篇文章主要对Uniswap V3协议的新特性、工作原理、项目构成、源码实现等部分进行详细解读。
协议简介
Uniswap v1版本于2018年11月面世,其本质上是一个运行在以太坊区块链上的基于"恒定乘积"算法的"自动化流动性"协议,我们可以将其看做是一个建立在以太坊上的去中心化数字货币交易所(DEX),在该交易所上的所有交易(代币互换)都由智能合约来执行且免信任。
Uniswap v2版本于2020年5月面世,相较于Uniswap v1最主要的变化是在原先只支持ERC-20/ETH流动性池的基础之上增加了对ERC-20/ERC-20流动性池的支持,任意ERC-20之间都可以直接进行币币交易。
Uniswap v2版本于2021年5月面世,相较于Uniswap v2最主要的变化是引入了集中流动性(Concentrated Liquidity)概念,实现了资本效率的最大化,一方面使得LP可以赚取更多的回报,另一方面提高了交易的执行力,Uniswap V3还改进了Oracles以提供灵活的费率以及范围订单功能。
协议变化
下面我们对UniSwap v3协议中的主要变化做以简单的介绍:
集中流动性
在早期版本中,流动性沿Curve曲线均匀分布,其中𝑥和𝑦分别是两个资产X和Y的储备,而𝑘是一个常数,换句话说,早期版本旨在提供整个价格范围为(0, +∞)的流动性,这很容易实现并且可以有效地聚合流动性,但这意味着池中持有的大部分资产永远不会被触及,例如,v2版本中的DAI/USDC对仅保留约0.50%的资本用于在0.99美元和1.01美元之间进行交易,这是LP期望看到最多交易量并因此赚取最多费用的价格范围。
考虑到这一点,允许LP将其流动性集中在比(0,+∞)更小的价格范围内似乎是合理的,在Uniswap V3中将流动性集中在一个有限范围内称为头寸,一个头寸只需要保持足够的储备(reserve)来支持其范围内的交易,它可以像一个恒定的产品池一样在该范围内拥有更多的储备(我们将其称之为虚拟资金)。
具体而言,一个头寸只需要持有足够的资产X来覆盖价格变动到其上限,因为向上的价格变动对应于X储备的耗尽,相似地,它只需要持有足够的资产Y来覆盖价格向其下限的变动,上图描述了区间[𝑎, 𝑏]上的头寸和当前价格𝑐∈ [𝑎, 𝑏]的这种关系。𝑥real和𝑦real表示头寸的实际储备,而当价格超出仓位范围时,仓位的流动性不再活跃,也不再赚取费用,那时它的流动性完全由单一资产组成,因为另一资产的储备肯定已经完全耗尽,如果价格重新进入该范围,流动性将再次活跃,提供的流动性数量可以用𝐿来衡量,它等于√𝑘,头寸的实际储备由曲线描述:
函数式2.2对应的曲线图如下,此曲线是公式X*Y=K(2.1)的平移,因此位置正好在其范围内:
流动性提供者可以根据自己的价格范围自由创建任意数量的头寸,通过这种方式LP可以在价格空间上近似任何所需的流动性分布,此外这是一种让市场决定流动性分配的机制,理性的LP可以通过将其流动性集中在当前价格附近的狭窄范围内,并在价格变动时添加或移除代币以保持其流动性活跃,从而降低资本成本。
资本效率改变
通过集中流动性,LP可以在指定的价格区间内提供与v2相同的流动性深度,同时将远低于v2的资本风险,节省下来的资本可以对外持有,投资于不同的资产,存放在DeFi的其他地方,或者用于增加指定价格区间内的风险敞口,赚取更多的交易费用。
现在我们假设:
Alice和Bob都想在Uniswap v3上的ETH/DAI池中提供流动性,他们每人有100万美元,目前ETH的价格是1500DAI。
Alice决定在整个价格范围内部署她的资本(就像她在Uniswap v2中一样),于是她存入50万DAI和333.33ETH(共值100万美元)
Bob则建立了一个集中的仓位,只在1000到2250的价格范围内存款,他存入了91751DAI和61.17ETH,总价值约18.35万美元,他自己保留了另外的81.65万美元,按照自己的喜好进行投资。
此时Alice投入的资金是Bob的5.44倍,但只要ETH/DAI价格保持在1000到2250的区间内,则他们赚取的费用是一样的:
Bob的定制仓位也是他流动资金的一种止损,如果ETH价格跌至0美元,Alice和Bob的流动资金都将完全以ETH计价,然而Bob将只损失15.9万美元,而Alice则损失100万美元,Bob可以用他额外的816,500美元来对冲下行风险,或者投资于任何其他可以想象的策略。
Uniswap v3中LP不需要像v2中的LP那样以较少的资本提供同等的流动性深度,而是可以选择与v2 LP一样以相同的资本量提供更大的深度,这就需要承担更多的价格风险(无常损失),同时支持更多的交易量,赚取更高的费用.
较稳定的资金池中的LP可能会在特别狭窄的范围内提供流动性,如果目前在Uniswap v2 DAI/USDC对中持有的约2500万美元改成在v3中集中在0.99-1.01之间,只要价格保持在这个范围内,就能提供与Uniswap v2中50亿美元相同的深度,如果约2500万美元集中在0.999-1.001的范围内,它将提供与Uniswap v2中50亿美元相同的深度
下面的工具(https://uniswap.org/blog/uniswap-v3/)可以计算集中流动性头寸(以当前价格为中心)相对于在整个价格曲线上配置资本的资本效率收益:
在V3发布后,对于在0.10%的单一价格区间内提供流动性的LP来说,资本效率收益最高将达到4000倍,v3资金池工厂在技术上能够支持0.02%的颗粒度,相对于v2来说,最高可获得20000倍的资本效率收益,然而更多颗粒度的资金池会增加兑换时的Gas成本,因此在2层网络上可能更有用。
活跃的流动性
如果市场价格超出LP指定的价格范围,他们的流动性将被有效地从池中移除,并且不再赚取费用,在这种状态下,LP的流动性完全由两种资产中价值较低的资产组成,直到市场价格回到其指定的价格范围,在v3中,理论上可能在给定的价格范围内不存在流动性,但是理性LP会不断更新其价格范围以覆盖当前的市场价格:
范围订单机制
非常小的范围内的头寸的作用类似于限价单——如果超出范围,头寸将从完全由一种资产组成,转变为完全由另一种资产组成(加上应计费用),这个范围指令和传统的限价指令有两个区别:
-
一个仓位的范围有多窄是有限制的,当价格在这个范围内时,限价单可能会被部分执行
-
当仓位被越过时,它需要撤回,如果价格回穿该范围,则该头寸将被交易回,从而有效地逆转交易
假设用户手中有1个ETH,计划在价格上涨到4000美元时售出为USDC止盈,那么他应该这样操作:
-
选择在ETH/USDC资金池提供流动性,费率选择资金规模最大的0.3%
-
将做市价格范围的上限与下限都尽量保持在4000USDC附近
-
在下方Deposit Amounts处输入存入的金额1ETH
-
点击最下方的按钮,执行交易即可
之后当ETH的价格上涨到3999.8美元时,用户存入的1ETH头寸便会开始被兑换为USDC,当价格上涨超过4023.8美元时,用户的头寸将全部转换为USDC,如果用户即时的撤回流动性,那么便相当于通过Uniswap V3自动执行了一个价格大概等于4011.78美元的止盈卖单。
于此同时,需要注意的是在实际交易过程中,一笔交易可能会跨越不同的流动性阶段,所以合约需要维护每个用户提供流动性的价格边界,当价格达到边界时需要增加或移除用户对应的流动性,例如上方最右侧的示意图。
协议治理费用
Uniswap v3与Uniswap v2类似,也有协议费用,协议费用收取可以由UNI治理开启,在Uniswap v3中,UNI治理在选择进入协议的交易费用的比例方面具有更大的灵活性,并且可以选择任何比例1𝑁,其中4 ≤ 𝑁 ≤ 10或0。
UNI治理还可以添加额外的费用等级,当它添加一个新的费用等级时,它还可以定义与该费用等级对应的tickSpacing,一旦费用等级被添加到工厂,它就不能被删除(并且不能更改tickSpacing),支持的初始费用等级和刻度间距为0.05%(刻度间距为10,可初始化刻度之间约为0.10%)、0.30%(刻度间距为60,可初始化刻度之间约为0.60%)和1%(带有刻度间距为200,刻度之间约为2.02%),同时UNI治理有权将所有权转移到另一个地址
主要架构调整
Uniswap v3进行了许多架构更改,其中一些是集中流动性所必需的,而其中一些是独立改进,下面进行逐一介绍:
Multiple Pools Per Pair
在Uniswap v1和v2中,每对代币对应一个流动性池,对所有交易统一收取0.30%的费用,虽然这个默认费用对许多代币都足够合理,但对于某些池(例如:两个稳定币之间的池)来说可能太高了,而对于其它池(例如:包含高度波动或很少交易的代币的池)来说可能太低了。
Uniswap v3为每对代币引入了多个池,每个池都有不同的交换费用,所有池都是由同一个工厂合约创建的, 工厂合约最初允许以三个费用等级创建矿池:0.05%、0.30%和1%,UNI治理可以启用额外的费用等级。
Non-Fungible Liquidity
a、非复合费用
早期版本中赚取的费用作为流动性不断存入池中,这意味着即使没有明确的存款,池中的流动性也会随着时间的推移而增长,并且费用收入会增加,在 Uniswap v3中,由于头寸的不可替代性,这不再可能,相反费用收入单独存储并作为支付费用的代币持有
b、移除原生流动性代币
在Uniswap v1和v2中,矿池合约也是ERC-20代币合约,其代币代表矿池中持有的流动性, 虽然这很方便,但它实际上与Uniswap v2的理念不符,即任何不需要在核心合约中的东西都应该在外围,并且一个"规范的"ERC-20实施不鼓励创建改进ERC-20代币,而ERC-20代币实施应该在外围,作为核心合约中单一流动性头寸的包装器。
Uniswap v3中所做的更改通过使完全可替代的流动性代币变得不可能来解决这个问题,由于自定义流动性提供功能,费用现在由池作为单独的代币收取和持有,而不是作为池中的流动性自动再投资。因此,在Uniswap v3中,矿池合约没有实现ERC-20标准,任何人都可以在外围创建ERC-20代币合约,使流动性头寸更具可互换性,但它必须有额外的逻辑来处理收取的费用的分配或再投资,或者任何人都可以创建一个外围合约,将个人流动性头寸(包括收取的费用)包装在ERC-721不可替代的代币中。
预言机的升级
Uniswap v2引入了时间加权平均价格(TWAP)预言机,这些预言机是DeFi基础设施的关键部分,并已集成到数十个项目中,包括Compound和Reflexer,v2预言机通过每秒存储Uniswap货币对价格的累计总和来工作,这些价格总和可以在一个时期的开始和结束时检查一次,以计算该时期的准确TWAP
Uniswap v3对TWAP预言机进行了重大改进,使其可以在链上调用中计算过去约9天内的任何最近TWAP,这是通过存储一组累积和来实现的,这一系列历史价格累加器使创建包括简单移动平均线(SMA)、指数移动平均线(EMA)、异常值过滤等在内的更高级预言机变得更加容易和便宜,有了这一重大改进,交易者保持预言机最新的gas成本相对于v2减少了约50%,在外部智能合约中计算TWAP的成本也便宜得多。
源码分析
Uniswap v3将合约分成了以下两个仓库:
-
uniswap-v3-core:https://github.com/Uniswap/uniswap-v3-core
-
uniswap-v3-periphery:https://github.com/Uniswap/uniswap-v3-periphery
Uniswap-v3-core
Tick
Tick合约包含用于管理报价过程和相关计算的函数,下面我们进行逐一分析:
首先声明一个info结构体,用于存储每个初始化后个人的trick信息,具体代码如下所示:
// info stored for each initialized individual tick
struct Info {
// the total position liquidity that references this tick
uint128 liquidityGross;
// amount of net liquidity added (subtracted) when tick is crossed from left to right (right to left),
int128 liquidityNet;
// fee growth per unit of liquidity on the _other_ side of this tick (relative to the current tick)
// only has relative meaning, not absolute — the value depends on when the tick is initialized
uint256 feeGrowthOutside0X128;
uint256 feeGrowthOutside1X128;
// the cumulative tick value on the other side of the tick
int56 tickCumulativeOutside;
// the seconds per unit of liquidity on the _other_ side of this tick (relative to the current tick)
// only has relative meaning, not absolute — the value depends on when the tick is initialized
uint160 secondsPerLiquidityOutsideX128;
// the seconds spent on the other side of the tick (relative to the current tick)
// only has relative meaning, not absolute — the value depends on when the tick is initialized
uint32 secondsOutside;
// true iff the tick is initialized, i.e. the value is exactly equivalent to the expression liquidityGross != 0
// these 8 bits are set to prevent fresh sstores when crossing newly initialized ticks
bool initialized;
}
tickSpacingToMaxLiquidityPerTick函数用于根据给定的tickSpacing得出每一个trick的最大流动性值:
/// @notice Derives max liquidity per tick from given tick spacing
/// @dev Executed within the pool constructor
/// @param tickSpacing The amount of required tick separation, realized in multiples of `tickSpacing`
/// e.g., a tickSpacing of 3 requires ticks to be initialized every 3rd tick i.e., ..., -6, -3, 0, 3, 6, ...
/// @return The max liquidity per tick
function tickSpacingToMaxLiquidityPerTick(int24 tickSpacing) internal pure returns (uint128) {
int24 minTick = (TickMath.MIN_TICK / tickSpacing) * tickSpacing;
int24 maxTick = (TickMath.MAX_TICK / tickSpacing) * tickSpacing;
uint24 numTicks = uint24((maxTick - minTick) / tickSpacing) + 1;
return type(uint128).max / numTicks;
}
getFeeGrowthInside函数用于检查费用增长的数据:
/// @notice Retrieves fee growth data
/// @param self The mapping containing all tick information for initialized ticks
/// @param tickLower The lower tick boundary of the position
/// @param tickUpper The upper tick boundary of the position
/// @param tickCurrent The current tick
/// @param feeGrowthGlobal0X128 The all-time global fee growth, per unit of liquidity, in token0
/// @param feeGrowthGlobal1X128 The all-time global fee growth, per unit of liquidity, in token1
/// @return feeGrowthInside0X128 The all-time fee growth in token0, per unit of liquidity, inside the position's tick boundaries
/// @return feeGrowthInside1X128 The all-time fee growth in token1, per unit of liquidity, inside the position's tick boundaries
function getFeeGrowthInside(
mapping(int24 => Tick.Info) storage self,
int24 tickLower,
int24 tickUpper,
int24 tickCurrent,
uint256 feeGrowthGlobal0X128,
uint256 feeGrowthGlobal1X128
) internal view returns (uint256 feeGrowthInside0X128, uint256 feeGrowthInside1X128) {
Info storage lower = self[tickLower];
Info storage upper = self[tickUpper];
// calculate fee growth below
uint256 feeGrowthBelow0X128;
uint256 feeGrowthBelow1X128;
if (tickCurrent >= tickLower) {
feeGrowthBelow0X128 = lower.feeGrowthOutside0X128;
feeGrowthBelow1X128 = lower.feeGrowthOutside1X128;
} else {
feeGrowthBelow0X128 = feeGrowthGlobal0X128 - lower.feeGrowthOutside0X128;
feeGrowthBelow1X128 = feeGrowthGlobal1X128 - lower.feeGrowthOutside1X128;
}
// calculate fee growth above
uint256 feeGrowthAbove0X128;
uint256 feeGrowthAbove1X128;
if (tickCurrent < tickUpper) {
feeGrowthAbove0X128 = upper.feeGrowthOutside0X128;
feeGrowthAbove1X128 = upper.feeGrowthOutside1X128;
} else {
feeGrowthAbove0X128 = feeGrowthGlobal0X128 - upper.feeGrowthOutside0X128;
feeGrowthAbove1X128 = feeGrowthGlobal1X128 - upper.feeGrowthOutside1X128;
}
feeGrowthInside0X128 = feeGrowthGlobal0X128 - feeGrowthBelow0X128 - feeGrowthAbove0X128;
feeGrowthInside1X128 = feeGrowthGlobal1X128 - feeGrowthBelow1X128 - feeGrowthAbove1X128;
}
update函数用于更新trick,每当trick从初始化转为未初始化时需要更新trick并返回真,反之亦然
/// @notice Updates a tick and returns true if the tick was flipped from initialized to uninitialized, or vice versa
/// @param self The mapping containing all tick information for initialized ticks
/// @param tick The tick that will be updated
/// @param tickCurrent The current tick
/// @param liquidityDelta A new amount of liquidity to be added (subtracted) when tick is crossed from left to right (right to left)
/// @param feeGrowthGlobal0X128 The all-time global fee growth, per unit of liquidity, in token0
/// @param feeGrowthGlobal1X128 The all-time global fee growth, per unit of liquidity, in token1
/// @param secondsPerLiquidityCumulativeX128 The all-time seconds per max(1, liquidity) of the pool
/// @param time The current block timestamp cast to a uint32
/// @param upper true for updating a position's upper tick, or false for updating a position's lower tick
/// @param maxLiquidity The maximum liquidity allocation for a single tick
/// @return flipped Whether the tick was flipped from initialized to uninitialized, or vice versa
function update(
mapping(int24 => Tick.Info) storage self,
int24 tick,
int24 tickCurrent,
int128 liquidityDelta,
uint256 feeGrowthGlobal0X128,
uint256 feeGrowthGlobal1X128,
uint160 secondsPerLiquidityCumulativeX128,
int56 tickCumulative,
uint32 time,
bool upper,
uint128 maxLiquidity
) internal returns (bool flipped) {
Tick.Info storage info = self[tick];
uint128 liquidityGrossBefore = info.liquidityGross;
uint128 liquidityGrossAfter = LiquidityMath.addDelta(liquidityGrossBefore, liquidityDelta);
require(liquidityGrossAfter <= maxLiquidity, 'LO');
flipped = (liquidityGrossAfter == 0) != (liquidityGrossBefore == 0);
if (liquidityGrossBefore == 0) {
// by convention, we assume that all growth before a tick was initialized happened _below_ the tick
if (tick <= tickCurrent) {
info.feeGrowthOutside0X128 = feeGrowthGlobal0X128;
info.feeGrowthOutside1X128 = feeGrowthGlobal1X128;
info.secondsPerLiquidityOutsideX128 = secondsPerLiquidityCumulativeX128;
info.tickCumulativeOutside = tickCumulative;
info.secondsOutside = time;
}
info.initialized = true;
}
info.liquidityGross = liquidityGrossAfter;
// when the lower (upper) tick is crossed left to right (right to left), liquidity must be added (removed)
info.liquidityNet = upper
? int256(info.liquidityNet).sub(liquidityDelta).toInt128()
: int256(info.liquidityNet).add(liquidityDelta).toInt128();
}
clear函数用于清除trick数据
/// @notice Clears tick data
/// @param self The mapping containing all initialized tick information for initialized ticks
/// @param tick The tick that will be cleared
function clear(mapping(int24 => Tick.Info) storage self, int24 tick) internal {
delete self[tick];
}
cross函数用于根据价格变动的需要转换到下一个trick
/// @notice Transitions to next tick as needed by price movement
/// @param self The mapping containing all tick information for initialized ticks
/// @param tick The destination tick of the transition
/// @param feeGrowthGlobal0X128 The all-time global fee growth, per unit of liquidity, in token0
/// @param feeGrowthGlobal1X128 The all-time global fee growth, per unit of liquidity, in token1
/// @param secondsPerLiquidityCumulativeX128 The current seconds per liquidity
/// @param time The current block.timestamp
/// @return liquidityNet The amount of liquidity added (subtracted) when tick is crossed from left to right (right to left)
function cross(
mapping(int24 => Tick.Info) storage self,
int24 tick,
uint256 feeGrowthGlobal0X128,
uint256 feeGrowthGlobal1X128,
uint160 secondsPerLiquidityCumulativeX128,
int56 tickCumulative,
uint32 time
) internal returns (int128 liquidityNet) {
Tick.Info storage info = self[tick];
info.feeGrowthOutside0X128 = feeGrowthGlobal0X128 - info.feeGrowthOutside0X128;
info.feeGrowthOutside1X128 = feeGrowthGlobal1X128 - info.feeGrowthOutside1X128;
info.secondsPerLiquidityOutsideX128 = secondsPerLiquidityCumulativeX128 - info.secondsPerLiquidityOutsideX128;
info.tickCumulativeOutside = tickCumulative - info.tickCumulativeOutside;
info.secondsOutside = time - info.secondsOutside;
liquidityNet = info.liquidityNet;
}
Oracle
Uniswap v3的Oracle默认会存储一个最近价格的累计值,同时可以根据需要扩展为最近N个历史价格的时间累计值,最多支持65535个最近历史价格信息,同时Oracle还记录了对应流动性的时间累计值,因为v3中相同交易对在不同费率的交易池中各不相同,所以在后续使用Oracle时可以选择流动性较大的池最为价格参考来源,在整个Uniswap V3中,使用到预言机的情况主要有下面几个场景:
-
初始化交易池时需要初始化Oracle,此时Oracle中只有一个槽位,即只保存最近的一份数据
-
发生交易时价格会变动,此时需要更新Oracle来获取最新的价格
合约开头首先使用了一个结构体Observation来存储Oracle数据:
struct Observation {
// the block timestamp of the observation
uint32 blockTimestamp;
// the tick accumulator, i.e. tick * time elapsed since the pool was first initialized
int56 tickCumulative; // tick index的时间加权累积值
// the seconds per liquidity, i.e. seconds elapsed / max(1, liquidity) since the pool was first initialized
uint160 secondsPerLiquidityCumulativeX128; //每个流动性的秒数,即自第一次初始化池以来经过的秒数/max(1,流动性)
// whether or not the observation is initialized
bool initialized; //是否初始化
}
initialize函数用于对Oracle进行初始化操作,此时返回的当前Oracle的个数和最大可用个数皆为1,相关参数如下:
-
self:存储Oracle条目的数组
-
time:Oracle初始化的时间,将block.timestamp截断成uint32进行记录
-
cardinality:Oracle数组中填充元素的数量
-
cardinalityNext:Oracle数组的新长度
/// @notice Initialize the oracle array by writing the first slot. Called once for the lifecycle of the observations array
/// @param self The stored oracle array
/// @param time The time of the oracle initialization, via block.timestamp truncated to uint32
/// @return cardinality The number of populated elements in the oracle array
/// @return cardinalityNext The new length of the oracle array, independent of population
function initialize(Observation[65535] storage self, uint32 time)
internal
returns (uint16 cardinality, uint16 cardinalityNext)
{
self[0] = Observation({
blockTimestamp: time,
tickCumulative: 0,
secondsPerLiquidityCumulativeX128: 0,
initialized: true
});
return (1, 1);
}
write函数主要用于写Oracle数据,在这里首先获取当前的Oracle数据,之后检查当下时间戳是否与最新的时间戳一致,如果一致,则直接返回索引以及对应的cardinality,因为在同一区块内,只会在第一笔交易中写入Oracle数据,之后检查是否需要新数组空间,并更新写入的索引,写入Oracle数据:
/// @notice Writes an oracle observation to the array
/// @dev Writable at most once per block. Index represents the most recently written element. cardinality and index must be tracked externally.
/// If the index is at the end of the allowable array length (according to cardinality), and the next cardinality
/// is greater than the current one, cardinality may be increased. This restriction is created to preserve ordering.
/// @param self The stored oracle array
/// @param index The index of the observation that was most recently written to the observations array
/// @param blockTimestamp The timestamp of the new observation
/// @param tick The active tick at the time of the new observation
/// @param liquidity The total in-range liquidity at the time of the new observation
/// @param cardinality The number of populated elements in the oracle array
/// @param cardinalityNext The new length of the oracle array, independent of population
/// @return indexUpdated The new index of the most recently written element in the oracle array
/// @return cardinalityUpdated The new cardinality of the oracle array
function write(
Observation[65535] storage self,
uint16 index,
uint32 blockTimestamp,
int24 tick,
uint128 liquidity,
uint16 cardinality,
uint16 cardinalityNext
) internal returns (uint16 indexUpdated, uint16 cardinalityUpdated) {
Observation memory last = self[index];
// early return if we've already written an observation this block
if (last.blockTimestamp == blockTimestamp) return (index, cardinality);
// if the conditions are right, we can bump the cardinality
if (cardinalityNext > cardinality && index == (cardinality - 1)) {
cardinalityUpdated = cardinalityNext;
} else {
cardinalityUpdated = cardinality;
}
indexUpdated = (index + 1) % cardinalityUpdated;
self[indexUpdated] = transform(last, blockTimestamp, tick, liquidity);
}
grow函数的作用是扩展Oracle数组以储存更多下一个observations:
/// @notice Prepares the oracle array to store up to `next` observations
/// @param self The stored oracle array
/// @param current The current next cardinality of the oracle array
/// @param next The proposed next cardinality which will be populated in the oracle array
/// @return next The next cardinality which will be populated in the oracle array
function grow(
Observation[65535] storage self,
uint16 current,
uint16 next
) internal returns (uint16) {
require(current > 0, 'I');
// no-op if the passed next value isn't greater than the current next value
if (next <= current) return current;
// store in each slot to prevent fresh SSTOREs in swaps
// this data will not be used because the initialized boolean is still false
for (uint16 i = current; i < next; i++) self[i].blockTimestamp = 1;
return next;
}
lte函数的作用是比较32位的timestamp:
/// @notice comparator for 32-bit timestamps
/// @dev safe for 0 or 1 overflows, a and b _must_ be chronologically before or equal to time
/// @param time A timestamp truncated to 32 bits
/// @param a A comparison timestamp from which to determine the relative position of `time`
/// @param b From which to determine the relative position of `time`
/// @return bool Whether `a` is chronologically <= `b`
function lte(
uint32 time,
uint32 a,
uint32 b
) private pure returns (bool) {
// if there hasn't been overflow, no need to adjust
if (a <= time && b <= time) return a <= b;
uint256 aAdjusted = a > time ? a : a + 2**32;
uint256 bAdjusted = b > time ? b : b + 2**32;
return aAdjusted <= bAdjusted;
}
binarySearch主要实现二分法查找数据:
/// @notice Fetches the observations beforeOrAt and atOrAfter a target, i.e. where [beforeOrAt, atOrAfter] is satisfied.
/// The result may be the same observation, or adjacent observations.
/// @dev The answer must be contained in the array, used when the target is located within the stored observation
/// boundaries: older than the most recent observation and younger, or the same age as, the oldest observation
/// @param self The stored oracle array
/// @param time The current block.timestamp
/// @param target The timestamp at which the reserved observation should be for
/// @param index The index of the observation that was most recently written to the observations array
/// @param cardinality The number of populated elements in the oracle array
/// @return beforeOrAt The observation recorded before, or at, the target
/// @return atOrAfter The observation recorded at, or after, the target
function binarySearch(
Observation[65535] storage self,
uint32 time,
uint32 target,
uint16 index,
uint16 cardinality
) private view returns (Observation memory beforeOrAt, Observation memory atOrAfter) {
uint256 l = (index + 1) % cardinality; // oldest observation
uint256 r = l + cardinality - 1; // newest observation
uint256 i;
while (true) {
i = (l + r) / 2;
beforeOrAt = self[i % cardinality];
// we've landed on an uninitialized tick, keep searching higher (more recently)
if (!beforeOrAt.initialized) {
l = i + 1;
continue;
}
atOrAfter = self[(i + 1) % cardinality];
bool targetAtOrAfter = lte(time, beforeOrAt.blockTimestamp, target);
// check if we've found the answer!
if (targetAtOrAfter && lte(time, target, atOrAfter.blockTimestamp)) break;
if (!targetAtOrAfter) r = i - 1;
else l = i + 1;
}
}
getSurroundingObservations函数的作用是在已记录的Oracle数组中检索与目标时间戳最近的两个,该函数实现代码如下所示,在这里首先将beforeOrAt设置为当前最新的数据,之后检查beforeOrAt是否<=target,如果满足条件则进一步检查时间戳是否相等,如果相等则直接忽略atOrAfter并返回,如果不相等则将当前还未持久化的数据,封装成一个Oracle数据返回,如果beforeOrAt>target则将beforeOrAt调整为当前index的下一个数据,或者index为0的数据,最后通过二分查找的方式找到离目标时间点最近的前后两个Oracle数据并返回:
/// @notice Fetches the observations beforeOrAt and atOrAfter a given target, i.e. where [beforeOrAt, atOrAfter] is satisfied
/// @dev Assumes there is at least 1 initialized observation.
/// Used by observeSingle() to compute the counterfactual accumulator values as of a given block timestamp.
/// @param self The stored oracle array
/// @param time The current block.timestamp
/// @param target The timestamp at which the reserved observation should be for
/// @param tick The active tick at the time of the returned or simulated observation
/// @param index The index of the observation that was most recently written to the observations array
/// @param liquidity The total pool liquidity at the time of the call
/// @param cardinality The number of populated elements in the oracle array
/// @return beforeOrAt The observation which occurred at, or before, the given timestamp
/// @return atOrAfter The observation which occurred at, or after, the given timestamp
function getSurroundingObservations(
Observation[65535] storage self,
uint32 time,
uint32 target,
int24 tick,
uint16 index,
uint128 liquidity,
uint16 cardinality
) private view returns (Observation memory beforeOrAt, Observation memory atOrAfter) {
// optimistically set before to the newest observation
beforeOrAt = self[index];
// if the target is chronologically at or after the newest observation, we can early return
if (lte(time, beforeOrAt.blockTimestamp, target)) {
if (beforeOrAt.blockTimestamp == target) {
// if newest observation equals target, we're in the same block, so we can ignore atOrAfter
return (beforeOrAt, atOrAfter);
} else {
// otherwise, we need to transform
return (beforeOrAt, transform(beforeOrAt, target, tick, liquidity));
}
}
// now, set before to the oldest observation
beforeOrAt = self[(index + 1) % cardinality];
if (!beforeOrAt.initialized) beforeOrAt = self[0];
// ensure that the target is chronologically at or after the oldest observation
require(lte(time, beforeOrAt.blockTimestamp, target), 'OLD');
// if we've reached this point, we have to binary search
return binarySearch(self, time, target, index, cardinality);
}
observeSingle函数的主要用于获取请求时间点的Oracle数据,函数开头首先会检查secondsAgo是否为0,如果为0则表示当前的最新Oracle数据,如果不为零则首先计算需要请求的Oracle的时间戳范围,之后通过getSurroundingObservations来获取与请求时间戳最近的两个Oracle数据,如果请求时间和返回的左侧时间戳一致,则直接采用左侧时间,如果和右侧一致,则直接采用右侧数据,如果请求时间介于相邻的左侧与右侧时间戳范围之内,则计算请求时间点的增长率并将其作为请求时间点的Oracle数据值返回:
/// @dev Reverts if an observation at or before the desired observation timestamp does not exist.
/// 0 may be passed as `secondsAgo' to return the current cumulative values.
/// If called with a timestamp falling between two observations, returns the counterfactual accumulator values
/// at exactly the timestamp between the two observations.
/// @param self The stored oracle array
/// @param time The current block timestamp
/// @param secondsAgo The amount of time to look back, in seconds, at which point to return an observation
/// @param tick The current tick
/// @param index The index of the observation that was most recently written to the observations array
/// @param liquidity The current in-range pool liquidity
/// @param cardinality The number of populated elements in the oracle array
/// @return tickCumulative The tick * time elapsed since the pool was first initialized, as of `secondsAgo`
/// @return secondsPerLiquidityCumulativeX128 The time elapsed / max(1, liquidity) since the pool was first initialized, as of `secondsAgo`
function observeSingle(
Observation[65535] storage self,
uint32 time,
uint32 secondsAgo,
int24 tick,
uint16 index,
uint128 liquidity,
uint16 cardinality
) internal view returns (int56 tickCumulative, uint160 secondsPerLiquidityCumulativeX128) {
if (secondsAgo == 0) {
Observation memory last = self[index];
if (last.blockTimestamp != time) last = transform(last, time, tick, liquidity);
return (last.tickCumulative, last.secondsPerLiquidityCumulativeX128);
}
uint32 target = time - secondsAgo;
(Observation memory beforeOrAt, Observation memory atOrAfter) =
getSurroundingObservations(self, time, target, tick, index, liquidity, cardinality);
if (target == beforeOrAt.blockTimestamp) {
// we're at the left boundary
return (beforeOrAt.tickCumulative, beforeOrAt.secondsPerLiquidityCumulativeX128);
} else if (target == atOrAfter.blockTimestamp) {
// we're at the right boundary
return (atOrAfter.tickCumulative, atOrAfter.secondsPerLiquidityCumulativeX128);
} else {
// we're in the middle
uint32 observationTimeDelta = atOrAfter.blockTimestamp - beforeOrAt.blockTimestamp;
uint32 targetDelta = target - beforeOrAt.blockTimestamp;
return (
beforeOrAt.tickCumulative +
((atOrAfter.tickCumulative - beforeOrAt.tickCumulative) / observationTimeDelta) *
targetDelta,
beforeOrAt.secondsPerLiquidityCumulativeX128 +
uint160(
(uint256(
atOrAfter.secondsPerLiquidityCumulativeX128 - beforeOrAt.secondsPerLiquidityCumulativeX128
) * targetDelta) / observationTimeDelta
)
);
}
}
observe函数用于请求前N秒之前的历史数据,这里的参数secondsAgos为一个动态数组,故而可以一次请求多个历史数据,返回变量tickCumulatives和 liquidityCumulatives也是动态数组,用于记录请求参数中对应时间戳的tick index累积值和流动性累积值,之后调用observations.observe()处理数据:
/// @notice Returns the accumulator values as of each time seconds ago from the given time in the array of `secondsAgos`
/// @dev Reverts if `secondsAgos` > oldest observation
/// @param self The stored oracle array
/// @param time The current block.timestamp
/// @param secondsAgos Each amount of time to look back, in seconds, at which point to return an observation
/// @param tick The current tick
/// @param index The index of the observation that was most recently written to the observations array
/// @param liquidity The current in-range pool liquidity
/// @param cardinality The number of populated elements in the oracle array
/// @return tickCumulatives The tick * time elapsed since the pool was first initialized, as of each `secondsAgo`
/// @return secondsPerLiquidityCumulativeX128s The cumulative seconds / max(1, liquidity) since the pool was first initialized, as of each `secondsAgo`
function observe(
Observation[65535] storage self,
uint32 time,
uint32[] memory secondsAgos,
int24 tick,
uint16 index,
uint128 liquidity,
uint16 cardinality
) internal view returns (int56[] memory tickCumulatives, uint160[] memory secondsPerLiquidityCumulativeX128s) {
require(cardinality > 0, 'I');
tickCumulatives = new int56[](secondsAgos.length);
secondsPerLiquidityCumulativeX128s = new uint160[](secondsAgos.length);
for (uint256 i = 0; i < secondsAgos.length; i++) {
(tickCumulatives[i], secondsPerLiquidityCumulativeX128s[i]) = observeSingle(
self,
time,
secondsAgos[i],
tick,
index,
liquidity,
cardinality
);
}
}
原文始发于微信公众号(七芒星实验室):UniSwap V3协议浅析(上)
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论