From 69f864ab98e750881124c5fc7307d47dca684ec5 Mon Sep 17 00:00:00 2001 From: KimiWu Date: Wed, 23 Feb 2022 15:58:04 +0800 Subject: [PATCH 1/3] extract the logic of cumulative twap to a standalone contract --- contracts/BandPriceFeed.sol | 167 +---------------------------------- contracts/CumulativeTwap.sol | 157 ++++++++++++++++++++++++++++++++ test/BandPriceFeed.spec.ts | 2 +- 3 files changed, 162 insertions(+), 164 deletions(-) create mode 100644 contracts/CumulativeTwap.sol diff --git a/contracts/BandPriceFeed.sol b/contracts/BandPriceFeed.sol index b3a4a98..f984bdb 100644 --- a/contracts/BandPriceFeed.sol +++ b/contracts/BandPriceFeed.sol @@ -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 @@ -54,33 +35,7 @@ contract BandPriceFeed is IPriceFeed, BlockContext { 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"); - - // 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); } // @@ -88,55 +43,11 @@ contract BandPriceFeed is IPriceFeed, BlockContext { // 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(); 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); } // @@ -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; - } - } } diff --git a/contracts/CumulativeTwap.sol b/contracts/CumulativeTwap.sol new file mode 100644 index 0000000..b7becd3 --- /dev/null +++ b/contracts/CumulativeTwap.sol @@ -0,0 +1,157 @@ +// SPDX-License-Identifier: MIT License +pragma solidity 0.7.6; +pragma experimental ABIEncoderV2; + +import { BlockContext } from "./base/BlockContext.sol"; + +contract CumulativeTwap is BlockContext { + // + // STRUCT + // + struct Observation { + uint256 price; + uint256 priceCumulative; + uint256 timestamp; + } + + // + // EVENT + // + event PriceUpdated(uint256 price, uint256 timestamp, uint8 indexAt); + + // + // STATE + // + // 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; + + uint8 public currentObservationIndex; + + function _update(uint256 price, uint256 lastUpdatedTimestamp) internal { + // for the first time update + if (currentObservationIndex == 0 && observations[0].timestamp == 0) { + observations[0] = Observation({ price: price, priceCumulative: 0, timestamp: lastUpdatedTimestamp }); + emit PriceUpdated(price, lastUpdatedTimestamp, 0); + return; + } + + // BPF_IT: invalid timestamp + Observation memory lastObservation = observations[currentObservationIndex]; + require(lastUpdatedTimestamp > lastObservation.timestamp, "BPF_IT"); + + // overflow of currentObservationIndex is desired since currentObservationIndex is uint8 (0 - 255), + // so 255 + 1 will be 0 + currentObservationIndex++; + + uint256 elapsedTime = lastUpdatedTimestamp - lastObservation.timestamp; + observations[currentObservationIndex] = Observation({ + priceCumulative: lastObservation.priceCumulative + (lastObservation.price * elapsedTime), + timestamp: lastUpdatedTimestamp, + price: price + }); + + emit PriceUpdated(price, lastUpdatedTimestamp, currentObservationIndex); + } + + function _getPrice( + uint256 interval, + uint256 latestPrice, + uint256 latestUpdatedTimestamp + ) internal view returns (uint256) { + Observation memory lastestObservation = observations[currentObservationIndex]; + if (lastestObservation.price == 0) { + // BPF_ND: no data + revert("BPF_ND"); + } + + // IStdReference.ReferenceData memory latestBandData = getReferenceData(); + // if (interval == 0) { + // return latestPrice; + // } + + uint256 currentTimestamp = _blockTimestamp(); + uint256 targetTimestamp = currentTimestamp - interval; + (Observation memory beforeOrAt, Observation memory atOrAfter) = _getSurroundingObservations(targetTimestamp); + uint256 currentPriceCumulative = + lastestObservation.priceCumulative + + (lastestObservation.price * (latestUpdatedTimestamp - lastestObservation.timestamp)) + + (latestPrice * (currentTimestamp - latestUpdatedTimestamp)); + + // + // 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); + } + + function _getSurroundingObservations(uint256 targetTimestamp) + internal + view + returns (Observation memory beforeOrAt, Observation memory atOrAfter) + { + uint8 index = currentObservationIndex; + uint8 beforeOrAtIndex; + uint8 atOrAfterIndex; + + // 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; + } + } +} diff --git a/test/BandPriceFeed.spec.ts b/test/BandPriceFeed.spec.ts index 6847d30..f9204e3 100644 --- a/test/BandPriceFeed.spec.ts +++ b/test/BandPriceFeed.spec.ts @@ -63,7 +63,7 @@ describe("BandPriceFeed Spec", () => { expect(await bandPriceFeed.update()) .to.be.emit(bandPriceFeed, "PriceUpdated") - .withArgs("ETH", parseEther("400"), currentTime, 0) + .withArgs(parseEther("400"), currentTime, 0) const observation = await bandPriceFeed.observations(0) const round = roundData[0] From a6943e1c3fcdb6ebde45d81be63ca2ba96716c27 Mon Sep 17 00:00:00 2001 From: KimiWu Date: Wed, 23 Feb 2022 16:49:01 +0800 Subject: [PATCH 2/3] apply cumulative twap for Chainlink price feed --- contracts/ChainlinkPriceFeed.sol | 61 +++----------- contracts/CumulativeTwap.sol | 19 ++--- test/BandPriceFeed.spec.ts | 19 +++-- test/ChainlinkPriceFeed.spec.ts | 134 +++++++------------------------ 4 files changed, 60 insertions(+), 173 deletions(-) diff --git a/contracts/ChainlinkPriceFeed.sol b/contracts/ChainlinkPriceFeed.sol index 6d24e6b..4540201 100644 --- a/contracts/ChainlinkPriceFeed.sol +++ b/contracts/ChainlinkPriceFeed.sol @@ -6,8 +6,9 @@ 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 { +contract ChainlinkPriceFeed is IPriceFeed, BlockContext, CumulativeTwap { using SafeMath for uint256; using Address for address; @@ -20,65 +21,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() diff --git a/contracts/CumulativeTwap.sol b/contracts/CumulativeTwap.sol index b7becd3..6b6dfca 100644 --- a/contracts/CumulativeTwap.sol +++ b/contracts/CumulativeTwap.sol @@ -36,9 +36,11 @@ contract CumulativeTwap is BlockContext { return; } - // BPF_IT: invalid timestamp + // CT_IT: invalid timestamp + // add `==` in the require statement in case that two or more price with the same timestamp + // this might happen on Optimism bcs their timestamp is not up-to-date Observation memory lastObservation = observations[currentObservationIndex]; - require(lastUpdatedTimestamp > lastObservation.timestamp, "BPF_IT"); + require(lastUpdatedTimestamp >= lastObservation.timestamp, "CT_IT"); // overflow of currentObservationIndex is desired since currentObservationIndex is uint8 (0 - 255), // so 255 + 1 will be 0 @@ -61,15 +63,10 @@ contract CumulativeTwap is BlockContext { ) internal view returns (uint256) { Observation memory lastestObservation = observations[currentObservationIndex]; if (lastestObservation.price == 0) { - // BPF_ND: no data - revert("BPF_ND"); + // CT_ND: no data + revert("CT_ND"); } - // IStdReference.ReferenceData memory latestBandData = getReferenceData(); - // if (interval == 0) { - // return latestPrice; - // } - uint256 currentTimestamp = _blockTimestamp(); uint256 targetTimestamp = currentTimestamp - interval; (Observation memory beforeOrAt, Observation memory atOrAfter) = _getSurroundingObservations(targetTimestamp); @@ -139,8 +136,8 @@ contract CumulativeTwap is BlockContext { // not enough historical data to query if (i == observationLen) { - // BPF_NEH: no enough historical data - revert("BPF_NEH"); + // CT_NEH: no enough historical data + revert("CT_NEH"); } beforeOrAt = observations[beforeOrAtIndex]; diff --git a/test/BandPriceFeed.spec.ts b/test/BandPriceFeed.spec.ts index f9204e3..1bebc36 100644 --- a/test/BandPriceFeed.spec.ts +++ b/test/BandPriceFeed.spec.ts @@ -21,7 +21,7 @@ async function bandPriceFeedFixture(): Promise { return { bandPriceFeed, bandReference: testStdReference, baseAsset } } -describe("BandPriceFeed Spec", () => { +describe("BandPriceFeed/CumulativeTwap Spec", () => { const [admin] = waffle.provider.getWallets() const loadFixture: ReturnType = waffle.createFixtureLoader([admin]) let bandPriceFeed: BandPriceFeed @@ -88,14 +88,15 @@ describe("BandPriceFeed Spec", () => { expect(observation.priceCumulative).to.eq(parseEther("6000")) }) - it("force error, the second update is the same timestamp", async () => { + // skip this test for being compatible with Chainlink aggregator + it.skip("force error, the second update is the same timestamp", async () => { await updatePrice(400, false) roundData.push([parseEther("440"), currentTime, currentTime]) bandReference.getReferenceData.returns(() => { return roundData[roundData.length - 1] }) - await expect(bandPriceFeed.update()).to.be.revertedWith("BPF_IT") + await expect(bandPriceFeed.update()).to.be.revertedWith("CT_IT") }) }) @@ -259,17 +260,23 @@ describe("BandPriceFeed Spec", () => { // the longest interval = 255 * 15 = 3825, it should be revert when interval > 3826 // here, we set interval to 3827 because hardhat increases the timestamp by 1 when any tx happens - await expect(bandPriceFeed.getPrice(255 * 15 + 2)).to.be.revertedWith("BPF_NEH") + await expect(bandPriceFeed.getPrice(255 * 15 + 2)).to.be.revertedWith("CT_NEH") }) }) describe("price is not updated yet", () => { + beforeEach(async () => { + roundData.push([parseEther("100"), currentTime, currentTime]) + bandReference.getReferenceData.returns(() => { + return roundData[roundData.length - 1] + }) + }) it("get spot price", async () => { - await expect(bandPriceFeed.getPrice(900)).to.be.revertedWith("BPF_ND") + await expect(bandPriceFeed.getPrice(900)).to.be.revertedWith("CT_ND") }) it("force error, get twap price", async () => { - await expect(bandPriceFeed.getPrice(900)).to.be.revertedWith("BPF_ND") + await expect(bandPriceFeed.getPrice(900)).to.be.revertedWith("CT_ND") }) }) }) diff --git a/test/ChainlinkPriceFeed.spec.ts b/test/ChainlinkPriceFeed.spec.ts index 63ee3c3..d931a4f 100644 --- a/test/ChainlinkPriceFeed.spec.ts +++ b/test/ChainlinkPriceFeed.spec.ts @@ -27,6 +27,20 @@ describe("ChainlinkPriceFeed Spec", () => { let currentTime: number let roundData: any[] + async function updatePrice(index: number, price: number, forward: boolean = true): Promise { + roundData.push([index, parseEther(price.toString()), currentTime, currentTime, index]) + aggregator.latestRoundData.returns(() => { + return roundData[roundData.length - 1] + }) + await chainlinkPriceFeed.update() + + if (forward) { + currentTime += 15 + await ethers.provider.send("evm_setNextBlockTimestamp", [currentTime]) + await ethers.provider.send("evm_mine", []) + } + } + beforeEach(async () => { const _fixture = await loadFixture(chainlinkPriceFeedFixture) chainlinkPriceFeed = _fixture.chainlinkPriceFeed @@ -50,17 +64,20 @@ describe("ChainlinkPriceFeed Spec", () => { // [roundId, answer, startedAt, updatedAt, answeredInRound] ] - // have the same timestamp for rounds - roundData.push([0, parseEther("400"), currentTime, currentTime, 0]) - roundData.push([1, parseEther("405"), currentTime, currentTime, 1]) - roundData.push([2, parseEther("410"), currentTime, currentTime, 2]) - - aggregator.latestRoundData.returns(() => { - return roundData[roundData.length - 1] - }) - aggregator.getRoundData.returns(round => { - return roundData[round] - }) + await updatePrice(0, 400, false) + await updatePrice(1, 405, false) + await updatePrice(2, 410, false) + // // have the same timestamp for rounds + // roundData.push([0, parseEther("400"), currentTime, currentTime, 0]) + // roundData.push([1, parseEther("405"), currentTime, currentTime, 1]) + // roundData.push([2, parseEther("410"), currentTime, currentTime, 2]) + + // aggregator.latestRoundData.returns(() => { + // return roundData[roundData.length - 1] + // }) + // aggregator.getRoundData.returns(round => { + // return roundData[round] + // }) currentTime += 15 await ethers.provider.send("evm_setNextBlockTimestamp", [currentTime]) @@ -77,99 +94,4 @@ describe("ChainlinkPriceFeed Spec", () => { expect(price).to.eq(parseEther("410")) }) }) - - describe("twap", () => { - beforeEach(async () => { - // `base` = now - _interval - // aggregator's answer - // timestamp(base + 0) : 400 - // timestamp(base + 15) : 405 - // timestamp(base + 30) : 410 - // now = base + 45 - // - // --+------+-----+-----+-----+-----+-----+ - // base now - const latestTimestamp = (await waffle.provider.getBlock("latest")).timestamp - currentTime = latestTimestamp - roundData = [ - // [roundId, answer, startedAt, updatedAt, answeredInRound] - ] - - currentTime += 0 - roundData.push([0, parseEther("400"), currentTime, currentTime, 0]) - - currentTime += 15 - roundData.push([1, parseEther("405"), currentTime, currentTime, 1]) - - currentTime += 15 - roundData.push([2, parseEther("410"), currentTime, currentTime, 2]) - - aggregator.latestRoundData.returns(() => { - return roundData[roundData.length - 1] - }) - aggregator.getRoundData.returns(round => { - return roundData[round] - }) - - currentTime += 15 - await ethers.provider.send("evm_setNextBlockTimestamp", [currentTime]) - await ethers.provider.send("evm_mine", []) - }) - - it("twap price", async () => { - const price = await chainlinkPriceFeed.getPrice(45) - expect(price).to.eq(parseEther("405")) - }) - - it("asking interval more than aggregator has", async () => { - const price = await chainlinkPriceFeed.getPrice(46) - expect(price).to.eq(parseEther("405")) - }) - - it("asking interval less than aggregator has", async () => { - const price = await chainlinkPriceFeed.getPrice(44) - expect(price).to.eq("405113636363636363636") - }) - - it("given variant price period", async () => { - roundData.push([4, parseEther("420"), currentTime + 30, currentTime + 30, 4]) - await ethers.provider.send("evm_setNextBlockTimestamp", [currentTime + 50]) - await ethers.provider.send("evm_mine", []) - // twap price should be ((400 * 15) + (405 * 15) + (410 * 45) + (420 * 20)) / 95 = 409.736 - const price = await chainlinkPriceFeed.getPrice(95) - expect(price).to.eq("409736842105263157894") - }) - - it("latest price update time is earlier than the request, return the latest price", async () => { - await ethers.provider.send("evm_setNextBlockTimestamp", [currentTime + 100]) - await ethers.provider.send("evm_mine", []) - - // latest update time is base + 30, but now is base + 145 and asking for (now - 45) - // should return the latest price directly - const price = await chainlinkPriceFeed.getPrice(45) - expect(price).to.eq(parseEther("410")) - }) - - it("if current price < 0, ignore the current price", async () => { - roundData.push([3, parseEther("-10"), 250, 250, 3]) - const price = await chainlinkPriceFeed.getPrice(45) - expect(price).to.eq(parseEther("405")) - }) - - it("if there is a negative price in the middle, ignore that price", async () => { - roundData.push([3, parseEther("-100"), currentTime + 20, currentTime + 20, 3]) - roundData.push([4, parseEther("420"), currentTime + 30, currentTime + 30, 4]) - await ethers.provider.send("evm_setNextBlockTimestamp", [currentTime + 50]) - await ethers.provider.send("evm_mine", []) - - // twap price should be ((400 * 15) + (405 * 15) + (410 * 45) + (420 * 20)) / 95 = 409.736 - const price = await chainlinkPriceFeed.getPrice(95) - expect(price).to.eq("409736842105263157894") - }) - - it("return latest price if interval is zero", async () => { - const price = await chainlinkPriceFeed.getPrice(0) - expect(price).to.eq(parseEther("410")) - }) - }) }) From e0dc3afc0af924449525a5f28bd2e6422392bf7b Mon Sep 17 00:00:00 2001 From: KimiWu Date: Tue, 1 Mar 2022 11:30:41 +0800 Subject: [PATCH 3/3] comment applied --- contracts/BandPriceFeed.sol | 6 +++--- contracts/ChainlinkPriceFeed.sol | 2 -- contracts/CumulativeTwap.sol | 32 +++++++++++++++++++------------- test/BandPriceFeed.spec.ts | 1 + 4 files changed, 23 insertions(+), 18 deletions(-) diff --git a/contracts/BandPriceFeed.sol b/contracts/BandPriceFeed.sol index f984bdb..dc2d5ec 100644 --- a/contracts/BandPriceFeed.sol +++ b/contracts/BandPriceFeed.sol @@ -33,7 +33,7 @@ contract BandPriceFeed is IPriceFeed, BlockContext, CumulativeTwap { /// @dev anyone can help update it. function update() external { - IStdReference.ReferenceData memory bandData = getReferenceData(); + IStdReference.ReferenceData memory bandData = _getReferenceData(); _update(bandData.rate, bandData.lastUpdatedBase); } @@ -43,7 +43,7 @@ contract BandPriceFeed is IPriceFeed, BlockContext, CumulativeTwap { // function getPrice(uint256 interval) public view override returns (uint256) { - IStdReference.ReferenceData memory latestBandData = getReferenceData(); + IStdReference.ReferenceData memory latestBandData = _getReferenceData(); if (interval == 0) { return latestBandData.rate; } @@ -64,7 +64,7 @@ contract BandPriceFeed is IPriceFeed, BlockContext, CumulativeTwap { // 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"); diff --git a/contracts/ChainlinkPriceFeed.sol b/contracts/ChainlinkPriceFeed.sol index 4540201..04c142b 100644 --- a/contracts/ChainlinkPriceFeed.sol +++ b/contracts/ChainlinkPriceFeed.sol @@ -2,14 +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, CumulativeTwap { - using SafeMath for uint256; using Address for address; AggregatorV3Interface private immutable _aggregator; diff --git a/contracts/CumulativeTwap.sol b/contracts/CumulativeTwap.sol index 6b6dfca..0796952 100644 --- a/contracts/CumulativeTwap.sol +++ b/contracts/CumulativeTwap.sol @@ -3,8 +3,11 @@ pragma solidity 0.7.6; pragma experimental ABIEncoderV2; import { BlockContext } from "./base/BlockContext.sol"; +import { SafeMath } from "@openzeppelin/contracts/math/SafeMath.sol"; contract CumulativeTwap is BlockContext { + using SafeMath for uint256; + // // STRUCT // @@ -68,12 +71,14 @@ contract CumulativeTwap is BlockContext { } uint256 currentTimestamp = _blockTimestamp(); - uint256 targetTimestamp = currentTimestamp - interval; + uint256 targetTimestamp = currentTimestamp.sub(interval); (Observation memory beforeOrAt, Observation memory atOrAfter) = _getSurroundingObservations(targetTimestamp); - uint256 currentPriceCumulative = - lastestObservation.priceCumulative + - (lastestObservation.price * (latestUpdatedTimestamp - lastestObservation.timestamp)) + - (latestPrice * (currentTimestamp - latestUpdatedTimestamp)); + uint256 currentCumulativePrice = + lastestObservation.priceCumulative.add( + (lastestObservation.price.mul(latestUpdatedTimestamp.sub(lastestObservation.timestamp))).add( + latestPrice.mul(currentTimestamp.sub(latestUpdatedTimestamp)) + ) + ); // // beforeOrAt atOrAfter @@ -83,28 +88,29 @@ contract CumulativeTwap is BlockContext { // case 2 | targetTimestamp // case 3 targetTimestamp // - uint256 targetPriceCumulative; + uint256 targetCumulativePrice; // case1. not enough historical data or just enough (`==` case) if (targetTimestamp <= beforeOrAt.timestamp) { targetTimestamp = beforeOrAt.timestamp; - targetPriceCumulative = beforeOrAt.priceCumulative; + targetCumulativePrice = beforeOrAt.priceCumulative; } // case2. the latest data is older than or equal the request else if (atOrAfter.timestamp <= targetTimestamp) { targetTimestamp = atOrAfter.timestamp; - targetPriceCumulative = atOrAfter.priceCumulative; + targetCumulativePrice = 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; + targetCumulativePrice = beforeOrAt.priceCumulative.add( + ((atOrAfter.priceCumulative.sub(beforeOrAt.priceCumulative)).mul(targetTimeDelta)).div( + observationTimeDelta + ) + ); } - return (currentPriceCumulative - targetPriceCumulative) / (currentTimestamp - targetTimestamp); + return currentCumulativePrice.sub(targetCumulativePrice).div(currentTimestamp - targetTimestamp); } function _getSurroundingObservations(uint256 targetTimestamp) diff --git a/test/BandPriceFeed.spec.ts b/test/BandPriceFeed.spec.ts index 1bebc36..9d3801a 100644 --- a/test/BandPriceFeed.spec.ts +++ b/test/BandPriceFeed.spec.ts @@ -89,6 +89,7 @@ describe("BandPriceFeed/CumulativeTwap Spec", () => { }) // skip this test for being compatible with Chainlink aggregator + // Chainlink aggregator might have the same timestamp in different round it.skip("force error, the second update is the same timestamp", async () => { await updatePrice(400, false)