Skip to content

Commit bf8829e

Browse files
author
justin j. moses
authored
Sip 75 keeper isynths phase one (Synthetixio#669)
1 parent 15ae9b2 commit bf8829e

File tree

8 files changed

+619
-256
lines changed

8 files changed

+619
-256
lines changed

contracts/BinaryOptionMarketManager.sol

+1-1
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ contract BinaryOptionMarketManager is Owned, Pausable, SelfDestructible, MixinRe
156156
}
157157

158158
// and not inverse rates
159-
(uint entryPoint, , , ) = exchangeRates.inversePricing(oracleKey);
159+
(uint entryPoint, , , , ) = exchangeRates.inversePricing(oracleKey);
160160
if (entryPoint != 0) {
161161
return false;
162162
}

contracts/ExchangeRates.sol

+121-79
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,6 @@ contract ExchangeRates is Owned, SelfDestructible, MixinResolver, MixinSystemSet
2222
using SafeMath for uint;
2323
using SafeDecimalMath for uint;
2424

25-
struct RateAndUpdatedTime {
26-
uint216 rate;
27-
uint40 time;
28-
}
29-
3025
// Exchange rates and update times stored by currency code, e.g. 'SNX', or 'sUSD'
3126
mapping(bytes32 => mapping(uint => RateAndUpdatedTime)) private _rates;
3227

@@ -44,14 +39,8 @@ contract ExchangeRates is Owned, SelfDestructible, MixinResolver, MixinSystemSet
4439

4540
int private constant AGGREGATOR_RATE_MULTIPLIER = 1e10;
4641

47-
// For inverted prices, keep a mapping of their entry, limits and frozen status
48-
struct InversePricing {
49-
uint entryPoint;
50-
uint upperLimit;
51-
uint lowerLimit;
52-
bool frozen;
53-
}
5442
mapping(bytes32 => InversePricing) public inversePricing;
43+
5544
bytes32[] public invertedKeys;
5645

5746
mapping(bytes32 => uint) public currentRoundForRate;
@@ -110,43 +99,47 @@ contract ExchangeRates is Owned, SelfDestructible, MixinResolver, MixinSystemSet
11099
uint entryPoint,
111100
uint upperLimit,
112101
uint lowerLimit,
113-
bool freeze,
114-
bool freezeAtUpperLimit
102+
bool freezeAtUpperLimit,
103+
bool freezeAtLowerLimit
115104
) external onlyOwner {
116105
// 0 < lowerLimit < entryPoint => 0 < entryPoint
117106
require(lowerLimit > 0, "lowerLimit must be above 0");
118107
require(upperLimit > entryPoint, "upperLimit must be above the entryPoint");
119108
require(upperLimit < entryPoint.mul(2), "upperLimit must be less than double entryPoint");
120109
require(lowerLimit < entryPoint, "lowerLimit must be below the entryPoint");
121110

122-
if (inversePricing[currencyKey].entryPoint <= 0) {
111+
require(!(freezeAtUpperLimit && freezeAtLowerLimit), "Cannot freeze at both limits");
112+
113+
InversePricing storage inverse = inversePricing[currencyKey];
114+
if (inverse.entryPoint == 0) {
123115
// then we are adding a new inverse pricing, so add this
124116
invertedKeys.push(currencyKey);
125117
}
126-
inversePricing[currencyKey].entryPoint = entryPoint;
127-
inversePricing[currencyKey].upperLimit = upperLimit;
128-
inversePricing[currencyKey].lowerLimit = lowerLimit;
129-
inversePricing[currencyKey].frozen = freeze;
118+
inverse.entryPoint = entryPoint;
119+
inverse.upperLimit = upperLimit;
120+
inverse.lowerLimit = lowerLimit;
121+
122+
if (freezeAtUpperLimit || freezeAtLowerLimit) {
123+
// When indicating to freeze, we need to know the rate to freeze it at - either upper or lower
124+
// this is useful in situations where ExchangeRates is updated and there are existing inverted
125+
// rates already frozen in the current contract that need persisting across the upgrade
126+
127+
inverse.frozenAtUpperLimit = freezeAtUpperLimit;
128+
inverse.frozenAtLowerLimit = freezeAtLowerLimit;
129+
emit InversePriceFrozen(currencyKey, freezeAtUpperLimit ? upperLimit : lowerLimit, msg.sender);
130+
} else {
131+
// unfreeze if need be
132+
inverse.frozenAtUpperLimit = false;
133+
inverse.frozenAtLowerLimit = false;
134+
}
130135

131136
emit InversePriceConfigured(currencyKey, entryPoint, upperLimit, lowerLimit);
132-
133-
// When indicating to freeze, we need to know the rate to freeze it at - either upper or lower
134-
// this is useful in situations where ExchangeRates is updated and there are existing inverted
135-
// rates already frozen in the current contract that need persisting across the upgrade
136-
if (freeze) {
137-
emit InversePriceFrozen(currencyKey);
138-
139-
_setRate(currencyKey, freezeAtUpperLimit ? upperLimit : lowerLimit, now);
140-
}
141137
}
142138

143139
function removeInversePricing(bytes32 currencyKey) external onlyOwner {
144140
require(inversePricing[currencyKey].entryPoint > 0, "No inverted price exists");
145141

146-
inversePricing[currencyKey].entryPoint = 0;
147-
inversePricing[currencyKey].upperLimit = 0;
148-
inversePricing[currencyKey].lowerLimit = 0;
149-
inversePricing[currencyKey].frozen = false;
142+
delete inversePricing[currencyKey];
150143

151144
// now remove inverted key from array
152145
bool wasRemoved = removeFromArray(currencyKey, invertedKeys);
@@ -180,8 +173,47 @@ contract ExchangeRates is Owned, SelfDestructible, MixinResolver, MixinSystemSet
180173
}
181174
}
182175

