From 279f81da553cc913e73d4f48d9b4ea300669b065 Mon Sep 17 00:00:00 2001 From: Opass Chang Date: Mon, 23 May 2022 15:49:40 +0800 Subject: [PATCH 1/7] rename ChainlinkPriceFee to ChainlinkPriceFeedWithCachedTwap --- ....sol => ChainlinkPriceFeedWithCachedTwap.sol} | 2 +- test/CachedTwap.spec.ts | 16 +++++++++++----- ... => ChainlinkPriceFeedWithCachedTwap.spec.ts} | 13 ++++++++----- test/PriceFeed.gas.test.ts | 14 ++++++++++---- 4 files changed, 30 insertions(+), 15 deletions(-) rename contracts/{ChainlinkPriceFeed.sol => ChainlinkPriceFeedWithCachedTwap.sol} (97%) rename test/{ChainlinkPriceFeed.spec.ts => ChainlinkPriceFeedWithCachedTwap.spec.ts} (88%) diff --git a/contracts/ChainlinkPriceFeed.sol b/contracts/ChainlinkPriceFeedWithCachedTwap.sol similarity index 97% rename from contracts/ChainlinkPriceFeed.sol rename to contracts/ChainlinkPriceFeedWithCachedTwap.sol index d7b0025..8e9edb4 100644 --- a/contracts/ChainlinkPriceFeed.sol +++ b/contracts/ChainlinkPriceFeedWithCachedTwap.sol @@ -7,7 +7,7 @@ import { IPriceFeed } from "./interface/IPriceFeed.sol"; import { BlockContext } from "./base/BlockContext.sol"; import { CachedTwap } from "./twap/CachedTwap.sol"; -contract ChainlinkPriceFeed is IPriceFeed, BlockContext, CachedTwap { +contract ChainlinkPriceFeedWithCachedTwap is IPriceFeed, BlockContext, CachedTwap { using Address for address; AggregatorV3Interface private immutable _aggregator; diff --git a/test/CachedTwap.spec.ts b/test/CachedTwap.spec.ts index 9823fc8..d7c599c 100644 --- a/test/CachedTwap.spec.ts +++ b/test/CachedTwap.spec.ts @@ -1,7 +1,13 @@ import { expect } from "chai" import { parseEther } from "ethers/lib/utils" import { ethers, waffle } from "hardhat" -import { BandPriceFeed, ChainlinkPriceFeed, TestAggregatorV3, TestPriceFeed, TestStdReference } from "../typechain" +import { + BandPriceFeed, + ChainlinkPriceFeedWithCachedTwap, + TestAggregatorV3, + TestPriceFeed, + TestStdReference, +} from "../typechain" interface PriceFeedFixture { bandPriceFeed: BandPriceFeed @@ -9,7 +15,7 @@ interface PriceFeedFixture { baseAsset: string // chainlinik - chainlinkPriceFeed: ChainlinkPriceFeed + chainlinkPriceFeed: ChainlinkPriceFeedWithCachedTwap aggregator: TestAggregatorV3 } async function priceFeedFixture(): Promise { @@ -30,11 +36,11 @@ async function priceFeedFixture(): Promise { const testAggregatorFactory = await ethers.getContractFactory("TestAggregatorV3") const testAggregator = await testAggregatorFactory.deploy() - const chainlinkPriceFeedFactory = await ethers.getContractFactory("ChainlinkPriceFeed") + const chainlinkPriceFeedFactory = await ethers.getContractFactory("ChainlinkPriceFeedWithCachedTwap") const chainlinkPriceFeed = (await chainlinkPriceFeedFactory.deploy( testAggregator.address, twapInterval, - )) as ChainlinkPriceFeed + )) as ChainlinkPriceFeedWithCachedTwap return { bandPriceFeed, bandReference: testStdReference, baseAsset, chainlinkPriceFeed, aggregator: testAggregator } } @@ -44,7 +50,7 @@ describe("Cached Twap Spec", () => { const loadFixture: ReturnType = waffle.createFixtureLoader([admin]) let bandPriceFeed: BandPriceFeed let bandReference: TestStdReference - let chainlinkPriceFeed: ChainlinkPriceFeed + let chainlinkPriceFeed: ChainlinkPriceFeedWithCachedTwap let aggregator: TestAggregatorV3 let currentTime: number let testPriceFeed: TestPriceFeed diff --git a/test/ChainlinkPriceFeed.spec.ts b/test/ChainlinkPriceFeedWithCachedTwap.spec.ts similarity index 88% rename from test/ChainlinkPriceFeed.spec.ts rename to test/ChainlinkPriceFeedWithCachedTwap.spec.ts index 92dbda6..0bcbadf 100644 --- a/test/ChainlinkPriceFeed.spec.ts +++ b/test/ChainlinkPriceFeedWithCachedTwap.spec.ts @@ -2,10 +2,10 @@ import { FakeContract, smock } from "@defi-wonderland/smock" import { expect } from "chai" import { parseEther } from "ethers/lib/utils" import { ethers, waffle } from "hardhat" -import { ChainlinkPriceFeed, TestAggregatorV3 } from "../typechain" +import { ChainlinkPriceFeedWithCachedTwap, TestAggregatorV3 } from "../typechain" interface ChainlinkPriceFeedFixture { - chainlinkPriceFeed: ChainlinkPriceFeed + chainlinkPriceFeed: ChainlinkPriceFeedWithCachedTwap aggregator: FakeContract } @@ -13,8 +13,11 @@ async function chainlinkPriceFeedFixture(): Promise { const aggregator = await smock.fake("TestAggregatorV3") aggregator.decimals.returns(() => 18) - const chainlinkPriceFeedFactory = await ethers.getContractFactory("ChainlinkPriceFeed") - const chainlinkPriceFeed = (await chainlinkPriceFeedFactory.deploy(aggregator.address, 900)) as ChainlinkPriceFeed + const chainlinkPriceFeedFactory = await ethers.getContractFactory("ChainlinkPriceFeedWithCachedTwap") + const chainlinkPriceFeed = (await chainlinkPriceFeedFactory.deploy( + aggregator.address, + 900, + )) as ChainlinkPriceFeedWithCachedTwap return { chainlinkPriceFeed, aggregator } } @@ -22,7 +25,7 @@ async function chainlinkPriceFeedFixture(): Promise { describe("ChainlinkPriceFeed Spec", () => { const [admin] = waffle.provider.getWallets() const loadFixture: ReturnType = waffle.createFixtureLoader([admin]) - let chainlinkPriceFeed: ChainlinkPriceFeed + let chainlinkPriceFeed: ChainlinkPriceFeedWithCachedTwap let aggregator: FakeContract let currentTime: number let roundData: any[] diff --git a/test/PriceFeed.gas.test.ts b/test/PriceFeed.gas.test.ts index a39c33d..b75502a 100644 --- a/test/PriceFeed.gas.test.ts +++ b/test/PriceFeed.gas.test.ts @@ -1,6 +1,12 @@ import { parseEther } from "ethers/lib/utils" import { ethers, waffle } from "hardhat" -import { BandPriceFeed, ChainlinkPriceFeed, TestAggregatorV3, TestPriceFeed, TestStdReference } from "../typechain" +import { + BandPriceFeed, + ChainlinkPriceFeedWithCachedTwap, + TestAggregatorV3, + TestPriceFeed, + TestStdReference, +} from "../typechain" const twapInterval = 900 interface PriceFeedFixture { @@ -9,7 +15,7 @@ interface PriceFeedFixture { baseAsset: string // chainlinik - chainlinkPriceFeed: ChainlinkPriceFeed + chainlinkPriceFeed: ChainlinkPriceFeedWithCachedTwap aggregator: TestAggregatorV3 } @@ -34,7 +40,7 @@ async function priceFeedFixture(): Promise { const chainlinkPriceFeed = (await chainlinkPriceFeedFactory.deploy( testAggregator.address, twapInterval, - )) as ChainlinkPriceFeed + )) as ChainlinkPriceFeedWithCachedTwap return { bandPriceFeed, bandReference: testStdReference, baseAsset, chainlinkPriceFeed, aggregator: testAggregator } } @@ -44,7 +50,7 @@ describe.skip("Price feed gas test", () => { const loadFixture: ReturnType = waffle.createFixtureLoader([admin]) let bandPriceFeed: BandPriceFeed let bandReference: TestStdReference - let chainlinkPriceFeed: ChainlinkPriceFeed + let chainlinkPriceFeed: ChainlinkPriceFeedWithCachedTwap let aggregator: TestAggregatorV3 let currentTime: number let testPriceFeed: TestPriceFeed From 9971c3436ffc0771a5fd7411da0f23bd3f070691 Mon Sep 17 00:00:00 2001 From: Opass Chang Date: Mon, 23 May 2022 16:03:23 +0800 Subject: [PATCH 2/7] add original ChainlinkPriceFeed --- contracts/ChainlinkPriceFeed.sol | 124 +++++++++++++++++++++++++++++++ 1 file changed, 124 insertions(+) create mode 100644 contracts/ChainlinkPriceFeed.sol diff --git a/contracts/ChainlinkPriceFeed.sol b/contracts/ChainlinkPriceFeed.sol new file mode 100644 index 0000000..6d24e6b --- /dev/null +++ b/contracts/ChainlinkPriceFeed.sol @@ -0,0 +1,124 @@ +// SPDX-License-Identifier: MIT License +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"; + +contract ChainlinkPriceFeed is IPriceFeed, BlockContext { + using SafeMath for uint256; + using Address for address; + + AggregatorV3Interface private immutable _aggregator; + + constructor(AggregatorV3Interface aggregator) { + // CPF_ANC: Aggregator address is not contract + require(address(aggregator).isContract(), "CPF_ANC"); + + _aggregator = aggregator; + } + + 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) { + 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); + } + + function _getLatestRoundData() + private + view + returns ( + uint80, + uint256 finalPrice, + uint256 + ) + { + (uint80 round, int256 latestPrice, , uint256 latestTimestamp, ) = _aggregator.latestRoundData(); + finalPrice = uint256(latestPrice); + if (latestPrice < 0) { + _requireEnoughHistory(round); + (round, finalPrice, latestTimestamp) = _getRoundData(round - 1); + } + return (round, finalPrice, latestTimestamp); + } + + function _getRoundData(uint80 _round) + private + view + returns ( + uint80, + uint256, + uint256 + ) + { + (uint80 round, int256 latestPrice, , uint256 latestTimestamp, ) = _aggregator.getRoundData(_round); + while (latestPrice < 0) { + _requireEnoughHistory(round); + round = round - 1; + (, latestPrice, , latestTimestamp, ) = _aggregator.getRoundData(round); + } + return (round, uint256(latestPrice), latestTimestamp); + } + + function _requireEnoughHistory(uint80 _round) private pure { + // CPF_NEH: no enough history + require(_round > 0, "CPF_NEH"); + } +} From 09c047838f8fd226e4dbba67e106bbfa9ae5ef66 Mon Sep 17 00:00:00 2001 From: Opass Chang Date: Mon, 23 May 2022 16:09:27 +0800 Subject: [PATCH 3/7] add cacheTwap function To conform IPriceFeed --- contracts/ChainlinkPriceFeed.sol | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/contracts/ChainlinkPriceFeed.sol b/contracts/ChainlinkPriceFeed.sol index 6d24e6b..463ba2a 100644 --- a/contracts/ChainlinkPriceFeed.sol +++ b/contracts/ChainlinkPriceFeed.sol @@ -20,6 +20,12 @@ contract ChainlinkPriceFeed is IPriceFeed, BlockContext { _aggregator = aggregator; } + /// @dev not support cached twap + function cacheTwap(uint256 interval) external override returns (uint256) { + // CPF_NS: not supported + revert("CPF_NS"); + } + function decimals() external view override returns (uint8) { return _aggregator.decimals(); } From 2641749535a488297363b73d06d117dde13a06ac Mon Sep 17 00:00:00 2001 From: Opass Chang Date: Mon, 23 May 2022 16:23:33 +0800 Subject: [PATCH 4/7] Add test for original ChainlinkPriceFeed --- test/ChainlinkPriceFeed.spec.ts | 175 ++++++++++++++++++ test/ChainlinkPriceFeedWithCachedTwap.spec.ts | 2 +- test/PriceFeed.gas.test.ts | 2 +- 3 files changed, 177 insertions(+), 2 deletions(-) create mode 100644 test/ChainlinkPriceFeed.spec.ts diff --git a/test/ChainlinkPriceFeed.spec.ts b/test/ChainlinkPriceFeed.spec.ts new file mode 100644 index 0000000..63ee3c3 --- /dev/null +++ b/test/ChainlinkPriceFeed.spec.ts @@ -0,0 +1,175 @@ +import { FakeContract, smock } from "@defi-wonderland/smock" +import { expect } from "chai" +import { parseEther } from "ethers/lib/utils" +import { ethers, waffle } from "hardhat" +import { ChainlinkPriceFeed, TestAggregatorV3 } from "../typechain" + +interface ChainlinkPriceFeedFixture { + chainlinkPriceFeed: ChainlinkPriceFeed + aggregator: FakeContract +} + +async function chainlinkPriceFeedFixture(): Promise { + const aggregator = await smock.fake("TestAggregatorV3") + aggregator.decimals.returns(() => 18) + + const chainlinkPriceFeedFactory = await ethers.getContractFactory("ChainlinkPriceFeed") + const chainlinkPriceFeed = (await chainlinkPriceFeedFactory.deploy(aggregator.address)) as ChainlinkPriceFeed + + return { chainlinkPriceFeed, aggregator } +} + +describe("ChainlinkPriceFeed Spec", () => { + const [admin] = waffle.provider.getWallets() + const loadFixture: ReturnType = waffle.createFixtureLoader([admin]) + let chainlinkPriceFeed: ChainlinkPriceFeed + let aggregator: FakeContract + let currentTime: number + let roundData: any[] + + beforeEach(async () => { + const _fixture = await loadFixture(chainlinkPriceFeedFixture) + chainlinkPriceFeed = _fixture.chainlinkPriceFeed + aggregator = _fixture.aggregator + }) + + describe("edge cases, have the same timestamp for several rounds", () => { + 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] + ] + + // 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]) + await ethers.provider.send("evm_mine", []) + }) + + it("get the latest price", async () => { + const price = await chainlinkPriceFeed.getPrice(45) + expect(price).to.eq(parseEther("410")) + }) + + it("asking interval more than aggregator has", async () => { + const price = await chainlinkPriceFeed.getPrice(46) + 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")) + }) + }) +}) diff --git a/test/ChainlinkPriceFeedWithCachedTwap.spec.ts b/test/ChainlinkPriceFeedWithCachedTwap.spec.ts index 0bcbadf..b82957e 100644 --- a/test/ChainlinkPriceFeedWithCachedTwap.spec.ts +++ b/test/ChainlinkPriceFeedWithCachedTwap.spec.ts @@ -22,7 +22,7 @@ async function chainlinkPriceFeedFixture(): Promise { return { chainlinkPriceFeed, aggregator } } -describe("ChainlinkPriceFeed Spec", () => { +describe("ChainlinkPriceFeedWithCachedTwap Spec", () => { const [admin] = waffle.provider.getWallets() const loadFixture: ReturnType = waffle.createFixtureLoader([admin]) let chainlinkPriceFeed: ChainlinkPriceFeedWithCachedTwap diff --git a/test/PriceFeed.gas.test.ts b/test/PriceFeed.gas.test.ts index b75502a..1461f1e 100644 --- a/test/PriceFeed.gas.test.ts +++ b/test/PriceFeed.gas.test.ts @@ -36,7 +36,7 @@ async function priceFeedFixture(): Promise { const testAggregatorFactory = await ethers.getContractFactory("TestAggregatorV3") const testAggregator = await testAggregatorFactory.deploy() - const chainlinkPriceFeedFactory = await ethers.getContractFactory("ChainlinkPriceFeed") + const chainlinkPriceFeedFactory = await ethers.getContractFactory("ChainlinkPriceFeedWithCachedTwap") const chainlinkPriceFeed = (await chainlinkPriceFeedFactory.deploy( testAggregator.address, twapInterval, From f17b691b64b8f0682afdd814da1fbb7644630bbe Mon Sep 17 00:00:00 2001 From: Opass Chang Date: Mon, 23 May 2022 16:34:28 +0800 Subject: [PATCH 5/7] update CHANGELOG --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5336a87..a534efa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [unreleased] +- Change contract name `ChainlinkPriceFeed` to `ChainlinkPriceFeedWithCachedTwap`. +- Add origin `ChainlinkPriceFeed`, which calculates the twap by round data instead of cached twap. ## [0.3.4] - 2022-04-01 - Add `cacheTwap(uint256)` to `IPriceFeed.sol` and `EmergencyPriceFeed.sol` From f6ceba7be57af0b217c1c4ac80b35ff66605ad63 Mon Sep 17 00:00:00 2001 From: Opass Chang Date: Mon, 23 May 2022 17:26:52 +0800 Subject: [PATCH 6/7] Change contract name `ChainlinkPriceFeedWithCachedTwap` to `ChainlinkPriceFeedV2`, add IPriceFeedV2 --- CHANGELOG.md | 2 +- contracts/BandPriceFeed.sol | 4 ++-- contracts/ChainlinkPriceFeed.sol | 6 ----- ...achedTwap.sol => ChainlinkPriceFeedV2.sol} | 4 ++-- contracts/EmergencyPriceFeed.sol | 4 ++-- contracts/interface/IPriceFeed.sol | 4 ---- contracts/interface/IPriceFeedV2.sol | 10 +++++++++ contracts/test/TestPriceFeed.sol | 22 +++++++++---------- test/CachedTwap.spec.ts | 16 +++++--------- ...p.spec.ts => ChainlinkPriceFeedV2.spec.ts} | 15 +++++-------- test/PriceFeed.gas.test.ts | 16 +++++--------- 11 files changed, 44 insertions(+), 59 deletions(-) rename contracts/{ChainlinkPriceFeedWithCachedTwap.sol => ChainlinkPriceFeedV2.sol} (95%) create mode 100644 contracts/interface/IPriceFeedV2.sol rename test/{ChainlinkPriceFeedWithCachedTwap.spec.ts => ChainlinkPriceFeedV2.spec.ts} (86%) diff --git a/CHANGELOG.md b/CHANGELOG.md index a534efa..faeccc2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [unreleased] -- Change contract name `ChainlinkPriceFeed` to `ChainlinkPriceFeedWithCachedTwap`. +- Change contract name `ChainlinkPriceFeed` to `ChainlinkPriceFeedV2`. - Add origin `ChainlinkPriceFeed`, which calculates the twap by round data instead of cached twap. ## [0.3.4] - 2022-04-01 diff --git a/contracts/BandPriceFeed.sol b/contracts/BandPriceFeed.sol index 76af3c2..d3ebd74 100644 --- a/contracts/BandPriceFeed.sol +++ b/contracts/BandPriceFeed.sol @@ -4,11 +4,11 @@ pragma experimental ABIEncoderV2; import { Address } from "@openzeppelin/contracts/utils/Address.sol"; import { BlockContext } from "./base/BlockContext.sol"; -import { IPriceFeed } from "./interface/IPriceFeed.sol"; +import { IPriceFeedV2 } from "./interface/IPriceFeedV2.sol"; import { IStdReference } from "./interface/bandProtocol/IStdReference.sol"; import { CachedTwap } from "./twap/CachedTwap.sol"; -contract BandPriceFeed is IPriceFeed, BlockContext, CachedTwap { +contract BandPriceFeed is IPriceFeedV2, BlockContext, CachedTwap { using Address for address; // diff --git a/contracts/ChainlinkPriceFeed.sol b/contracts/ChainlinkPriceFeed.sol index 463ba2a..6d24e6b 100644 --- a/contracts/ChainlinkPriceFeed.sol +++ b/contracts/ChainlinkPriceFeed.sol @@ -20,12 +20,6 @@ contract ChainlinkPriceFeed is IPriceFeed, BlockContext { _aggregator = aggregator; } - /// @dev not support cached twap - function cacheTwap(uint256 interval) external override returns (uint256) { - // CPF_NS: not supported - revert("CPF_NS"); - } - function decimals() external view override returns (uint8) { return _aggregator.decimals(); } diff --git a/contracts/ChainlinkPriceFeedWithCachedTwap.sol b/contracts/ChainlinkPriceFeedV2.sol similarity index 95% rename from contracts/ChainlinkPriceFeedWithCachedTwap.sol rename to contracts/ChainlinkPriceFeedV2.sol index 8e9edb4..9d81585 100644 --- a/contracts/ChainlinkPriceFeedWithCachedTwap.sol +++ b/contracts/ChainlinkPriceFeedV2.sol @@ -3,11 +3,11 @@ pragma solidity 0.7.6; import { Address } from "@openzeppelin/contracts/utils/Address.sol"; import { AggregatorV3Interface } from "@chainlink/contracts/src/v0.6/interfaces/AggregatorV3Interface.sol"; -import { IPriceFeed } from "./interface/IPriceFeed.sol"; +import { IPriceFeedV2 } from "./interface/IPriceFeedV2.sol"; import { BlockContext } from "./base/BlockContext.sol"; import { CachedTwap } from "./twap/CachedTwap.sol"; -contract ChainlinkPriceFeedWithCachedTwap is IPriceFeed, BlockContext, CachedTwap { +contract ChainlinkPriceFeedV2 is IPriceFeedV2, BlockContext, CachedTwap { using Address for address; AggregatorV3Interface private immutable _aggregator; diff --git a/contracts/EmergencyPriceFeed.sol b/contracts/EmergencyPriceFeed.sol index 58b7abe..434777f 100644 --- a/contracts/EmergencyPriceFeed.sol +++ b/contracts/EmergencyPriceFeed.sol @@ -7,10 +7,10 @@ import { IUniswapV3Pool } from "@uniswap/v3-core/contracts/interfaces/IUniswapV3 import { FixedPoint96 } from "@uniswap/v3-core/contracts/libraries/FixedPoint96.sol"; import { FullMath } from "@uniswap/v3-core/contracts/libraries/FullMath.sol"; import { TickMath } from "@uniswap/v3-core/contracts/libraries/TickMath.sol"; -import { IPriceFeed } from "./interface/IPriceFeed.sol"; +import { IPriceFeedV2 } from "./interface/IPriceFeedV2.sol"; import { BlockContext } from "./base/BlockContext.sol"; -contract EmergencyPriceFeed is IPriceFeed, BlockContext { +contract EmergencyPriceFeed is IPriceFeedV2, BlockContext { using Address for address; // diff --git a/contracts/interface/IPriceFeed.sol b/contracts/interface/IPriceFeed.sol index caf2de3..391429b 100644 --- a/contracts/interface/IPriceFeed.sol +++ b/contracts/interface/IPriceFeed.sol @@ -2,10 +2,6 @@ pragma solidity 0.7.6; interface IPriceFeed { - /// @dev Returns the cached index price of the token. - /// @param interval The interval represents twap interval. - function cacheTwap(uint256 interval) external returns (uint256); - function decimals() external view returns (uint8); /// @dev Returns the index price of the token. diff --git a/contracts/interface/IPriceFeedV2.sol b/contracts/interface/IPriceFeedV2.sol new file mode 100644 index 0000000..9e2757f --- /dev/null +++ b/contracts/interface/IPriceFeedV2.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.7.6; + +import "./IPriceFeed.sol"; + +interface IPriceFeedV2 is IPriceFeed { + /// @dev Returns the cached index price of the token. + /// @param interval The interval represents twap interval. + function cacheTwap(uint256 interval) external returns (uint256); +} diff --git a/contracts/test/TestPriceFeed.sol b/contracts/test/TestPriceFeed.sol index e1d9a6b..8737059 100644 --- a/contracts/test/TestPriceFeed.sol +++ b/contracts/test/TestPriceFeed.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity 0.7.6; -import { IPriceFeed } from "../interface/IPriceFeed.sol"; +import { IPriceFeedV2 } from "../interface/IPriceFeedV2.sol"; contract TestPriceFeed { address public chainlink; @@ -20,30 +20,30 @@ contract TestPriceFeed { // function fetchChainlinkPrice(uint256 interval) external { for (uint256 i = 0; i < 17; i++) { - IPriceFeed(chainlink).getPrice(interval); + IPriceFeedV2(chainlink).getPrice(interval); } - currentPrice = IPriceFeed(chainlink).getPrice(interval); + currentPrice = IPriceFeedV2(chainlink).getPrice(interval); } function fetchBandProtocolPrice(uint256 interval) external { for (uint256 i = 0; i < 17; i++) { - IPriceFeed(bandProtocol).getPrice(interval); + IPriceFeedV2(bandProtocol).getPrice(interval); } - currentPrice = IPriceFeed(bandProtocol).getPrice(interval); + currentPrice = IPriceFeedV2(bandProtocol).getPrice(interval); } function cachedChainlinkPrice(uint256 interval) external { for (uint256 i = 0; i < 17; i++) { - IPriceFeed(chainlink).cacheTwap(interval); + IPriceFeedV2(chainlink).cacheTwap(interval); } - currentPrice = IPriceFeed(chainlink).cacheTwap(interval); + currentPrice = IPriceFeedV2(chainlink).cacheTwap(interval); } function cachedBandProtocolPrice(uint256 interval) external { for (uint256 i = 0; i < 17; i++) { - IPriceFeed(bandProtocol).cacheTwap(interval); + IPriceFeedV2(bandProtocol).cacheTwap(interval); } - currentPrice = IPriceFeed(bandProtocol).cacheTwap(interval); + currentPrice = IPriceFeedV2(bandProtocol).cacheTwap(interval); } // @@ -53,7 +53,7 @@ contract TestPriceFeed { // having this function for testing getPrice() and cacheTwap() // timestamp moves if any txs happen in hardhat env and which causes cacheTwap() will recalculate all the time function getPrice(uint256 interval) external returns (uint256 twap, uint256 cachedTwap) { - twap = IPriceFeed(bandProtocol).getPrice(interval); - cachedTwap = IPriceFeed(bandProtocol).cacheTwap(interval); + twap = IPriceFeedV2(bandProtocol).getPrice(interval); + cachedTwap = IPriceFeedV2(bandProtocol).cacheTwap(interval); } } diff --git a/test/CachedTwap.spec.ts b/test/CachedTwap.spec.ts index d7c599c..8df47e1 100644 --- a/test/CachedTwap.spec.ts +++ b/test/CachedTwap.spec.ts @@ -1,13 +1,7 @@ import { expect } from "chai" import { parseEther } from "ethers/lib/utils" import { ethers, waffle } from "hardhat" -import { - BandPriceFeed, - ChainlinkPriceFeedWithCachedTwap, - TestAggregatorV3, - TestPriceFeed, - TestStdReference, -} from "../typechain" +import { BandPriceFeed, ChainlinkPriceFeedV2, TestAggregatorV3, TestPriceFeed, TestStdReference } from "../typechain" interface PriceFeedFixture { bandPriceFeed: BandPriceFeed @@ -15,7 +9,7 @@ interface PriceFeedFixture { baseAsset: string // chainlinik - chainlinkPriceFeed: ChainlinkPriceFeedWithCachedTwap + chainlinkPriceFeed: ChainlinkPriceFeedV2 aggregator: TestAggregatorV3 } async function priceFeedFixture(): Promise { @@ -36,11 +30,11 @@ async function priceFeedFixture(): Promise { const testAggregatorFactory = await ethers.getContractFactory("TestAggregatorV3") const testAggregator = await testAggregatorFactory.deploy() - const chainlinkPriceFeedFactory = await ethers.getContractFactory("ChainlinkPriceFeedWithCachedTwap") + const chainlinkPriceFeedFactory = await ethers.getContractFactory("ChainlinkPriceFeedV2") const chainlinkPriceFeed = (await chainlinkPriceFeedFactory.deploy( testAggregator.address, twapInterval, - )) as ChainlinkPriceFeedWithCachedTwap + )) as ChainlinkPriceFeedV2 return { bandPriceFeed, bandReference: testStdReference, baseAsset, chainlinkPriceFeed, aggregator: testAggregator } } @@ -50,7 +44,7 @@ describe("Cached Twap Spec", () => { const loadFixture: ReturnType = waffle.createFixtureLoader([admin]) let bandPriceFeed: BandPriceFeed let bandReference: TestStdReference - let chainlinkPriceFeed: ChainlinkPriceFeedWithCachedTwap + let chainlinkPriceFeed: ChainlinkPriceFeedV2 let aggregator: TestAggregatorV3 let currentTime: number let testPriceFeed: TestPriceFeed diff --git a/test/ChainlinkPriceFeedWithCachedTwap.spec.ts b/test/ChainlinkPriceFeedV2.spec.ts similarity index 86% rename from test/ChainlinkPriceFeedWithCachedTwap.spec.ts rename to test/ChainlinkPriceFeedV2.spec.ts index b82957e..f6ccb38 100644 --- a/test/ChainlinkPriceFeedWithCachedTwap.spec.ts +++ b/test/ChainlinkPriceFeedV2.spec.ts @@ -2,10 +2,10 @@ import { FakeContract, smock } from "@defi-wonderland/smock" import { expect } from "chai" import { parseEther } from "ethers/lib/utils" import { ethers, waffle } from "hardhat" -import { ChainlinkPriceFeedWithCachedTwap, TestAggregatorV3 } from "../typechain" +import { ChainlinkPriceFeedV2, TestAggregatorV3 } from "../typechain" interface ChainlinkPriceFeedFixture { - chainlinkPriceFeed: ChainlinkPriceFeedWithCachedTwap + chainlinkPriceFeed: ChainlinkPriceFeedV2 aggregator: FakeContract } @@ -13,19 +13,16 @@ async function chainlinkPriceFeedFixture(): Promise { const aggregator = await smock.fake("TestAggregatorV3") aggregator.decimals.returns(() => 18) - const chainlinkPriceFeedFactory = await ethers.getContractFactory("ChainlinkPriceFeedWithCachedTwap") - const chainlinkPriceFeed = (await chainlinkPriceFeedFactory.deploy( - aggregator.address, - 900, - )) as ChainlinkPriceFeedWithCachedTwap + const chainlinkPriceFeedFactory = await ethers.getContractFactory("ChainlinkPriceFeedV2") + const chainlinkPriceFeed = (await chainlinkPriceFeedFactory.deploy(aggregator.address, 900)) as ChainlinkPriceFeedV2 return { chainlinkPriceFeed, aggregator } } -describe("ChainlinkPriceFeedWithCachedTwap Spec", () => { +describe("ChainlinkPriceFeedV2 Spec", () => { const [admin] = waffle.provider.getWallets() const loadFixture: ReturnType = waffle.createFixtureLoader([admin]) - let chainlinkPriceFeed: ChainlinkPriceFeedWithCachedTwap + let chainlinkPriceFeed: ChainlinkPriceFeedV2 let aggregator: FakeContract let currentTime: number let roundData: any[] diff --git a/test/PriceFeed.gas.test.ts b/test/PriceFeed.gas.test.ts index 1461f1e..457a3d4 100644 --- a/test/PriceFeed.gas.test.ts +++ b/test/PriceFeed.gas.test.ts @@ -1,12 +1,6 @@ import { parseEther } from "ethers/lib/utils" import { ethers, waffle } from "hardhat" -import { - BandPriceFeed, - ChainlinkPriceFeedWithCachedTwap, - TestAggregatorV3, - TestPriceFeed, - TestStdReference, -} from "../typechain" +import { BandPriceFeed, ChainlinkPriceFeedV2, TestAggregatorV3, TestPriceFeed, TestStdReference } from "../typechain" const twapInterval = 900 interface PriceFeedFixture { @@ -15,7 +9,7 @@ interface PriceFeedFixture { baseAsset: string // chainlinik - chainlinkPriceFeed: ChainlinkPriceFeedWithCachedTwap + chainlinkPriceFeed: ChainlinkPriceFeedV2 aggregator: TestAggregatorV3 } @@ -36,11 +30,11 @@ async function priceFeedFixture(): Promise { const testAggregatorFactory = await ethers.getContractFactory("TestAggregatorV3") const testAggregator = await testAggregatorFactory.deploy() - const chainlinkPriceFeedFactory = await ethers.getContractFactory("ChainlinkPriceFeedWithCachedTwap") + const chainlinkPriceFeedFactory = await ethers.getContractFactory("ChainlinkPriceFeedV2") const chainlinkPriceFeed = (await chainlinkPriceFeedFactory.deploy( testAggregator.address, twapInterval, - )) as ChainlinkPriceFeedWithCachedTwap + )) as ChainlinkPriceFeedV2 return { bandPriceFeed, bandReference: testStdReference, baseAsset, chainlinkPriceFeed, aggregator: testAggregator } } @@ -50,7 +44,7 @@ describe.skip("Price feed gas test", () => { const loadFixture: ReturnType = waffle.createFixtureLoader([admin]) let bandPriceFeed: BandPriceFeed let bandReference: TestStdReference - let chainlinkPriceFeed: ChainlinkPriceFeedWithCachedTwap + let chainlinkPriceFeed: ChainlinkPriceFeedV2 let aggregator: TestAggregatorV3 let currentTime: number let testPriceFeed: TestPriceFeed From 9a6f23802d84ece726647aee53d27997173544f3 Mon Sep 17 00:00:00 2001 From: Miya Chen Date: Tue, 24 May 2022 10:29:26 +0800 Subject: [PATCH 7/7] chore: udpate license to GPL-3.0-or-later --- contracts/ChainlinkPriceFeed.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/ChainlinkPriceFeed.sol b/contracts/ChainlinkPriceFeed.sol index 6d24e6b..8d5c5e6 100644 --- a/contracts/ChainlinkPriceFeed.sol +++ b/contracts/ChainlinkPriceFeed.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: MIT License +// SPDX-License-Identifier: GPL-3.0-or-later pragma solidity 0.7.6; import { Address } from "@openzeppelin/contracts/utils/Address.sol";