|
| 1 | +pragma solidity ^0.5.16; |
| 2 | + |
| 3 | +// Inheritance |
| 4 | +import "./Owned.sol"; |
| 5 | +import "./MixinResolver.sol"; |
| 6 | +import "./MixinSystemSettings.sol"; |
| 7 | +import "./interfaces/ICircuitBreaker.sol"; |
| 8 | + |
| 9 | +// Libraries |
| 10 | +import "./SafeDecimalMath.sol"; |
| 11 | + |
| 12 | +// Internal references |
| 13 | +import "./interfaces/ISynth.sol"; |
| 14 | +import "./interfaces/ISystemStatus.sol"; |
| 15 | +import "./interfaces/IExchangeRates.sol"; |
| 16 | +import "./Proxyable.sol"; |
| 17 | + |
| 18 | +// Chainlink |
| 19 | +import "@chainlink/contracts-0.0.10/src/v0.5/interfaces/AggregatorV2V3Interface.sol"; |
| 20 | + |
| 21 | +/** |
| 22 | + * Compares current exchange rate to previous, and suspends a synth if the |
| 23 | + * difference is outside of deviation bounds. |
| 24 | + * Stores last "good" rate for each synth on each invocation. |
| 25 | + * Inteded use is to use in combination with ExchangeRates on mutative exchange-like |
| 26 | + * methods. |
| 27 | + * Suspend functionality is public, resume functionality is controlled by owner. |
| 28 | + * |
| 29 | + * https://docs.synthetix.io/contracts/source/contracts/CircuitBreaker |
| 30 | + */ |
| 31 | +contract CircuitBreaker is Owned, MixinSystemSettings, ICircuitBreaker { |
| 32 | + using SafeMath for uint; |
| 33 | + using SafeDecimalMath for uint; |
| 34 | + |
| 35 | + bytes32 public constant CONTRACT_NAME = "CircuitBreaker"; |
| 36 | + |
| 37 | + // is internal to have lastValue getter in interface in solidity v0.5 |
| 38 | + // TODO: after upgrading solidity, switch to just public lastValue instead |
| 39 | + // of maintaining this internal one |
| 40 | + mapping(address => uint) internal _lastValue; |
| 41 | + mapping(address => bool) internal _circuitBroken; |
| 42 | + |
| 43 | + /* ========== ADDRESS RESOLVER CONFIGURATION ========== */ |
| 44 | + |
| 45 | + bytes32 private constant CONTRACT_SYSTEMSTATUS = "SystemStatus"; |
| 46 | + bytes32 private constant CONTRACT_ISSUER = "Issuer"; |
| 47 | + bytes32 private constant CONTRACT_EXRATES = "ExchangeRates"; |
| 48 | + |
| 49 | + constructor(address _owner, address _resolver) public Owned(_owner) MixinSystemSettings(_resolver) {} |
| 50 | + |
| 51 | + /* ========== VIEWS ========== */ |
| 52 | + |
| 53 | + function resolverAddressesRequired() public view returns (bytes32[] memory addresses) { |
| 54 | + bytes32[] memory existingAddresses = MixinSystemSettings.resolverAddressesRequired(); |
| 55 | + bytes32[] memory newAddresses = new bytes32[](3); |
| 56 | + newAddresses[0] = CONTRACT_SYSTEMSTATUS; |
| 57 | + newAddresses[1] = CONTRACT_ISSUER; |
| 58 | + newAddresses[2] = CONTRACT_EXRATES; |
| 59 | + addresses = combineArrays(existingAddresses, newAddresses); |
| 60 | + } |
| 61 | + |
| 62 | + // Returns whether or not a rate would be come invalid |
| 63 | + // ignores systemStatus check |
| 64 | + function isInvalid(address oracleAddress, uint value) external view returns (bool) { |
| 65 | + return _circuitBroken[oracleAddress] || _isRateOutOfBounds(oracleAddress, value) || value == 0; |
| 66 | + } |
| 67 | + |
| 68 | + function isDeviationAboveThreshold(uint base, uint comparison) external view returns (bool) { |
| 69 | + return _isDeviationAboveThreshold(base, comparison); |
| 70 | + } |
| 71 | + |
| 72 | + function priceDeviationThresholdFactor() external view returns (uint) { |
| 73 | + return getPriceDeviationThresholdFactor(); |
| 74 | + } |
| 75 | + |
| 76 | + function lastValue(address oracleAddress) external view returns (uint) { |
| 77 | + return _lastValue[oracleAddress]; |
| 78 | + } |
| 79 | + |
| 80 | + function circuitBroken(address oracleAddress) external view returns (bool) { |
| 81 | + return _circuitBroken[oracleAddress]; |
| 82 | + } |
| 83 | + |
| 84 | + /* ========== Internal views ========== */ |
| 85 | + |
| 86 | + function systemStatus() internal view returns (ISystemStatus) { |
| 87 | + return ISystemStatus(requireAndGetAddress(CONTRACT_SYSTEMSTATUS)); |
| 88 | + } |
| 89 | + |
| 90 | + /* ========== Mutating ========== */ |
| 91 | + |
| 92 | + /** |
| 93 | + * Checks rate deviation from previous and its "invalid" oracle state (stale rate, of flagged by oracle). |
| 94 | + * If its valid, set the `circuitBoken` flag and return false. Continue storing price updates as normal. |
| 95 | + * Also, checks that system is not suspended currently, if it is - doesn't perform any checks, and |
| 96 | + * returns last rate and the current broken state, to prevent synths suspensions during maintenance. |
| 97 | + */ |
| 98 | + function probeCircuitBreaker(address oracleAddress, uint value) external onlyProbers returns (bool circuitBroken) { |
| 99 | + require(oracleAddress != address(0), "Oracle address is 0"); |
| 100 | + |
| 101 | + // these conditional statements are ordered for short circuit (heh) efficiency to reduce gas usage |
| 102 | + // in the usual case of no circuit broken. |
| 103 | + if ( |
| 104 | + // cases where the new price should be triggering a circuit break |
| 105 | + (value == 0 || _isRateOutOfBounds(oracleAddress, value)) && |
| 106 | + // other necessary states in order to break |
| 107 | + !_circuitBroken[oracleAddress] && |
| 108 | + !systemStatus().systemSuspended() |
| 109 | + ) { |
| 110 | + _circuitBroken[oracleAddress] = true; |
| 111 | + emit CircuitBroken(oracleAddress, _lastValue[oracleAddress], value); |
| 112 | + } |
| 113 | + |
| 114 | + _lastValue[oracleAddress] = value; |
| 115 | + |
| 116 | + return _circuitBroken[oracleAddress]; |
| 117 | + } |
| 118 | + |
| 119 | + /** |
| 120 | + * SIP-139 |
| 121 | + * resets the stored value for _lastValue for multiple currencies to the latest rate |
| 122 | + * can be used to enable synths after a broken circuit happenned |
| 123 | + * doesn't check deviations here, so believes that owner knows better |
| 124 | + * emits LastRateOverridden |
| 125 | + */ |
| 126 | + function resetLastValue(address[] calldata oracleAddresses, uint[] calldata values) external onlyOwner { |
| 127 | + for (uint i = 0; i < oracleAddresses.length; i++) { |
| 128 | + require(oracleAddresses[i] != address(0), "Oracle address is 0"); |
| 129 | + emit LastValueOverridden(oracleAddresses[i], _lastValue[oracleAddresses[i]], values[i]); |
| 130 | + _lastValue[oracleAddresses[i]] = values[i]; |
| 131 | + _circuitBroken[oracleAddresses[i]] = false; |
| 132 | + } |
| 133 | + } |
| 134 | + |
| 135 | + /* ========== INTERNAL FUNCTIONS ========== */ |
| 136 | + |
| 137 | + function _isDeviationAboveThreshold(uint base, uint comparison) internal view returns (bool) { |
| 138 | + if (base == 0 || comparison == 0) { |
| 139 | + return true; |
| 140 | + } |
| 141 | + |
| 142 | + uint factor; |
| 143 | + if (comparison > base) { |
| 144 | + factor = comparison.divideDecimal(base); |
| 145 | + } else { |
| 146 | + factor = base.divideDecimal(comparison); |
| 147 | + } |
| 148 | + |
| 149 | + return factor >= getPriceDeviationThresholdFactor(); |
| 150 | + } |
| 151 | + |
| 152 | + /** |
| 153 | + * Rate is invalid if it is outside of deviation bounds relative to previous non-zero rate |
| 154 | + */ |
| 155 | + function _isRateOutOfBounds(address oracleAddress, uint current) internal view returns (bool) { |
| 156 | + uint last = _lastValue[oracleAddress]; |
| 157 | + |
| 158 | + // `last == 0` indicates unset/unpopulated oracle. If we dont have any data on the previous oracle price, |
| 159 | + // we should skip the deviation check and allow it to be populated. |
| 160 | + if (last > 0) { |
| 161 | + return _isDeviationAboveThreshold(last, current); |
| 162 | + } |
| 163 | + |
| 164 | + return false; |
| 165 | + } |
| 166 | + |
| 167 | + // ========== MODIFIERS ======= |
| 168 | + |
| 169 | + modifier onlyProbers() { |
| 170 | + require( |
| 171 | + msg.sender == requireAndGetAddress(CONTRACT_ISSUER) || msg.sender == requireAndGetAddress(CONTRACT_EXRATES), |
| 172 | + "Only internal contracts can call this function" |
| 173 | + ); |
| 174 | + |
| 175 | + _; |
| 176 | + } |
| 177 | + |
| 178 | + // ========== EVENTS ========== |
| 179 | + |
| 180 | + // @notice signals that a the "last value" was overridden by one of the admin methods |
| 181 | + // with a value that didn't come directly from the ExchangeRates.getRates methods |
| 182 | + event LastValueOverridden(address indexed oracleAddress, uint256 previousValue, uint256 newValue); |
| 183 | + |
| 184 | + // @notice signals that the circuit was broken |
| 185 | + event CircuitBroken(address indexed oracleAddress, uint256 previousValue, uint256 newValue); |
| 186 | +} |
0 commit comments