// SPDX-License-Identifier: BUSL-1.1 pragma solidity ^0.8.4; library Oracle { struct Observation { uint32 timestamp; // sigma (point_i * time_i - time_(i-1)) int56 accPoint; // true if this observation is inited bool init; } /// @notice Record a new observation with a circular queue. /// @param last the specified observation to be updated /// @param timestamp the timestamp of the new observation, > last.timestamp /// @param currentPoint log 1.0001 of price /// @return observation generated function newObservation( Observation memory last, uint32 timestamp, int24 currentPoint ) private pure returns (Observation memory) { uint56 delta = uint56(timestamp - last.timestamp); return Observation({ timestamp: timestamp, accPoint: last.accPoint + int56(currentPoint) * int56(delta), init: true }); } function init(Observation[65535] storage self, uint32 timestamp) internal returns (uint16 queueLen, uint16 nextQueueLen) { self[0] = Observation({ timestamp: timestamp, accPoint: 0, init: true }); return (1, 1); } /// @notice Append a price oracle observation data in the pool /// @param self circular-queue of observation data in array form /// @param currentIndex the index of the last observation in the array /// @param timestamp timestamp of new observation /// @param currentPoint current point of new observation (usually we append the point value just-before exchange) /// @param queueLen max-length of circular queue /// @param nextQueueLen next max-length of circular queue, if length of queue increase over queueLen, queueLen will become nextQueueLen /// @return newIndex index of new observation /// @return newQueueLen queueLen value after appending function append( Observation[65535] storage self, uint16 currentIndex, uint32 timestamp, int24 currentPoint, uint16 queueLen, uint16 nextQueueLen ) internal returns (uint16 newIndex, uint16 newQueueLen) { Observation memory last = self[currentIndex]; if (last.timestamp == timestamp) return (currentIndex, queueLen); // if the conditions are right, we can bump the cardinality if (nextQueueLen > queueLen && currentIndex == (queueLen - 1)) { newQueueLen = nextQueueLen; } else { newQueueLen = queueLen; } newIndex = (currentIndex + 1) % newQueueLen; self[newIndex] = newObservation(last, timestamp, currentPoint); } /// @notice Expand the max-length of observation queue /// @param queueLen current max-length of queue /// @param nextQueueLen next max-length /// @return next max-length function expand( Observation[65535] storage self, uint16 queueLen, uint16 nextQueueLen ) internal returns (uint16) { require(queueLen > 0, 'LEN'); if (nextQueueLen <= queueLen) return queueLen; for (uint16 i = queueLen; i < nextQueueLen; i++) self[i].timestamp = 1; return nextQueueLen; } function lte( uint32 time, uint32 a, uint32 b ) private pure returns (bool) { 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; } /// @notice Binary search to find two neighbor observations for a target timestamp /// @param self observation queue in array form /// @param timestamp timestamp of current block /// @param targetTimestamp target time stamp /// @param currentIdx The index of the last observation in the array /// @param queueLen current max-length of queue /// @return beforeNeighbor before-or-at observation neighbor to target timestamp /// @return afterNeighbor after-or-at observation neighbor to target timestamp function findNeighbor( Observation[65535] storage self, uint32 timestamp, uint32 targetTimestamp, uint16 currentIdx, uint16 queueLen ) private view returns (Observation memory beforeNeighbor, Observation memory afterNeighbor) { uint256 l = (currentIdx + 1) % queueLen; // oldest observation uint256 r = l + queueLen - 1; // newest observation uint256 i; while (true) { i = (l + r) / 2; beforeNeighbor = self[i % queueLen]; if (!beforeNeighbor.init) { l = i + 1; continue; } afterNeighbor = self[(i + 1) % queueLen]; bool leftLessOrEq = lte(timestamp, beforeNeighbor.timestamp, targetTimestamp); if (leftLessOrEq && lte(timestamp, targetTimestamp, afterNeighbor.timestamp)) break; if (!leftLessOrEq) r = i - 1; else l = i + 1; } } /// @notice Find two neighbor observations for a target timestamp /// @param self observation queue in array form /// @param timestamp timestamp of current block /// @param targetTimestamp target time stamp /// @param currentPoint current point of swap /// @param currentIndex the index of the last observation in the array /// @param queueLen current max-length of queue /// @return beforeNeighbor before-or-at observation neighbor to target timestamp /// @return afterNeighbor after-or-at observation neighbor to target timestamp, if the targetTimestamp is later than last observation in queue, /// the afterNeighbor observation does not exist in the queue function getTwoNeighborObservation( Observation[65535] storage self, uint32 timestamp, uint32 targetTimestamp, int24 currentPoint, uint16 currentIndex, uint16 queueLen ) private view returns (Observation memory beforeNeighbor, Observation memory afterNeighbor) { beforeNeighbor = self[currentIndex]; if (lte(timestamp, beforeNeighbor.timestamp, targetTimestamp)) { if (beforeNeighbor.timestamp == targetTimestamp) { return (beforeNeighbor, beforeNeighbor); } else { return (beforeNeighbor, newObservation(beforeNeighbor, targetTimestamp, currentPoint)); } } beforeNeighbor = self[(currentIndex + 1) % queueLen]; if (!beforeNeighbor.init) beforeNeighbor = self[0]; require(lte(timestamp, beforeNeighbor.timestamp, targetTimestamp), 'OLD'); return findNeighbor(self, timestamp, targetTimestamp, currentIndex, queueLen); } /// @dev Revert if secondsAgo too large. /// @param self the observation circular queue in array form /// @param timestamp the current block timestamp /// @param secondsAgo target timestamp is timestamp-secondsAg, 0 to return the current cumulative values. /// @param currentPoint the current point of pool /// @param currentIndex the index of the last observation in the array /// @param queueLen max-length of circular queue /// @return accPoint integral value of point(time) from 0 to each timestamp function observeSingle( Observation[65535] storage self, uint32 timestamp, uint32 secondsAgo, int24 currentPoint, uint16 currentIndex, uint16 queueLen ) internal view returns (int56 accPoint ) { if (secondsAgo == 0) { Observation memory last = self[currentIndex]; if (last.timestamp != timestamp) last = newObservation(last, timestamp, currentPoint); return last.accPoint; } uint32 targetTimestamp = timestamp - secondsAgo; (Observation memory beforeNeighbor, Observation memory afterNeighbor) = getTwoNeighborObservation(self, timestamp, targetTimestamp, currentPoint, currentIndex, queueLen); if (targetTimestamp == beforeNeighbor.timestamp) { // we're at the left boundary return beforeNeighbor.accPoint; } else if (targetTimestamp == afterNeighbor.timestamp) { // we're at the right boundary return afterNeighbor.accPoint; } else { // we're in the middle uint56 leftRightTimeDelta = afterNeighbor.timestamp - beforeNeighbor.timestamp; uint56 targetTimeDelta = targetTimestamp - beforeNeighbor.timestamp; return beforeNeighbor.accPoint + (afterNeighbor.accPoint - beforeNeighbor.accPoint) / int56(leftRightTimeDelta) * int56(targetTimeDelta); } } /// @notice Returns the integral value of point with time /// @dev Reverts if target timestamp is early than oldest observation in the queue /// @dev If you call this method with secondsAgos = [3600, 0]. the average point of this pool during recent hour is (accPoints[1] - accPoints[0]) / 3600 /// @param self the observation circular queue in array form /// @param timestamp the current block timestamp /// @param secondsAgos describe the target timestamp , targetTimestimp[i] = block.timestamp - secondsAgo[i] /// @param currentPoint the current point of pool /// @param currentIndex the index of the last observation in the array /// @param queueLen max-length of circular queue /// @return accPoints integral value of point(time) from 0 to each timestamp function observe( Observation[65535] storage self, uint32 timestamp, uint32[] memory secondsAgos, int24 currentPoint, uint16 currentIndex, uint16 queueLen ) internal view returns (int56[] memory accPoints ) { require(queueLen > 0, 'I'); accPoints = new int56[](secondsAgos.length); for (uint256 i = 0; i < secondsAgos.length; i++) { accPoints[i] = observeSingle( self, timestamp, secondsAgos[i], currentPoint, currentIndex, queueLen ); } } }