Skip to content

Commit

Permalink
Merge pull request #11 from perpetual-protocol/feature/apply-cumuulat…
Browse files Browse the repository at this point in the history
…ive-twap-to-Chainlink-priceFeed

Feature/apply cumuulative twap to chainlink price feed
  • Loading branch information
KimiWu123 authored Mar 2, 2022
2 parents 29554b9 + e0dc3af commit 8a7d44d
Show file tree
Hide file tree
Showing 5 changed files with 221 additions and 331 deletions.
173 changes: 7 additions & 166 deletions contracts/BandPriceFeed.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,37 +6,18 @@ import { Address } from "@openzeppelin/contracts/utils/Address.sol";
import { BlockContext } from "./base/BlockContext.sol";
import { IPriceFeed } from "./interface/IPriceFeed.sol";
import { IStdReference } from "./interface/bandProtocol/IStdReference.sol";
import { CumulativeTwap } from "./CumulativeTwap.sol";

contract BandPriceFeed is IPriceFeed, BlockContext {
contract BandPriceFeed is IPriceFeed, BlockContext, CumulativeTwap {
using Address for address;

//
// STRUCT
//
struct Observation {
uint256 price;
uint256 priceCumulative;
uint256 timestamp;
}

//
// EVENT
//

event PriceUpdated(string indexed baseAsset, uint256 price, uint256 timestamp, uint8 indexAt);

//
// STATE
//
string public constant QUOTE_ASSET = "USD";

string public baseAsset;
// let's use 15 mins and 1 hr twap as example
// if the price is being updated 15 secs, then needs 60 and 240 historical data for 15mins and 1hr twap.
Observation[256] public observations;

IStdReference public stdRef;
uint8 public currentObservationIndex;

//
// EXTERNAL NON-VIEW
Expand All @@ -52,91 +33,21 @@ contract BandPriceFeed is IPriceFeed, BlockContext {

/// @dev anyone can help update it.
function update() external {
IStdReference.ReferenceData memory bandData = getReferenceData();

// for the first time update
if (currentObservationIndex == 0 && observations[0].timestamp == 0) {
observations[0] = Observation({
price: bandData.rate,
priceCumulative: 0,
timestamp: bandData.lastUpdatedBase
});
emit PriceUpdated(baseAsset, bandData.rate, bandData.lastUpdatedBase, 0);
return;
}

// BPF_IT: invalid timestamp
Observation memory lastObservation = observations[currentObservationIndex];
require(bandData.lastUpdatedBase > lastObservation.timestamp, "BPF_IT");
IStdReference.ReferenceData memory bandData = _getReferenceData();

// overflow of currentObservationIndex is desired since currentObservationIndex is uint8 (0 - 255),
// so 255 + 1 will be 0
currentObservationIndex++;

uint256 elapsedTime = bandData.lastUpdatedBase - lastObservation.timestamp;
observations[currentObservationIndex] = Observation({
priceCumulative: lastObservation.priceCumulative + (lastObservation.price * elapsedTime),
timestamp: bandData.lastUpdatedBase,
price: bandData.rate
});

emit PriceUpdated(baseAsset, bandData.rate, bandData.lastUpdatedBase, currentObservationIndex);
_update(bandData.rate, bandData.lastUpdatedBase);
}

//
// EXTERNAL VIEW
//

function getPrice(uint256 interval) public view override returns (uint256) {
Observation memory lastestObservation = observations[currentObservationIndex];
if (lastestObservation.price == 0) {
// BPF_ND: no data
revert("BPF_ND");
}

IStdReference.ReferenceData memory latestBandData = getReferenceData();
IStdReference.ReferenceData memory latestBandData = _getReferenceData();
if (interval == 0) {
return latestBandData.rate;
}

uint256 currentTimestamp = _blockTimestamp();
uint256 targetTimestamp = currentTimestamp - interval;
(Observation memory beforeOrAt, Observation memory atOrAfter) = getSurroundingObservations(targetTimestamp);
uint256 currentPriceCumulative =
lastestObservation.priceCumulative +
(lastestObservation.price * (latestBandData.lastUpdatedBase - lastestObservation.timestamp)) +
(latestBandData.rate * (currentTimestamp - latestBandData.lastUpdatedBase));

//
// beforeOrAt atOrAfter
// ------------------+-------------+---------------+------------------
// <-------| | |
// case 1 targetTimestamp | |------->
// case 2 | targetTimestamp
// case 3 targetTimestamp
//
uint256 targetPriceCumulative;
// case1. not enough historical data or just enough (`==` case)
if (targetTimestamp <= beforeOrAt.timestamp) {
targetTimestamp = beforeOrAt.timestamp;
targetPriceCumulative = beforeOrAt.priceCumulative;
}
// case2. the latest data is older than or equal the request
else if (atOrAfter.timestamp <= targetTimestamp) {
targetTimestamp = atOrAfter.timestamp;
targetPriceCumulative = atOrAfter.priceCumulative;
}
// case3. in the middle
else {
uint256 observationTimeDelta = atOrAfter.timestamp - beforeOrAt.timestamp;
uint256 targetTimeDelta = targetTimestamp - beforeOrAt.timestamp;
targetPriceCumulative =
beforeOrAt.priceCumulative +
((atOrAfter.priceCumulative - beforeOrAt.priceCumulative) * targetTimeDelta) /
observationTimeDelta;
}

return (currentPriceCumulative - targetPriceCumulative) / (currentTimestamp - targetTimestamp);
return _getPrice(interval, latestBandData.rate, latestBandData.lastUpdatedBase);
}

//
Expand All @@ -153,7 +64,7 @@ contract BandPriceFeed is IPriceFeed, BlockContext {
// INTERNAL VIEW
//

function getReferenceData() internal view returns (IStdReference.ReferenceData memory) {
function _getReferenceData() internal view returns (IStdReference.ReferenceData memory) {
IStdReference.ReferenceData memory bandData = stdRef.getReferenceData(baseAsset, QUOTE_ASSET);
// BPF_TQZ: timestamp for quote is zero
require(bandData.lastUpdatedQuote > 0, "BPF_TQZ");
Expand All @@ -164,74 +75,4 @@ contract BandPriceFeed is IPriceFeed, BlockContext {

return bandData;
}

function getSurroundingObservations(uint256 targetTimestamp)
internal
view
returns (Observation memory beforeOrAt, Observation memory atOrAfter)
{
uint8 index = currentObservationIndex;
uint8 beforeOrAtIndex;
uint8 atOrAfterIndex;

// == case 1 ==
// now: 3:45
// target: 3:30
// index 0: 3:40 --> chosen
// index 1: 3:50
// beforeOrAtIndex = 0
// atOrAfterIndex = 0

// == case 2 ==
// now: 3:45
// target: 3:30
// index 0: 2:00
// index 1: 2:10 --> chosen
// beforeOrAtIndex = 1
// atOrAfterIndex = 1

// == case 3 ==
// now: 3:45
// target: 3:01
// index 0: 3:00 --> chosen
// index 1: 3:15
// index 2: 3:30
// beforeOrAtIndex = 0
// atOrAfterIndex = 1

// run at most 256 times
uint256 observationLen = observations.length;
uint256 i;
for (i = 0; i < observationLen; i++) {
if (observations[index].timestamp <= targetTimestamp) {
// if the next observation is empty, using the last one
// it implies the historical data is not enough
if (observations[index].timestamp == 0) {
atOrAfterIndex = beforeOrAtIndex = index + 1;
break;
}
beforeOrAtIndex = index;
atOrAfterIndex = beforeOrAtIndex + 1;
break;
}
index--;
}

// not enough historical data to query
if (i == observationLen) {
// BPF_NEH: no enough historical data
revert("BPF_NEH");
}

beforeOrAt = observations[beforeOrAtIndex];
atOrAfter = observations[atOrAfterIndex];

// if timestamp of the right bound is earlier than timestamp of the left bound,
// it means the left bound is the lastest observation.
// It implies the latest observation is older than requested
// Then we set the right bound to the left bound.
if (atOrAfter.timestamp < beforeOrAt.timestamp) {
atOrAfter = beforeOrAt;
}
}
}
63 changes: 11 additions & 52 deletions contracts/ChainlinkPriceFeed.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,12 @@
pragma solidity 0.7.6;

import { Address } from "@openzeppelin/contracts/utils/Address.sol";
import { SafeMath } from "@openzeppelin/contracts/math/SafeMath.sol";
import { AggregatorV3Interface } from "@chainlink/contracts/src/v0.6/interfaces/AggregatorV3Interface.sol";
import { IPriceFeed } from "./interface/IPriceFeed.sol";
import { BlockContext } from "./base/BlockContext.sol";
import { CumulativeTwap } from "./CumulativeTwap.sol";

contract ChainlinkPriceFeed is IPriceFeed, BlockContext {
using SafeMath for uint256;
contract ChainlinkPriceFeed is IPriceFeed, BlockContext, CumulativeTwap {
using Address for address;

AggregatorV3Interface private immutable _aggregator;
Expand All @@ -20,65 +19,25 @@ contract ChainlinkPriceFeed is IPriceFeed, BlockContext {
_aggregator = aggregator;
}

/// @dev anyone can help update it.
function update() external {
(, uint256 latestPrice, uint256 latestTimestamp) = _getLatestRoundData();

_update(latestPrice, latestTimestamp);
}

function decimals() external view override returns (uint8) {
return _aggregator.decimals();
}

function getPrice(uint256 interval) external view override returns (uint256) {
// there are 3 timestamps: base(our target), previous & current
// base: now - _interval
// current: the current round timestamp from aggregator
// previous: the previous round timestamp from aggregator
// now >= previous > current > = < base
//
// while loop i = 0
// --+------+-----+-----+-----+-----+-----+
// base current now(previous)
//
// while loop i = 1
// --+------+-----+-----+-----+-----+-----+
// base current previous now

(uint80 round, uint256 latestPrice, uint256 latestTimestamp) = _getLatestRoundData();
uint256 timestamp = _blockTimestamp();
uint256 baseTimestamp = timestamp.sub(interval);

// if the latest timestamp <= base timestamp, which means there's no new price, return the latest price
if (interval == 0 || round == 0 || latestTimestamp <= baseTimestamp) {
if (interval == 0 || round == 0) {
return latestPrice;
}

// rounds are like snapshots, latestRound means the latest price snapshot; follow Chainlink's namings here
uint256 previousTimestamp = latestTimestamp;
uint256 cumulativeTime = timestamp.sub(previousTimestamp);
uint256 weightedPrice = latestPrice.mul(cumulativeTime);
uint256 timeFraction;
while (true) {
if (round == 0) {
// to prevent from div 0 error, return the latest price if `cumulativeTime == 0`
return cumulativeTime == 0 ? latestPrice : weightedPrice.div(cumulativeTime);
}

round = round - 1;
(, uint256 currentPrice, uint256 currentTimestamp) = _getRoundData(round);

// check if the current round timestamp is earlier than the base timestamp
if (currentTimestamp <= baseTimestamp) {
// the weighted time period is (base timestamp - previous timestamp)
// ex: now is 1000, interval is 100, then base timestamp is 900
// if timestamp of the current round is 970, and timestamp of NEXT round is 880,
// then the weighted time period will be (970 - 900) = 70 instead of (970 - 880)
weightedPrice = weightedPrice.add(currentPrice.mul(previousTimestamp.sub(baseTimestamp)));
break;
}

timeFraction = previousTimestamp.sub(currentTimestamp);
weightedPrice = weightedPrice.add(currentPrice.mul(timeFraction));
cumulativeTime = cumulativeTime.add(timeFraction);
previousTimestamp = currentTimestamp;
}

return weightedPrice == 0 ? latestPrice : weightedPrice.div(interval);
return _getPrice(interval, latestPrice, latestTimestamp);
}

function _getLatestRoundData()
Expand Down
Loading

0 comments on commit 8a7d44d

Please sign in to comment.