176+
// SIP-75 Public keeper function to freeze a synth that is out of bounds
177+
function freezeRate(bytes32 currencyKey) external {
178+
InversePricing storage inverse = inversePricing[currencyKey];
179+
require(inverse.entryPoint > 0, "Cannot freeze non-inverse rate");
180+
require(!inverse.frozenAtUpperLimit && !inverse.frozenAtLowerLimit, "The rate is already frozen");
181+
182+
uint rate = _getRate(currencyKey);
183+
184+
if (rate > 0 && (rate >= inverse.upperLimit || rate <= inverse.lowerLimit)) {
185+
inverse.frozenAtUpperLimit = (rate == inverse.upperLimit);
186+
inverse.frozenAtLowerLimit = (rate == inverse.lowerLimit);
187+
emit InversePriceFrozen(currencyKey, rate, msg.sender);
188+
} else {
189+
revert("Rate within bounds");
190+
}
191+
}
192+
183193
/* ========== VIEWS ========== */
184194

195+
// SIP-75 View to determine if freezeRate can be called safely
196+
function canFreezeRate(bytes32 currencyKey) external view returns (bool) {
197+
InversePricing memory inverse = inversePricing[currencyKey];
198+
if (inverse.entryPoint == 0 || inverse.frozenAtUpperLimit || inverse.frozenAtLowerLimit) {
199+
return false;
200+
} else {
201+
uint rate = _getRate(currencyKey);
202+
return (rate > 0 && (rate >= inverse.upperLimit || rate <= inverse.lowerLimit));
203+
}
204+
}
205+
206+
function currenciesUsingAggregator(address aggregator) external view returns (bytes32[] memory currencies) {
207+
uint count = 0;
208+
currencies = new bytes32[](aggregatorKeys.length);
209+
for (uint i = 0; i < aggregatorKeys.length; i++) {
210+
bytes32 currencyKey = aggregatorKeys[i];
211+
if (address(aggregators[currencyKey]) == aggregator) {
212+
currencies[count++] = currencyKey;
213+
}
214+
}
215+
}
216+
185217
function rateStalePeriod() external view returns (uint) {
186218
return getRateStalePeriod();
187219
}
@@ -337,7 +369,7 @@ contract ExchangeRates is Owned, SelfDestructible, MixinResolver, MixinSystemSet
337369
}
338370

339371
function rateIsFrozen(bytes32 currencyKey) external view returns (bool) {
340-
return inversePricing[currencyKey].frozen;
372+
return _rateIsFrozen(currencyKey);
341373
}
342374

343375
function rateIsInvalid(bytes32 currencyKey) external view returns (bool) {
@@ -421,8 +453,6 @@ contract ExchangeRates is Owned, SelfDestructible, MixinResolver, MixinSystemSet
421453
continue;
422454
}
423455

424-
newRates[i] = rateOrInverted(currencyKey, newRates[i]);
425-
426456
// Ok, go ahead with the update.
427457
_setRate(currencyKey, newRates[i], timeSent);
428458
}
@@ -432,44 +462,6 @@ contract ExchangeRates is Owned, SelfDestructible, MixinResolver, MixinSystemSet
432462
return true;
433463
}
434464

