Skip to content

Commit

Permalink
Exchange rates circuit breaker refactor (#1540)
Browse files Browse the repository at this point in the history
  • Loading branch information
artdgn authored Oct 25, 2021
1 parent 916b31c commit 095d193
Show file tree
Hide file tree
Showing 19 changed files with 1,273 additions and 254 deletions.
12 changes: 6 additions & 6 deletions contracts/ExchangeRates.sol
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import "./SafeDecimalMath.sol";
import "@chainlink/contracts-0.0.10/src/v0.5/interfaces/AggregatorV2V3Interface.sol";
// FlagsInterface from Chainlink addresses SIP-76
import "@chainlink/contracts-0.0.10/src/v0.5/interfaces/FlagsInterface.sol";
import "./interfaces/IExchanger.sol";
import "./interfaces/IExchangeRatesCircuitBreaker.sol";

// https://docs.synthetix.io/contracts/source/contracts/exchangerates
contract ExchangeRates is Owned, MixinSystemSettings, IExchangeRates {
Expand Down Expand Up @@ -47,7 +47,7 @@ contract ExchangeRates is Owned, MixinSystemSettings, IExchangeRates {
mapping(bytes32 => uint) public roundFrozen;

/* ========== ADDRESS RESOLVER CONFIGURATION ========== */
bytes32 private constant CONTRACT_EXCHANGER = "Exchanger";
bytes32 private constant CONTRACT_CICRUIT_BREAKER = "ExchangeRatesCircuitBreaker";

//
// ========== CONSTRUCTOR ==========
Expand Down Expand Up @@ -142,7 +142,7 @@ contract ExchangeRates is Owned, MixinSystemSettings, IExchangeRates {
// SIP-78
uint rate = _getRate(currencyKey);
if (rate > 0) {
exchanger().setLastExchangeRateForSynth(currencyKey, rate);
exchangeRatesCircuitBreaker().setLastExchangeRateForSynth(currencyKey, rate);
}

emit InversePriceConfigured(currencyKey, entryPoint, upperLimit, lowerLimit);
Expand Down Expand Up @@ -214,7 +214,7 @@ contract ExchangeRates is Owned, MixinSystemSettings, IExchangeRates {
function resolverAddressesRequired() public view returns (bytes32[] memory addresses) {
bytes32[] memory existingAddresses = MixinSystemSettings.resolverAddressesRequired();
bytes32[] memory newAddresses = new bytes32[](1);
newAddresses[0] = CONTRACT_EXCHANGER;
newAddresses[0] = CONTRACT_CICRUIT_BREAKER;
addresses = combineArrays(existingAddresses, newAddresses);
}

Expand Down Expand Up @@ -446,8 +446,8 @@ contract ExchangeRates is Owned, MixinSystemSettings, IExchangeRates {

/* ========== INTERNAL FUNCTIONS ========== */

function exchanger() internal view returns (IExchanger) {
return IExchanger(requireAndGetAddress(CONTRACT_EXCHANGER));
function exchangeRatesCircuitBreaker() internal view returns (IExchangeRatesCircuitBreaker) {
return IExchangeRatesCircuitBreaker(requireAndGetAddress(CONTRACT_CICRUIT_BREAKER));
}

function getFlagsForRates(bytes32[] memory currencyKeys) internal view returns (bool[] memory flagList) {
Expand Down
229 changes: 229 additions & 0 deletions contracts/ExchangeRatesCircuitBreaker.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
pragma solidity ^0.5.16;

// Inheritance
import "./Owned.sol";
import "./MixinResolver.sol";
import "./MixinSystemSettings.sol";
import "./interfaces/IExchangeRatesCircuitBreaker.sol";

// Libraries
import "./SafeDecimalMath.sol";

// Internal references
import "./interfaces/ISynth.sol";
import "./interfaces/IIssuer.sol";
import "./interfaces/ISystemStatus.sol";
import "./interfaces/IExchangeRates.sol";
import "./Proxyable.sol";

/**
* Compares current exchange rate to previous, and suspends a synth if the
* difference is outside of deviation bounds.
* Stores last "good" rate for each synth on each invocation.
* Inteded use is to use in combination with ExchangeRates on mutative exchange-like
* methods.
* Suspend functionality is public, resume functionality is controlled by owner.
*
* https://docs.synthetix.io/contracts/source/contracts/ExchangeRatesCircuitBreaker
*/
contract ExchangeRatesCircuitBreaker is Owned, MixinSystemSettings, IExchangeRatesCircuitBreaker {
using SafeMath for uint;
using SafeDecimalMath for uint;

bytes32 public constant CONTRACT_NAME = "ExchangeRatesCircuitBreaker";

// SIP-65: Decentralized circuit breaker
uint public constant CIRCUIT_BREAKER_SUSPENSION_REASON = 65;

// is internal to have lastExchangeRate getter in interface in solidity v0.5
// TODO: after upgrading solidity, switch to just public lastExchangeRate instead
// of maintaining this internal one
mapping(bytes32 => uint) internal _lastExchangeRate;

/* ========== ADDRESS RESOLVER CONFIGURATION ========== */

bytes32 private constant CONTRACT_SYSTEMSTATUS = "SystemStatus";
bytes32 private constant CONTRACT_EXRATES = "ExchangeRates";
bytes32 private constant CONTRACT_ISSUER = "Issuer";

constructor(address _owner, address _resolver) public Owned(_owner) MixinSystemSettings(_resolver) {}

/* ========== VIEWS ========== */

function resolverAddressesRequired() public view returns (bytes32[] memory addresses) {
bytes32[] memory existingAddresses = MixinSystemSettings.resolverAddressesRequired();
bytes32[] memory newAddresses = new bytes32[](3);
newAddresses[0] = CONTRACT_SYSTEMSTATUS;
newAddresses[1] = CONTRACT_EXRATES;
newAddresses[2] = CONTRACT_ISSUER;
addresses = combineArrays(existingAddresses, newAddresses);
}

// Returns rate and its "invalid" state.
// Rate can be invalid either due to:
// 1. Returned as invalid from ExchangeRates - due to being stale, or flagged by oracle.
// 2, Out of deviation dounds w.r.t. to previously stored rate or if there is no
// valid stored rate, w.r.t. to previous 3 oracle rates.
function rateWithInvalid(bytes32 currencyKey) external view returns (uint, bool) {
(uint rate, bool invalid) = exchangeRates().rateAndInvalid(currencyKey);
return (rate, invalid || _isRateOutOfBounds(currencyKey, rate));
}

function isDeviationAboveThreshold(uint base, uint comparison) external view returns (bool) {
return _isDeviationAboveThreshold(base, comparison);
}

function priceDeviationThresholdFactor() external view returns (uint) {
return getPriceDeviationThresholdFactor();
}

function lastExchangeRate(bytes32 currencyKey) external view returns (uint) {
return _lastExchangeRate[currencyKey];
}

/* ========== Internal views ========== */

function systemStatus() internal view returns (ISystemStatus) {
return ISystemStatus(requireAndGetAddress(CONTRACT_SYSTEMSTATUS));
}

function exchangeRates() internal view returns (IExchangeRates) {
return IExchangeRates(requireAndGetAddress(CONTRACT_EXRATES));
}

function issuer() internal view returns (IIssuer) {
return IIssuer(requireAndGetAddress(CONTRACT_ISSUER));
}

/* ========== Mutating ========== */

/**
* Checks rate deviation from previous and its "invalid" oracle state (stale rate, of flagged by oracle).
* if it's valid and within deviation bounds, stores it and returns it and "false" (circuit not broken).
* If rate is invalid or outside of deviation bounds - doesn't store it, suspends the the synth, and returns
* last rate and "true" (circuit broken).
* Also, checks that system is not suspended currently, if it is - doesn't perform any checks, and
* returns last rate and "false" (not broken), to prevent synths suspensions during maintenance.
*/
function rateWithBreakCircuit(bytes32 currencyKey) external returns (uint lastValidRate, bool circuitBroken) {
// check system status
if (systemStatus().systemSuspended()) {
// if system is inactive this call has no effect, but will neither revert,
// nor persist new rate, nor suspend the synth - because the system is inactive.
// not reverting is needed for performing admin operations during system suspension
// e.g. purging synths that use some exchanging functionality
} else {
// get new rate and check oracle "invalid" state
(uint rate, bool invalid) = exchangeRates().rateAndInvalid(currencyKey);
// check and suspend
if (invalid || _isRateOutOfBounds(currencyKey, rate)) {
// check synth exists, to prevent spamming settings for non existant synths
// gas savings: check here instead of every call, because synth existance is only
// important if we want to suspend it, and most regular calls don't need to incur
// the extra gas costs of another external call
require(issuer().synths(currencyKey) != ISynth(0), "No such synth");
systemStatus().suspendSynth(currencyKey, CIRCUIT_BREAKER_SUSPENSION_REASON);
circuitBroken = true;
} else {
// store the last passing rate
_lastExchangeRate[currencyKey] = rate;
}
}
return (_lastExchangeRate[currencyKey], circuitBroken);
}

/**
* SIP-78
*
* sets the last-rate to an externally provided value
* access restricted to only the ExchageRates contract, and is used there in setInversePricing
* for iSynths
* emits LastRateOverriden
* TODO: deprecate when iSynths are removed from the system
*/
function setLastExchangeRateForSynth(bytes32 currencyKey, uint rate) external onlyExchangeRates {
require(rate > 0, "Rate must be above 0");
emit LastRateOverriden(currencyKey, _lastExchangeRate[currencyKey], rate);
_lastExchangeRate[currencyKey] = rate;
}

/**
* SIP-139
* resets the stored value for _lastExchangeRate for multiple currencies to the latest rate
* can be used to un-suspend synths after a suspension happenned
* doesn't check deviations here, so believes that owner knows better
* emits LastRateOverriden
*/
function resetLastExchangeRate(bytes32[] calldata currencyKeys) external onlyOwner {
(uint[] memory rates, bool anyRateInvalid) = exchangeRates().ratesAndInvalidForCurrencies(currencyKeys);

require(!anyRateInvalid, "Rates for given synths not valid");

for (uint i = 0; i < currencyKeys.length; i++) {
emit LastRateOverriden(currencyKeys[i], _lastExchangeRate[currencyKeys[i]], rates[i]);
_lastExchangeRate[currencyKeys[i]] = rates[i];
}
}

/* ========== INTERNAL FUNCTIONS ========== */

function _isDeviationAboveThreshold(uint base, uint comparison) internal view returns (bool) {
if (base == 0 || comparison == 0) {
return true;
}

uint factor;
if (comparison > base) {
factor = comparison.divideDecimal(base);
} else {
factor = base.divideDecimal(comparison);
}

return factor >= getPriceDeviationThresholdFactor();
}

/**
* Rate is invalid if:
* - is outside of deviation bounds relative to previous non-zero rate
* - (warm up case) if previous rate was 0 (init), gets last 4 rates from oracle, and checks
* if rate is outside of deviation w.r.t any of the 3 previous ones (excluding the last one).
*/
function _isRateOutOfBounds(bytes32 currencyKey, uint currentRate) internal view returns (bool) {
if (currentRate == 0) {
return true;
}

uint lastRateFromExchange = _lastExchangeRate[currencyKey];

if (lastRateFromExchange > 0) {
return _isDeviationAboveThreshold(lastRateFromExchange, currentRate);
}

// if no last exchange for this synth, then we need to look up last 3 rates (+1 for current rate)
(uint[] memory rates, ) = exchangeRates().ratesAndUpdatedTimeForCurrencyLastNRounds(currencyKey, 4);

// start at index 1 to ignore current rate
for (uint i = 1; i < rates.length; i++) {
// ignore any empty rates in the past (otherwise we will never be able to get validity)
if (rates[i] > 0 && _isDeviationAboveThreshold(rates[i], currentRate)) {
return true;
}
}

return false;
}

// ========== MODIFIERS ==========

modifier onlyExchangeRates() {
IExchangeRates _exchangeRates = exchangeRates();
require(msg.sender == address(_exchangeRates), "Restricted to ExchangeRates");
_;
}

// ========== EVENTS ==========

// @notice signals that a the "last rate" was overriden by one of the admin methods
// with a value that didn't come direclty from the ExchangeRates.getRates methods
event LastRateOverriden(bytes32 currencyKey, uint256 previousRate, uint256 newRate);
}
Loading

0 comments on commit 095d193

Please sign in to comment.