uniswap
solidity
oracle
uniswap-v3
本合约定义预言机相关方法。
在交易对合约创建时,默认初始化长度为1的数组用于存储观测点数据;任何用户都可以调用UniswapV3Pool.sol
的increaseObservationCardinalityNext方法扩展预言机的观测点空间,但需要承担扩展空间带来的手续费。
一个预言机观测点包括以下内容:
blockTimestamp
:该观测点写入时的时间tickCumulative
:截止该观测点时间的累计tick
secondsPerLiquidityCumulativeX128
:截止该观测点的累计每流动性持续时间initialized
:观测点是否初始化观测点结构定义如下:
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;
// the seconds per liquidity, i.e. seconds elapsed / max(1, liquidity) since the pool was first initialized
uint160 secondsPerLiquidityCumulativeX128;
// whether or not the observation is initialized
bool initialized;
}
本合约包含以下方法:
基于上一个观测点,返回临时观测点对象,但是不写入观测点。
function transform(
Observation memory last,
uint32 blockTimestamp,
int24 tick,
uint128 liquidity
) private pure returns (Observation memory) {
uint32 delta = blockTimestamp - last.blockTimestamp;
return
Observation({
blockTimestamp: blockTimestamp,
tickCumulative: last.tickCumulative + int56(tick) * delta,
secondsPerLiquidityCumulativeX128: last.secondsPerLiquidityCumulativeX128 +
((uint160(delta) << 128) / (liquidity > 0 ? liquidity : 1)),
initialized: true
});
}
首先,计算当前时间与上一个观测点的时间差:delta = blockTimestamp - last.blockTimestamp
。
接着主要计算tickCumulative
和secondsPerLiquidityCumulativeX128
,
其中:tickCumulative: last.tickCumulative + int56(tick) * delta
。
根据白皮书公式5.3-5.5:
\[ \log_{1.0001}(P_{t_1,t_2}) = \frac{\sum^{t_2}_{i=t_1} \log_{1.0001}(P_i)}{t_2 - t_1} \tag{5.3} \]
\[ \log_{1.0001}(P_{t_1,t_2}) = \frac{a_{t_2} - a_{t_1}}{t_2 - t_1} \tag{5.4} \]
\[ P_{t_1,t_2} = 1.0001^{\frac{a_{t_2} - a_{t_1}}{t_2 - t_1}} \tag{5.5} \]
这里保存的tickCumulative
即为\(a_{t_n}\),其对应的公式为:
\[ tickCumulative = \sum^{t_n}_{i=0} \log_{1.0001}(P_i) \]
同样,每流动性持续时间secondsPerLiquidityCumulative
为:
\[ secondsPerLiquidityCumulative = \sum^{n}_{i=0} \frac{t_i}{L_i} \]
初始化预言机存储空间,设置第一个slot
。
/// @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);
}
默认返回的cardinality
和cardinalityNext
都为1,即仅能存放一个观测点数据。
该方法只能被调用一次,实际上是在UniswapV3Pool.sol
的initialize方法中调用:
(uint16 cardinality, uint16 cardinalityNext) = observations.initialize(_blockTimestamp());
写入一次观测点数据。根据之前的描述,只有在UniswapV3Pool
发生mint
、burn
和swap
操作时,才可能触发写入操作。
可以将预言机观测点数组看作一个循环列表,可写入空间由cardinality
和cardinalityNext
决定;当数组空间写满以后,会继续从0位置开始覆盖写。
本方法的参数如下:
self
:预言机观测点数组index
:最后一次写入的观测点索引,从0开始blockTimestamp
:待添加观测点的时间tick
:待添加观测点的tick
liquidity
:待添加观测点时间的全局可用流动性cardinality
:观测点数组当前长度(可写入空间)cardinalityNext
:观测点数组(扩展后的)最新长度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;
}
cardinalityNext > cardinality
,则表示预言机数组被扩容过;如果index == (cardinality - 1)
即上一次写入的位置是最后一个观测点,则本次需要继续写入扩容后的空间,因此cardinalityUpdated
使用扩容后的数组长度cardinalityNext
;cardinality
,因为还未写满。 indexUpdated = (index + 1) % cardinalityUpdated;
更新本次写入观测点数组的索引indexUpdated
,% cardinalityUpdated
是为了计算循环写的索引。
self[indexUpdated] = transform(last, blockTimestamp, tick, liquidity);
调用transform方法计算最新的观测点数据,并写入观测点数组的indexUpdated
位置。
扩容观测点数组,增加可写入的观测点数量。由于合约默认只能保存1个观测点,为了能够支持更多观测点,任何用户都可以手动调用合约以扩容观测点数组。
注意,
grow
方法使用internal
修饰,用户实际上是通过UniswapV3Pool.sol
的increaseObservationCardinalityNext方法进行扩容。
扩容方法会触发SSTORE
操作,因此调用该方法的用户需要支付由此带来的gas开销。
/// @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;
}
比较两个时间戳的大小。
由于合约使用uint32
类型表示时间戳,其最大值\(2^{32}-1 = 4294967295\),对应的时间为February 7, 2106 6:28:15 AM GMT+00:00
,如果两个时间a
和b
(\(a < b\))正好位于uint32
最大值的两边,b
由于溢出,因此\(a > b\),直接比较数值会导致错误的结果,因此需要统一时间。
注:实际上每136年左右就会有溢出问题。
方法接受3个参数:time
、a
和b
;其中,time
为基准时间,a
和b
在逻辑(时间)上小于等于time
;方法返回一个bool
,表示a
是否在逻辑时间点上小于等于b
。
/// @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;
}
a
和b
都小于等于time
,则表示没有发生溢出,因此直接返回a <= b
a
和b
在时间线上均小于等于time
aAdjusted
为补齐2**32
后的时间a
bAdjusted
为补齐后的时间b
aAdjusted
和bAdjusted
因此,该方法在a
、b
与time
存在0到1个\(2^{32}\)的时间差时是溢出安全的。
不能跨越两个\(2^{32} - 1\)。
二分查找指定目标时间的观测点。
参数如下:
self
:观测点数组time
:当前区块时间target
:目标时间index
:最后一次写入的观测点索引cardinality
:观测点数组当前长度(可写入空间)/// @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
由于最后一次索引为index
,因此循环继续右移一位(并取模)即为左边界点l
(最老的索引,如果已写满一轮的话);右边界点为l + cardinality - 1
,注意,右边界点r
没有取模,因为在二分查找中右边节点一定不能小于左边节点。
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];
如果计算的中点未初始化(即此时数组空间未写满),则使用右半部分区间(往时间点更近的方向)继续进行二分查找。
atOrAfter
为右侧紧邻的观测点。
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;
}
}
target
位于beforeOrAt
与atOrAfter
时间之间,则退出二分查找,返回beforeOrAt
与atOrAfter
两个观测点beforeOrAt
时间大于目标时间target
,则继续在左半部分(往更小的时间)进行二分查找target
大于atOrAfter
时间,则继续往右半部分(往更大的时间)进行二分查找假设cardinality
为10,index
为5,我们可以画出几个变量的逻辑关系如下:
获取目标时间target
的观测点数据beforeOrAt
和atOrAfter
,满足target
位于[beforeOrAt, atOrAfter]
之间。
/// @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));
}
}
首先将beforeOrAt
设置为最近一次的观测点:
如果beforeOrAt
时间小于等于目标时间target
:
beforeOrAt
,忽略atOrAfter
beforeOrAt
和target
临时生成一个观测点作为atOrAfter
// now, set before to the oldest observation
beforeOrAt = self[(index + 1) % cardinality];
if (!beforeOrAt.initialized) beforeOrAt = self[0];
否则,可以确认最后(最晚)一个观测点的时间大于target
。
设置beforeOrAt
为循环右移一个位置的观测点,即最老的观测点;如果该观测点未初始化,则表示数组没有写满,因此最早的一定是索引为0的观测点。
// ensure that the target is chronologically at or after the oldest observation
require(lte(time, beforeOrAt.blockTimestamp, target), 'OLD');
确认target
时间大于等于最早的观测点时间,因此此时target
一定位于最早的和最晚观测点的时间区间内,可以使用binarySearch进行二分查找。
// if we've reached this point, we have to binary search
return binarySearch(self, time, target, index, cardinality);
}
获取指定时间的观测点数据。
方法的参数如下:
self
:预言机观测点数组time
:当前区块时间secondsAgo
:距离当前时间多少秒以前的指定时间,根据该时间寻找观测点tick
:目标时间的tick
(价格)index
:最后一次写入的观测点索引liquidity
:当前全局可用流动性cardinality
:预言机数组当前长度(可写入空间)返回:
tickCumulative
:从交易对池子创建以来,截止到secondsAgo
的累计tick
secondsPerLiquidityCumulativeX128
:从交易对池子创建以来,截止到secondsAgo
的累计每流动性持续时间/// @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);
}
如果secondsAgo == 0
,表示取当前时间的观测点。如果当前时间不等于最后一次写入的观测点时间,则使用transform生成一个临时观测点(注意,这里没有写入该观测点),并返回相关数据。
uint32 target = time - secondsAgo;
(Observation memory beforeOrAt, Observation memory atOrAfter) =
getSurroundingObservations(self, time, target, tick, index, liquidity, cardinality);
根据secondsAgo
计算目标时间target
,使用getSurroundingObservations方法寻找距离目标时间最近的观测点边界beforeOrAt
和atOrAfter
。
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
)
);
}
根据getSurroundingObservations方法,需要优先使用左边界点beforeOrAt
:
beforeOrAt
时间,则直接返回该观测点的相关数据atOrAfter
时间,则也返回相关数据beforeOrAt
和atOrAfter
之间,则需要根据时间比例计算相关值:
observationTimeDelta
为beforeOrAt
和atOrAfter
的时间差值(下面用\(\Delta{t}\)表示),targetDelta
为beforeOrAt
和target
的时间差值target
的值应为:\(\frac{\Delta{tickCumulative}}{\Delta{t}} \cdot targetDelta\)target
的值应为:\(\frac{\Delta{secondsPerLiquidityCumulativeX128}}{\Delta{t}} \cdot targetDelta\)批量获取指定时间的观测点数据。
该方法主要调用observeSingle获取单个指定时间的观测点数据,然后批量返回。
/// @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
);
}
}