@@ -22,8 +22,12 @@ contract ExchangeRates is Owned, MixinSystemSettings, IExchangeRates {
22
22
using SafeDecimalMath for uint ;
23
23
24
24
bytes32 public constant CONTRACT_NAME = "ExchangeRates " ;
25
- //slither-disable-next-line naming-convention
26
- bytes32 internal constant sUSD = "sUSD " ;
25
+
26
+ // Exchange rates and update times stored by currency code, e.g. 'SNX', or 'sUSD'
27
+ mapping (bytes32 => mapping (uint => RateAndUpdatedTime)) private _rates;
28
+
29
+ // The address of the oracle which pushes rate updates to this contract
30
+ address public oracle;
27
31
28
32
// Decentralized oracle networks that feed into pricing aggregators
29
33
mapping (bytes32 => AggregatorV2V3Interface) public aggregators;
@@ -33,12 +37,58 @@ contract ExchangeRates is Owned, MixinSystemSettings, IExchangeRates {
33
37
// List of aggregator keys for convenient iteration
34
38
bytes32 [] public aggregatorKeys;
35
39
40
+ // Do not allow the oracle to submit times any further forward into the future than this constant.
41
+ uint private constant ORACLE_FUTURE_LIMIT = 10 minutes ;
42
+
43
+ mapping (bytes32 => uint ) public currentRoundForRate;
44
+
45
+ //
36
46
// ========== CONSTRUCTOR ==========
37
47
38
- constructor (address _owner , address _resolver ) public Owned (_owner) MixinSystemSettings (_resolver) {}
48
+ constructor (
49
+ address _owner ,
50
+ address _oracle ,
51
+ address _resolver ,
52
+ bytes32 [] memory _currencyKeys ,
53
+ uint [] memory _newRates
54
+ ) public Owned (_owner) MixinSystemSettings (_resolver) {
55
+ require (_currencyKeys.length == _newRates.length , "Currency key length and rate length must match. " );
56
+
57
+ oracle = _oracle;
58
+
59
+ // The sUSD rate is always 1 and is never stale.
60
+ _setRate ("sUSD " , SafeDecimalMath.unit (), now );
61
+
62
+ internalUpdateRates (_currencyKeys, _newRates, now );
63
+ }
64
+
65
+ /* ========== SETTERS ========== */
66
+
67
+ function setOracle (address _oracle ) external onlyOwner {
68
+ oracle = _oracle;
69
+ emit OracleUpdated (oracle);
70
+ }
39
71
40
72
/* ========== MUTATIVE FUNCTIONS ========== */
41
73
74
+ function updateRates (
75
+ bytes32 [] calldata currencyKeys ,
76
+ uint [] calldata newRates ,
77
+ uint timeSent
78
+ ) external onlyOracle returns (bool ) {
79
+ return internalUpdateRates (currencyKeys, newRates, timeSent);
80
+ }
81
+
82
+ function deleteRate (bytes32 currencyKey ) external onlyOracle {
83
+ require (_getRate (currencyKey) > 0 , "Rate is zero " );
84
+
85
+ delete _rates[currencyKey][currentRoundForRate[currencyKey]];
86
+
87
+ currentRoundForRate[currencyKey]-- ;
88
+
89
+ emit RateDeleted (currencyKey);
90
+ }
91
+
42
92
function addAggregator (bytes32 currencyKey , address aggregatorAddress ) external onlyOwner {
43
93
AggregatorV2V3Interface aggregator = AggregatorV2V3Interface (aggregatorAddress);
44
94
// This check tries to make sure that a valid aggregator is being added.
@@ -238,7 +288,7 @@ contract ExchangeRates is Owned, MixinSystemSettings, IExchangeRates {
238
288
function rateAndInvalid (bytes32 currencyKey ) external view returns (uint rate , bool isInvalid ) {
239
289
RateAndUpdatedTime memory rateAndTime = _getRateAndUpdatedTime (currencyKey);
240
290
241
- if (currencyKey == sUSD) {
291
+ if (currencyKey == " sUSD " ) {
242
292
return (rateAndTime.rate, false );
243
293
}
244
294
return (
@@ -264,7 +314,7 @@ contract ExchangeRates is Owned, MixinSystemSettings, IExchangeRates {
264
314
// do one lookup of the rate & time to minimize gas
265
315
RateAndUpdatedTime memory rateEntry = _getRateAndUpdatedTime (currencyKeys[i]);
266
316
rates[i] = rateEntry.rate;
267
- if (! anyRateInvalid && currencyKeys[i] != sUSD) {
317
+ if (! anyRateInvalid && currencyKeys[i] != " sUSD " ) {
268
318
anyRateInvalid = flagList[i] || _rateIsStaleWithTime (_rateStalePeriod, rateEntry.time);
269
319
}
270
320
}
@@ -322,6 +372,52 @@ contract ExchangeRates is Owned, MixinSystemSettings, IExchangeRates {
322
372
}
323
373
}
324
374
375
+ function _setRate (
376
+ bytes32 currencyKey ,
377
+ uint256 rate ,
378
+ uint256 time
379
+ ) internal {
380
+ // Note: this will effectively start the rounds at 1, which matches Chainlink's Agggregators
381
+ currentRoundForRate[currencyKey]++ ;
382
+
383
+ _rates[currencyKey][currentRoundForRate[currencyKey]] = RateAndUpdatedTime ({
384
+ rate: uint216 (rate),
385
+ time: uint40 (time)
386
+ });
387
+ }
388
+
389
+ function internalUpdateRates (
390
+ bytes32 [] memory currencyKeys ,
391
+ uint [] memory newRates ,
392
+ uint timeSent
393
+ ) internal returns (bool ) {
394
+ require (currencyKeys.length == newRates.length , "Currency key array length must match rates array length. " );
395
+ require (timeSent < (now + ORACLE_FUTURE_LIMIT), "Time is too far into the future " );
396
+
397
+ // Loop through each key and perform update.
398
+ for (uint i = 0 ; i < currencyKeys.length ; i++ ) {
399
+ bytes32 currencyKey = currencyKeys[i];
400
+
401
+ // Should not set any rate to zero ever, as no asset will ever be
402
+ // truely worthless and still valid. In this scenario, we should
403
+ // delete the rate and remove it from the system.
404
+ require (newRates[i] != 0 , "Zero is not a valid rate, please call deleteRate instead. " );
405
+ require (currencyKey != "sUSD " , "Rate of sUSD cannot be updated, it's always UNIT. " );
406
+
407
+ // We should only update the rate if it's at least the same age as the last rate we've got.
408
+ if (timeSent < _getUpdatedTime (currencyKey)) {
409
+ continue ;
410
+ }
411
+
412
+ // Ok, go ahead with the update.
413
+ _setRate (currencyKey, newRates[i], timeSent);
414
+ }
415
+
416
+ emit RatesUpdated (currencyKeys, newRates);
417
+
418
+ return true ;
419
+ }
420
+
325
421
function removeFromArray (bytes32 entry , bytes32 [] storage array ) internal returns (bool ) {
326
422
for (uint i = 0 ; i < array.length ; i++ ) {
327
423
if (array[i] == entry) {
@@ -351,64 +447,60 @@ contract ExchangeRates is Owned, MixinSystemSettings, IExchangeRates {
351
447
}
352
448
353
449
function _getRateAndUpdatedTime (bytes32 currencyKey ) internal view returns (RateAndUpdatedTime memory ) {
354
- // sUSD rate is 1.0
355
- if (currencyKey == sUSD) {
356
- return RateAndUpdatedTime ({rate: uint216 (SafeDecimalMath.unit ()), time: 0 });
450
+ AggregatorV2V3Interface aggregator = aggregators[currencyKey];
451
+
452
+ if (aggregator != AggregatorV2V3Interface (0 )) {
453
+ // this view from the aggregator is the most gas efficient but it can throw when there's no data,
454
+ // so let's call it low-level to suppress any reverts
455
+ bytes memory payload = abi.encodeWithSignature ("latestRoundData() " );
456
+ // solhint-disable avoid-low-level-calls
457
+ (bool success , bytes memory returnData ) = address (aggregator).staticcall (payload);
458
+
459
+ if (success) {
460
+ (, int256 answer , , uint256 updatedAt , ) =
461
+ abi.decode (returnData, (uint80 , int256 , uint256 , uint256 , uint80 ));
462
+ return
463
+ RateAndUpdatedTime ({
464
+ rate: uint216 (_formatAggregatorAnswer (currencyKey, answer)),
465
+ time: uint40 (updatedAt)
466
+ });
467
+ }
357
468
} else {
358
- AggregatorV2V3Interface aggregator = aggregators[currencyKey];
359
- if (aggregator != AggregatorV2V3Interface (0 )) {
360
- // this view from the aggregator is the most gas efficient but it can throw when there's no data,
361
- // so let's call it low-level to suppress any reverts
362
- bytes memory payload = abi.encodeWithSignature ("latestRoundData() " );
363
- // solhint-disable avoid-low-level-calls
364
- // slither-disable-next-line low-level-calls
365
- (bool success , bytes memory returnData ) = address (aggregator).staticcall (payload);
366
-
367
- if (success) {
368
- (, int256 answer , , uint256 updatedAt , ) =
369
- abi.decode (returnData, (uint80 , int256 , uint256 , uint256 , uint80 ));
370
- return
371
- RateAndUpdatedTime ({
372
- rate: uint216 (_formatAggregatorAnswer (currencyKey, answer)),
373
- time: uint40 (updatedAt)
374
- });
375
- } // else return defaults, to avoid reverting in views
376
- } // else return defaults, to avoid reverting in views
469
+ uint roundId = currentRoundForRate[currencyKey];
470
+ RateAndUpdatedTime memory entry = _rates[currencyKey][roundId];
471
+
472
+ return RateAndUpdatedTime ({rate: uint216 (entry.rate), time: entry.time});
377
473
}
378
474
}
379
475
380
476
function _getCurrentRoundId (bytes32 currencyKey ) internal view returns (uint ) {
381
- if (currencyKey == sUSD) {
382
- return 0 ; // no roundIds for sUSD
383
- }
384
477
AggregatorV2V3Interface aggregator = aggregators[currencyKey];
478
+
385
479
if (aggregator != AggregatorV2V3Interface (0 )) {
386
480
return aggregator.latestRound ();
387
- } // else return defaults, to avoid reverting in views
481
+ } else {
482
+ return currentRoundForRate[currencyKey];
483
+ }
388
484
}
389
485
390
486
function _getRateAndTimestampAtRound (bytes32 currencyKey , uint roundId ) internal view returns (uint rate , uint time ) {
391
- // short circuit sUSD
392
- if (currencyKey == sUSD) {
393
- // sUSD has no rounds, and 0 time is preferrable for "volatility" heuristics
394
- // which are used in atomic swaps and fee reclamation
395
- return (SafeDecimalMath.unit (), 0 );
487
+ AggregatorV2V3Interface aggregator = aggregators[currencyKey];
488
+
489
+ if (aggregator != AggregatorV2V3Interface (0 )) {
490
+ // this view from the aggregator is the most gas efficient but it can throw when there's no data,
491
+ // so let's call it low-level to suppress any reverts
492
+ bytes memory payload = abi.encodeWithSignature ("getRoundData(uint80) " , roundId);
493
+ // solhint-disable avoid-low-level-calls
494
+ (bool success , bytes memory returnData ) = address (aggregator).staticcall (payload);
495
+
496
+ if (success) {
497
+ (, int256 answer , , uint256 updatedAt , ) =
498
+ abi.decode (returnData, (uint80 , int256 , uint256 , uint256 , uint80 ));
499
+ return (_formatAggregatorAnswer (currencyKey, answer), updatedAt);
500
+ }
396
501
} else {
397
- AggregatorV2V3Interface aggregator = aggregators[currencyKey];
398
-
399
- if (aggregator != AggregatorV2V3Interface (0 )) {
400
- // this view from the aggregator is the most gas efficient but it can throw when there's no data,
401
- // so let's call it low-level to suppress any reverts
402
- bytes memory payload = abi.encodeWithSignature ("getRoundData(uint80) " , roundId);
403
- // solhint-disable avoid-low-level-calls
404
- (bool success , bytes memory returnData ) = address (aggregator).staticcall (payload);
405
-
406
- if (success) {
407
- (, int256 answer , , uint256 updatedAt , ) =
408
- abi.decode (returnData, (uint80 , int256 , uint256 , uint256 , uint80 ));
409
- return (_formatAggregatorAnswer (currencyKey, answer), updatedAt);
410
- } // else return defaults, to avoid reverting in views
411
- } // else return defaults, to avoid reverting in views
502
+ RateAndUpdatedTime memory update = _rates[currencyKey][roundId];
503
+ return (update.rate, update.time);
412
504
}
413
505
}
414
506
@@ -450,7 +542,7 @@ contract ExchangeRates is Owned, MixinSystemSettings, IExchangeRates {
450
542
451
543
function _rateIsStale (bytes32 currencyKey , uint _rateStalePeriod ) internal view returns (bool ) {
452
544
// sUSD is a special case and is never stale (check before an SLOAD of getRateAndUpdatedTime)
453
- if (currencyKey == sUSD) return false ;
545
+ if (currencyKey == " sUSD " ) return false ;
454
546
455
547
return _rateIsStaleWithTime (_rateStalePeriod, _getUpdatedTime (currencyKey));
456
548
}
@@ -461,7 +553,7 @@ contract ExchangeRates is Owned, MixinSystemSettings, IExchangeRates {
461
553
462
554
function _rateIsFlagged (bytes32 currencyKey , FlagsInterface flags ) internal view returns (bool ) {
463
555
// sUSD is a special case and is never invalid
464
- if (currencyKey == sUSD) return false ;
556
+ if (currencyKey == " sUSD " ) return false ;
465
557
address aggregator = address (aggregators[currencyKey]);
466
558
// when no aggregator or when the flags haven't been setup
467
559
if (aggregator == address (0 ) || flags == FlagsInterface (0 )) {
@@ -471,12 +563,25 @@ contract ExchangeRates is Owned, MixinSystemSettings, IExchangeRates {
471
563
}
472
564
473
565
function _notImplemented () internal pure {
474
- // slither-disable-next-line dead-code
475
566
revert ("Cannot be run on this layer " );
476
567
}
477
568
569
+ /* ========== MODIFIERS ========== */
570
+
571
+ modifier onlyOracle {
572
+ _onlyOracle ();
573
+ _;
574
+ }
575
+
576
+ function _onlyOracle () internal view {
577
+ require (msg .sender == oracle, "Only the oracle can perform this action " );
578
+ }
579
+
478
580
/* ========== EVENTS ========== */
479
581
582
+ event OracleUpdated (address newOracle );
583
+ event RatesUpdated (bytes32 [] currencyKeys , uint [] newRates );
584
+ event RateDeleted (bytes32 currencyKey );
480
585
event AggregatorAdded (bytes32 currencyKey , address aggregator );
481
586
event AggregatorRemoved (bytes32 currencyKey , address aggregator );
482
587
}
0 commit comments