Address
0xc65abc8b9b4b3cee03430f6fc3d8a4760221a113Current Holdings
$0.00
TXs sent
not counted
First Active
2023-12-01
block 18,971,009
Last Active
487 days ago
block 23,510,484
Funded By
not identified
Net worth historyi
2 snapshots · to block 27,580,936coverage change 26 Augcoverage change 27 Augcoverage change 27 Augcoverage change 27 Augcoverage change 27 Aug
exact matchPriceFeedsolc 0.6.11+commit.5ef660b1runtime exact · creation not verified
// SPDX-License-Identifier: MIT
pragma solidity 0.6.11;
import "./Dependencies/Ownable.sol";
import "./Interfaces/IPriceFeed.sol";
import "./Interfaces/ISecondaryOracle.sol";
import "./Dependencies/PLSXLibrary.sol";
import "./Interfaces/IFetchCaller.sol";
import "./Dependencies/SafeMath.sol";
import "./Dependencies/BaseMath.sol";
import "./Dependencies/LiquidLoansMath.sol";
import "./Dependencies/console.sol";
import "./Dependencies/CheckContract.sol";
/*
* PriceFeed contract is for mainnet deployment, to be connected to an Oracle service
* This version of the contract should be deployed to staging (testnet), preprod (testnet) and prod (mainnet).
*
* The current PriceFeed implementation usese the TWAP price diractly from the PLSX LP contract
* as a temporary solution until an Oracle is available on PulseChain.
*
* The PriceFeed contract should be deployed to staging (testnet), preprod (testnet) and prod (mainnet) environments ONLY.
*/
contract PriceFeed is Ownable, CheckContract, BaseMath, IPriceFeed {
using SafeMath for uint256;
string constant public NAME = "PriceFeed";
IFetchCaller public fetchCaller; // Wrapper contract that calls the Fetch system
ISecondaryOracle public secondaryOracle; // Secondary Oracle contract
/*
Query Descriptor:
{ type: "SpotPrice", asset: "pls", currency: "usd" }
Query Data (Bytes):
0x00000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000953706f745072696365000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000003706c73000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000037573640000000000000000000000000000000000000000000000000000000000
Query ID (Hash):
0x83245f6a6a2f6458558a706270fbcc35ac3a81917602c1313d3bfa998dcc2d4b
*/
bytes32 public constant PLSUSD_FETCH_REQ_ID = 0x83245f6a6a2f6458558a706270fbcc35ac3a81917602c1313d3bfa998dcc2d4b;
// Maximum time period allowed since Fetch's latest round data timestamp, beyond which Fetch is considered frozen.
uint constant public TIMEOUT = 14400; // 4 hours: 60 * 60 * 4
/*
* The maximum relative price difference between two oracle responses allowed in order for the PriceFeed
* to return to using the Fetch oracle. 18-digit precision.
*/
uint constant public MAX_PRICE_DIFFERENCE_BETWEEN_ORACLES = 5e16; // 5%
// Maximum deviation allowed between two consecutive Fetch oracle prices. 18-digit precision.
uint constant public MAX_PRICE_DEVIATION_FROM_PREVIOUS_PRICE = 5e17; // 50%
// The last good price seen from an oracle by LiquidLoans
uint public lastGoodPrice;
struct FetchResponse {
bool ifRetrieve;
uint256 value;
uint256 timestamp;
bool success;
}
struct SecondaryOracleResponse {
bool ifRetrieve;
uint256 value;
uint256 timestamp;
bool success;
}
// TODO implement the PriceFeedStatus functionality below. Left here for PriceFeedTestnet to compile:
enum Status {
fetchWorking,
usingSecondaryFetchUntrusted,
bothOraclesUntrusted,
usingSecondaryFetchFrozen,
usingFetchSecondaryUntrusted
}
// 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 _fetchCallerAddress,
address _secondaryOracleAddress
)
external
onlyOwner
{
checkContract(_fetchCallerAddress);
checkContract(_secondaryOracleAddress);
fetchCaller = IFetchCaller(_fetchCallerAddress);
secondaryOracle = ISecondaryOracle(_secondaryOracleAddress);
//Explicitly set initial system status
status = Status.fetchWorking;
//Get an initial price from Fetch to serve as first reference for lastGoodPrice
FetchResponse memory fetchResponse = _getCurrentFetchResponse();
SecondaryOracleResponse memory secondaryResponse = _getCurrentSecondaryResponse();
require(_bothOraclesLiveAndUnbrokenAndSimilarPrice(fetchResponse, secondaryResponse),
"PriceFeed: Oracles must be working and current");
_storeFetchPrice(fetchResponse);
_renounceOwnership();
}
//--- Fetch response wrapper functions ---
function _getCurrentFetchResponse() internal returns (FetchResponse memory fetchResponse) {
try fetchCaller.getFetchCurrentValue(PLSUSD_FETCH_REQ_ID) returns
(
bool ifRetrieve,
uint256 value,
uint256 _timestampRetrieved
)
{
//If call to Fetch succeeds, return the response and success = true
fetchResponse.ifRetrieve = ifRetrieve;
fetchResponse.value = value;
fetchResponse.timestamp = _timestampRetrieved;
fetchResponse.success = true;
return (fetchResponse);
} catch {
//If call to Fetch reverts, return a zero response with success = false
return (fetchResponse);
}
}
function _getPreviousFetchResponse(uint timestamp) internal returns (FetchResponse memory fetchResponse) {
try fetchCaller.getFetchPreviousValue(PLSUSD_FETCH_REQ_ID, timestamp) returns
(
bool ifRetrieve,
uint256 value,
uint256 _timestampRetrieved
)
{
//If call to Fetch succeeds, return the response and success = true
fetchResponse.ifRetrieve = ifRetrieve;
fetchResponse.value = value;
fetchResponse.timestamp = _timestampRetrieved;
fetchResponse.success = true;
return (fetchResponse);
} catch {
//If call to Fetch reverts, return a zero response with success = false
return (fetchResponse);
}
}
function _fetchIsFrozen(FetchResponse memory _fetchResponse) internal view returns (bool) {
return block.timestamp.sub(_fetchResponse.timestamp) > TIMEOUT;
}
function _fetchIsBroken(FetchResponse 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 _storeFetchPrice(FetchResponse memory _fetchResponse) internal returns (uint) {
_storePrice(_fetchResponse.value);
return _fetchResponse.value;
}
function _changeStatus(Status _status) internal {
status = _status;
emit PriceFeedStatusChanged(_status);
}
function _storePrice(uint _currentPrice) internal {
lastGoodPrice = _currentPrice;
emit LastGoodPriceUpdated(_currentPrice);
}
/*
* fetchPrice():
* Returns the latest price obtained from the Oracle. Called by LiquidLoans functions that require a current price.
*
* Also callable by anyone externally.
*
* Non-view function - it stores the last good price seen by LiquidLoans.
*
* Uses a main oracle (Fetch) and a fallback oracle (SecondaryOracle) in case Fetch fails. If both fail,
* it uses the last good price seen by LiquidLoans.
*
*/
function fetchPrice() external override returns (uint) {
//Get current and previous price data from Fetch and current price data from SecondaryOracle
FetchResponse memory fetchResponse = _getCurrentFetchResponse();
FetchResponse memory prevFetchResponse = _getPreviousFetchResponse(fetchResponse.timestamp);
SecondaryOracleResponse memory secondaryResponse = _getCurrentSecondaryResponse();
//--- CASE 1: System fetched last price from Fetch ---
if (status == Status.fetchWorking) {
//If Fetch is broken, try SecondaryOracle
if (_fetchIsBroken(fetchResponse)) {
//If SecondaryOracle is broken then both oracles are untrusted, so return the last good price
if (_secondaryIsBroken(secondaryResponse)) {
_changeStatus(Status.bothOraclesUntrusted);
return lastGoodPrice;
}
/*
* If SecondaryOracle is only frozen but otherwise returning valid data, return the last good price.
*/
if (_secondaryIsFrozen(secondaryResponse)) {
_changeStatus(Status.usingSecondaryFetchUntrusted);
return lastGoodPrice;
}
//If Fetch is broken and SecondaryOracle is working, switch to SecondaryOracle and return current SecondaryOracle price
_changeStatus(Status.usingSecondaryFetchUntrusted);
return _storeSecondaryPrice(secondaryResponse);
}
//If Fetch is frozen, try SecondaryOracle
if (_fetchIsFrozen(fetchResponse)) {
//If SecondaryOracle is broken too, remember SecondaryOracle broke, and return last good price
if (_secondaryIsBroken(secondaryResponse)) {
_changeStatus(Status.usingFetchSecondaryUntrusted);
return lastGoodPrice;
}
//If SecondaryOracle is frozen or working, remember Fetch froze, and switch to SecondaryOracle
_changeStatus(Status.usingSecondaryFetchFrozen);
if (_secondaryIsFrozen(secondaryResponse)) {return lastGoodPrice;}
//If SecondaryOracle is working, use it
return _storeSecondaryPrice(secondaryResponse);
}
//If Fetch price has changed by > 50% between two consecutive rounds, compare it to SecondaryOracle's price
if (_fetchPriceChangeAboveMax(fetchResponse, prevFetchResponse)) {
//If SecondaryOracle is broken, both oracles are untrusted, and return last good price
if (_secondaryIsBroken(secondaryResponse)) {
_changeStatus(Status.bothOraclesUntrusted);
return lastGoodPrice;
}
//If SecondaryOracle is frozen, switch to SecondaryOracle and return last good price
if (_secondaryIsFrozen(secondaryResponse)) {
_changeStatus(Status.usingSecondaryFetchUntrusted);
return lastGoodPrice;
}
/*
* If SecondaryOracle is live and both oracles have a similar price, conclude that Fetch's large price deviation between
* two consecutive rounds was likely a legitmate market price movement, and so continue using Fetch
*/
if (_bothOraclesSimilarPrice(fetchResponse, secondaryResponse)) {
return _storeFetchPrice(fetchResponse);
}
//If SecondaryOracle is live but the oracles differ too much in price, conclude that Fetch's initial price deviation was
//an oracle failure. Switch to SecondaryOracle, and use SecondaryOracle price
_changeStatus(Status.usingSecondaryFetchUntrusted);
return _storeSecondaryPrice(secondaryResponse);
}
//If Fetch is working and SecondaryOracle is broken, remember SecondaryOracle is broken
if (_secondaryIsBroken(secondaryResponse)) {
_changeStatus(Status.usingFetchSecondaryUntrusted);
}
//If Fetch is working, return Fetch current price (no status change)
return _storeFetchPrice(fetchResponse);
}
//--- CASE 2: The system fetched last price from SecondaryOracle ---
if (status == Status.usingSecondaryFetchUntrusted) {
//If both SecondaryOracle and Fetch are live, unbroken, and reporting similar prices, switch back to Fetch
if (_bothOraclesLiveAndUnbrokenAndSimilarPrice(fetchResponse, secondaryResponse)) {
_changeStatus(Status.fetchWorking);
return _storeFetchPrice(fetchResponse);
}
if (_secondaryIsBroken(secondaryResponse)) {
_changeStatus(Status.bothOraclesUntrusted);
return lastGoodPrice;
}
/*
* If SecondaryOracle is only frozen but otherwise returning valid data, just return the last good price.
*/
if (_secondaryIsFrozen(secondaryResponse)) {return lastGoodPrice;}
//Otherwise, use SecondaryOracle price
return _storeSecondaryPrice(secondaryResponse);
}
//--- 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 Fetch.
*/
if (_bothOraclesLiveAndUnbrokenAndSimilarPrice(fetchResponse, secondaryResponse)) {
_changeStatus(Status.fetchWorking);
return _storeFetchPrice(fetchResponse);
}
//Otherwise, return the last good price - both oracles are still untrusted (no status change)
return lastGoodPrice;
}
//--- CASE 4: Using SecondaryOracle, and Fetch is frozen ---
if (status == Status.usingSecondaryFetchFrozen) {
if (_fetchIsBroken(fetchResponse)) {
//If both Oracles are broken, return last good price
if (_secondaryIsBroken(secondaryResponse)) {
_changeStatus(Status.bothOraclesUntrusted);
return lastGoodPrice;
}
//If Fetch is broken, remember it and switch to using SecondaryOracle
_changeStatus(Status.usingSecondaryFetchUntrusted);
if (_secondaryIsFrozen(secondaryResponse)) {
return lastGoodPrice;
}
//If SecondaryOracle is working, return SecondaryOracle current price
return _storeSecondaryPrice(secondaryResponse);
}
if (_fetchIsFrozen(fetchResponse)) {
//if Fetch is frozen and SecondaryOracle is broken, remember SecondaryOracle broke, and return last good price
if (_secondaryIsBroken(secondaryResponse)) {
_changeStatus(Status.usingFetchSecondaryUntrusted);
return lastGoodPrice;
}
//If both are frozen, just use lastGoodPrice
if (_secondaryIsFrozen(secondaryResponse)) {
return lastGoodPrice;
}
//if Fetch is frozen and SecondaryOracle is working, keep using SecondaryOracle (no status change)
return _storeSecondaryPrice(secondaryResponse);
}
//if Fetch is live and SecondaryOracle is broken, remember SecondaryOracle broke, and return Fetch price
if (_secondaryIsBroken(secondaryResponse)) {
_changeStatus(Status.usingFetchSecondaryUntrusted);
return _storeFetchPrice(fetchResponse);
}
//If Fetch is live and SecondaryOracle is frozen, just use last good price (no status change) since we have no basis for comparison
if (_secondaryIsFrozen(secondaryResponse)) {
return lastGoodPrice;
}
//If Fetch is live and SecondaryOracle is working, compare prices. Switch to Fetch
//if prices are within 5%, and return Fetch price.
if (_bothOraclesSimilarPrice(fetchResponse, secondaryResponse)) {
_changeStatus(Status.fetchWorking);
return _storeFetchPrice(fetchResponse);
}
//Otherwise if Fetch is live but price not within 5% of SecondaryOracle, distrust Fetch, and return SecondaryOracle price
_changeStatus(Status.usingSecondaryFetchUntrusted);
return _storeSecondaryPrice(secondaryResponse);
}
//--- CASE 5: Using Fetch, SecondaryOracle is untrusted ---
if (status == Status.usingFetchSecondaryUntrusted) {
//If Fetch breaks, now both oracles are untrusted
if (_fetchIsBroken(fetchResponse)) {
_changeStatus(Status.bothOraclesUntrusted);
return lastGoodPrice;
}
//If Fetch is frozen, return last good price (no status change)
if (_fetchIsFrozen(fetchResponse)) {
return lastGoodPrice;
}
//If Fetch and SecondaryOracle are both live, unbroken and similar price, switch back to fetchWorking and return Fetch price
if (_bothOraclesLiveAndUnbrokenAndSimilarPrice(fetchResponse, secondaryResponse)) {
_changeStatus(Status.fetchWorking);
return _storeFetchPrice(fetchResponse);
}
//If Fetch is live but deviated >50% from it's previous price and SecondaryOracle is still untrusted, switch
//to bothOraclesUntrusted and return last good price
if (_fetchPriceChangeAboveMax(fetchResponse, prevFetchResponse)) {
_changeStatus(Status.bothOraclesUntrusted);
return lastGoodPrice;
}
//Otherwise if Fetch is live and deviated <50% from it's previous price and SecondaryOracle is still untrusted,
//return Fetch price (no status change)
return _storeFetchPrice(fetchResponse);
}
}
function _bothOraclesLiveAndUnbrokenAndSimilarPrice
(
FetchResponse memory _fetchResponse,
SecondaryOracleResponse memory _secondaryOracleResponse
)
internal
view
returns (bool)
{
//Return false if either oracle is broken or frozen
if
(
_secondaryIsBroken(_secondaryOracleResponse) ||
_secondaryIsFrozen(_secondaryOracleResponse) ||
_fetchIsBroken(_fetchResponse) ||
_fetchIsFrozen(_fetchResponse)
)
{
return false;
}
return _bothOraclesSimilarPrice(_fetchResponse, _secondaryOracleResponse);
}
//TODO: Check this function as SecondaryOracle digits differs from fetch
function _bothOraclesSimilarPrice(FetchResponse memory _fetchResponse, SecondaryOracleResponse memory _secondaryOracleResponse) internal pure returns (bool) {
//Get the relative price difference between the oracles. Use the lower price as the denominator, i.e. the reference for the calculation.
uint minPrice = LiquidLoansMath._min(_fetchResponse.value, _secondaryOracleResponse.value);
uint maxPrice = LiquidLoansMath._max(_fetchResponse.value, _secondaryOracleResponse.value);
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 _fetchPriceChangeAboveMax(FetchResponse memory _currentResponse, FetchResponse memory _prevResponse) internal pure returns (bool) {
uint currentPrice = _currentResponse.value;
uint prevPrice = _prevResponse.value;
uint minPrice = LiquidLoansMath._min(currentPrice, prevPrice);
uint maxPrice = LiquidLoansMath._max(currentPrice, prevPrice);
/*
* 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_PRICE;
}
// --- SecondaryOracle Functions ---
function _storeSecondaryPrice(SecondaryOracleResponse memory _secondaryOracleResponse) internal returns (uint) {
_storePrice(_secondaryOracleResponse.value);
return _secondaryOracleResponse.value;
}
function _secondaryIsFrozen(SecondaryOracleResponse memory _response) internal view returns (bool) {
return block.timestamp.sub(_response.timestamp) > TIMEOUT;
}
function _secondaryIsBroken(SecondaryOracleResponse 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;
}
/*
* Get the response from the secondary oracle
*/
function _getCurrentSecondaryResponse() internal returns (SecondaryOracleResponse memory _response) {
(_response.ifRetrieve, _response.value, _response.timestamp, _response.success) = secondaryOracle.getPrice();
}
}
// 2022 Liquid Loans