Address
0x4c517d4e2c851ca76d7ec94b805269df0f2201deCurrent Holdings
$0.00
TXs sent
not counted
First Active
2024-02-02
block 19,512,089
Last Active
190 days ago
block 26,002,258
Funded By
not identified
Net worth historyi
No net-worth snapshots recorded yet
partial matchPriceFeedsolc 0.6.11+commit.5ef660b1runtime partial · creation partial
// SPDX-License-Identifier: MIT
pragma solidity 0.6.11;
import "./Interfaces/IPriceFeed.sol";
import "./Interfaces/ITellorCaller.sol";
import "./Dependencies/AggregatorV3Interface.sol";
import "./Dependencies/SafeMath.sol";
import "./Dependencies/Ownable.sol";
import "./Dependencies/CheckContract.sol";
import "./Dependencies/BaseMath.sol";
import "./Dependencies/LiquityMath.sol";
import "./Dependencies/console.sol";
/*
* PriceFeed for mainnet deployment, to be connected to Chainlink's live ETH:USD aggregator reference
* contract, and a wrapper contract TellorCaller, which connects to TellorMaster contract.
*
* The PriceFeed uses Chainlink as primary oracle, and Tellor as fallback. It contains logic for
* switching oracles based on oracle failures, timeouts, and conditions for returning to the primary
* Chainlink oracle.
*/
contract PriceFeed is Ownable, CheckContract, BaseMath, IPriceFeed {
using SafeMath for uint256;
string constant public NAME = "PriceFeed";
AggregatorV3Interface public priceAggregator; // Mainnet Chainlink aggregator
ITellorCaller public tellorCaller; // Wrapper contract that calls the Tellor system
// Core Liquity contracts
address borrowerOperationsAddress;
address troveManagerAddress;
uint constant public ETHUSD_TELLOR_REQ_ID = 1;
// Use to convert a price answer to an 18-digit precision uint
uint constant public TARGET_DIGITS = 18;
uint constant public TELLOR_DIGITS = 6;
// Maximum time period allowed since Chainlink's latest round data timestamp, beyond which Chainlink is considered frozen.
uint constant public TIMEOUT = 14400; // 4 hours: 60 * 60 * 4
// Maximum deviation allowed between two consecutive Chainlink oracle prices. 18-digit precision.
uint constant public MAX_PRICE_DEVIATION_FROM_PREVIOUS_ROUND = 5e17; // 50%
/*
* The maximum relative price difference between two oracle responses allowed in order for the PriceFeed
* to return to using the Chainlink oracle. 18-digit precision.
*/
uint constant public MAX_PRICE_DIFFERENCE_BETWEEN_ORACLES = 5e16; // 5%
// The last good price seen from an oracle by Liquity
uint public lastGoodPrice;
struct ChainlinkResponse {
uint80 roundId;
int256 answer;
uint256 timestamp;
bool success;
uint8 decimals;
}
struct TellorResponse {
bool ifRetrieve;
uint256 value;
uint256 timestamp;
bool success;
}
enum Status {
chainlinkWorking,
usingTellorChainlinkUntrusted,
bothOraclesUntrusted,
usingTellorChainlinkFrozen,
usingChainlinkTellorUntrusted
}
// The current status of the PricFeed, which determines the conditions for the next price fetch attempt
Status public status;
event LastGoodPriceUpdated(uint _lastGoodPrice);
event PriceFeedStatusChanged(Status newStatus);
// --- Dependency setters ---
function setAddresses(
address _priceAggregatorAddress,
address _tellorCallerAddress
)
external
onlyOwner
{
checkContract(_priceAggregatorAddress);
checkContract(_tellorCallerAddress);
priceAggregator = AggregatorV3Interface(_priceAggregatorAddress);
tellorCaller = ITellorCaller(_tellorCallerAddress);
// Explicitly set initial system status
status = Status.chainlinkWorking;
// Get an initial price from Chainlink to serve as first reference for lastGoodPrice
ChainlinkResponse memory chainlinkResponse = _getCurrentChainlinkResponse();
ChainlinkResponse memory prevChainlinkResponse = _getPrevChainlinkResponse(chainlinkResponse.roundId, chainlinkResponse.decimals);
require(!_chainlinkIsBroken(chainlinkResponse, prevChainlinkResponse) && !_chainlinkIsFrozen(chainlinkResponse),
"PriceFeed: Chainlink must be working and current");
_storeChainlinkPrice(chainlinkResponse);
_renounceOwnership();
}
// --- Functions ---
/*
* fetchPrice():
* Returns the latest price obtained from the Oracle. Called by Liquity functions that require a current price.
*
* Also callable by anyone externally.
*
* Non-view function - it stores the last good price seen by Liquity.
*
* Uses a main oracle (Chainlink) and a fallback oracle (Tellor) in case Chainlink fails. If both fail,
* it uses the last good price seen by Liquity.
*
*/
function fetchPrice() external override returns (uint) {
// Get current and previous price data from Chainlink, and current price data from Tellor
ChainlinkResponse memory chainlinkResponse = _getCurrentChainlinkResponse();
ChainlinkResponse memory prevChainlinkResponse = _getPrevChainlinkResponse(chainlinkResponse.roundId, chainlinkResponse.decimals);
TellorResponse memory tellorResponse = _getCurrentTellorResponse();
// --- CASE 1: System fetched last price from Chainlink ---
if (status == Status.chainlinkWorking) {
// If Chainlink is broken, try Tellor
if (_chainlinkIsBroken(chainlinkResponse, prevChainlinkResponse)) {
// If Tellor is broken then both oracles are untrusted, so return the last good price
if (_tellorIsBroken(tellorResponse)) {
_changeStatus(Status.bothOraclesUntrusted);
return lastGoodPrice;
}
/*
* If Tellor is only frozen but otherwise returning valid data, return the last good price.
* Tellor may need to be tipped to return current data.
*/
if (_tellorIsFrozen(tellorResponse)) {
_changeStatus(Status.usingTellorChainlinkUntrusted);
return lastGoodPrice;
}
// If Chainlink is broken and Tellor is working, switch to Tellor and return current Tellor price
_changeStatus(Status.usingTellorChainlinkUntrusted);
return _storeTellorPrice(tellorResponse);
}
// If Chainlink is frozen, try Tellor
if (_chainlinkIsFrozen(chainlinkResponse)) {
// If Tellor is broken too, remember Tellor broke, and return last good price
if (_tellorIsBroken(tellorResponse)) {
_changeStatus(Status.usingChainlinkTellorUntrusted);
return lastGoodPrice;
}
// If Tellor is frozen or working, remember Chainlink froze, and switch to Tellor
_changeStatus(Status.usingTellorChainlinkFrozen);
if (_tellorIsFrozen(tellorResponse)) {return lastGoodPrice;}
// If Tellor is working, use it
return _storeTellorPrice(tellorResponse);
}
// If Chainlink price has changed by > 50% between two consecutive rounds, compare it to Tellor's price
if (_chainlinkPriceChangeAboveMax(chainlinkResponse, prevChainlinkResponse)) {
// If Tellor is broken, both oracles are untrusted, and return last good price
if (_tellorIsBroken(tellorResponse)) {
_changeStatus(Status.bothOraclesUntrusted);
return lastGoodPrice;
}
// If Tellor is frozen, switch to Tellor and return last good price
if (_tellorIsFrozen(tellorResponse)) {
_changeStatus(Status.usingTellorChainlinkUntrusted);
return lastGoodPrice;
}
/*
* If Tellor is live and both oracles have a similar price, conclude that Chainlink's large price deviation between
* two consecutive rounds was likely a legitmate market price movement, and so continue using Chainlink
*/
if (_bothOraclesSimilarPrice(chainlinkResponse, tellorResponse)) {
return _storeChainlinkPrice(chainlinkResponse);
}
// If Tellor is live but the oracles differ too much in price, conclude that Chainlink's initial price deviation was
// an oracle failure. Switch to Tellor, and use Tellor price
_changeStatus(Status.usingTellorChainlinkUntrusted);
return _storeTellorPrice(tellorResponse);
}
// If Chainlink is working and Tellor is broken, remember Tellor is broken
if (_tellorIsBroken(tellorResponse)) {
_changeStatus(Status.usingChainlinkTellorUntrusted);
}
// If Chainlink is working, return Chainlink current price (no status change)
return _storeChainlinkPrice(chainlinkResponse);
}
// --- CASE 2: The system fetched last price from Tellor ---
if (status == Status.usingTellorChainlinkUntrusted) {
// If both Tellor and Chainlink are live, unbroken, and reporting similar prices, switch back to Chainlink
if (_bothOraclesLiveAndUnbrokenAndSimilarPrice(chainlinkResponse, prevChainlinkResponse, tellorResponse)) {
_changeStatus(Status.chainlinkWorking);
return _storeChainlinkPrice(chainlinkResponse);
}
if (_tellorIsBroken(tellorResponse)) {
_changeStatus(Status.bothOraclesUntrusted);
return lastGoodPrice;
}
/*
* If Tellor is only frozen but otherwise returning valid data, just return the last good price.
* Tellor may need to be tipped to return current data.
*/
if (_tellorIsFrozen(tellorResponse)) {return lastGoodPrice;}
// Otherwise, use Tellor price
return _storeTellorPrice(tellorResponse);
}
// --- CASE 3: Both oracles were untrusted at the last price fetch ---
if (status == Status.bothOraclesUntrusted) {
/*
* If both oracles are now live, unbroken and similar price, we assume that they are reporting
* accurately, and so we switch back to Chainlink.
*/
if (_bothOraclesLiveAndUnbrokenAndSimilarPrice(chainlinkResponse, prevChainlinkResponse, tellorResponse)) {
_changeStatus(Status.chainlinkWorking);
return _storeChainlinkPrice(chainlinkResponse);
}
// Otherwise, return the last good price - both oracles are still untrusted (no status change)
return lastGoodPrice;
}
// --- CASE 4: Using Tellor, and Chainlink is frozen ---
if (status == Status.usingTellorChainlinkFrozen) {
if (_chainlinkIsBroken(chainlinkResponse, prevChainlinkResponse)) {
// If both Oracles are broken, return last good price
if (_tellorIsBroken(tellorResponse)) {
_changeStatus(Status.bothOraclesUntrusted);
return lastGoodPrice;
}
// If Chainlink is broken, remember it and switch to using Tellor
_changeStatus(Status.usingTellorChainlinkUntrusted);
if (_tellorIsFrozen(tellorResponse)) {return lastGoodPrice;}
// If Tellor is working, return Tellor current price
return _storeTellorPrice(tellorResponse);
}
if (_chainlinkIsFrozen(chainlinkResponse)) {
// if Chainlink is frozen and Tellor is broken, remember Tellor broke, and return last good price
if (_tellorIsBroken(tellorResponse)) {
_changeStatus(Status.usingChainlinkTellorUntrusted);
return lastGoodPrice;
}
// If both are frozen, just use lastGoodPrice
if (_tellorIsFrozen(tellorResponse)) {return lastGoodPrice;}
// if Chainlink is frozen and Tellor is working, keep using Tellor (no status change)
return _storeTellorPrice(tellorResponse);
}
// if Chainlink is live and Tellor is broken, remember Tellor broke, and return Chainlink price
if (_tellorIsBroken(tellorResponse)) {
_changeStatus(Status.usingChainlinkTellorUntrusted);
return _storeChainlinkPrice(chainlinkResponse);
}
// If Chainlink is live and Tellor is frozen, just use last good price (no status change) since we have no basis for comparison
if (_tellorIsFrozen(tellorResponse)) {return lastGoodPrice;}
// If Chainlink is live and Tellor is working, compare prices. Switch to Chainlink
// if prices are within 5%, and return Chainlink price.
if (_bothOraclesSimilarPrice(chainlinkResponse, tellorResponse)) {
_changeStatus(Status.chainlinkWorking);
return _storeChainlinkPrice(chainlinkResponse);
}
// Otherwise if Chainlink is live but price not within 5% of Tellor, distrust Chainlink, and return Tellor price
_changeStatus(Status.usingTellorChainlinkUntrusted);
return _storeTellorPrice(tellorResponse);
}
// --- CASE 5: Using Chainlink, Tellor is untrusted ---
if (status == Status.usingChainlinkTellorUntrusted) {
// If Chainlink breaks, now both oracles are untrusted
if (_chainlinkIsBroken(chainlinkResponse, prevChainlinkResponse)) {
_changeStatus(Status.bothOraclesUntrusted);
return lastGoodPrice;
}
// If Chainlink is frozen, return last good price (no status change)
if (_chainlinkIsFrozen(chainlinkResponse)) {
return lastGoodPrice;
}
// If Chainlink and Tellor are both live, unbroken and similar price, switch back to chainlinkWorking and return Chainlink price
if (_bothOraclesLiveAndUnbrokenAndSimilarPrice(chainlinkResponse, prevChainlinkResponse, tellorResponse)) {
_changeStatus(Status.chainlinkWorking);
return _storeChainlinkPrice(chainlinkResponse);
}
// If Chainlink is live but deviated >50% from it's previous price and Tellor is still untrusted, switch
// to bothOraclesUntrusted and return last good price
if (_chainlinkPriceChangeAboveMax(chainlinkResponse, prevChainlinkResponse)) {
_changeStatus(Status.bothOraclesUntrusted);
return lastGoodPrice;
}
// Otherwise if Chainlink is live and deviated <50% from it's previous price and Tellor is still untrusted,
// return Chainlink price (no status change)
return _storeChainlinkPrice(chainlinkResponse);
}
}
// --- Helper functions ---
/* Chainlink is considered broken if its current or previous round data is in any way bad. We check the previous round
* for two reasons:
*
* 1) It is necessary data for the price deviation check in case 1,
* and
* 2) Chainlink is the PriceFeed's preferred primary oracle - having two consecutive valid round responses adds
* peace of mind when using or returning to Chainlink.
*/
function _chainlinkIsBroken(ChainlinkResponse memory _currentResponse, ChainlinkResponse memory _prevResponse) internal view returns (bool) {
return _badChainlinkResponse(_currentResponse) || _badChainlinkResponse(_prevResponse);
}
function _badChainlinkResponse(ChainlinkResponse memory _response) internal view returns (bool) {
// Check for response call reverted
if (!_response.success) {return true;}
// Check for an invalid roundId that is 0
if (_response.roundId == 0) {return true;}
// Check for an invalid timeStamp that is 0, or in the future
if (_response.timestamp == 0 || _response.timestamp > block.timestamp) {return true;}
// Check for non-positive price
if (_response.answer <= 0) {return true;}
return false;
}
function _chainlinkIsFrozen(ChainlinkResponse memory _response) internal view returns (bool) {
return block.timestamp.sub(_response.timestamp) > TIMEOUT;
}
function _chainlinkPriceChangeAboveMax(ChainlinkResponse memory _currentResponse, ChainlinkResponse memory _prevResponse) internal pure returns (bool) {
uint currentScaledPrice = _scaleChainlinkPriceByDigits(uint256(_currentResponse.answer), _currentResponse.decimals);
uint prevScaledPrice = _scaleChainlinkPriceByDigits(uint256(_prevResponse.answer), _prevResponse.decimals);
uint minPrice = LiquityMath._min(currentScaledPrice, prevScaledPrice);
uint maxPrice = LiquityMath._max(currentScaledPrice, prevScaledPrice);
/*
* Use the larger price as the denominator:
* - If price decreased, the percentage deviation is in relation to the the previous price.
* - If price increased, the percentage deviation is in relation to the current price.
*/
uint percentDeviation = maxPrice.sub(minPrice).mul(DECIMAL_PRECISION).div(maxPrice);
// Return true if price has more than doubled, or more than halved.
return percentDeviation > MAX_PRICE_DEVIATION_FROM_PREVIOUS_ROUND;
}
function _tellorIsBroken(TellorResponse memory _response) internal view returns (bool) {
// Check for response call reverted
if (!_response.success) {return true;}
// Check for an invalid timeStamp that is 0, or in the future
if (_response.timestamp == 0 || _response.timestamp > block.timestamp) {return true;}
// Check for zero price
if (_response.value == 0) {return true;}
return false;
}
function _tellorIsFrozen(TellorResponse memory _tellorResponse) internal view returns (bool) {
return block.timestamp.sub(_tellorResponse.timestamp) > TIMEOUT;
}
function _bothOraclesLiveAndUnbrokenAndSimilarPrice
(
ChainlinkResponse memory _chainlinkResponse,
ChainlinkResponse memory _prevChainlinkResponse,
TellorResponse memory _tellorResponse
)
internal
view
returns (bool)
{
// Return false if either oracle is broken or frozen
if
(
_tellorIsBroken(_tellorResponse) ||
_tellorIsFrozen(_tellorResponse) ||
_chainlinkIsBroken(_chainlinkResponse, _prevChainlinkResponse) ||
_chainlinkIsFrozen(_chainlinkResponse)
)
{
return false;
}
return _bothOraclesSimilarPrice(_chainlinkResponse, _tellorResponse);
}
function _bothOraclesSimilarPrice( ChainlinkResponse memory _chainlinkResponse, TellorResponse memory _tellorResponse) internal pure returns (bool) {
uint scaledChainlinkPrice = _scaleChainlinkPriceByDigits(uint256(_chainlinkResponse.answer), _chainlinkResponse.decimals);
uint scaledTellorPrice = _scaleTellorPriceByDigits(_tellorResponse.value);
// Get the relative price difference between the oracles. Use the lower price as the denominator, i.e. the reference for the calculation.
uint minPrice = LiquityMath._min(scaledTellorPrice, scaledChainlinkPrice);
uint maxPrice = LiquityMath._max(scaledTellorPrice, scaledChainlinkPrice);
uint percentPriceDifference = maxPrice.sub(minPrice).mul(DECIMAL_PRECISION).div(minPrice);
/*
* Return true if the relative price difference is <= 3%: if so, we assume both oracles are probably reporting
* the honest market price, as it is unlikely that both have been broken/hacked and are still in-sync.
*/
return percentPriceDifference <= MAX_PRICE_DIFFERENCE_BETWEEN_ORACLES;
}
function _scaleChainlinkPriceByDigits(uint _price, uint _answerDigits) internal pure returns (uint) {
/*
* Convert the price returned by the Chainlink oracle to an 18-digit decimal for use by Liquity.
* At date of Liquity launch, Chainlink uses an 8-digit price, but we also handle the possibility of
* future changes.
*
*/
uint price;
if (_answerDigits >= TARGET_DIGITS) {
// Scale the returned price value down to Liquity's target precision
price = _price.div(10 ** (_answerDigits - TARGET_DIGITS));
}
else if (_answerDigits < TARGET_DIGITS) {
// Scale the returned price value up to Liquity's target precision
price = _price.mul(10 ** (TARGET_DIGITS - _answerDigits));
}
return price;
}
function _scaleTellorPriceByDigits(uint _price) internal pure returns (uint) {
return _price.mul(10**(TARGET_DIGITS - TELLOR_DIGITS));
}
function _changeStatus(Status _status) internal {
status = _status;
emit PriceFeedStatusChanged(_status);
}
function _storePrice(uint _currentPrice) internal {
lastGoodPrice = _currentPrice;
emit LastGoodPriceUpdated(_currentPrice);
}
function _storeTellorPrice(TellorResponse memory _tellorResponse) internal returns (uint) {
uint scaledTellorPrice = _scaleTellorPriceByDigits(_tellorResponse.value);
_storePrice(scaledTellorPrice);
return scaledTellorPrice;
}
function _storeChainlinkPrice(ChainlinkResponse memory _chainlinkResponse) internal returns (uint) {
uint scaledChainlinkPrice = _scaleChainlinkPriceByDigits(uint256(_chainlinkResponse.answer), _chainlinkResponse.decimals);
_storePrice(scaledChainlinkPrice);
return scaledChainlinkPrice;
}
// --- Oracle response wrapper functions ---
function _getCurrentTellorResponse() internal view returns (TellorResponse memory tellorResponse) {
try tellorCaller.getTellorCurrentValue(ETHUSD_TELLOR_REQ_ID) returns
(
bool ifRetrieve,
uint256 value,
uint256 _timestampRetrieved
)
{
// If call to Tellor succeeds, return the response and success = true
tellorResponse.ifRetrieve = ifRetrieve;
tellorResponse.value = value;
tellorResponse.timestamp = _timestampRetrieved;
tellorResponse.success = true;
return (tellorResponse);
}catch {
// If call to Tellor reverts, return a zero response with success = false
return (tellorResponse);
}
}
function _getCurrentChainlinkResponse() internal view returns (ChainlinkResponse memory chainlinkResponse) {
// First, try to get current decimal precision:
try priceAggregator.decimals() returns (uint8 decimals) {
// If call to Chainlink succeeds, record the current decimal precision
chainlinkResponse.decimals = decimals;
} catch {
// If call to Chainlink aggregator reverts, return a zero response with success = false
return chainlinkResponse;
}
// Secondly, try to get latest price data:
try priceAggregator.latestRoundData() returns
(
uint80 roundId,
int256 answer,
uint256 /* startedAt */,
uint256 timestamp,
uint80 /* answeredInRound */
)
{
// If call to Chainlink succeeds, return the response and success = true
chainlinkResponse.roundId = roundId;
chainlinkResponse.answer = answer;
chainlinkResponse.timestamp = timestamp;
chainlinkResponse.success = true;
return chainlinkResponse;
} catch {
// If call to Chainlink aggregator reverts, return a zero response with success = false
return chainlinkResponse;
}
}
function _getPrevChainlinkResponse(uint80 _currentRoundId, uint8 _currentDecimals) internal view returns (ChainlinkResponse memory prevChainlinkResponse) {
/*
* NOTE: Chainlink only offers a current decimals() value - there is no way to obtain the decimal precision used in a
* previous round. We assume the decimals used in the previous round are the same as the current round.
*/
// Try to get the price data from the previous round:
try priceAggregator.getRoundData(_currentRoundId - 1) returns
(
uint80 roundId,
int256 answer,
uint256 /* startedAt */,
uint256 timestamp,
uint80 /* answeredInRound */
)
{
// If call to Chainlink succeeds, return the response and success = true
prevChainlinkResponse.roundId = roundId;
prevChainlinkResponse.answer = answer;
prevChainlinkResponse.timestamp = timestamp;
prevChainlinkResponse.decimals = _currentDecimals;
prevChainlinkResponse.success = true;
return prevChainlinkResponse;
} catch {
// If call to Chainlink aggregator reverts, return a zero response with success = false
return prevChainlinkResponse;
}
}
}