435-
function rateOrInverted(bytes32 currencyKey, uint rate) internal returns (uint) {
436-
// if an inverse mapping exists, adjust the price accordingly
437-
InversePricing storage inverse = inversePricing[currencyKey];
438-
if (inverse.entryPoint <= 0) {
439-
return rate;
440-
}
441-
442-
// set the rate to the current rate initially (if it's frozen, this is what will be returned)
443-
uint newInverseRate = _getRate(currencyKey);
444-
445-
// get the new inverted rate if not frozen
446-
if (!inverse.frozen) {
447-
uint doubleEntryPoint = inverse.entryPoint.mul(2);
448-
if (doubleEntryPoint <= rate) {
449-
// avoid negative numbers for unsigned ints, so set this to 0
450-
// which by the requirement that lowerLimit be > 0 will
451-
// cause this to freeze the price to the lowerLimit
452-
newInverseRate = 0;
453-
} else {
454-
newInverseRate = doubleEntryPoint.sub(rate);
455-
}
456-
457-
// now if new rate hits our limits, set it to the limit and freeze
458-
if (newInverseRate >= inverse.upperLimit) {
459-
newInverseRate = inverse.upperLimit;
460-
} else if (newInverseRate <= inverse.lowerLimit) {
461-
newInverseRate = inverse.lowerLimit;
462-
}
463-
464-
if (newInverseRate == inverse.upperLimit || newInverseRate == inverse.lowerLimit) {
465-
inverse.frozen = true;
466-
emit InversePriceFrozen(currencyKey);
467-
}
468-
}
469-
470-
return newInverseRate;
471-
}
472-
473465
function removeFromArray(bytes32 entry, bytes32[] storage array) internal returns (bool) {
474466
for (uint i = 0; i < array.length; i++) {
475467
if (array[i] == entry) {
@@ -489,17 +481,59 @@ contract ExchangeRates is Owned, SelfDestructible, MixinResolver, MixinSystemSet
489481
return false;
490482
}
491483

484+
function _rateOrInverted(bytes32 currencyKey, uint rate) internal view returns (uint newRate) {
485+
// if an inverse mapping exists, adjust the price accordingly
486+
InversePricing memory inverse = inversePricing[currencyKey];
487+
if (inverse.entryPoint == 0 || rate == 0) {
488+
// when no inverse is set or when given a 0 rate, return the rate, regardless of the inverse status
489+
// (the latter is so when a new inverse is set but the underlying has no rate, it will return 0 as
490+
// the rate, not the lowerLimit)
491+
return rate;
492+
}
493+
494+
newRate = rate;
495+
496+
// These cases ensures that if a price has been frozen, it stays frozen even if it returns to the bounds
497+
if (inverse.frozenAtUpperLimit) {
498+
newRate = inverse.upperLimit;
499+
} else if (inverse.frozenAtLowerLimit) {
500+
newRate = inverse.lowerLimit;
501+
} else {
502+
// this ensures any rate outside the limit will never be returned
503+
uint doubleEntryPoint = inverse.entryPoint.mul(2);
504+
if (doubleEntryPoint <= rate) {
505+
// avoid negative numbers for unsigned ints, so set this to 0
506+
// which by the requirement that lowerLimit be > 0 will
507+
// cause this to freeze the price to the lowerLimit
508+
newRate = 0;
509+
} else {
510+
newRate = doubleEntryPoint.sub(rate);
511+
}
512+
513+
// now ensure the rate is between the bounds
514+
if (newRate >= inverse.upperLimit) {
515+
newRate = inverse.upperLimit;
516+
} else if (newRate <= inverse.lowerLimit) {
517+
newRate = inverse.lowerLimit;
518+
}
519+
}
520+
}
521+
492522
function _getRateAndUpdatedTime(bytes32 currencyKey) internal view returns (RateAndUpdatedTime memory) {
493523
AggregatorInterface aggregator = aggregators[currencyKey];
494524

495525
if (aggregator != AggregatorInterface(0)) {
496526
return
497527
RateAndUpdatedTime({
498-
rate: uint216(aggregator.latestAnswer() * AGGREGATOR_RATE_MULTIPLIER),
528+
rate: uint216(
529+
_rateOrInverted(currencyKey, uint(aggregator.latestAnswer() * AGGREGATOR_RATE_MULTIPLIER))
530+
),
499531
time: uint40(aggregator.latestTimestamp())
500532
});
501533
} else {
502-
return _rates[currencyKey][currentRoundForRate[currencyKey]];
534+
RateAndUpdatedTime memory entry = _rates[currencyKey][currentRoundForRate[currencyKey]];
535+
536+
return RateAndUpdatedTime({rate: uint216(_rateOrInverted(currencyKey, entry.rate)), time: entry.time});
503537
}
504538
}
505539

@@ -517,10 +551,13 @@ contract ExchangeRates is Owned, SelfDestructible, MixinResolver, MixinSystemSet
517551
AggregatorInterface aggregator = aggregators[currencyKey];
518552

519553
if (aggregator != AggregatorInterface(0)) {
520-
return (uint(aggregator.getAnswer(roundId) * AGGREGATOR_RATE_MULTIPLIER), aggregator.getTimestamp(roundId));
554+
return (
555+
_rateOrInverted(currencyKey, uint(aggregator.getAnswer(roundId) * AGGREGATOR_RATE_MULTIPLIER)),
556+
aggregator.getTimestamp(roundId)
557+
);
521558
} else {
522-
RateAndUpdatedTime storage update = _rates[currencyKey][roundId];
523-
return (update.rate, update.time);
559+
RateAndUpdatedTime memory update = _rates[currencyKey][roundId];
560+
return (_rateOrInverted(currencyKey, update.rate), update.time);
524561
}
525562
}
526563

@@ -568,6 +605,11 @@ contract ExchangeRates is Owned, SelfDestructible, MixinResolver, MixinSystemSet
568605
return _time.add(_rateStalePeriod) < now;
569606
}
570607

