From e2387470a4f4cddeaa8641ddd73464b0f3e72b49 Mon Sep 17 00:00:00 2001 From: evandrosaturnino Date: Thu, 16 May 2024 03:51:36 -0300 Subject: [PATCH 1/3] feat: add pricefeed base for redstone adapter --- .../contracts/BOB/IRedstoneAdapter.sol | 36 ++++ .../contracts/BOB/MockRedstoneAdapterBase.sol | 90 ++++++++++ .../contracts/contracts/BOB/PriceFeedBase.sol | 161 ++++++++++++++++++ 3 files changed, 287 insertions(+) create mode 100644 packages/contracts/contracts/BOB/IRedstoneAdapter.sol create mode 100644 packages/contracts/contracts/BOB/MockRedstoneAdapterBase.sol create mode 100644 packages/contracts/contracts/BOB/PriceFeedBase.sol diff --git a/packages/contracts/contracts/BOB/IRedstoneAdapter.sol b/packages/contracts/contracts/BOB/IRedstoneAdapter.sol new file mode 100644 index 000000000..c99a863b0 --- /dev/null +++ b/packages/contracts/contracts/BOB/IRedstoneAdapter.sol @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.17; + +/** + * @title Interface of RedStone adapter + * @author The Redstone Oracles team + */ +interface IRedstoneAdapter { + + /** + * @notice Returns the latest properly reported value of the data feed + * @return value The latest value of the given data feed + */ + function getValueForDataFeed(bytes32) external view returns (uint256); + + /** + * @notice Returns details for the given round and data feed + * @return dataFeedValue + * @return roundDataTimestamp + * @return roundBlockTimestamp + */ + function getRoundDataFromAdapter(bytes32, uint256) external view returns (uint256 dataFeedValue, uint128 roundDataTimestamp, uint128 roundBlockTimestamp); + + /** + * @notice Returns the latest properly reported round id + * @return value The latest value of the round id + */ + function getLatestRoundId() external view returns (uint256); + + /** + * @notice Returns block timestamp of the latest successful update + * @return blockTimestamp The block timestamp of the latest successful update + */ + function getBlockTimestampFromLatestUpdate() external view returns (uint256 blockTimestamp); +} diff --git a/packages/contracts/contracts/BOB/MockRedstoneAdapterBase.sol b/packages/contracts/contracts/BOB/MockRedstoneAdapterBase.sol new file mode 100644 index 000000000..9566c376a --- /dev/null +++ b/packages/contracts/contracts/BOB/MockRedstoneAdapterBase.sol @@ -0,0 +1,90 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.17; + +import "./IRedstoneAdapter.sol"; + +contract MockRedstoneAdapterBase is IRedstoneAdapter { + uint256 private latestPrice; + uint256 private previousPrice; + uint256 private latestRoundId; + uint256 private blockTimestampFromLatestUpdate; + uint128 private blockTimestampFromPreviousUpdate; + + bool latestRevert; + bool previousRevert; + + // --- Setter Functions --- + + function setPrice(uint256 _price) external { + latestPrice = _price; + } + + function setPrevPrice(uint256 _price) external { + previousPrice = _price; + } + + function setUpdateTime(uint256 _blockTimestampFromLatestUpdate) external { + blockTimestampFromLatestUpdate = _blockTimestampFromLatestUpdate; + } + + function setPrevTime(uint128 _blockTimestampFromPreviousUpdate) external { + blockTimestampFromPreviousUpdate = _blockTimestampFromPreviousUpdate; + } + + function setLatestRoundId(uint256 _latestRoundId) external { + latestRoundId = _latestRoundId; + } + + function setLatestRevert() external { + latestRevert = !latestRevert; + } + + function setPrevRevert() external { + previousRevert = !previousRevert; + } + + // --- Getters that adhere to the RedstoneAdapter interface --- + + /** + * @notice Returns the latest properly reported value of the data feed + * @return value The latest value of the given data feed + */ + function getValueForDataFeed(bytes32) public view returns (uint256) { + if (latestRevert) {require( 1== 0, "getRoundData reverted");} + return latestPrice; + } + + /** + * @notice Returns details for the given round and data feed + * @return dataFeedValue + * @return roundDataTimestamp + * @return roundBlockTimestamp + */ + function getRoundDataFromAdapter(bytes32, uint256) public view returns ( + uint256 dataFeedValue, + uint128 roundDataTimestamp, + uint128 roundBlockTimestamp + ) { + if (previousRevert) {require( 1== 0, "getRoundData reverted");} + dataFeedValue = previousPrice; + roundDataTimestamp = blockTimestampFromPreviousUpdate; + roundBlockTimestamp = blockTimestampFromPreviousUpdate; + } + + /** + * @notice Returns block timestamp of the latest successful update + * @return blockTimestamp The block timestamp of the latest successful update + */ + function getBlockTimestampFromLatestUpdate() public view returns (uint256) { + return blockTimestampFromLatestUpdate; + } + + /** + * @notice Returns latest successful round number + * @return latestRoundId + */ + function getLatestRoundId() public view returns (uint256) { + return latestRoundId; + } +} diff --git a/packages/contracts/contracts/BOB/PriceFeedBase.sol b/packages/contracts/contracts/BOB/PriceFeedBase.sol new file mode 100644 index 000000000..d3fcfd21a --- /dev/null +++ b/packages/contracts/contracts/BOB/PriceFeedBase.sol @@ -0,0 +1,161 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.17; + +import "./IRedstoneAdapter.sol"; +import "../Dependencies/CheckContract.sol"; +import "../Dependencies/AggregatorV3Interface.sol"; + +/** + * @title Main logic of the price feed contract + * @author The Redstone Oracles team + * @dev Implementation of common functions for the PriceFeed contract + * that queries data from the specified PriceFeedAdapter + * + * It can be used by projects that have already implemented with Chainlink-like + * price feeds and would like to minimise changes in their existing codebase. + * + * If you are flexible, it's much better (and cheaper in terms of gas) to query + * the PriceFeedAdapter contract directly + */ +contract PriceFeedBase is AggregatorV3Interface, CheckContract { + uint256 internal constant UINT80_MAX = uint256(type(uint80).max); + uint256 internal constant INT256_MAX = uint256(type(int256).max); + + IRedstoneAdapter public immutable redstoneAdapter; + bytes32 public immutable dataFeedId; + + error UnsafeUintToIntConversion(uint256 value); + error UnsafeUint256ToUint80Conversion(uint256 value); + + constructor (address _redstoneAdapterAddress, bytes32 _dataFeedId) { + checkContract(_redstoneAdapterAddress); + + redstoneAdapter = IRedstoneAdapter(_redstoneAdapterAddress); + dataFeedId = _dataFeedId; + } + + /** + * @notice Returns the number of decimals for the price feed + * @dev By default, RedStone uses 8 decimals for data feeds + * @return decimals The number of decimals in the price feed values + */ + function decimals() public virtual pure override returns (uint8) { + return 8; + } + + /** + * @notice Description of the Price Feed + * @return description + */ + function description() public view virtual override returns (string memory) { + return "Redstone Price Feed"; + } + + /** + * @notice Version of the Price Feed + * @dev Currently it has no specific motivation and was added + * only to be compatible with the Chainlink interface + * @return version + */ + function version() public virtual pure override returns (uint256) { + return 1; + } + + /** + * @notice Returns details for the given round + * @param _requestedRoundId Requested round identifier + */ + function getRoundData(uint80 _requestedRoundId) public view virtual returns ( + uint80 roundId, + int256 answer, + uint256 startedAt, + uint256 updatedAt, + uint80 answeredInRound + ) { + ( + uint256 dataFeedValue, + uint128 roundDataTimestamp, + uint128 roundBlockTimestamp + ) = redstoneAdapter.getRoundDataFromAdapter(dataFeedId, _requestedRoundId); + + roundId = _requestedRoundId; + + if (dataFeedValue > INT256_MAX) { + revert UnsafeUintToIntConversion(dataFeedValue); + } + + answer = int256(dataFeedValue); + startedAt = roundDataTimestamp; + updatedAt = roundBlockTimestamp; + + // We want to be compatible with Chainlink's interface + // And in our case the roundId is always equal to answeredInRound + answeredInRound = _requestedRoundId; + } + + /** + * @notice Returns details of the latest successful update round + * @dev It uses few helpful functions to abstract logic of getting + * latest round id and value + * @return roundId The number of the latest round + * @return answer The latest reported value + * @return startedAt Block timestamp when the latest successful round started + * @return updatedAt Block timestamp of the latest successful round + * @return answeredInRound The number of the latest round + */ + function latestRoundData() + public + view + override + virtual + returns ( + uint80 roundId, + int256 answer, + uint256 startedAt, + uint256 updatedAt, + uint80 answeredInRound + ) + { + roundId = _latestRound(); + answer = _latestAnswer(); + + uint256 blockTimestamp = redstoneAdapter.getBlockTimestampFromLatestUpdate(); + + // These values are equal after chainlink’s OCR update + startedAt = blockTimestamp; + updatedAt = blockTimestamp; + + // We want to be compatible with Chainlink's interface + // And in our case the roundId is always equal to answeredInRound + answeredInRound = roundId; + } + + /** + * @notice Chainlink function for getting the latest successfully reported value + * @return latestAnswer The latest successfully reported value + */ + function _latestAnswer() internal view returns (int256) { + uint256 uintAnswer = redstoneAdapter.getValueForDataFeed(dataFeedId); + + if (uintAnswer > INT256_MAX) { + revert UnsafeUintToIntConversion(uintAnswer); + } + + return int256(uintAnswer); + } + + /** + * @notice Chainlink function for getting the number of latest round + * @return latestRound The number of the latest update round + */ + function _latestRound() internal view returns (uint80) { + uint256 roundIdUint256 = redstoneAdapter.getLatestRoundId(); + + if (roundIdUint256 > UINT80_MAX) { + revert UnsafeUint256ToUint80Conversion(roundIdUint256); + } + + return uint80(roundIdUint256); + } +} From 6f2931a5ffc75ea692edf9a29d68993015f03a9e Mon Sep 17 00:00:00 2001 From: evandrosaturnino Date: Thu, 16 May 2024 03:52:16 -0300 Subject: [PATCH 2/3] refactor: update price feed tests to reflect redstone changes --- .../contracts/test/BOB/PriceFeedBaseTest.js | 110 + packages/contracts/test/PriceFeedTest.js | 2350 +++++++++++++++++ 2 files changed, 2460 insertions(+) create mode 100644 packages/contracts/test/BOB/PriceFeedBaseTest.js diff --git a/packages/contracts/test/BOB/PriceFeedBaseTest.js b/packages/contracts/test/BOB/PriceFeedBaseTest.js new file mode 100644 index 000000000..f5cab2dde --- /dev/null +++ b/packages/contracts/test/BOB/PriceFeedBaseTest.js @@ -0,0 +1,110 @@ +const PriceFeedBase = artifacts.require("./PriceFeedBase.sol") +const MockRedstoneAdapterBase = artifacts.require("./MockRedstoneAdapterBase.sol") + +const testHelpers = require("../../utils/testHelpers.js") +const th = testHelpers.TestHelper + +const { dec } = th +const priceFeedBaseDataFeedId = ethers.utils.formatBytes32String("BTC"); + +contract('PriceFeedBase', async () => { + + let priceFeedBase + let mockRedstoneAdapterBase + let now + let previousUpdate + + beforeEach(async () => { + mockRedstoneAdapterBase = await MockRedstoneAdapterBase.new() + MockRedstoneAdapterBase.setAsDeployed(mockRedstoneAdapterBase) + + priceFeedBase = await PriceFeedBase.new(mockRedstoneAdapterBase.address, priceFeedBaseDataFeedId) + PriceFeedBase.setAsDeployed(priceFeedBase) + + // Set Chainlink latest and prev round Id's to non-zero + await mockRedstoneAdapterBase.setLatestRoundId(3) + + //Set current and prev prices in both oracles + await mockRedstoneAdapterBase.setPrice(dec(10, 8)) + await mockRedstoneAdapterBase.setPrevPrice(dec(5, 8)) + + // Set mock price updateTimes in both oracles to very recent + now = await th.getLatestBlockTimestamp(web3) + previousUpdate = now - 1 + + await mockRedstoneAdapterBase.setUpdateTime(now) + await mockRedstoneAdapterBase.setPrevTime(previousUpdate) + }) + + describe('Tests for getting the price feed details', async accounts => { + it("should properly get the price feed adapter", async () => { + const redstoneAdapter = await priceFeedBase.redstoneAdapter() + assert.equal(redstoneAdapter, mockRedstoneAdapterBase.address) + }) + + it("should properly get data feed id", async () => { + const dataFeedId = await priceFeedBase.dataFeedId() + assert.equal(dataFeedId, ethers.utils.formatBytes32String("BTC")) + }) + + it("should properly get the decimals", async () => { + const decimals = await priceFeedBase.decimals() + assert.equal(decimals, 8) + }) + + it("should properly get the description", async () => { + const description = await priceFeedBase.description() + assert.equal(description, "Redstone Price Feed") + }) + + it("should properly get the version", async () => { + const version = await priceFeedBase.version() + assert.equal(version, 1) + }) + }) + + describe('Tests for getting the price feed values', async () => { + it("should properly get latest round data", async () => { + let latestRoundData = await priceFeedBase.latestRoundData() + assert.equal(latestRoundData.roundId, 3) + assert.equal(latestRoundData.answer, dec(10, 8)) + assert.equal(latestRoundData.startedAt, now) + assert.equal(latestRoundData.updatedAt, now) + assert.equal(latestRoundData.answeredInRound, 3) + + let updatedTime = await th.getLatestBlockTimestamp(web3) + + await mockRedstoneAdapterBase.setLatestRoundId(4) + await mockRedstoneAdapterBase.setPrice(dec(12, 8)) + await mockRedstoneAdapterBase.setUpdateTime(updatedTime) + + latestRoundData = await priceFeedBase.latestRoundData() + assert.equal(latestRoundData.roundId, 4) + assert.equal(latestRoundData.answer, dec(12, 8)) + assert.equal(latestRoundData.startedAt, updatedTime) + assert.equal(latestRoundData.updatedAt, updatedTime) + assert.equal(latestRoundData.answeredInRound, 4) + }) + + it("should properly get a previous round data", async () => { + let previousRoundData = await priceFeedBase.getRoundData(2) + assert.equal(previousRoundData.roundId, 2) + assert.equal(previousRoundData.answer, dec(5, 8)) + assert.equal(previousRoundData.startedAt, previousUpdate) + assert.equal(previousRoundData.updatedAt, previousUpdate) + assert.equal(previousRoundData.answeredInRound, 2) + + let updatedPreviousTime = now + + await mockRedstoneAdapterBase.setPrevPrice(dec(10, 8)) + await mockRedstoneAdapterBase.setPrevTime(updatedPreviousTime) + + previousRoundData = await priceFeedBase.getRoundData(3) + assert.equal(previousRoundData.roundId, 3) + assert.equal(previousRoundData.answer, dec(10, 8)) + assert.equal(previousRoundData.startedAt, updatedPreviousTime) + assert.equal(previousRoundData.updatedAt, updatedPreviousTime) + assert.equal(previousRoundData.answeredInRound, 3) + }) + }) +}) diff --git a/packages/contracts/test/PriceFeedTest.js b/packages/contracts/test/PriceFeedTest.js index fe341c612..9df9cc993 100644 --- a/packages/contracts/test/PriceFeedTest.js +++ b/packages/contracts/test/PriceFeedTest.js @@ -5,6 +5,8 @@ const MockChainlink = artifacts.require("./MockAggregator.sol") const MockTellor = artifacts.require("./MockTellor.sol") const BrokenMockTellor = artifacts.require("./MockTellor.sol") const TellorCaller = artifacts.require("./TellorCaller.sol") +const PriceFeedBase = artifacts.require("./PriceFeedBase.sol") +const MockRedstoneAdapterBase = artifacts.require("./MockRedstoneAdapterBase.sol") const testHelpers = require("../utils/testHelpers.js") const th = testHelpers.TestHelper @@ -16,6 +18,8 @@ const queryDataArgs = abiCoder.encode(["string", "string"], ["erc20", "usd"]); const queryData = abiCoder.encode(["string", "bytes"], ["SpotPrice", queryDataArgs]); const queryId = ethers.utils.keccak256(queryData); +const priceFeedBaseDataFeedId = ethers.utils.formatBytes32String("BTC"); + contract('PriceFeed', async accounts => { const [owner, alice] = accounts; @@ -2470,6 +2474,2352 @@ contract('PriceFeed', async accounts => { }) } + context("when Price feed base is being used as price aggregator", () => { + let priceFeed + let priceFeedBase + let mockRedstoneAdapterBase + let mockTellor + let tellorCaller + + const tellorDigits = 18 + + const setAddresses = async () => { + await priceFeed.setAddresses(priceFeedBase.address, tellorCaller.address, { from: owner }) + } + + async function setTellorPrice(price) { + // Tests set price with 6 digits, adjust it if tellorDigits bigger than that + adjustedPrice = dec(price, tellorDigits - 6) + let valueBytes = abiCoder.encode(["uint256"], [adjustedPrice]); + await mockTellor.submitValue(queryId, valueBytes, 0, queryData) + await th.fastForwardTime(15 * 60 + 1, web3.currentProvider) // 15 minutes + } + + beforeEach(async () => { + priceFeed = await PriceFeed.new(tellorDigits) + PriceFeed.setAsDeployed(priceFeed) + + mockRedstoneAdapterBase = await MockRedstoneAdapterBase.new() + MockRedstoneAdapterBase.setAsDeployed(mockRedstoneAdapterBase) + + priceFeedBase = await PriceFeedBase.new(mockRedstoneAdapterBase.address, priceFeedBaseDataFeedId) + PriceFeedBase.setAsDeployed(priceFeedBase) + + mockTellor = await MockTellor.new(queryData) + MockTellor.setAsDeployed(mockTellor) + + tellorCaller = await TellorCaller.new(mockTellor.address, queryId) + TellorCaller.setAsDeployed(tellorCaller) + + // Set Chainlink latest and prev round Id's to non-zero + await mockRedstoneAdapterBase.setLatestRoundId(3) + + //Set current and prev prices in both oracles + await mockRedstoneAdapterBase.setPrice(dec(10, 8)) + await mockRedstoneAdapterBase.setPrevPrice(dec(5, 8)) + + // Set mock price updateTimes in both oracles to very recent + now = await th.getLatestBlockTimestamp(web3) + previousUpdate = now - 1 + + await mockRedstoneAdapterBase.setUpdateTime(now) + await mockRedstoneAdapterBase.setPrevTime(previousUpdate) + await setTellorPrice(dec(100, 18)) + }) + + describe('Mainnet PriceFeed setup', async accounts => { + it("setAddresses should fail whe called by nonOwner", async () => { + await assertRevert( + priceFeed.setAddresses(priceFeedBase.address, mockTellor.address, { from: alice }), + "Ownable: caller is not the owner" + ) + }) + + it("setAddresses should fail after address has already been set", async () => { + // Owner can successfully set any address + const txOwner = await priceFeed.setAddresses(priceFeedBase.address, mockTellor.address, { from: owner }) + assert.isTrue(txOwner.receipt.status) + + await assertRevert( + priceFeed.setAddresses(priceFeedBase.address, mockTellor.address, { from: owner }), + "PriceFeed: contacts already set" + ) + + await assertRevert( + priceFeed.setAddresses(priceFeedBase.address, mockTellor.address, { from: alice }), + "Ownable: caller is not the owner" + ) + }) + }) + + it("C1 Chainlink working: fetchPrice should return the correct price, taking into account the number of decimal digits on the aggregator", async () => { + await setAddresses() + + // Oracle price price is 10.00000000 + await mockRedstoneAdapterBase.setPrevPrice(dec(1, 9)) + await mockRedstoneAdapterBase.setPrice(dec(1, 9)) + await priceFeed.fetchPrice() + let price = await priceFeed.lastGoodPrice() + // Check Liquity PriceFeed gives 10, with 18 digit precision + assert.equal(price, dec(10, 18)) + + // Oracle price is 1e9 + await mockRedstoneAdapterBase.setPrevPrice(dec(1, 18)) + await mockRedstoneAdapterBase.setPrice(dec(1, 18)) + await priceFeed.fetchPrice() + price = await priceFeed.lastGoodPrice() + // Check Liquity PriceFeed gives 1e9, with 18 digit precision + assert.isTrue(price.eq(toBN(dec(1, 28)))) + + // Oracle price is 0.0001 + await mockRedstoneAdapterBase.setPrevPrice(dec(1, 4)) + await mockRedstoneAdapterBase.setPrice(dec(1, 4)) + await priceFeed.fetchPrice() + price = await priceFeed.lastGoodPrice() + // Check Liquity PriceFeed gives 0.0001 with 18 digit precision + assert.isTrue(price.eq(toBN(dec(1, 14)))) + + // Oracle price is 1.23456789 + await mockRedstoneAdapterBase.setPrevPrice(dec(123456789)) + await mockRedstoneAdapterBase.setPrice(dec(123456789)) + await priceFeed.fetchPrice() + price = await priceFeed.lastGoodPrice() + // Check Liquity PriceFeed gives the value with 18 digit precision + assert.equal(price, '1234567890000000000') + }) + + // --- Chainlink breaks --- + it("C1 Chainlink breaks, Tellor working: fetchPrice should return the correct Tellor price, taking into account Tellor's 6-digit granularity", async () => { + await setAddresses() + // --- Chainlink fails, system switches to Tellor --- + const statusBefore = await priceFeed.status() + assert.equal(statusBefore, '0') // status 0: Chainlink working + + // Chainlink breaks with negative price + await mockRedstoneAdapterBase.setPrevPrice(dec(1, 8)) + await mockRedstoneAdapterBase.setPrice(dec(1, 4)) + + await setTellorPrice(dec(123, 6)) + await mockRedstoneAdapterBase.setUpdateTime(0) + + const priceFetchTx = await priceFeed.fetchPrice() + const statusAfter = await priceFeed.status() + assert.equal(statusAfter, '1') // status 1: using Tellor, Chainlink untrusted + + let price = await priceFeed.lastGoodPrice() + assert.equal(price, dec(123, 18)) + + // Tellor price is 10 at 6-digit precision + await setTellorPrice(dec(10, 6)) + await priceFeed.fetchPrice() + price = await priceFeed.lastGoodPrice() + // Check Liquity PriceFeed gives 10, with 18 digit precision + assert.equal(price, dec(10, 18)) + + // Tellor price is 1e9 at 6-digit precision + await setTellorPrice(dec(1, 15)) + await priceFeed.fetchPrice() + price = await priceFeed.lastGoodPrice() + // Check Liquity PriceFeed gives 1e9, with 18 digit precision + assert.equal(price, dec(1, 27)) + + // Tellor price is 0.0001 at 6-digit precision + await setTellorPrice(100) + await priceFeed.fetchPrice() + price = await priceFeed.lastGoodPrice() + // Check Liquity PriceFeed gives 0.0001 with 18 digit precision + + assert.equal(price, dec(1, 14)) + + // Tellor price is 1234.56789 at 6-digit precision + await setTellorPrice(dec(1234567890)) + await priceFeed.fetchPrice() + price = await priceFeed.lastGoodPrice() + // Check Liquity PriceFeed gives 0.0001 with 18 digit precision + assert.equal(price, '1234567890000000000000') + }) + + it("C1 chainlinkWorking: Chainlink broken by zero latest roundId, Tellor working: switch to usingChainlinkTellorUntrusted", async () => { + await setAddresses() + const statusBefore = await priceFeed.status() + assert.equal(statusBefore, '0') // status 0: Chainlink working + + await mockRedstoneAdapterBase.setPrevPrice(dec(999, 8)) + await mockRedstoneAdapterBase.setPrice(dec(999, 8)) + await priceFeed.setLastGoodPrice(dec(999, 18)) + + await setTellorPrice(dec(123, 6)) + await mockRedstoneAdapterBase.setLatestRoundId(0) + + const priceFetchTx = await priceFeed.fetchPrice() + const statusAfter = await priceFeed.status() + assert.equal(statusAfter, '1') // status 1: using Tellor, Chainlink untrusted + }) + + it("C1 chainlinkWorking: Chainlink broken by zero timestamp, Tellor working, switch to usingChainlinkTellorUntrusted", async () => { + await setAddresses() + const statusBefore = await priceFeed.status() + assert.equal(statusBefore, '0') // status 0: Chainlink working + + await mockRedstoneAdapterBase.setPrevPrice(dec(999, 8)) + await mockRedstoneAdapterBase.setPrice(dec(999, 8)) + await priceFeed.setLastGoodPrice(dec(999, 18)) + + await setTellorPrice(dec(123, 6)) + await mockRedstoneAdapterBase.setUpdateTime(0) + + const priceFetchTx = await priceFeed.fetchPrice() + const statusAfter = await priceFeed.status() + assert.equal(statusAfter, '1') // status 1: using Tellor, Chainlink untrusted + }) + + it("C1 chainlinkWorking: Chainlink broken by zero timestamp, Tellor working, return Tellor price", async () => { + await setAddresses() + const statusBefore = await priceFeed.status() + assert.equal(statusBefore, '0') // status 0: Chainlink working + + await mockRedstoneAdapterBase.setPrevPrice(dec(999, 8)) + await mockRedstoneAdapterBase.setPrice(dec(999, 8)) + await priceFeed.setLastGoodPrice(dec(999, 18)) + + await setTellorPrice(dec(123, 6)) + await mockRedstoneAdapterBase.setUpdateTime(0) + + const priceFetchTx = await priceFeed.fetchPrice() + + let price = await priceFeed.lastGoodPrice() + assert.equal(price, dec(123, 18)) + }) + + it("C1 chainlinkWorking: Chainlink broken by future timestamp, Tellor working, switch to usingChainlinkTellorUntrusted", async () => { + await setAddresses() + const statusBefore = await priceFeed.status() + assert.equal(statusBefore, '0') // status 0: Chainlink working + + await mockRedstoneAdapterBase.setPrevPrice(dec(999, 8)) + await mockRedstoneAdapterBase.setPrice(dec(999, 8)) + await priceFeed.setLastGoodPrice(dec(999, 18)) + + const now = await th.getLatestBlockTimestamp(web3) + const future = toBN(now).add(toBN('1000')) + + await setTellorPrice(dec(123, 6)) + await mockRedstoneAdapterBase.setUpdateTime(future) + + const priceFetchTx = await priceFeed.fetchPrice() + const statusAfter = await priceFeed.status() + assert.equal(statusAfter, '1') // status 1: using Tellor, Chainlink untrusted + }) + + it("C1 chainlinkWorking: Chainlink broken by future timestamp, Tellor working, return Tellor price", async () => { + await setAddresses() + const statusBefore = await priceFeed.status() + assert.equal(statusBefore, '0') // status 0: Chainlink working + + await mockRedstoneAdapterBase.setPrevPrice(dec(999, 8)) + await mockRedstoneAdapterBase.setPrice(dec(999, 8)) + await priceFeed.setLastGoodPrice(dec(999, 18)) + + const now = await th.getLatestBlockTimestamp(web3) + const future = toBN(now).add(toBN('1000')) + + await setTellorPrice(dec(123, 6)) + await mockRedstoneAdapterBase.setUpdateTime(future) + + const priceFetchTx = await priceFeed.fetchPrice() + + let price = await priceFeed.lastGoodPrice() + assert.equal(price, dec(123, 18)) + }) + + it("C1 chainlinkWorking: Chainlink broken by negative price, Tellor working, switch to usingChainlinkTellorUntrusted", async () => { + await setAddresses() + const statusBefore = await priceFeed.status() + assert.equal(statusBefore, '0') // status 0: Chainlink working + + await mockRedstoneAdapterBase.setPrevPrice(dec(999, 8)) + await priceFeed.setLastGoodPrice(dec(999, 18)) + + await setTellorPrice(dec(123, 6)) + await mockRedstoneAdapterBase.setPrice(dec(1, 8)) + + const priceFetchTx = await priceFeed.fetchPrice() + const statusAfter = await priceFeed.status() + assert.equal(statusAfter, '1') // status 1: using Tellor, Chainlink untrusted + }) + + it("C1 chainlinkWorking: Chainlink broken by negative price, Tellor working, return Tellor price", async () => { + await setAddresses() + const statusBefore = await priceFeed.status() + assert.equal(statusBefore, '0') // status 0: Chainlink working + + await mockRedstoneAdapterBase.setPrevPrice(dec(999, 8)) + await priceFeed.setLastGoodPrice(dec(999, 18)) + + await setTellorPrice(dec(123, 6)) + await mockRedstoneAdapterBase.setPrice(dec(1, 8)) + + const priceFetchTx = await priceFeed.fetchPrice() + + let price = await priceFeed.lastGoodPrice() + assert.equal(price, dec(123, 18)) + }) + + it("C1 chainlinkWorking: Chainlink broken - latest round call reverted, Tellor working, switch to usingChainlinkTellorUntrusted", async () => { + await setAddresses() + const statusBefore = await priceFeed.status() + assert.equal(statusBefore, '0') // status 0: Chainlink working + + await mockRedstoneAdapterBase.setPrevPrice(dec(999, 8)) + await mockRedstoneAdapterBase.setPrice(dec(999, 8)) + await priceFeed.setLastGoodPrice(dec(999, 18)) + + await setTellorPrice(dec(123, 6)) + await mockRedstoneAdapterBase.setLatestRevert() + + const priceFetchTx = await priceFeed.fetchPrice() + const statusAfter = await priceFeed.status() + assert.equal(statusAfter, '1') // status 1: using Tellor, Chainlink untrusted + }) + + it("C1 chainlinkWorking: latest round call reverted, Tellor working, return the Tellor price", async () => { + await setAddresses() + const statusBefore = await priceFeed.status() + assert.equal(statusBefore, '0') // status 0: Chainlink working + + await mockRedstoneAdapterBase.setPrevPrice(dec(999, 8)) + await mockRedstoneAdapterBase.setPrice(dec(999, 8)) + await priceFeed.setLastGoodPrice(dec(999, 18)) + + await setTellorPrice(dec(123, 6)) + await mockRedstoneAdapterBase.setLatestRevert() + + const priceFetchTx = await priceFeed.fetchPrice() + + let price = await priceFeed.lastGoodPrice() + assert.equal(price, dec(123, 18)) + }) + + it("C1 chainlinkWorking: previous round call reverted, Tellor working, switch to usingChainlinkTellorUntrusted", async () => { + await setAddresses() + const statusBefore = await priceFeed.status() + assert.equal(statusBefore, '0') // status 0: Chainlink working + + await mockRedstoneAdapterBase.setPrevPrice(dec(999, 8)) + await mockRedstoneAdapterBase.setPrice(dec(999, 8)) + await priceFeed.setLastGoodPrice(dec(999, 18)) + + await setTellorPrice(dec(123, 6)) + await mockRedstoneAdapterBase.setPrevRevert() + + const priceFetchTx = await priceFeed.fetchPrice() + const statusAfter = await priceFeed.status() + assert.equal(statusAfter, '1') // status 1: using Tellor, Chainlink untrusted + }) + + it("C1 chainlinkWorking: previous round call reverted, Tellor working, return Tellor Price", async () => { + await setAddresses() + const statusBefore = await priceFeed.status() + assert.equal(statusBefore, '0') // status 0: Chainlink working + + await mockRedstoneAdapterBase.setPrevPrice(dec(999, 8)) + await mockRedstoneAdapterBase.setPrice(dec(999, 8)) + await priceFeed.setLastGoodPrice(dec(999, 18)) + + await setTellorPrice(dec(123, 6)) + await mockRedstoneAdapterBase.setPrevRevert() + + const priceFetchTx = await priceFeed.fetchPrice() + + let price = await priceFeed.lastGoodPrice() + assert.equal(price, dec(123, 18)) + }) + + // --- Chainlink timeout --- + + it("C1 chainlinkWorking: Chainlink frozen, Tellor working: switch to usingTellorChainlinkFrozen", async () => { + await setAddresses() + const statusBefore = await priceFeed.status() + assert.equal(statusBefore, '0') // status 0: Chainlink working + + await mockRedstoneAdapterBase.setPrevPrice(dec(999, 8)) + await mockRedstoneAdapterBase.setPrice(dec(999, 8)) + await priceFeed.setLastGoodPrice(dec(999, 18)) + + await th.fastForwardTime(14400, web3.currentProvider) // fast forward 4 hours + const now = await th.getLatestBlockTimestamp(web3) + + // Tellor price is recent + await setTellorPrice(dec(123, 6)) + + const priceFetchTx = await priceFeed.fetchPrice() + const statusAfter = await priceFeed.status() + assert.equal(statusAfter, '3') // status 3: using Tellor, Chainlink frozen + }) + + it("C1 chainlinkWorking: Chainlink frozen, Tellor working: return Tellor price", async () => { + await setAddresses() + const statusBefore = await priceFeed.status() + assert.equal(statusBefore, '0') // status 0: Chainlink working + + await mockRedstoneAdapterBase.setPrevPrice(dec(999, 8)) + await mockRedstoneAdapterBase.setPrice(dec(999, 8)) + await priceFeed.setLastGoodPrice(dec(999, 18)) + + await th.fastForwardTime(14400, web3.currentProvider) // Fast forward 4 hours + const now = await th.getLatestBlockTimestamp(web3) + // Tellor price is recent + await setTellorPrice(dec(123, 6)) + + const priceFetchTx = await priceFeed.fetchPrice() + + let price = await priceFeed.lastGoodPrice() + assert.equal(price, dec(123, 18)) + }) + + it("C1 chainlinkWorking: Chainlink frozen, Tellor frozen: switch to usingTellorChainlinkFrozen", async () => { + await setAddresses() + const statusBefore = await priceFeed.status() + assert.equal(statusBefore, '0') // status 0: Chainlink working + + await mockRedstoneAdapterBase.setPrevPrice(dec(999, 8)) + await mockRedstoneAdapterBase.setPrice(dec(999, 8)) + await priceFeed.setLastGoodPrice(dec(999, 18)) + + await setTellorPrice(dec(123, 6)) + + await th.fastForwardTime(14400, web3.currentProvider) // fast forward 4 hours + + // check Tellor price timestamp is out of date by > 4 hours + const now = await th.getLatestBlockTimestamp(web3) + const tellorUpdateTime = (await tellorCaller.getTellorCurrentValue.call())[2] + assert.isTrue(tellorUpdateTime.lt(toBN(now).sub(toBN(14400)))) + + const priceFetchTx = await priceFeed.fetchPrice() + const statusAfter = await priceFeed.status() + assert.equal(statusAfter, '3') // status 3: using Tellor, Chainlink frozen + }) + + it("C1 chainlinkWorking: Chainlink frozen, Tellor frozen: return last good price", async () => { + await setAddresses() + const statusBefore = await priceFeed.status() + assert.equal(statusBefore, '0') // status 0: Chainlink working + + await mockRedstoneAdapterBase.setPrevPrice(dec(999, 8)) + await mockRedstoneAdapterBase.setPrice(dec(999, 8)) + await priceFeed.setLastGoodPrice(dec(999, 18)) + + await setTellorPrice(dec(123, 6)) + + await th.fastForwardTime(14400, web3.currentProvider) // Fast forward 4 hours + + // check Tellor price timestamp is out of date by > 4 hours + const now = await th.getLatestBlockTimestamp(web3) + const tellorUpdateTime = (await tellorCaller.getTellorCurrentValue.call())[2] + assert.isTrue(tellorUpdateTime.lt(toBN(now).sub(toBN(14400)))) + + const priceFetchTx = await priceFeed.fetchPrice() + let price = await priceFeed.lastGoodPrice() + // Expect lastGoodPrice has not updated + assert.equal(price, dec(999, 18)) + }) + + it("C1 chainlinkWorking: Chainlink times out, Tellor broken by 0 price: switch to usingChainlinkTellorUntrusted", async () => { + await setAddresses() + const statusBefore = await priceFeed.status() + assert.equal(statusBefore, '0') // status 0: Chainlink working + + await mockRedstoneAdapterBase.setPrevPrice(dec(999, 8)) + await mockRedstoneAdapterBase.setPrice(dec(999, 8)) + await priceFeed.setLastGoodPrice(dec(999, 18)) + + await th.fastForwardTime(14400, web3.currentProvider) // Fast forward 4 hours + + // Tellor breaks by 0 price + await setTellorPrice(0) + + const priceFetchTx = await priceFeed.fetchPrice() + const statusAfter = await priceFeed.status() + assert.equal(statusAfter, '4') // status 4: using Chainlink, Tellor untrusted + }) + + it("C1 chainlinkWorking: Chainlink times out, Tellor broken by 0 price: return last good price", async () => { + await setAddresses() + const statusBefore = await priceFeed.status() + assert.equal(statusBefore, '0') // status 0: Chainlink working + + await mockRedstoneAdapterBase.setPrevPrice(dec(999, 8)) + await mockRedstoneAdapterBase.setPrice(dec(999, 8)) + await priceFeed.setLastGoodPrice(dec(999, 18)) + + await th.fastForwardTime(14400, web3.currentProvider) // Fast forward 4 hours + + await setTellorPrice(0) + + const priceFetchTx = await priceFeed.fetchPrice() + let price = await priceFeed.lastGoodPrice() + + // Expect lastGoodPrice has not updated + assert.equal(price, dec(999, 18)) + }) + + it("C1 chainlinkWorking: Chainlink is out of date by <3hrs: remain chainlinkWorking", async () => { + await setAddresses() + const statusBefore = await priceFeed.status() + assert.equal(statusBefore, '0') // status 0: Chainlink working + + await mockRedstoneAdapterBase.setPrevPrice(dec(1234, 8)) + await mockRedstoneAdapterBase.setPrice(dec(1234, 8)) + await th.fastForwardTime(10740, web3.currentProvider) // fast forward 2hrs 59 minutes + + const priceFetchTx = await priceFeed.fetchPrice() + const statusAfter = await priceFeed.status() + assert.equal(statusAfter, '0') // status 0: Chainlink working + }) + + it("C1 chainlinkWorking: Chainlink is out of date by <3hrs: return Chainklink price", async () => { + await setAddresses() + const statusBefore = await priceFeed.status() + assert.equal(statusBefore, '0') // status 0: Chainlink working + + await mockRedstoneAdapterBase.setPrevPrice(dec(1234, 8)) + await mockRedstoneAdapterBase.setPrice(dec(1234, 8)) + await th.fastForwardTime(10740, web3.currentProvider) // fast forward 2hrs 59 minutes + + const priceFetchTx = await priceFeed.fetchPrice() + const price = await priceFeed.lastGoodPrice() + assert.equal(price, dec(1234, 18)) + }) + + // --- Chainlink price deviation --- + + it("C1 chainlinkWorking: Chainlink price drop of >50%, switch to usingChainlinkTellorUntrusted", async () => { + await setAddresses() + priceFeed.setLastGoodPrice(dec(2, 18)) + + const statusBefore = await priceFeed.status() + assert.equal(statusBefore, '0') // status 0: Chainlink working + + await setTellorPrice(dec(203, 4)) + await mockRedstoneAdapterBase.setPrevPrice(dec(2, 8)) // price = 2 + await mockRedstoneAdapterBase.setPrice(99999999) // price drops to 0.99999999: a drop of > 50% from previous + + const priceFetchTx = await priceFeed.fetchPrice() + const statusAfter = await priceFeed.status() + assert.equal(statusAfter, '1') // status 1: using Tellor, Chainlink untrusted + }) + + it("C1 chainlinkWorking: Chainlink price drop of >50%, return the Tellor price", async () => { + await setAddresses() + priceFeed.setLastGoodPrice(dec(2, 18)) + + const statusBefore = await priceFeed.status() + assert.equal(statusBefore, '0') // status 0: Chainlink working + + await setTellorPrice(dec(203,4)) + await mockRedstoneAdapterBase.setPrevPrice(dec(2, 8)) // price = 2 + await mockRedstoneAdapterBase.setPrice(99999999) // price drops to 0.99999999: a drop of > 50% from previous + + const priceFetchTx = await priceFeed.fetchPrice() + + let price = await priceFeed.lastGoodPrice() + assert.equal(price, dec(203, 16)) + }) + + it("C1 chainlinkWorking: Chainlink price drop of 50%, remain chainlinkWorking", async () => { + await setAddresses() + priceFeed.setLastGoodPrice(dec(2, 18)) + + const statusBefore = await priceFeed.status() + assert.equal(statusBefore, '0') // status 0: Chainlink working + + await setTellorPrice(dec(203, 4)) + await mockRedstoneAdapterBase.setPrevPrice(dec(2, 8)) // price = 2 + await mockRedstoneAdapterBase.setPrice(dec(1, 8)) // price drops to 1 + + const priceFetchTx = await priceFeed.fetchPrice() + const statusAfter = await priceFeed.status() + assert.equal(statusAfter, '0') // status 0: Chainlink working + }) + + it("C1 chainlinkWorking: Chainlink price drop of 50%, return the Chainlink price", async () => { + await setAddresses() + priceFeed.setLastGoodPrice(dec(2, 18)) + + const statusBefore = await priceFeed.status() + assert.equal(statusBefore, '0') // status 0: Chainlink working + + await setTellorPrice(dec(203, 4)) + await mockRedstoneAdapterBase.setPrevPrice(dec(2, 8)) // price = 2 + await mockRedstoneAdapterBase.setPrice(dec(1, 8)) // price drops to 1 + + const priceFetchTx = await priceFeed.fetchPrice() + + let price = await priceFeed.lastGoodPrice() + assert.equal(price, dec(1, 18)) + }) + + it("C1 chainlinkWorking: Chainlink price drop of <50%, remain chainlinkWorking", async () => { + await setAddresses() + priceFeed.setLastGoodPrice(dec(2, 18)) + + const statusBefore = await priceFeed.status() + assert.equal(statusBefore, '0') // status 0: Chainlink working + + await setTellorPrice(dec(203, 4)) + await mockRedstoneAdapterBase.setPrevPrice(dec(2, 8)) // price = 2 + await mockRedstoneAdapterBase.setPrice(dec(100000001)) // price drops to 1.00000001: a drop of < 50% from previous + + const priceFetchTx = await priceFeed.fetchPrice() + const statusAfter = await priceFeed.status() + assert.equal(statusAfter, '0') // status 0: Chainlink working + }) + + it("C1 chainlinkWorking: Chainlink price drop of <50%, return Chainlink price", async () => { + await setAddresses() + priceFeed.setLastGoodPrice(dec(2, 18)) + + const statusBefore = await priceFeed.status() + assert.equal(statusBefore, '0') // status 0: Chainlink working + + await setTellorPrice(dec(203, 4)) + await mockRedstoneAdapterBase.setPrevPrice(dec(2, 8)) // price = 2 + await mockRedstoneAdapterBase.setPrice(100000001) // price drops to 1.00000001: a drop of < 50% from previous + + const priceFetchTx = await priceFeed.fetchPrice() + + let price = await priceFeed.lastGoodPrice() + assert.equal(price, dec(100000001, 10)) + }) + + // Price increase + it("C1 chainlinkWorking: Chainlink price increase of >100%, switch to usingChainlinkTellorUntrusted", async () => { + await setAddresses() + priceFeed.setLastGoodPrice(dec(2, 18)) + + const statusBefore = await priceFeed.status() + assert.equal(statusBefore, '0') // status 0: Chainlink working + + await setTellorPrice(dec(203, 4)) + await mockRedstoneAdapterBase.setPrevPrice(dec(2, 8)) // price = 2 + await mockRedstoneAdapterBase.setPrice(400000001) // price increases to 4.000000001: an increase of > 100% from previous + + const priceFetchTx = await priceFeed.fetchPrice() + const statusAfter = await priceFeed.status() + assert.equal(statusAfter, '1') // status 1: using Tellor, Chainlink untrusted + }) + + it("C1 chainlinkWorking: Chainlink price increase of >100%, return Tellor price", async () => { + await setAddresses() + priceFeed.setLastGoodPrice(dec(2, 18)) + + const statusBefore = await priceFeed.status() + assert.equal(statusBefore, '0') // status 0: Chainlink working + + await setTellorPrice(dec(203, 4)) + await mockRedstoneAdapterBase.setPrevPrice(dec(2, 8)) // price = 2 + await mockRedstoneAdapterBase.setPrice(400000001) // price increases to 4.000000001: an increase of > 100% from previous + + const priceFetchTx = await priceFeed.fetchPrice() + let price = await priceFeed.lastGoodPrice() + assert.equal(price, dec(203, 16)) + }) + + it("C1 chainlinkWorking: Chainlink price increase of 100%, remain chainlinkWorking", async () => { + await setAddresses() + priceFeed.setLastGoodPrice(dec(2, 18)) + + const statusBefore = await priceFeed.status() + assert.equal(statusBefore, '0') // status 0: Chainlink working + + await setTellorPrice(dec(203, 4)) + await mockRedstoneAdapterBase.setPrevPrice(dec(2, 8)) // price = 2 + await mockRedstoneAdapterBase.setPrice(dec(4, 8)) // price increases to 4: an increase of 100% from previous + + const priceFetchTx = await priceFeed.fetchPrice() + const statusAfter = await priceFeed.status() + assert.equal(statusAfter, '0') // status 0: Chainlink working + }) + + it("C1 chainlinkWorking: Chainlink price increase of 100%, return Chainlink price", async () => { + await setAddresses() + priceFeed.setLastGoodPrice(dec(2, 18)) + + const statusBefore = await priceFeed.status() + assert.equal(statusBefore, '0') // status 0: Chainlink working + + await setTellorPrice(dec(203, 4)) + await mockRedstoneAdapterBase.setPrevPrice(dec(2, 8)) // price = 2 + await mockRedstoneAdapterBase.setPrice(dec(4, 8)) // price increases to 4: an increase of 100% from previous + + const priceFetchTx = await priceFeed.fetchPrice() + let price = await priceFeed.lastGoodPrice() + assert.equal(price, dec(4, 18)) + }) + + it("C1 chainlinkWorking: Chainlink price increase of <100%, remain chainlinkWorking", async () => { + await setAddresses() + priceFeed.setLastGoodPrice(dec(2, 18)) + + const statusBefore = await priceFeed.status() + assert.equal(statusBefore, '0') // status 0: Chainlink working + + await setTellorPrice(dec(203, 4)) + await mockRedstoneAdapterBase.setPrevPrice(dec(2, 8)) // price = 2 + await mockRedstoneAdapterBase.setPrice(399999999) // price increases to 3.99999999: an increase of < 100% from previous + + const priceFetchTx = await priceFeed.fetchPrice() + const statusAfter = await priceFeed.status() + assert.equal(statusAfter, '0') // status 0: Chainlink working + }) + + it("C1 chainlinkWorking: Chainlink price increase of <100%, return Chainlink price", async () => { + await setAddresses() + priceFeed.setLastGoodPrice(dec(2, 18)) + + const statusBefore = await priceFeed.status() + assert.equal(statusBefore, '0') // status 0: Chainlink working + + await setTellorPrice(dec(203, 4)) + await mockRedstoneAdapterBase.setPrevPrice(dec(2, 8)) // price = 2 + await mockRedstoneAdapterBase.setPrice(399999999) // price increases to 3.99999999: an increase of < 100% from previous + + const priceFetchTx = await priceFeed.fetchPrice() + let price = await priceFeed.lastGoodPrice() + assert.equal(price, dec(399999999, 10)) + }) + + it("C1 chainlinkWorking: Chainlink price drop of >50% and Tellor price matches: remain chainlinkWorking", async () => { + await setAddresses() + priceFeed.setLastGoodPrice(dec(2, 18)) + + const statusBefore = await priceFeed.status() + assert.equal(statusBefore, '0') // status 0: Chainlink working + + await mockRedstoneAdapterBase.setPrevPrice(dec(2, 8)) // price = 2 + await mockRedstoneAdapterBase.setPrice(99999999) // price drops to 0.99999999: a drop of > 50% from previous + await setTellorPrice(999999) // Tellor price drops to same value (6 ecimals) + + const priceFetchTx = await priceFeed.fetchPrice() + const statusAfter = await priceFeed.status() + assert.equal(statusAfter, '0') // status 0: Chainlink working + }) + + it("C1 chainlinkWorking: Chainlink price drop of >50% and Tellor price matches: return Chainlink price", async () => { + await setAddresses() + priceFeed.setLastGoodPrice(dec(2, 18)) + + const statusBefore = await priceFeed.status() + assert.equal(statusBefore, '0') // status 0: Chainlink working + + await mockRedstoneAdapterBase.setPrevPrice(dec(2, 8)) // price = 2 + await mockRedstoneAdapterBase.setPrice(99999999) // price drops to 0.99999999: a drop of > 50% from previous + await setTellorPrice(999999) // Tellor price drops to same value (at 6 decimals) + + const priceFetchTx = await priceFeed.fetchPrice() + let price = await priceFeed.lastGoodPrice() + assert.equal(price, dec(99999999, 10)) + }) + + it("C1 chainlinkWorking: Chainlink price drop of >50% and Tellor price within 5% of Chainlink: remain chainlinkWorking", async () => { + await setAddresses() + priceFeed.setLastGoodPrice(dec(2, 18)) + + const statusBefore = await priceFeed.status() + assert.equal(statusBefore, '0') // status 0: Chainlink working + + await mockRedstoneAdapterBase.setPrevPrice(dec(1000, 8)) // prev price = 1000 + await mockRedstoneAdapterBase.setPrice(dec(100, 8)) // price drops to 100: a drop of > 50% from previous + await setTellorPrice(104999999) // Tellor price drops to 104.99: price difference with new Chainlink price is now just under 5% + + const priceFetchTx = await priceFeed.fetchPrice() + const statusAfter = await priceFeed.status() + assert.equal(statusAfter, '0') // status 0: Chainlink working + }) + + it("C1 chainlinkWorking: Chainlink price drop of >50% and Tellor price within 5% of Chainlink: return Chainlink price", async () => { + await setAddresses() + priceFeed.setLastGoodPrice(dec(2, 18)) + + const statusBefore = await priceFeed.status() + assert.equal(statusBefore, '0') // status 0: Chainlink working + + await mockRedstoneAdapterBase.setPrevPrice(dec(1000, 8)) // prev price = 1000 + await mockRedstoneAdapterBase.setPrice(dec(100, 8)) // price drops to 100: a drop of > 50% from previous + await setTellorPrice(104999999) // Tellor price drops to 104.99: price difference with new Chainlink price is now just under 5% + + const priceFetchTx = await priceFeed.fetchPrice() + let price = await priceFeed.lastGoodPrice() + assert.equal(price, dec(100, 18)) + }) + + it("C1 chainlinkWorking: Chainlink price drop of >50% and Tellor live but not within 5% of Chainlink: switch to usingChainlinkTellorUntrusted", async () => { + await setAddresses() + priceFeed.setLastGoodPrice(dec(2, 18)) + + const statusBefore = await priceFeed.status() + assert.equal(statusBefore, '0') // status 0: Chainlink working + + await mockRedstoneAdapterBase.setPrevPrice(dec(1000, 8)) // prev price = 1000 + await mockRedstoneAdapterBase.setPrice(dec(100, 8)) // price drops to 100: a drop of > 50% from previous + await setTellorPrice(105000001) // Tellor price drops to 105.000001: price difference with new Chainlink price is now > 5% + + const priceFetchTx = await priceFeed.fetchPrice() + const statusAfter = await priceFeed.status() + assert.equal(statusAfter, '1') // status 1: using Tellor, Chainlink untrusted + }) + + it("C1 chainlinkWorking: Chainlink price drop of >50% and Tellor live but not within 5% of Chainlink: return Tellor price", async () => { + await setAddresses() + priceFeed.setLastGoodPrice(dec(2, 18)) + + const statusBefore = await priceFeed.status() + assert.equal(statusBefore, '0') // status 0: Chainlink working + + await mockRedstoneAdapterBase.setPrevPrice(dec(1000, 8)) // prev price = 1000 + await mockRedstoneAdapterBase.setPrice(dec(100, 8)) // price drops to 100: a drop of > 50% from previous + await setTellorPrice(105000001) // Tellor price drops to 105.000001: price difference with new Chainlink price is now > 5% + + const priceFetchTx = await priceFeed.fetchPrice() + let price = await priceFeed.lastGoodPrice() + + assert.equal(price, dec(105000001, 12)) // return Tellor price + }) + + it("C1 chainlinkWorking: Chainlink price drop of >50% and Tellor frozen: switch to usingChainlinkTellorUntrusted", async () => { + await setAddresses() + const statusBefore = await priceFeed.status() + assert.equal(statusBefore, '0') // status 0: Chainlink working + + await mockRedstoneAdapterBase.setPrevPrice(dec(1000, 8)) // prev price = 1000 + await mockRedstoneAdapterBase.setPrice(dec(100, 8)) // price drops to 100: a drop of > 50% from previous + await setTellorPrice(dec(100, 8)) + + // 4 hours pass with no Tellor updates + await th.fastForwardTime(14400, web3.currentProvider) + + // check Tellor price timestamp is out of date by > 4 hours + const now = await th.getLatestBlockTimestamp(web3) + const tellorUpdateTime = (await tellorCaller.getTellorCurrentValue.call())[2] + assert.isTrue(tellorUpdateTime.lt(toBN(now).sub(toBN(14400)))) + + await mockRedstoneAdapterBase.setUpdateTime(now) + + const priceFetchTx = await priceFeed.fetchPrice() + + const statusAfter = await priceFeed.status() + assert.equal(statusAfter, '1') // status 1: using Tellor, Chainlink untrusted + }) + + it("C1 chainlinkWorking: Chainlink price drop of >50% and Tellor frozen: return last good price", async () => { + await setAddresses() + priceFeed.setLastGoodPrice(dec(1200, 18)) // establish a "last good price" from the previous price fetch + + const statusBefore = await priceFeed.status() + assert.equal(statusBefore, '0') // status 0: Chainlink working + + await mockRedstoneAdapterBase.setPrevPrice(dec(1000, 8)) // prev price = 1000 + await mockRedstoneAdapterBase.setPrice(dec(100, 8)) // price drops to 100: a drop of > 50% from previous + await setTellorPrice(dec(100, 8)) + + // 4 hours pass with no Tellor updates + await th.fastForwardTime(14400, web3.currentProvider) + + // check Tellor price timestamp is out of date by > 4 hours + const now = await th.getLatestBlockTimestamp(web3) + const tellorUpdateTime = (await tellorCaller.getTellorCurrentValue.call())[2] + assert.isTrue(tellorUpdateTime.lt(toBN(now).sub(toBN(14400)))) + + await mockRedstoneAdapterBase.setUpdateTime(now) + + const priceFetchTx = await priceFeed.fetchPrice() + let price = await priceFeed.lastGoodPrice() + + // Check that the returned price is the last good price + assert.equal(price, dec(1200, 18)) + }) + + //--- Chainlink fails and Tellor is broken --- + + it("C1 chainlinkWorking: Chainlink price drop of >50% and Tellor is broken by 0 price: switch to bothOracleSuspect", async () => { + await setAddresses() + const statusBefore = await priceFeed.status() + assert.equal(statusBefore, '0') // status 0: Chainlink working + + await mockRedstoneAdapterBase.setPrevPrice(dec(2, 8)) // price = 2 + await mockRedstoneAdapterBase.setPrice(99999999) // price drops to 0.99999999: a drop of > 50% from previous + + // Make mock Tellor return 0 price + await setTellorPrice(0) + + const priceFetchTx = await priceFeed.fetchPrice() + + const statusAfter = await priceFeed.status() + assert.equal(statusAfter, '2') // status 2: both oracles untrusted + }) + + it("C1 chainlinkWorking: Chainlink price drop of >50% and Tellor is broken by 0 price: return last good price", async () => { + await setAddresses() + priceFeed.setLastGoodPrice(dec(1200, 18)) // establish a "last good price" from the previous price fetch + + const statusBefore = await priceFeed.status() + assert.equal(statusBefore, '0') // status 0: Chainlink working + + await setTellorPrice(dec(1300, 6)) + + // Make mock Chainlink price deviate too much + await mockRedstoneAdapterBase.setPrevPrice(dec(2, 8)) // price = 2 + await mockRedstoneAdapterBase.setPrice(99999999) // price drops to 0.99999999: a drop of > 50% from previous + + // Make mock Tellor return 0 price + await setTellorPrice(0) + + const priceFetchTx = await priceFeed.fetchPrice() + let price = await priceFeed.lastGoodPrice() + + // Check that the returned price is in fact the previous price + assert.equal(price, dec(1200, 18)) + }) + + it("C1 chainlinkWorking: Chainlink price drop of >50% and Tellor is broken by 0 timestamp: switch to bothOracleSuspect", async () => { + await setAddresses() + const statusBefore = await priceFeed.status() + assert.equal(statusBefore, '0') // status 0: Chainlink working + + // Make mock Chainlink price deviate too much + await mockRedstoneAdapterBase.setPrevPrice(dec(2, 8)) // price = 2 + await mockRedstoneAdapterBase.setPrice(99999999) // price drops to 0.99999999: a drop of > 50% from previous + + // Make mock Tellor return 0 timestamp + await mockTellor.setUpdateTime(0) + const priceFetchTx = await priceFeed.fetchPrice() + + const statusAfter = await priceFeed.status() + assert.equal(statusAfter, '2') // status 2: both oracles untrusted + }) + + it("C1 chainlinkWorking: Chainlink price drop of >50% and Tellor is broken by 0 timestamp: return last good price", async () => { + await setAddresses() + priceFeed.setLastGoodPrice(dec(1200, 18)) // establish a "last good price" from the previous price fetch + + const statusBefore = await priceFeed.status() + assert.equal(statusBefore, '0') // status 0: Chainlink working + + await setTellorPrice(dec(1300, 6)) + + // Make mock Chainlink price deviate too much + await mockRedstoneAdapterBase.setPrevPrice(dec(2, 8)) // price = 2 + await mockRedstoneAdapterBase.setPrice(99999999) // price drops to 0.99999999: a drop of > 50% from previous + + // Make mock Tellor return 0 timestamp + await mockTellor.setUpdateTime(0) + + const priceFetchTx = await priceFeed.fetchPrice() + let price = await priceFeed.lastGoodPrice() + + // Check that the returned price is in fact the previous price + assert.equal(price, dec(1200, 18)) + }) + + it("C1 chainlinkWorking: Chainlink price drop of >50% and Tellor is broken by stale timestamp: Pricefeed switches to bothOracleSuspect", async () => { + await setAddresses() + const statusBefore = await priceFeed.status() + assert.equal(statusBefore, '0') // status 0: Chainlink working + + // Make mock Chainlink price deviate too much + await mockRedstoneAdapterBase.setPrevPrice(dec(2, 8)) // price = 2 + await mockRedstoneAdapterBase.setPrice(99999999) // price drops to 0.99999999: a drop of > 50% from previous + + // Make mock Tellor return 0 timestamp + await mockTellor.setUpdateTime(0) + + const priceFetchTx = await priceFeed.fetchPrice() + + const statusAfter = await priceFeed.status() + assert.equal(statusAfter, '2') // status 2: both oracles untrusted + }) + + it("C1 chainlinkWorking: Chainlink price drop of >50% and Tellor is broken by stale timestamp: return last good price", async () => { + await setAddresses() + priceFeed.setLastGoodPrice(dec(1200, 18)) // establish a "last good price" from the previous price fetch + + const statusBefore = await priceFeed.status() + assert.equal(statusBefore, '0') // status 0: Chainlink working + + await setTellorPrice(dec(1300, 6)) + + // Make mock Chainlink price deviate too much + await mockRedstoneAdapterBase.setPrevPrice(dec(2, 8)) // price = 2 + await mockRedstoneAdapterBase.setPrice(99999999) // price drops to 0.99999999: a drop of > 50% from previous + + // Make mock stale Tellor + await th.fastForwardTime(24 * 60 * 60 + 1, web3.currentProvider) // 24 hours + + const priceFetchTx = await priceFeed.fetchPrice() + let price = await priceFeed.lastGoodPrice() + + // Check that the returned price is in fact the previous price + assert.equal(price, dec(1200, 18)) + }) + + // -- Chainlink is working + it("C1 chainlinkWorking: Chainlink is working and Tellor is working - remain on chainlinkWorking", async () => { + await setAddresses() + priceFeed.setLastGoodPrice(dec(1200, 18)) + + const statusBefore = await priceFeed.status() + assert.equal(statusBefore, '0') // status 0: Chainlink working + + await mockRedstoneAdapterBase.setPrevPrice(dec(101, 8)) + await mockRedstoneAdapterBase.setPrice(dec(102, 8)) + + await setTellorPrice(dec(103, 18)) + + const priceFetchTx = await priceFeed.fetchPrice() + + const statusAfter = await priceFeed.status() + assert.equal(statusAfter, '0') // status 0: Chainlink working + }) + + it("C1 chainlinkWorking: Chainlink is working and Tellor is working - return Chainlink price", async () => { + await setAddresses() + priceFeed.setLastGoodPrice(dec(1200, 18)) + + const statusBefore = await priceFeed.status() + assert.equal(statusBefore, '0') // status 0: Chainlink working + + await mockRedstoneAdapterBase.setPrevPrice(dec(101, 8)) + await mockRedstoneAdapterBase.setPrice(dec(102, 8)) + + await setTellorPrice(dec(103, 18)) + + const priceFetchTx = await priceFeed.fetchPrice() + let price = await priceFeed.lastGoodPrice() + + // Check that the returned price is current Chainlink price + assert.equal(price, dec(102, 18)) + }) + + it("C1 chainlinkWorking: Chainlink is working and Tellor freezes - remain on chainlinkWorking", async () => { + await setAddresses() + priceFeed.setLastGoodPrice(dec(1200, 18)) + + const statusBefore = await priceFeed.status() + assert.equal(statusBefore, '0') // status 0: Chainlink working + + await mockRedstoneAdapterBase.setPrevPrice(dec(101, 8)) + await mockRedstoneAdapterBase.setPrice(dec(102, 8)) + + await setTellorPrice(dec(103, 18)) + + // 4 hours pass with no Tellor updates + await th.fastForwardTime(14400, web3.currentProvider) + + // check Tellor price timestamp is out of date by > 4 hours + const now = await th.getLatestBlockTimestamp(web3) + const tellorUpdateTime = (await tellorCaller.getTellorCurrentValue.call())[2] + assert.isTrue(tellorUpdateTime.lt(toBN(now).sub(toBN(14400)))) + + await mockRedstoneAdapterBase.setUpdateTime(now) // Chainlink's price is current + + const priceFetchTx = await priceFeed.fetchPrice() + + const statusAfter = await priceFeed.status() + assert.equal(statusAfter, '0') // status 0: Chainlink working + }) + + it("C1 chainlinkWorking: Chainlink is working and Tellor freezes - return Chainlink price", async () => { + await setAddresses() + priceFeed.setLastGoodPrice(dec(1200, 18)) + + const statusBefore = await priceFeed.status() + assert.equal(statusBefore, '0') // status 0: Chainlink working + + await mockRedstoneAdapterBase.setPrevPrice(dec(101, 8)) + await mockRedstoneAdapterBase.setPrice(dec(102, 8)) + + await setTellorPrice(dec(103, 18)) + + // 4 hours pass with no Tellor updates + await th.fastForwardTime(14400, web3.currentProvider) + + // check Tellor price timestamp is out of date by > 4 hours + const now = await th.getLatestBlockTimestamp(web3) + const tellorUpdateTime = (await tellorCaller.getTellorCurrentValue.call())[2] + assert.isTrue(tellorUpdateTime.lt(toBN(now).sub(toBN(14400)))) + + await mockRedstoneAdapterBase.setUpdateTime(now) // Chainlink's price is current + + const priceFetchTx = await priceFeed.fetchPrice() + let price = await priceFeed.lastGoodPrice() + + // Check that the returned price is current Chainlink price + assert.equal(price, dec(102, 18)) + }) + + it("C1 chainlinkWorking: Chainlink is working and Tellor breaks: switch to usingChainlinkTellorUntrusted", async () => { + await setAddresses() + priceFeed.setLastGoodPrice(dec(1200, 18)) // establish a "last good price" from the previous price fetch + + const statusBefore = await priceFeed.status() + assert.equal(statusBefore, '0') // status 0: Chainlink working + + await mockRedstoneAdapterBase.setPrevPrice(dec(101, 8)) + await mockRedstoneAdapterBase.setPrice(dec(102, 8)) + + await setTellorPrice(0) + + const priceFetchTx = await priceFeed.fetchPrice() + + const statusAfter = await priceFeed.status() + assert.equal(statusAfter, '4') // status 4: Using Tellor, Chainlink untrusted + }) + + it("C1 chainlinkWorking: Chainlink is working and Tellor breaks: return Chainlink price", async () => { + await setAddresses() + priceFeed.setLastGoodPrice(dec(1200, 18)) // establish a "last good price" from the previous price fetch + + const statusBefore = await priceFeed.status() + assert.equal(statusBefore, '0') // status 0: Chainlink working + + await mockRedstoneAdapterBase.setPrevPrice(dec(101, 8)) + await mockRedstoneAdapterBase.setPrice(dec(102, 8)) + + await setTellorPrice(0) + + const priceFetchTx = await priceFeed.fetchPrice() + let price = await priceFeed.lastGoodPrice() + + // Check that the returned price is current Chainlink price + assert.equal(price, dec(102, 18)) + }) + + it("C1 chainlinkWorking: Chainlink is working, revert transaction", async () => { + await setAddresses() + const statusBefore = await priceFeed.status() + assert.equal(statusBefore, '0') // status 0: Chainlink working + + await assertRevert( + priceFeed.forceExitBothUntrustedStatus(false, { from: owner }), + "PriceFeed: at least one oracle is working" + ) + await assertRevert( + priceFeed.forceExitBothUntrustedStatus(true, { from: owner }), + "PriceFeed: at least one oracle is working" + ) + }) + + // --- Case 2: Using Tellor --- + + // Using Tellor, Tellor breaks + it("C2 usingTellorChainlinkUntrusted: Tellor breaks by zero price: switch to bothOraclesSuspect", async () => { + await setAddresses() + priceFeed.setStatus(1) // status 1: using Tellor, Chainlink untrusted + + await mockRedstoneAdapterBase.setPrevPrice(dec(999, 8)) + await mockRedstoneAdapterBase.setPrice(dec(999, 8)) + + await priceFeed.setLastGoodPrice(dec(123, 18)) + + const now = await th.getLatestBlockTimestamp(web3) + await mockTellor.setUpdateTime(now) + await setTellorPrice(0) + + await priceFeed.fetchPrice() + + const status = await priceFeed.status() + assert.equal(status, 2) // status 2: both oracles untrusted + }) + + it("C2 usingTellorChainlinkUntrusted: Tellor breaks by zero price: return last good price", async () => { + await setAddresses() + priceFeed.setStatus(1) // status 1: using Tellor, Chainlink untrusted + + await mockRedstoneAdapterBase.setPrevPrice(dec(999, 8)) + await mockRedstoneAdapterBase.setPrice(dec(999, 8)) + + await priceFeed.setLastGoodPrice(dec(123, 18)) + + const now = await th.getLatestBlockTimestamp(web3) + await mockTellor.setUpdateTime(now) + await setTellorPrice(0) + + await priceFeed.fetchPrice() + const price = await priceFeed.lastGoodPrice() + + assert.equal(price, dec(123, 18)) + }) + + // Using Tellor, Tellor breaks + it("C2 usingTellorChainlinkUntrusted: Tellor breaks by call reverted: switch to bothOraclesSuspect", async () => { + // deploy broken mock + mockTellor = await BrokenMockTellor.new(queryData) + BrokenMockTellor.setAsDeployed(mockTellor) + tellorCaller = await TellorCaller.new(mockTellor.address, queryId) + await setAddresses() + + priceFeed.setStatus(1) // status 1: using Tellor, Chainlink untrusted + + await priceFeed.setLastGoodPrice(dec(123, 18)) + + await mockRedstoneAdapterBase.setPrevPrice(dec(999, 8)) + await mockRedstoneAdapterBase.setPrice(dec(999, 8)) + + await priceFeed.fetchPrice() + + const status = await priceFeed.status() + assert.equal(status, 2) // status 2: both oracles untrusted + }) + + it("C2 usingTellorChainlinkUntrusted: Tellor breaks by call reverted: return last good price", async () => { + // deploy broken mock + mockTellor = await BrokenMockTellor.new(queryData) + BrokenMockTellor.setAsDeployed(mockTellor) + tellorCaller = await TellorCaller.new(mockTellor.address, queryId) + await setAddresses() + + priceFeed.setStatus(1) // status 1: using Tellor, Chainlink untrusted + + await priceFeed.setLastGoodPrice(dec(123, 18)) + + await mockRedstoneAdapterBase.setPrevPrice(dec(999, 8)) + await mockRedstoneAdapterBase.setPrice(dec(999, 8)) + + await priceFeed.fetchPrice() + const price = await priceFeed.lastGoodPrice() + + assert.equal(price, dec(123, 18)) + }) + + // Using Tellor, Tellor breaks + it("C2 usingTellorChainlinkUntrusted: Tellor breaks by zero timestamp: switch to bothOraclesSuspect", async () => { + await setAddresses() + priceFeed.setStatus(1) // status 1: using Tellor, Chainlink untrusted + + await priceFeed.setLastGoodPrice(dec(123, 18)) + + await mockRedstoneAdapterBase.setPrevPrice(dec(999, 8)) + await mockRedstoneAdapterBase.setPrice(dec(999, 8)) + await setTellorPrice(dec(999, 6)) + + await mockTellor.setUpdateTime(0) + + await priceFeed.fetchPrice() + + const status = await priceFeed.status() + assert.equal(status, 2) // status 2: both oracles untrusted + }) + + it("C2 usingTellorChainlinkUntrusted: Tellor breaks by zero timestamp: return last good price", async () => { + await setAddresses() + priceFeed.setStatus(1) // status 1: using Tellor, Chainlink untrusted + + await priceFeed.setLastGoodPrice(dec(123, 18)) + + await mockRedstoneAdapterBase.setPrevPrice(dec(999, 8)) + await mockRedstoneAdapterBase.setPrice(dec(999, 8)) + await setTellorPrice(dec(999, 6)) + + await mockTellor.setUpdateTime(0) + + await priceFeed.fetchPrice() + const price = await priceFeed.lastGoodPrice() + + assert.equal(price, dec(123, 18)) + }) + + // Using Tellor, Tellor freezes + it("C2 usingTellorChainlinkUntrusted: Tellor freezes - remain usingChainlinkTellorUntrusted", async () => { + await setAddresses() + priceFeed.setStatus(1) // status 1: using Tellor, Chainlink untrusted + + await mockRedstoneAdapterBase.setPrevPrice(dec(999, 8)) + await mockRedstoneAdapterBase.setPrice(dec(999, 8)) + + await priceFeed.setLastGoodPrice(dec(246, 18)) + + await setTellorPrice(dec(123, 6)) + + await th.fastForwardTime(14400, web3.currentProvider) // Fast forward 4 hours + + // check Tellor price timestamp is out of date by > 4 hours + const now = await th.getLatestBlockTimestamp(web3) + const tellorUpdateTime = (await tellorCaller.getTellorCurrentValue.call())[2] + assert.isTrue(tellorUpdateTime.lt(toBN(now).sub(toBN(14400)))) + + await mockRedstoneAdapterBase.setUpdateTime(now) + + await priceFeed.fetchPrice() + + const status = await priceFeed.status() + assert.equal(status, 1) // status 1: using Tellor, Chainlink untrusted + }) + + it("C2 usingTellorChainlinkUntrusted: Tellor freezes - return last good price", async () => { + await setAddresses() + priceFeed.setStatus(1) // status 1: using Tellor, Chainlink untrusted + + await mockRedstoneAdapterBase.setPrevPrice(dec(999, 8)) + await mockRedstoneAdapterBase.setPrice(dec(999, 8)) + + await priceFeed.setLastGoodPrice(dec(246, 18)) + + await setTellorPrice(dec(123, 6)) + + await th.fastForwardTime(14400, web3.currentProvider) // Fast forward 4 hours + + // check Tellor price timestamp is out of date by > 4 hours + const now = await th.getLatestBlockTimestamp(web3) + const tellorUpdateTime = (await tellorCaller.getTellorCurrentValue.call())[2] + assert.isTrue(tellorUpdateTime.lt(toBN(now).sub(toBN(14400)))) + + await mockRedstoneAdapterBase.setUpdateTime(now) + + await priceFeed.fetchPrice() + const price = await priceFeed.lastGoodPrice() + + assert.equal(price, dec(246, 18)) + }) + + // Using Tellor, both Chainlink & Tellor go live + + it("C2 usingTellorChainlinkUntrusted: both Tellor and Chainlink are live and <= 5% price difference - switch to chainlinkWorking", async () => { + await setAddresses() + priceFeed.setStatus(1) // status 1: using Tellor, Chainlink untrusted + + await setTellorPrice(dec(100, 6)) // price = 100 + await mockRedstoneAdapterBase.setPrice(dec(105, 8)) // price = 105: 5% difference from Chainlink + + await priceFeed.fetchPrice() + + const status = await priceFeed.status() + assert.equal(status, 0) // status 0: Chainlink working + }) + + it("C2 usingTellorChainlinkUntrusted: both Tellor and Chainlink are live and <= 5% price difference - return Chainlink price", async () => { + await setAddresses() + priceFeed.setStatus(1) // status 1: using Tellor, Chainlink untrusted + + await setTellorPrice(dec(100, 6)) // price = 100 + await mockRedstoneAdapterBase.setPrice(dec(105, 8)) // price = 105: 5% difference from Chainlink + + await priceFeed.fetchPrice() + + const price = await priceFeed.lastGoodPrice() + assert.equal(price, dec(105, 18)) + }) + + it("C2 usingTellorChainlinkUntrusted: both Tellor and Chainlink are live and > 5% price difference - remain usingChainlinkTellorUntrusted", async () => { + await setAddresses() + priceFeed.setStatus(1) // status 1: using Tellor, Chainlink untrusted + + await setTellorPrice(dec(100, 6)) // price = 100 + await mockRedstoneAdapterBase.setPrice('10500000001') // price = 105.00000001: > 5% difference from Tellor + + await priceFeed.fetchPrice() + + const status = await priceFeed.status() + assert.equal(status, 1) // status 1: using Tellor, Chainlink untrusted + }) + + it("C2 usingTellorChainlinkUntrusted: both Tellor and Chainlink are live and > 5% price difference - return Tellor price", async () => { + await setAddresses() + priceFeed.setStatus(1) // status 1: using Tellor, Chainlink untrusted + + await setTellorPrice(dec(100, 6)) // price = 100 + await mockRedstoneAdapterBase.setPrice('10500000001') // price = 105.00000001: > 5% difference from Tellor + + await priceFeed.fetchPrice() + + const price = await priceFeed.lastGoodPrice() + assert.equal(price, dec(100, 18)) + }) + + it("C2 usingTellorChainlinkUntrusted: Tellor is working, revert transaction", async () => { + await setAddresses() + priceFeed.setStatus(1) // status 1: using Tellor, Chainlink untrusted + + await assertRevert( + priceFeed.forceExitBothUntrustedStatus(false, { from: owner }), + "PriceFeed: at least one oracle is working" + ) + await assertRevert( + priceFeed.forceExitBothUntrustedStatus(true, { from: owner }), + "PriceFeed: at least one oracle is working" + ) + }) + + + // --- Case 3: Both Oracles suspect + + it("C3 bothOraclesUntrusted: both Tellor and Chainlink are live and > 5% price difference remain bothOraclesSuspect", async () => { + await setAddresses() + priceFeed.setStatus(2) // status 2: both oracles untrusted + + await priceFeed.setLastGoodPrice(dec(50, 18)) + + await setTellorPrice(dec(100, 6)) // price = 100 + await mockRedstoneAdapterBase.setPrice('10500000001') // price = 105.00000001: > 5% difference from Tellor + + await priceFeed.fetchPrice() + + const status = await priceFeed.status() + assert.equal(status, 2) // status 2: both oracles untrusted + }) + + it("C3 bothOraclesUntrusted: both Tellor and Chainlink are live and > 5% price difference, return last good price", async () => { + await setAddresses() + priceFeed.setStatus(2) // status 2: both oracles untrusted + + await priceFeed.setLastGoodPrice(dec(50, 18)) + + await setTellorPrice(dec(100, 6)) // price = 100 + await mockRedstoneAdapterBase.setPrice('10500000001') // price = 105.00000001: > 5% difference from Tellor + + await priceFeed.fetchPrice() + const price = await priceFeed.lastGoodPrice() + + assert.equal(price, dec(50, 18)) + }) + + it("C3 bothOraclesUntrusted: both Tellor and Chainlink are live and <= 5% price difference, switch to chainlinkWorking", async () => { + await setAddresses() + priceFeed.setStatus(2) // status 2: both oracles untrusted + + await setTellorPrice(dec(100, 6)) // price = 100 + await mockRedstoneAdapterBase.setPrice(dec(105, 8)) // price = 105: 5% difference from Tellor + + await priceFeed.fetchPrice() + + const status = await priceFeed.status() + assert.equal(status, 0) // status 0: Chainlink working + }) + + it("C3 bothOraclesUntrusted: both Tellor and Chainlink are live and <= 5% price difference, return Chainlink price", async () => { + await setAddresses() + priceFeed.setStatus(2) // status 2: both oracles untrusted + + await setTellorPrice(dec(100, 6)) // price = 100 + await mockRedstoneAdapterBase.setPrice(dec(105, 8)) // price = 105: 5% difference from Tellor + + await priceFeed.fetchPrice() + + const price = await priceFeed.lastGoodPrice() + assert.equal(price, dec(105, 18)) + }) + + it("C3 bothOraclesUntrusted: both Tellor and Chainlink are broken, reverts transaction", async () => { + await setAddresses() + priceFeed.setStatus(2) // status 2: both oracles untrusted + + await setTellorPrice(0) // Tellor is broken + await mockRedstoneAdapterBase.setUpdateTime(0) // ChainLink is broken + + await assertRevert( + priceFeed.forceExitBothUntrustedStatus(false, { from: owner }), + "PriceFeed: both oracles are still untrusted" + ) + await assertRevert( + priceFeed.forceExitBothUntrustedStatus(true, { from: owner }), + "PriceFeed: both oracles are still untrusted" + ) + }) + + it("C3 bothOraclesUntrusted: Chainlink back online, check ChainLink first, switch to usingChainlinkTellorUntrusted", async () => { + await setAddresses() + priceFeed.setStatus(2) // status 2: both oracles untrusted + + await setTellorPrice(0) // Tellor is broken + await mockRedstoneAdapterBase.setPrevPrice(dec(100, 8)) + await mockRedstoneAdapterBase.setPrice(dec(105, 8)) // price = 105: 5% difference from previous + + await priceFeed.forceExitBothUntrustedStatus(false, { from: owner }) + + const status = await priceFeed.status() + assert.equal(status, 4) // status 4: using chainlink, tellor broken + + const price = await priceFeed.lastGoodPrice() + assert.equal(price, dec(105, 18)) + }) + + it("C3 bothOraclesUntrusted: Chainlink back online, check Tellor first, switch to usingChainlinkTellorUntrusted", async () => { + await setAddresses() + priceFeed.setStatus(2) // status 2: both oracles untrusted + + await setTellorPrice(dec(123, 6)) // Tellor frozen + await th.fastForwardTime(14400, web3.currentProvider) // Fast forward 4 hours + + await mockRedstoneAdapterBase.setPrevPrice(dec(199, 8)) + await mockRedstoneAdapterBase.setPrice(dec(100, 8)) // price = 100: 5% difference from previous + const now = await th.getLatestBlockTimestamp(web3) + await mockRedstoneAdapterBase.setUpdateTime(now) + + await priceFeed.forceExitBothUntrustedStatus(true, { from: owner }) + + const status = await priceFeed.status() + assert.equal(status, 4) // status 4: using chainlink, tellor broken + + const price = await priceFeed.lastGoodPrice() + assert.equal(price, dec(100, 18)) + }) + + it("C3 bothOraclesUntrusted: Tellor back online, check Chainlink first, switch to usingTellorChainlinkUntrusted", async () => { + await setAddresses() + priceFeed.setStatus(2) // status 2: both oracles untrusted + + await setTellorPrice(dec(123, 6)) // Tellor is online + await mockRedstoneAdapterBase.setPrevPrice(dec(201, 8)) + await mockRedstoneAdapterBase.setPrice(dec(100, 8)) // ChainLink: difference from previous more than 50% + + await priceFeed.forceExitBothUntrustedStatus(false, { from: owner }) + + const status = await priceFeed.status() + assert.equal(status, 1) // status 1: using tellor, chainlink broken + + const price = await priceFeed.lastGoodPrice() + assert.equal(price.toString(), dec(123, 18)) + }) + + it("C3 bothOraclesUntrusted: Tellor back online, check Tellor first, switch to usingTellorChainlinkUntrusted", async () => { + await setAddresses() + priceFeed.setStatus(2) // status 2: both oracles untrusted + + await mockRedstoneAdapterBase.setPrevPrice(dec(100, 8)) + await mockRedstoneAdapterBase.setPrice(dec(100, 8)) // ChainLink frozen + + await th.fastForwardTime(14400, web3.currentProvider) // Fast forward 4 hours + + // check Chainlink price timestamp is out of date by > 4 hours + const now = await th.getLatestBlockTimestamp(web3) + const chainlinkUpdateTime = (await priceFeedBase.latestRoundData())[3] + assert.isTrue(chainlinkUpdateTime.lt(toBN(now).sub(toBN(14400)))) + + // set Tellor to current time + await setTellorPrice(dec(123, 6)) // Tellor is online + + await priceFeed.forceExitBothUntrustedStatus(true, { from: owner }) + + const status = await priceFeed.status() + assert.equal(status, 1) // status 1: using tellor, chainlink broken + + const price = await priceFeed.lastGoodPrice() + assert.equal(price.toString(), dec(123, 18)) + }) + + it("C3 bothOraclesUntrusted: both oracles online with different prices, check ChainLink first, switch to usingChainlinkTellorUntrusted", async () => { + await setAddresses() + priceFeed.setStatus(2) // status 2: both oracles untrusted + + await mockRedstoneAdapterBase.setPrevPrice(dec(100, 8)) + await mockRedstoneAdapterBase.setPrice(dec(100, 8)) // ChainLink is online + await setTellorPrice(dec(110, 6)) // Tellor is online, differenece > 5% + + await priceFeed.forceExitBothUntrustedStatus(false, { from: owner }) + + const status = await priceFeed.status() + assert.equal(status, 4) // status 4: using chainlink, tellor broken + + const price = await priceFeed.lastGoodPrice() + assert.equal(price.toString(), dec(100, 18)) + }) + + it("C3 bothOraclesUntrusted: both oracles online with different prices, check Tellor first, switch to usingTellorChainlinkUntrusted", async () => { + await setAddresses() + priceFeed.setStatus(2) // status 2: both oracles untrusted + + await mockRedstoneAdapterBase.setPrevPrice(dec(100, 8)) + await mockRedstoneAdapterBase.setPrice(dec(100, 8)) // ChainLink is online + await setTellorPrice(dec(110, 6)) // Tellor is online, differenece > 5% + + await priceFeed.forceExitBothUntrustedStatus(true, { from: owner }) + + const status = await priceFeed.status() + assert.equal(status, 1) // status 1: using tellor, chainlink broken + + const price = await priceFeed.lastGoodPrice() + assert.equal(price.toString(), dec(110, 18)) + }) + + // --- Case 4 --- + it("C4 usingTellorChainlinkFrozen: when both Chainlink and Tellor break, switch to bothOraclesSuspect", async () => { + await setAddresses() + priceFeed.setStatus(3) // status 3: using Tellor, Chainlink frozen + + await mockRedstoneAdapterBase.setPrevPrice(dec(999, 8)) + + // Both Chainlink and Tellor break with 0 price + await mockRedstoneAdapterBase.setPrice(0) + await setTellorPrice(0) + + await priceFeed.fetchPrice() + + const status = await priceFeed.status() + assert.equal(status, 2) // status 2: both oracles untrusted + }) + + it("C4 usingTellorChainlinkFrozen: when both Chainlink and Tellor break, return last good price", async () => { + await setAddresses() + priceFeed.setStatus(2) // status 2: using tellor, chainlink frozen + + await priceFeed.setLastGoodPrice(dec(50, 18)) + + await mockRedstoneAdapterBase.setPrevPrice(dec(999, 8)) + + // Both Chainlink and Tellor break with 0 price + await mockRedstoneAdapterBase.setPrice(dec(0)) + await setTellorPrice(dec(0)) + + await priceFeed.fetchPrice() + + const price = await priceFeed.lastGoodPrice() + assert.equal(price, dec(50, 18)) + }) + + it("C4 usingTellorChainlinkFrozen: when Chainlink breaks and Tellor freezes, switch to usingChainlinkTellorUntrusted", async () => { + await setAddresses() + priceFeed.setStatus(3) // status 3: using Tellor, Chainlink frozen + + await priceFeed.setLastGoodPrice(dec(50, 18)) + + await mockRedstoneAdapterBase.setPrevPrice(dec(999, 8)) + + // Chainlink breaks + await mockRedstoneAdapterBase.setPrice(dec(0)) + + await setTellorPrice(dec(123, 6)) + + await th.fastForwardTime(14400, web3.currentProvider) // Fast forward 4 hours + + // check Tellor price timestamp is out of date by > 4 hours + const now = await th.getLatestBlockTimestamp(web3) + const tellorUpdateTime = (await tellorCaller.getTellorCurrentValue.call())[2] + assert.isTrue(tellorUpdateTime.lt(toBN(now).sub(toBN(14400)))) + + await priceFeed.fetchPrice() + + const status = await priceFeed.status() + assert.equal(status, 1) // status 1: using Tellor, Chainlink untrusted + }) + + it("C4 usingTellorChainlinkFrozen: when Chainlink breaks and Tellor freezes, return last good price", async () => { + await setAddresses() + priceFeed.setStatus(3) // status 3: using Tellor, Chainlink frozen + + await priceFeed.setLastGoodPrice(dec(50, 18)) + + await mockRedstoneAdapterBase.setPrevPrice(dec(999, 8)) + + // Chainlink breaks + await mockRedstoneAdapterBase.setPrice(dec(0)) + + await setTellorPrice(dec(123, 6)) + + await th.fastForwardTime(14400, web3.currentProvider) // Fast forward 4 hours + + // check Tellor price timestamp is out of date by > 4 hours + const now = await th.getLatestBlockTimestamp(web3) + const tellorUpdateTime = (await tellorCaller.getTellorCurrentValue.call())[2] + assert.isTrue(tellorUpdateTime.lt(toBN(now).sub(toBN(14400)))) + + await priceFeed.fetchPrice() + + const price = await priceFeed.lastGoodPrice() + assert.equal(price, dec(50, 18)) + }) + + it("C4 usingTellorChainlinkFrozen: when Chainlink breaks and Tellor live, switch to usingChainlinkTellorUntrusted", async () => { + await setAddresses() + priceFeed.setStatus(3) // status 3: using Tellor, Chainlink frozen + + await priceFeed.setLastGoodPrice(dec(50, 18)) + + await mockRedstoneAdapterBase.setPrevPrice(dec(999, 8)) + + // Chainlink breaks + await mockRedstoneAdapterBase.setPrice(dec(0)) + + await setTellorPrice(dec(123, 6)) + + await th.fastForwardTime(14400, web3.currentProvider) // Fast forward 4 hours + + await priceFeed.fetchPrice() + + const status = await priceFeed.status() + assert.equal(status, 1) // status 1: using Tellor, Chainlink untrusted + }) + + it("C4 usingTellorChainlinkFrozen: when Chainlink breaks and Tellor live, return Tellor price", async () => { + await setAddresses() + priceFeed.setStatus(3) // status 3: using Tellor, Chainlink frozen + + await priceFeed.setLastGoodPrice(dec(50, 18)) + + await mockRedstoneAdapterBase.setPrevPrice(dec(999, 8)) + + // Chainlink breaks + await mockRedstoneAdapterBase.setPrice(dec(0)) + + await setTellorPrice(dec(123, 6)) + + await priceFeed.fetchPrice() + + const price = await priceFeed.lastGoodPrice() + assert.equal(price, dec(123, 18)) + }) + + it("C4 usingTellorChainlinkFrozen: when Chainlink is live and Tellor is live with <5% price difference, switch back to chainlinkWorking", async () => { + await setAddresses() + priceFeed.setStatus(3) // status 3: using Tellor, Chainlink frozen + + await priceFeed.setLastGoodPrice(dec(50, 18)) + + await mockRedstoneAdapterBase.setPrevPrice(dec(999, 8)) + await mockRedstoneAdapterBase.setPrice(dec(999, 8)) + + await setTellorPrice(dec(998, 6)) + + await priceFeed.fetchPrice() + + const status = await priceFeed.status() + assert.equal(status, 0) // status 0: Chainlink working + }) + + it("C4 usingTellorChainlinkFrozen: when Chainlink is live and Tellor is live with <5% price difference, return Chainlink current price", async () => { + await setAddresses() + priceFeed.setStatus(3) // status 3: using Tellor, Chainlink frozen + + await priceFeed.setLastGoodPrice(dec(50, 18)) + + await mockRedstoneAdapterBase.setPrevPrice(dec(999, 8)) + await mockRedstoneAdapterBase.setPrice(dec(999, 8)) + + await setTellorPrice(dec(998, 6)) + + await priceFeed.fetchPrice() + + const price = await priceFeed.lastGoodPrice() + assert.equal(price, dec(999, 18)) // Chainlink price + }) + + it("C4 usingTellorChainlinkFrozen: when Chainlink is live and Tellor is live with >5% price difference, switch back to usingChainlinkTellorUntrusted", async () => { + await setAddresses() + priceFeed.setStatus(3) // status 3: using Tellor, Chainlink frozen + + await priceFeed.setLastGoodPrice(dec(50, 18)) + + await mockRedstoneAdapterBase.setPrevPrice(dec(999, 8)) + await mockRedstoneAdapterBase.setPrice(dec(999, 8)) + + await setTellorPrice(dec(123, 6)) + + await priceFeed.fetchPrice() + + const status = await priceFeed.status() + assert.equal(status, 1) // status 1: Using Tellor, Chainlink untrusted + }) + + it("C4 usingTellorChainlinkFrozen: when Chainlink is live and Tellor is live with >5% price difference, return Chainlink current price", async () => { + await setAddresses() + priceFeed.setStatus(3) // status 3: using Tellor, Chainlink frozen + + await priceFeed.setLastGoodPrice(dec(50, 18)) + + await mockRedstoneAdapterBase.setPrevPrice(dec(999, 8)) + await mockRedstoneAdapterBase.setPrice(dec(999, 8)) + + await setTellorPrice(dec(123, 6)) + + await priceFeed.fetchPrice() + + const price = await priceFeed.lastGoodPrice() + assert.equal(price, dec(123, 18)) // Tellor price + }) + + it("C4 usingTellorChainlinkFrozen: when Chainlink is live and Tellor is live with similar price, switch back to chainlinkWorking", async () => { + await setAddresses() + priceFeed.setStatus(3) // status 3: using Tellor, Chainlink frozen + + await priceFeed.setLastGoodPrice(dec(50, 18)) + + await mockRedstoneAdapterBase.setPrevPrice(dec(999, 8)) + await mockRedstoneAdapterBase.setPrice(dec(999, 8)) + + await setTellorPrice(dec(998, 6)) + + await priceFeed.fetchPrice() + + const status = await priceFeed.status() + assert.equal(status, 0) // status 0: Chainlink working + }) + + it("C4 usingTellorChainlinkFrozen: when Chainlink is live and Tellor is live with similar price, return Chainlink current price", async () => { + await setAddresses() + priceFeed.setStatus(3) // status 3: using Tellor, Chainlink frozen + + await priceFeed.setLastGoodPrice(dec(50, 18)) + + await mockRedstoneAdapterBase.setPrevPrice(dec(999, 8)) + await mockRedstoneAdapterBase.setPrice(dec(999, 8)) + + await setTellorPrice(dec(998, 6)) + + await priceFeed.fetchPrice() + + const price = await priceFeed.lastGoodPrice() + assert.equal(price, dec(999, 18)) // Chainlink price + }) + + it("C4 usingTellorChainlinkFrozen: when Chainlink is live and Tellor breaks, switch to usingChainlinkTellorUntrusted", async () => { + await setAddresses() + priceFeed.setStatus(3) // status 3: using Tellor, Chainlink frozen + + await priceFeed.setLastGoodPrice(dec(50, 18)) + + await mockRedstoneAdapterBase.setPrevPrice(dec(999, 8)) + await mockRedstoneAdapterBase.setPrice(dec(999, 8)) + + await setTellorPrice(0) + + await priceFeed.fetchPrice() + + const status = await priceFeed.status() + assert.equal(status, 4) // status 4: Using Chainlink, Tellor untrusted + }) + + it("C4 usingTellorChainlinkFrozen: when Chainlink is live and Tellor breaks, return Chainlink current price", async () => { + await setAddresses() + priceFeed.setStatus(3) // status 3: using Tellor, Chainlink frozen + + await priceFeed.setLastGoodPrice(dec(50, 18)) + + await mockRedstoneAdapterBase.setPrevPrice(dec(999, 8)) + await mockRedstoneAdapterBase.setPrice(dec(999, 8)) + + await setTellorPrice(0) + + await priceFeed.fetchPrice() + + const price = await priceFeed.lastGoodPrice() + assert.equal(price, dec(999, 18)) + }) + + it("C4 usingTellorChainlinkFrozen: when Chainlink still frozen and Tellor breaks, switch to usingChainlinkTellorUntrusted", async () => { + await setAddresses() + priceFeed.setStatus(3) // status 3: using Tellor, Chainlink frozen + + await priceFeed.setLastGoodPrice(dec(50, 18)) + + await mockRedstoneAdapterBase.setPrevPrice(dec(999, 8)) + await mockRedstoneAdapterBase.setPrice(dec(999, 8)) + + await th.fastForwardTime(14400, web3.currentProvider) // Fast forward 4 hours + + // check Chainlink price timestamp is out of date by > 4 hours + const now = await th.getLatestBlockTimestamp(web3) + const chainlinkUpdateTime = (await priceFeedBase.latestRoundData())[3] + assert.isTrue(chainlinkUpdateTime.lt(toBN(now).sub(toBN(14400)))) + + // set tellor broken + await setTellorPrice(0) + await mockTellor.set + + await priceFeed.fetchPrice() + + const status = await priceFeed.status() + assert.equal(status, 4) // status 4: using Chainlink, Tellor untrusted + }) + + it("C4 usingTellorChainlinkFrozen: when Chainlink still frozen and Tellor broken, return last good price", async () => { + await setAddresses() + priceFeed.setStatus(3) // status 3: using Tellor, Chainlink frozen + + await priceFeed.setLastGoodPrice(dec(50, 18)) + + await mockRedstoneAdapterBase.setPrevPrice(dec(999, 8)) + await mockRedstoneAdapterBase.setPrice(dec(999, 8)) + + await th.fastForwardTime(14400, web3.currentProvider) // Fast forward 4 hours + + // check Chainlink price timestamp is out of date by > 4 hours + const now = await th.getLatestBlockTimestamp(web3) + const chainlinkUpdateTime = (await priceFeedBase.latestRoundData())[3] + assert.isTrue(chainlinkUpdateTime.lt(toBN(now).sub(toBN(14400)))) + + // set tellor broken + await setTellorPrice(0) + + await priceFeed.fetchPrice() + + const price = await priceFeed.lastGoodPrice() + assert.equal(price, dec(50, 18)) + }) + + it("C4 usingTellorChainlinkFrozen: when Chainlink still frozen and Tellor live, remain usingTellorChainlinkFrozen", async () => { + await setAddresses() + priceFeed.setStatus(3) // status 3: using Tellor, Chainlink frozen + + await priceFeed.setLastGoodPrice(dec(50, 18)) + + await mockRedstoneAdapterBase.setPrevPrice(dec(999, 8)) + await mockRedstoneAdapterBase.setPrice(dec(999, 8)) + + await setTellorPrice(dec(123, 6)) + + await th.fastForwardTime(14400, web3.currentProvider) // Fast forward 4 hours + + // check Chainlink price timestamp is out of date by > 4 hours + const now = await th.getLatestBlockTimestamp(web3) + const chainlinkUpdateTime = (await priceFeedBase.latestRoundData())[3] + assert.isTrue(chainlinkUpdateTime.lt(toBN(now).sub(toBN(14400)))) + + // set Tellor to current time + await mockTellor.setUpdateTime(now) + + await priceFeed.fetchPrice() + + const status = await priceFeed.status() + assert.equal(status, 3) // status 3: using Tellor, Chainlink frozen + }) + + it("C4 usingTellorChainlinkFrozen: when Chainlink still frozen and Tellor live, return Tellor price", async () => { + await setAddresses() + priceFeed.setStatus(3) // status 3: using Tellor, Chainlink frozen + + await priceFeed.setLastGoodPrice(dec(50, 18)) + + await mockRedstoneAdapterBase.setPrevPrice(dec(999, 8)) + await mockRedstoneAdapterBase.setPrice(dec(999, 8)) + + await th.fastForwardTime(14400, web3.currentProvider) // Fast forward 4 hours + + // check Chainlink price timestamp is out of date by > 4 hours + const now = await th.getLatestBlockTimestamp(web3) + const chainlinkUpdateTime = (await priceFeedBase.latestRoundData())[3] + assert.isTrue(chainlinkUpdateTime.lt(toBN(now).sub(toBN(14400)))) + + // set Tellor price + await setTellorPrice(dec(123, 6)) + + await priceFeed.fetchPrice() + + const price = await priceFeed.lastGoodPrice() + assert.equal(price, dec(123, 18)) + }) + + it("C4 usingTellorChainlinkFrozen: when Chainlink still frozen and Tellor freezes, remain usingTellorChainlinkFrozen", async () => { + await setAddresses() + priceFeed.setStatus(3) // status 3: using Tellor, Chainlink frozen + + await priceFeed.setLastGoodPrice(dec(50, 18)) + + await mockRedstoneAdapterBase.setPrevPrice(dec(999, 8)) + await mockRedstoneAdapterBase.setPrice(dec(999, 8)) + + await setTellorPrice(dec(123, 6)) + + await th.fastForwardTime(14400, web3.currentProvider) // Fast forward 4 hours + + // check Chainlink price timestamp is out of date by > 4 hours + const now = await th.getLatestBlockTimestamp(web3) + const chainlinkUpdateTime = (await priceFeedBase.latestRoundData())[3] + assert.isTrue(chainlinkUpdateTime.lt(toBN(now).sub(toBN(14400)))) + + // check Tellor price timestamp is out of date by > 4 hours + const tellorUpdateTime = (await tellorCaller.getTellorCurrentValue.call())[2] + assert.isTrue(tellorUpdateTime.lt(toBN(now).sub(toBN(14400)))) + + await priceFeed.fetchPrice() + + const status = await priceFeed.status() + assert.equal(status, 3) // status 3: using Tellor, Chainlink frozen + }) + + it("C4 usingTellorChainlinkFrozen: when Chainlink still frozen and Tellor freezes, return last good price", async () => { + await setAddresses() + priceFeed.setStatus(3) // status 3: using Tellor, Chainlink frozen + + await priceFeed.setLastGoodPrice(dec(50, 18)) + + await mockRedstoneAdapterBase.setPrevPrice(dec(999, 8)) + await mockRedstoneAdapterBase.setPrice(dec(999, 8)) + + await setTellorPrice(dec(123, 6)) + + await th.fastForwardTime(14400, web3.currentProvider) // Fast forward 4 hours + + // check Chainlink price timestamp is out of date by > 4 hours + const now = await th.getLatestBlockTimestamp(web3) + const chainlinkUpdateTime = (await priceFeedBase.latestRoundData())[3] + assert.isTrue(chainlinkUpdateTime.lt(toBN(now).sub(toBN(14400)))) + + // check Tellor price timestamp is out of date by > 4 hours + const tellorUpdateTime = (await tellorCaller.getTellorCurrentValue.call())[2] + assert.isTrue(tellorUpdateTime.lt(toBN(now).sub(toBN(14400)))) + + await priceFeed.fetchPrice() + + const price = await priceFeed.lastGoodPrice() + assert.equal(price, dec(50, 18)) + }) + + it("C4 usingTellorChainlinkFrozen: Tellor is working, revert transaction", async () => { + await setAddresses() + priceFeed.setStatus(3) // status 3: using Tellor, Chainlink frozen + + await assertRevert( + priceFeed.forceExitBothUntrustedStatus(false, { from: owner }), + "PriceFeed: at least one oracle is working" + ) + await assertRevert( + priceFeed.forceExitBothUntrustedStatus(true, { from: owner }), + "PriceFeed: at least one oracle is working" + ) + }) + + // --- Case 5 --- + it("C5 usingChainlinkTellorUntrusted: when Chainlink is live and Tellor price >5% - no status change", async () => { + await setAddresses() + priceFeed.setStatus(4) // status 4: using chainlink, Tellor untrusted + + await priceFeed.setLastGoodPrice(dec(246, 18)) + + await mockRedstoneAdapterBase.setPrevPrice(dec(999, 8)) + await mockRedstoneAdapterBase.setPrice(dec(999, 8)) + + await setTellorPrice(dec(123, 6)) // Greater than 5% difference with chainlink + + await priceFeed.fetchPrice() + + const status = await priceFeed.status() + assert.equal(status, 4) // status 4: using Chainlink, Tellor untrusted + }) + + it("C5 usingChainlinkTellorUntrusted: when Chainlink is live and Tellor price >5% - return Chainlink price", async () => { + await setAddresses() + priceFeed.setStatus(4) // status 4: using chainlink, Tellor untrusted + + await priceFeed.setLastGoodPrice(dec(246, 18)) + + await mockRedstoneAdapterBase.setPrevPrice(dec(999, 8)) + await mockRedstoneAdapterBase.setPrice(dec(999, 8)) + + await setTellorPrice(dec(123, 6)) // Greater than 5% difference with chainlink + + await priceFeed.fetchPrice() + + const price = await priceFeed.lastGoodPrice() + assert.equal(price, dec(999, 18)) + }) + + it("C5 usingChainlinkTellorUntrusted: when Chainlink is live and Tellor price within <5%, switch to chainlinkWorking", async () => { + await setAddresses() + priceFeed.setStatus(4) // status 4: using chainlink, Tellor untrusted + + await priceFeed.setLastGoodPrice(dec(246, 18)) + + await mockRedstoneAdapterBase.setPrevPrice(dec(999, 8)) + await mockRedstoneAdapterBase.setPrice(dec(999, 8)) + + await setTellorPrice(dec(998, 6)) // within 5% of Chainlink + + await priceFeed.fetchPrice() + + const status = await priceFeed.status() + assert.equal(status, 0) // status 0: Chainlink working + }) + + it("C5 usingChainlinkTellorUntrusted: when Chainlink is live, Tellor price not within 5%, return Chainlink price", async () => { + await setAddresses() + priceFeed.setStatus(4) // status 4: using chainlink, Tellor untrusted + + await priceFeed.setLastGoodPrice(dec(246, 18)) + + await mockRedstoneAdapterBase.setPrevPrice(dec(999, 8)) + await mockRedstoneAdapterBase.setPrice(dec(999, 8)) + + await setTellorPrice(dec(998, 6)) // within 5% of Chainlink + + await priceFeed.fetchPrice() + + const price = await priceFeed.lastGoodPrice() + assert.equal(price, dec(999, 18)) + }) + + // --------- + + it("C5 usingChainlinkTellorUntrusted: when Chainlink is live, <50% price deviation from previous, Tellor price not within 5%, remain on usingChainlinkTellorUntrusted", async () => { + await setAddresses() + priceFeed.setStatus(4) // status 4: using chainlink, Tellor untrusted + + await priceFeed.setLastGoodPrice(dec(246, 18)) + + await mockRedstoneAdapterBase.setPrevPrice(dec(999, 8)) + await mockRedstoneAdapterBase.setPrice(dec(998, 8)) + await setTellorPrice(dec(123, 6)) // Tellor not close to current Chainlink + + await priceFeed.fetchPrice() + + const status = await priceFeed.status() + assert.equal(status, 4) // status 4: using Chainlink, Tellor untrusted + }) + + it("C5 usingChainlinkTellorUntrusted: when Chainlink is live, <50% price deviation from previous, Tellor price not within 5%, return Chainlink price", async () => { + await setAddresses() + priceFeed.setStatus(4) // status 4: using chainlink, Tellor untrusted + + await priceFeed.setLastGoodPrice(dec(246, 18)) + + await mockRedstoneAdapterBase.setPrevPrice(dec(999, 8)) + await mockRedstoneAdapterBase.setPrice(dec(998, 8)) + await setTellorPrice(dec(123, 6)) // Tellor not close to current Chainlink + + await priceFeed.fetchPrice() + + const price = await priceFeed.lastGoodPrice() + assert.equal(price, dec(998, 18)) + }) + + it("C5 usingChainlinkTellorUntrusted: when Chainlink is live, >50% price deviation from previous, Tellor price not within 5%, remain on usingChainlinkTellorUntrusted", async () => { + await setAddresses() + priceFeed.setStatus(4) // status 4: using chainlink, Tellor untrusted + + await priceFeed.setLastGoodPrice(dec(246, 18)) + + await mockRedstoneAdapterBase.setPrevPrice(dec(200, 8)) + await mockRedstoneAdapterBase.setPrice(dec(99, 8)) // >50% price drop from previous Chainlink price + await setTellorPrice(dec(123, 6)) // Tellor not close to current Chainlink + + await priceFeed.fetchPrice() + + const status = await priceFeed.status() + assert.equal(status, 2) // status 2: both Oracles untrusted + }) + + it("C5 usingChainlinkTellorUntrusted: when Chainlink is live, >50% price deviation from previous, Tellor price not within 5%, return Chainlink price", async () => { + await setAddresses() + priceFeed.setStatus(4) // status 4: using chainlink, Tellor untrusted + + await priceFeed.setLastGoodPrice(dec(246, 18)) + + await mockRedstoneAdapterBase.setPrevPrice(dec(200, 8)) + await mockRedstoneAdapterBase.setPrice(dec(99, 8)) // >50% price drop from previous Chainlink price + await setTellorPrice(dec(123, 6)) // Tellor not close to current Chainlink + + await priceFeed.fetchPrice() + + const price = await priceFeed.lastGoodPrice() + assert.equal(price, dec(246, 18)) // last good price + }) + + // ------- + + it("C5 usingChainlinkTellorUntrusted: when Chainlink is live, <50% price deviation from previous, and Tellor is frozen, remain on usingChainlinkTellorUntrusted", async () => { + await setAddresses() + priceFeed.setStatus(4) // status 4: using chainlink, Tellor untrusted + + await priceFeed.setLastGoodPrice(dec(246, 18)) + + await mockRedstoneAdapterBase.setPrevPrice(dec(999, 8)) + await mockRedstoneAdapterBase.setPrice(dec(999, 8)) + + await setTellorPrice(dec(123, 6)) + + await th.fastForwardTime(14400, web3.currentProvider) // fast forward 4 hours + + // check Tellor price timestamp is out of date by > 4 hours + const now = await th.getLatestBlockTimestamp(web3) + const tellorUpdateTime = (await tellorCaller.getTellorCurrentValue.call())[2] + assert.isTrue(tellorUpdateTime.lt(toBN(now).sub(toBN(14400)))) + + await mockRedstoneAdapterBase.setPrice(dec(998, 8)) + await mockRedstoneAdapterBase.setUpdateTime(now) // Chainlink is current + + await priceFeed.fetchPrice() + + const status = await priceFeed.status() + assert.equal(status, 4) // status 4: using Chainlink, Tellor untrusted + }) + + it("C5 usingChainlinkTellorUntrusted: when Chainlink is live, <50% price deviation from previous, Tellor is frozen, return Chainlink price", async () => { + await setAddresses() + priceFeed.setStatus(4) // status 4: using chainlink, Tellor untrusted + + await priceFeed.setLastGoodPrice(dec(246, 18)) + + await mockRedstoneAdapterBase.setPrevPrice(dec(999, 8)) + await mockRedstoneAdapterBase.setPrice(dec(999, 8)) + + await setTellorPrice(dec(123, 6)) + + await th.fastForwardTime(14400, web3.currentProvider) // fast forward 4 hours + + // check Tellor price timestamp is out of date by > 4 hours + const now = await th.getLatestBlockTimestamp(web3) + const tellorUpdateTime = (await tellorCaller.getTellorCurrentValue.call())[2] + assert.isTrue(tellorUpdateTime.lt(toBN(now).sub(toBN(14400)))) + + await mockRedstoneAdapterBase.setPrice(dec(998, 8)) + await mockRedstoneAdapterBase.setUpdateTime(now) // Chainlink is current + + await priceFeed.fetchPrice() + + const price = await priceFeed.lastGoodPrice() + assert.equal(price, dec(998, 18)) + }) + + it("C5 usingChainlinkTellorUntrusted: when Chainlink is live, >50% price deviation from previous, Tellor is frozen, remain on usingChainlinkTellorUntrusted", async () => { + await setAddresses() + priceFeed.setStatus(4) // status 4: using chainlink, Tellor untrusted + + await priceFeed.setLastGoodPrice(dec(246, 18)) + + await mockRedstoneAdapterBase.setPrevPrice(dec(200, 8)) + await mockRedstoneAdapterBase.setPrice(dec(200, 8)) + + await setTellorPrice(dec(123, 6)) + + await th.fastForwardTime(14400, web3.currentProvider) // fast forward 4 hours + + // check Tellor price timestamp is out of date by > 4 hours + const now = await th.getLatestBlockTimestamp(web3) + const tellorUpdateTime = (await tellorCaller.getTellorCurrentValue.call())[2] + assert.isTrue(tellorUpdateTime.lt(toBN(now).sub(toBN(14400)))) + + await mockRedstoneAdapterBase.setPrice(dec(99, 8)) // >50% price drop from previous Chainlink price + await mockRedstoneAdapterBase.setUpdateTime(now) // Chainlink is current + + await priceFeed.fetchPrice() + + const status = await priceFeed.status() + assert.equal(status, 2) // status 2: both Oracles untrusted + }) + + it("C5 usingChainlinkTellorUntrusted: when Chainlink is live, >50% price deviation from previous, Tellor is frozen, return Chainlink price", async () => { + await setAddresses() + priceFeed.setStatus(4) // status 4: using chainlink, Tellor untrusted + + await priceFeed.setLastGoodPrice(dec(246, 18)) + + await mockRedstoneAdapterBase.setPrevPrice(dec(200, 8)) + await mockRedstoneAdapterBase.setPrice(dec(200, 8)) + + await setTellorPrice(dec(123, 6)) + + await th.fastForwardTime(14400, web3.currentProvider) // fast forward 4 hours + + // check Tellor price timestamp is out of date by > 4 hours + const now = await th.getLatestBlockTimestamp(web3) + const tellorUpdateTime = (await tellorCaller.getTellorCurrentValue.call())[2] + assert.isTrue(tellorUpdateTime.lt(toBN(now).sub(toBN(14400)))) + + await mockRedstoneAdapterBase.setPrice(dec(99, 8)) // > 50% price drop from previous Chainlink price + await mockRedstoneAdapterBase.setUpdateTime(now) // Chainlink is current + + await priceFeed.fetchPrice() + + const price = await priceFeed.lastGoodPrice() + assert.equal(price, dec(246, 18)) // last good price + }) + + it("C5 usingChainlinkTellorUntrusted: when Chainlink frozen, remain on usingChainlinkTellorUntrusted", async () => { + await setAddresses() + priceFeed.setStatus(4) // status 4: using chainlink, Tellor untrusted + + await priceFeed.setLastGoodPrice(dec(246, 18)) + + await mockRedstoneAdapterBase.setPrevPrice(dec(999, 8)) + await mockRedstoneAdapterBase.setPrice(dec(999, 8)) + + await setTellorPrice(dec(123, 6)) + + await th.fastForwardTime(14400, web3.currentProvider) // Fast forward 4 hours + + // check Chainlink price timestamp is out of date by > 4 hours + const now = await th.getLatestBlockTimestamp(web3) + const chainlinkUpdateTime = (await priceFeedBase.latestRoundData())[3] + assert.isTrue(chainlinkUpdateTime.lt(toBN(now).sub(toBN(14400)))) + + await priceFeed.fetchPrice() + + const status = await priceFeed.status() + assert.equal(status, 4) // status 4: using Chainlink, Tellor untrusted + }) + + it("C5 usingChainlinkTellorUntrusted: when Chainlink frozen, return last good price", async () => { + await setAddresses() + priceFeed.setStatus(4) // status 4: using Chainlink, Tellor untrusted + + await priceFeed.setLastGoodPrice(dec(246, 18)) + + await mockRedstoneAdapterBase.setPrevPrice(dec(999, 8)) + await mockRedstoneAdapterBase.setPrice(dec(999, 8)) + + await setTellorPrice(dec(123, 6)) + + await th.fastForwardTime(14400, web3.currentProvider) // Fast forward 4 hours + + // check Chainlink price timestamp is out of date by > 4 hours + const now = await th.getLatestBlockTimestamp(web3) + const chainlinkUpdateTime = (await priceFeedBase.latestRoundData())[3] + assert.isTrue(chainlinkUpdateTime.lt(toBN(now).sub(toBN(14400)))) + + await priceFeed.fetchPrice() + + const price = await priceFeed.lastGoodPrice() + assert.equal(price, dec(246, 18)) + }) + + it("C5 usingChainlinkTellorUntrusted: when Chainlink breaks too, switch to bothOraclesSuspect", async () => { + await setAddresses() + priceFeed.setStatus(4) // status 4: using chainlink, Tellor untrusted + + await priceFeed.setLastGoodPrice(dec(246, 18)) + + await mockRedstoneAdapterBase.setPrevPrice(dec(999, 8)) + await mockRedstoneAdapterBase.setPrice(dec(999, 8)) + await mockRedstoneAdapterBase.setUpdateTime(0) // Chainlink breaks by 0 timestamp + + await setTellorPrice(dec(123, 6)) + + await priceFeed.fetchPrice() + + const status = await priceFeed.status() + assert.equal(status, 2) // status 2: both oracles untrusted + }) + + it("C5 usingChainlinkTellorUntrusted: Chainlink breaks too, return last good price", async () => { + await setAddresses() + priceFeed.setStatus(4) // status 4: using chainlink, Tellor untrusted + + await priceFeed.setLastGoodPrice(dec(246, 18)) + + await mockRedstoneAdapterBase.setPrevPrice(dec(999, 8)) + await mockRedstoneAdapterBase.setPrice(dec(999, 8)) + await mockRedstoneAdapterBase.setUpdateTime(0) // Chainlink breaks by 0 timestamp + + await setTellorPrice(dec(123, 6)) + + await priceFeed.fetchPrice() + + const price = await priceFeed.lastGoodPrice() + assert.equal(price, dec(246, 18)) + }) + + it("C5 usingChainlinkTellorUntrusted: Chainlink is working, revert transaction", async () => { + await setAddresses() + priceFeed.setStatus(4) // status 4: using chainlink, Tellor untrusted + + await assertRevert( + priceFeed.forceExitBothUntrustedStatus(false, { from: owner }), + "PriceFeed: at least one oracle is working" + ) + await assertRevert( + priceFeed.forceExitBothUntrustedStatus(true, { from: owner }), + "PriceFeed: at least one oracle is working" + ) + }) + }) + context("when Tellor feed has 6 decimals", () => { contextTestPriceFeed( 6 ) }) From 1abeeb004ef2ca0dda04011e1d4955553bcc5e91 Mon Sep 17 00:00:00 2001 From: evandrosaturnino Date: Thu, 16 May 2024 04:01:16 -0300 Subject: [PATCH 3/3] lint: lint fixes on redstone mock adapter interface --- .../contracts/contracts/BOB/IRedstoneAdapter.sol | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/contracts/contracts/BOB/IRedstoneAdapter.sol b/packages/contracts/contracts/BOB/IRedstoneAdapter.sol index c99a863b0..79efc24ad 100644 --- a/packages/contracts/contracts/BOB/IRedstoneAdapter.sol +++ b/packages/contracts/contracts/BOB/IRedstoneAdapter.sol @@ -14,12 +14,12 @@ interface IRedstoneAdapter { */ function getValueForDataFeed(bytes32) external view returns (uint256); - /** - * @notice Returns details for the given round and data feed - * @return dataFeedValue - * @return roundDataTimestamp - * @return roundBlockTimestamp - */ + /** + * @notice Returns details for the given round and data feed + * @return dataFeedValue + * @return roundDataTimestamp + * @return roundBlockTimestamp + */ function getRoundDataFromAdapter(bytes32, uint256) external view returns (uint256 dataFeedValue, uint128 roundDataTimestamp, uint128 roundBlockTimestamp); /**