pragma solidity ^0.5.16; // Inheritance import "./Owned.sol"; import "./SelfDestructible.sol"; import "./interfaces/IExchangeRates.sol"; // Libraries import "./SafeDecimalMath.sol"; // Internal references // AggregatorInterface from Chainlink represents a decentralized pricing network for a single currency key import "@chainlink/contracts-0.0.3/src/v0.5/dev/AggregatorInterface.sol"; // https://docs.oikos.cash/contracts/source/contracts/ExchangeRates contract ExchangeRates is Owned, SelfDestructible, IExchangeRates { using SafeMath for uint; using SafeDecimalMath for uint; struct RateAndUpdatedTime { uint216 rate; uint40 time; } // Exchange rates and update times stored by currency code, e.g. 'OKS', or 'oUSD' mapping(bytes32 => mapping(uint => RateAndUpdatedTime)) private _rates; // The address of the oracle which pushes rate updates to this contract address public oracle; // Decentralized oracle networks that feed into pricing aggregators mapping(bytes32 => AggregatorInterface) public aggregators; // List of aggregator keys for convenient iteration bytes32[] public aggregatorKeys; // Do not allow the oracle to submit times any further forward into the future than this constant. uint private constant ORACLE_FUTURE_LIMIT = 10 minutes; // How long will the contract assume the rate of any asset is correct uint public rateStalePeriod = 3 hours; // For inverted prices, keep a mapping of their entry, limits and frozen status struct InversePricing { uint entryPoint; uint upperLimit; uint lowerLimit; bool frozen; } mapping(bytes32 => InversePricing) public inversePricing; bytes32[] public invertedKeys; mapping(bytes32 => uint) public currentRoundForRate; // // ========== CONSTRUCTOR ========== constructor( address _owner, address _oracle, bytes32[] memory _currencyKeys, uint[] memory _newRates ) public Owned(_owner) SelfDestructible() { require(_currencyKeys.length == _newRates.length, "Currency key length and rate length must match."); oracle = _oracle; // The oUSD rate is always 1 and is never stale. _setRate("oUSD", SafeDecimalMath.unit(), now); internalUpdateRates(_currencyKeys, _newRates, now); } /* ========== SETTERS ========== */ function setOracle(address _oracle) external onlyOwner { oracle = _oracle; emit OracleUpdated(oracle); } function setRateStalePeriod(uint _time) external onlyOwner { rateStalePeriod = _time; emit RateStalePeriodUpdated(rateStalePeriod); } /* ========== MUTATIVE FUNCTIONS ========== */ function updateRates( bytes32[] calldata currencyKeys, uint[] calldata newRates, uint timeSent ) external onlyOracle returns (bool) { return internalUpdateRates(currencyKeys, newRates, timeSent); } function deleteRate(bytes32 currencyKey) external onlyOracle { require(_getRate(currencyKey) > 0, "Rate is zero"); delete _rates[currencyKey][currentRoundForRate[currencyKey]]; currentRoundForRate[currencyKey]--; emit RateDeleted(currencyKey); } function setInversePricing( bytes32 currencyKey, uint entryPoint, uint upperLimit, uint lowerLimit, bool freeze, bool freezeAtUpperLimit ) external onlyOwner { // 0 < lowerLimit < entryPoint => 0 < entryPoint require(lowerLimit > 0, "lowerLimit must be above 0"); require(upperLimit > entryPoint, "upperLimit must be above the entryPoint"); require(upperLimit < entryPoint.mul(2), "upperLimit must be less than double entryPoint"); require(lowerLimit < entryPoint, "lowerLimit must be below the entryPoint"); if (inversePricing[currencyKey].entryPoint <= 0) { // then we are adding a new inverse pricing, so add this invertedKeys.push(currencyKey); } inversePricing[currencyKey].entryPoint = entryPoint; inversePricing[currencyKey].upperLimit = upperLimit; inversePricing[currencyKey].lowerLimit = lowerLimit; inversePricing[currencyKey].frozen = freeze; emit InversePriceConfigured(currencyKey, entryPoint, upperLimit, lowerLimit); // When indicating to freeze, we need to know the rate to freeze it at - either upper or lower // this is useful in situations where ExchangeRates is updated and there are existing inverted // rates already frozen in the current contract that need persisting across the upgrade if (freeze) { emit InversePriceFrozen(currencyKey); _setRate(currencyKey, freezeAtUpperLimit ? upperLimit : lowerLimit, now); } } function removeInversePricing(bytes32 currencyKey) external onlyOwner { require(inversePricing[currencyKey].entryPoint > 0, "No inverted price exists"); inversePricing[currencyKey].entryPoint = 0; inversePricing[currencyKey].upperLimit = 0; inversePricing[currencyKey].lowerLimit = 0; inversePricing[currencyKey].frozen = false; // now remove inverted key from array bool wasRemoved = removeFromArray(currencyKey, invertedKeys); if (wasRemoved) { emit InversePriceConfigured(currencyKey, 0, 0, 0); } } function addAggregator(bytes32 currencyKey, address aggregatorAddress) external onlyOwner { AggregatorInterface aggregator = AggregatorInterface(aggregatorAddress); // This check tries to make sure that a valid aggregator is being added. // It checks if the aggregator is an existing smart contract that has implemented `latestTimestamp` function. require(aggregator.latestTimestamp() >= 0, "Given Aggregator is invalid"); if (address(aggregators[currencyKey]) == address(0)) { aggregatorKeys.push(currencyKey); } aggregators[currencyKey] = aggregator; emit AggregatorAdded(currencyKey, address(aggregator)); } function removeAggregator(bytes32 currencyKey) external onlyOwner { address aggregator = address(aggregators[currencyKey]); require(aggregator != address(0), "No aggregator exists for key"); delete aggregators[currencyKey]; bool wasRemoved = removeFromArray(currencyKey, aggregatorKeys); if (wasRemoved) { emit AggregatorRemoved(currencyKey, aggregator); } } /* ========== VIEWS ========== */ function rateAndUpdatedTime(bytes32 currencyKey) external view returns (uint rate, uint time) { RateAndUpdatedTime memory rateAndTime = _getRateAndUpdatedTime(currencyKey); return (rateAndTime.rate, rateAndTime.time); } function getLastRoundIdBeforeElapsedSecs( bytes32 currencyKey, uint startingRoundId, uint startingTimestamp, uint timediff ) external view returns (uint) { uint roundId = startingRoundId; uint nextTimestamp = 0; while (true) { (, nextTimestamp) = _getRateAndTimestampAtRound(currencyKey, roundId + 1); // if there's no new round, then the previous roundId was the latest if (nextTimestamp == 0 || nextTimestamp > startingTimestamp + timediff) { return roundId; } roundId++; } return roundId; } function getCurrentRoundId(bytes32 currencyKey) external view returns (uint) { return _getCurrentRoundId(currencyKey); } function effectiveValueAtRound( bytes32 sourceCurrencyKey, uint sourceAmount, bytes32 destinationCurrencyKey, uint roundIdForSrc, uint roundIdForDest ) external view returns (uint value) { // If there's no change in the currency, then just return the amount they gave us if (sourceCurrencyKey == destinationCurrencyKey) return sourceAmount; (uint srcRate, ) = _getRateAndTimestampAtRound(sourceCurrencyKey, roundIdForSrc); (uint destRate, ) = _getRateAndTimestampAtRound(destinationCurrencyKey, roundIdForDest); // Calculate the effective value by going from source -> USD -> destination value = sourceAmount.multiplyDecimalRound(srcRate).divideDecimalRound(destRate); } function rateAndTimestampAtRound(bytes32 currencyKey, uint roundId) external view returns (uint rate, uint time) { return _getRateAndTimestampAtRound(currencyKey, roundId); } function lastRateUpdateTimes(bytes32 currencyKey) external view returns (uint256) { return _getUpdatedTime(currencyKey); } function lastRateUpdateTimesForCurrencies(bytes32[] calldata currencyKeys) external view returns (uint[] memory) { uint[] memory lastUpdateTimes = new uint[](currencyKeys.length); for (uint i = 0; i < currencyKeys.length; i++) { lastUpdateTimes[i] = _getUpdatedTime(currencyKeys[i]); } return lastUpdateTimes; } function effectiveValue( bytes32 sourceCurrencyKey, uint sourceAmount, bytes32 destinationCurrencyKey ) external view returns (uint value) { (value, , ) = _effectiveValueAndRates(sourceCurrencyKey, sourceAmount, destinationCurrencyKey); } function effectiveValueAndRates( bytes32 sourceCurrencyKey, uint sourceAmount, bytes32 destinationCurrencyKey ) external view returns ( uint value, uint sourceRate, uint destinationRate ) { return _effectiveValueAndRates(sourceCurrencyKey, sourceAmount, destinationCurrencyKey); } function rateForCurrency(bytes32 currencyKey) external view returns (uint) { return _getRateAndUpdatedTime(currencyKey).rate; } function ratesAndUpdatedTimeForCurrencyLastNRounds(bytes32 currencyKey, uint numRounds) external view returns (uint[] memory rates, uint[] memory times) { rates = new uint[](numRounds); times = new uint[](numRounds); uint roundId = _getCurrentRoundId(currencyKey); for (uint i = 0; i < numRounds; i++) { (rates[i], times[i]) = _getRateAndTimestampAtRound(currencyKey, roundId); if (roundId == 0) { // if we hit the last round, then return what we have return (rates, times); } else { roundId--; } } } function ratesForCurrencies(bytes32[] calldata currencyKeys) external view returns (uint[] memory) { uint[] memory _localRates = new uint[](currencyKeys.length); for (uint i = 0; i < currencyKeys.length; i++) { _localRates[i] = _getRate(currencyKeys[i]); } return _localRates; } function ratesAndStaleForCurrencies(bytes32[] calldata currencyKeys) external view returns (uint[] memory, bool) { uint[] memory _localRates = new uint[](currencyKeys.length); bool anyRateStale = false; uint period = rateStalePeriod; for (uint i = 0; i < currencyKeys.length; i++) { RateAndUpdatedTime memory rateAndUpdateTime = _getRateAndUpdatedTime(currencyKeys[i]); _localRates[i] = uint256(rateAndUpdateTime.rate); if (!anyRateStale) { anyRateStale = (currencyKeys[i] != "oUSD" && uint256(rateAndUpdateTime.time).add(period) < now); } } return (_localRates, anyRateStale); } function rateIsStale(bytes32 currencyKey) external view returns (bool) { // oUSD is a special case and is never stale. if (currencyKey == "oUSD") return false; return _getUpdatedTime(currencyKey).add(rateStalePeriod) < now; } function rateIsFrozen(bytes32 currencyKey) external view returns (bool) { return inversePricing[currencyKey].frozen; } function anyRateIsStale(bytes32[] calldata currencyKeys) external view returns (bool) { // Loop through each key and check whether the data point is stale. uint256 i = 0; while (i < currencyKeys.length) { // oUSD is a special case and is never false if (currencyKeys[i] != "oUSD" && _getUpdatedTime(currencyKeys[i]).add(rateStalePeriod) < now) { return true; } i += 1; } return false; } /* ========== INTERNAL FUNCTIONS ========== */ function _setRate( bytes32 currencyKey, uint256 rate, uint256 time ) internal { // Note: this will effectively start the rounds at 1, which matches Chainlink's Agggregators currentRoundForRate[currencyKey]++; _rates[currencyKey][currentRoundForRate[currencyKey]] = RateAndUpdatedTime({ rate: uint216(rate), time: uint40(time) }); } function internalUpdateRates( bytes32[] memory currencyKeys, uint[] memory newRates, uint timeSent ) internal returns (bool) { require(currencyKeys.length == newRates.length, "Currency key array length must match rates array length."); require(timeSent < (now + ORACLE_FUTURE_LIMIT), "Time is too far into the future"); // Loop through each key and perform update. for (uint i = 0; i < currencyKeys.length; i++) { bytes32 currencyKey = currencyKeys[i]; // Should not set any rate to zero ever, as no asset will ever be // truely worthless and still valid. In this scenario, we should // delete the rate and remove it from the system. require(newRates[i] != 0, "Zero is not a valid rate, please call deleteRate instead."); require(currencyKey != "oUSD", "Rate of oUSD cannot be updated, it's always UNIT."); // We should only update the rate if it's at least the same age as the last rate we've got. if (timeSent < _getUpdatedTime(currencyKey)) { continue; } newRates[i] = rateOrInverted(currencyKey, newRates[i]); // Ok, go ahead with the update. _setRate(currencyKey, newRates[i], timeSent); } emit RatesUpdated(currencyKeys, newRates); return true; } function rateOrInverted(bytes32 currencyKey, uint rate) internal returns (uint) { // if an inverse mapping exists, adjust the price accordingly InversePricing storage inverse = inversePricing[currencyKey]; if (inverse.entryPoint <= 0) { return rate; } // set the rate to the current rate initially (if it's frozen, this is what will be returned) uint newInverseRate = _getRate(currencyKey); // get the new inverted rate if not frozen if (!inverse.frozen) { uint doubleEntryPoint = inverse.entryPoint.mul(2); if (doubleEntryPoint <= rate) { // avoid negative numbers for unsigned ints, so set this to 0 // which by the requirement that lowerLimit be > 0 will // cause this to freeze the price to the lowerLimit newInverseRate = 0; } else { newInverseRate = doubleEntryPoint.sub(rate); } // now if new rate hits our limits, set it to the limit and freeze if (newInverseRate >= inverse.upperLimit) { newInverseRate = inverse.upperLimit; } else if (newInverseRate <= inverse.lowerLimit) { newInverseRate = inverse.lowerLimit; } if (newInverseRate == inverse.upperLimit || newInverseRate == inverse.lowerLimit) { inverse.frozen = true; emit InversePriceFrozen(currencyKey); } } return newInverseRate; } function removeFromArray(bytes32 entry, bytes32[] storage array) internal returns (bool) { for (uint i = 0; i < array.length; i++) { if (array[i] == entry) { delete array[i]; // Copy the last key into the place of the one we just deleted // If there's only one key, this is array[0] = array[0]. // If we're deleting the last one, it's also a NOOP in the same way. array[i] = array[array.length - 1]; // Decrease the size of the array by one. array.length--; return true; } } return false; } function _getRateAndUpdatedTime(bytes32 currencyKey) internal view returns (RateAndUpdatedTime memory) { if (address(aggregators[currencyKey]) != address(0)) { return RateAndUpdatedTime({ rate: uint216(aggregators[currencyKey].latestAnswer() * 1e10), time: uint40(aggregators[currencyKey].latestTimestamp()) }); } else { return _rates[currencyKey][currentRoundForRate[currencyKey]]; } } function _getCurrentRoundId(bytes32 currencyKey) internal view returns (uint) { if (address(aggregators[currencyKey]) != address(0)) { AggregatorInterface aggregator = aggregators[currencyKey]; return aggregator.latestRound(); } else { return currentRoundForRate[currencyKey]; } } function _getRateAndTimestampAtRound(bytes32 currencyKey, uint roundId) internal view returns (uint rate, uint time) { if (address(aggregators[currencyKey]) != address(0)) { AggregatorInterface aggregator = aggregators[currencyKey]; return (uint(aggregator.getAnswer(roundId) * 1e10), aggregator.getTimestamp(roundId)); } else { RateAndUpdatedTime storage update = _rates[currencyKey][roundId]; return (update.rate, update.time); } } function _getRate(bytes32 currencyKey) internal view returns (uint256) { return _getRateAndUpdatedTime(currencyKey).rate; } function _getUpdatedTime(bytes32 currencyKey) internal view returns (uint256) { return _getRateAndUpdatedTime(currencyKey).time; } function _effectiveValueAndRates( bytes32 sourceCurrencyKey, uint sourceAmount, bytes32 destinationCurrencyKey ) internal view returns ( uint value, uint sourceRate, uint destinationRate ) { sourceRate = _getRate(sourceCurrencyKey); // If there's no change in the currency, then just return the amount they gave us if (sourceCurrencyKey == destinationCurrencyKey) { destinationRate = sourceRate; value = sourceAmount; } else { // Calculate the effective value by going from source -> USD -> destination destinationRate = _getRate(destinationCurrencyKey); value = sourceAmount.multiplyDecimalRound(sourceRate).divideDecimalRound(destinationRate); } } /* ========== MODIFIERS ========== */ modifier onlyOracle { require(msg.sender == oracle, "Only the oracle can perform this action"); _; } /* ========== EVENTS ========== */ event OracleUpdated(address newOracle); event RateStalePeriodUpdated(uint rateStalePeriod); event RatesUpdated(bytes32[] currencyKeys, uint[] newRates); event RateDeleted(bytes32 currencyKey); event InversePriceConfigured(bytes32 currencyKey, uint entryPoint, uint upperLimit, uint lowerLimit); event InversePriceFrozen(bytes32 currencyKey); event AggregatorAdded(bytes32 currencyKey, address aggregator); event AggregatorRemoved(bytes32 currencyKey, address aggregator); }