608+
function _rateIsFrozen(bytes32 currencyKey) internal view returns (bool) {
609+
InversePricing memory inverse = inversePricing[currencyKey];
610+
return inverse.frozenAtUpperLimit || inverse.frozenAtLowerLimit;
611+
}
612+
571613
function _rateIsFlagged(bytes32 currencyKey, FlagsInterface flags) internal view returns (bool) {
572614
// sUSD is a special case and is never invalid
573615
if (currencyKey == "sUSD") return false;
@@ -592,7 +634,7 @@ contract ExchangeRates is Owned, SelfDestructible, MixinResolver, MixinSystemSet
592634
event RatesUpdated(bytes32[] currencyKeys, uint[] newRates);
593635
event RateDeleted(bytes32 currencyKey);
594636
event InversePriceConfigured(bytes32 currencyKey, uint entryPoint, uint upperLimit, uint lowerLimit);
595-
event InversePriceFrozen(bytes32 currencyKey);
637+
event InversePriceFrozen(bytes32 currencyKey, uint rate, address initiator);
596638
event AggregatorAdded(bytes32 currencyKey, address aggregator);
597639
event AggregatorRemoved(bytes32 currencyKey, address aggregator);
598640
}

contracts/interfaces/IExchangeRates.sol

+23-1
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,33 @@ pragma solidity >=0.4.24;
33

44
// https://docs.synthetix.io/contracts/source/interfaces/IExchangeRates
55
interface IExchangeRates {
6+
// Structs
7+
struct RateAndUpdatedTime {
8+
uint216 rate;
9+
uint40 time;
10+
}
11+
12+
struct InversePricing {
13+
uint entryPoint;
14+
uint upperLimit;
15+
uint lowerLimit;
16+
bool frozenAtUpperLimit;
17+
bool frozenAtLowerLimit;
18+
}
19+
620
// Views
721
function aggregators(bytes32 currencyKey) external view returns (address);
822

923
function aggregatorWarningFlags() external view returns (address);
1024

1125
function anyRateIsInvalid(bytes32[] calldata currencyKeys) external view returns (bool);
1226

27+
function canFreezeRate(bytes32 currencyKey) external view returns (bool);
28+
1329
function currentRoundForRate(bytes32 currencyKey) external view returns (uint);
1430

31+
function currenciesUsingAggregator(address aggregator) external view returns (bytes32[] memory);
32+
1533
function effectiveValue(
1634
bytes32 sourceCurrencyKey,
1735
uint sourceAmount,
@@ -55,7 +73,8 @@ interface IExchangeRates {
5573
uint entryPoint,
5674
uint upperLimit,
5775
uint lowerLimit,
58-
bool frozen
76+
bool frozenAtUpperLimit,
77+
bool frozenAtLowerLimit
5978
);
6079

6180
function lastRateUpdateTimes(bytes32 currencyKey) external view returns (uint256);
@@ -89,4 +108,7 @@ interface IExchangeRates {
89108
returns (uint[] memory rates, bool anyRateInvalid);
90109

91110
function ratesForCurrencies(bytes32[] calldata currencyKeys) external view returns (uint[] memory);
111+
112+
// Mutative functions
113+
function freezeRate(bytes32 currencyKey) external;
92114
}

publish/releases.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,6 @@
1515
"Synthetix",
1616
"SystemSettings"
1717
],
18-
"sips": [64, 76]
18+
"sips": [64, 76, 75]
1919
}
2020
]

0 commit comments

Comments
 (0)