Skip to content

Commit

Permalink
Merge pull request #29 from perpetual-protocol/feat/add-back-original…
Browse files Browse the repository at this point in the history
…-chainlink-priceFeed

add back original ChainlinkPriceFeed
  • Loading branch information
tailingchen authored May 24, 2022
2 parents 12e6dbe + 9a6f238 commit 4dfb247
Show file tree
Hide file tree
Showing 12 changed files with 377 additions and 70 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `ChainlinkPriceFeedV2`.
- 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`
Expand Down
4 changes: 2 additions & 2 deletions contracts/BandPriceFeed.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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;

//
Expand Down
71 changes: 52 additions & 19 deletions contracts/ChainlinkPriceFeed.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,50 +2,83 @@
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 { CachedTwap } from "./twap/CachedTwap.sol";

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

AggregatorV3Interface private immutable _aggregator;

constructor(AggregatorV3Interface aggregator, uint80 cacheTwapInterval) CachedTwap(cacheTwapInterval) {
constructor(AggregatorV3Interface aggregator) {
// CPF_ANC: Aggregator address is not contract
require(address(aggregator).isContract(), "CPF_ANC");

_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 cacheTwap(uint256 interval) external override returns (uint256) {
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 (interval == 0 || round == 0) {
// 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;
}
return _cacheTwap(interval, latestPrice, latestTimestamp);
}

function decimals() external view override returns (uint8) {
return _aggregator.decimals();
}
// 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);
}

function getPrice(uint256 interval) external view override returns (uint256) {
(uint80 round, uint256 latestPrice, uint256 latestTimestamp) = _getLatestRoundData();
round = round - 1;
(, uint256 currentPrice, uint256 currentTimestamp) = _getRoundData(round);

if (interval == 0 || round == 0) {
return latestPrice;
// 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 _getCachedTwap(interval, latestPrice, latestTimestamp);
return weightedPrice == 0 ? latestPrice : weightedPrice.div(interval);
}

function _getLatestRoundData()
Expand Down
91 changes: 91 additions & 0 deletions contracts/ChainlinkPriceFeedV2.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
// SPDX-License-Identifier: GPL-3.0-or-later
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 { IPriceFeedV2 } from "./interface/IPriceFeedV2.sol";
import { BlockContext } from "./base/BlockContext.sol";
import { CachedTwap } from "./twap/CachedTwap.sol";

contract ChainlinkPriceFeedV2 is IPriceFeedV2, BlockContext, CachedTwap {
using Address for address;

AggregatorV3Interface private immutable _aggregator;

constructor(AggregatorV3Interface aggregator, uint80 cacheTwapInterval) CachedTwap(cacheTwapInterval) {
// CPF_ANC: Aggregator address is not contract
require(address(aggregator).isContract(), "CPF_ANC");

_aggregator = aggregator;
}

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

function cacheTwap(uint256 interval) external override returns (uint256) {
(uint80 round, uint256 latestPrice, uint256 latestTimestamp) = _getLatestRoundData();

if (interval == 0 || round == 0) {
return latestPrice;
}
return _cacheTwap(interval, latestPrice, latestTimestamp);
}

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

function getPrice(uint256 interval) external view override returns (uint256) {
(uint80 round, uint256 latestPrice, uint256 latestTimestamp) = _getLatestRoundData();

if (interval == 0 || round == 0) {
return latestPrice;
}

return _getCachedTwap(interval, latestPrice, latestTimestamp);
}

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");
}
}
4 changes: 2 additions & 2 deletions contracts/EmergencyPriceFeed.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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;

//
Expand Down
4 changes: 0 additions & 4 deletions contracts/interface/IPriceFeed.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
10 changes: 10 additions & 0 deletions contracts/interface/IPriceFeedV2.sol
Original file line number Diff line number Diff line change
@@ -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);
}
22 changes: 11 additions & 11 deletions contracts/test/TestPriceFeed.sol
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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);
}

//
Expand All @@ -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);
}
}
10 changes: 5 additions & 5 deletions test/CachedTwap.spec.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
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, ChainlinkPriceFeedV2, TestAggregatorV3, TestPriceFeed, TestStdReference } from "../typechain"

interface PriceFeedFixture {
bandPriceFeed: BandPriceFeed
bandReference: TestStdReference
baseAsset: string

// chainlinik
chainlinkPriceFeed: ChainlinkPriceFeed
chainlinkPriceFeed: ChainlinkPriceFeedV2
aggregator: TestAggregatorV3
}
async function priceFeedFixture(): Promise<PriceFeedFixture> {
Expand All @@ -30,11 +30,11 @@ async function priceFeedFixture(): Promise<PriceFeedFixture> {
const testAggregatorFactory = await ethers.getContractFactory("TestAggregatorV3")
const testAggregator = await testAggregatorFactory.deploy()

const chainlinkPriceFeedFactory = await ethers.getContractFactory("ChainlinkPriceFeed")
const chainlinkPriceFeedFactory = await ethers.getContractFactory("ChainlinkPriceFeedV2")
const chainlinkPriceFeed = (await chainlinkPriceFeedFactory.deploy(
testAggregator.address,
twapInterval,
)) as ChainlinkPriceFeed
)) as ChainlinkPriceFeedV2

return { bandPriceFeed, bandReference: testStdReference, baseAsset, chainlinkPriceFeed, aggregator: testAggregator }
}
Expand All @@ -44,7 +44,7 @@ describe("Cached Twap Spec", () => {
const loadFixture: ReturnType<typeof waffle.createFixtureLoader> = waffle.createFixtureLoader([admin])
let bandPriceFeed: BandPriceFeed
let bandReference: TestStdReference
let chainlinkPriceFeed: ChainlinkPriceFeed
let chainlinkPriceFeed: ChainlinkPriceFeedV2
let aggregator: TestAggregatorV3
let currentTime: number
let testPriceFeed: TestPriceFeed
Expand Down
Loading

0 comments on commit 4dfb247

Please sign in to comment.