forked from Synthetixio/synthetix
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathExchangeCircuitBreaker.sol
209 lines (172 loc) · 8.85 KB
/
ExchangeCircuitBreaker.sol
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
pragma solidity ^0.5.16;
// Inheritance
import "./Owned.sol";
import "./MixinResolver.sol";
import "./MixinSystemSettings.sol";
import "./interfaces/IExchangeCircuitBreaker.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/ExchangeCircuitBreaker
*/
contract ExchangeCircuitBreaker is Owned, MixinSystemSettings, IExchangeCircuitBreaker {
using SafeMath for uint;
using SafeDecimalMath for uint;
bytes32 public constant CONTRACT_NAME = "ExchangeCircuitBreaker";
// 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];
}
function exchangeRates() public view returns (address) {
return requireAndGetAddress(CONTRACT_EXRATES);
}
/* ========== Internal views ========== */
function _exchangeRates() internal view returns (IExchangeRates) {
return IExchangeRates(requireAndGetAddress(CONTRACT_EXRATES));
}
function systemStatus() internal view returns (ISystemStatus) {
return ISystemStatus(requireAndGetAddress(CONTRACT_SYSTEMSTATUS));
}
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-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, 0);
// 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;
}
// ========== 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);
}