-
Notifications
You must be signed in to change notification settings - Fork 111
/
Copy pathBasketHandler.sol
739 lines (632 loc) · 30.7 KB
/
BasketHandler.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
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
// SPDX-License-Identifier: BlueOak-1.0.0
pragma solidity 0.8.9;
import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/utils/structs/EnumerableSet.sol";
import "../interfaces/IAssetRegistry.sol";
import "../interfaces/IBasketHandler.sol";
import "../interfaces/IMain.sol";
import "../libraries/Array.sol";
import "../libraries/Fixed.sol";
import "./mixins/Component.sol";
// A "valid collateral array" is a an IERC20[] value without rtoken, rsr, or any duplicate values
// A BackupConfig value is valid if erc20s is a valid collateral array
struct BackupConfig {
uint256 max; // Maximum number of backup collateral erc20s to use in a basket
IERC20[] erc20s; // Ordered list of backup collateral ERC20s
}
// What does a BasketConfig value mean?
//
// erc20s, targetAmts, and targetNames should be interpreted together.
// targetAmts[erc20] is the quantity of target units of erc20 that one BU should hold
// targetNames[erc20] is the name of erc20's target unit
// and then backups[tgt] is the BackupConfig to use for the target unit named tgt
//
// For any valid BasketConfig value:
// erc20s == keys(targetAmts) == keys(targetNames)
// if name is in values(targetNames), then backups[name] is a valid BackupConfig
// erc20s is a valid collateral array
//
// In the meantime, treat erc20s as the canonical set of keys for the target* maps
struct BasketConfig {
// The collateral erc20s in the prime (explicitly governance-set) basket
IERC20[] erc20s;
// Amount of target units per basket for each prime collateral token. {target/BU}
mapping(IERC20 => uint192) targetAmts;
// Cached view of the target unit for each erc20 upon setup
mapping(IERC20 => bytes32) targetNames;
// Backup configurations, per target name.
mapping(bytes32 => BackupConfig) backups;
}
/// The type of BasketHandler.basket.
/// Defines a basket unit (BU) in terms of reference amounts of underlying tokens
// Logically, basket is just a mapping of erc20 addresses to ref-unit amounts.
// In the analytical comments I'll just refer to it that way.
//
// A Basket is valid if erc20s is a valid collateral array and erc20s == keys(refAmts)
struct Basket {
IERC20[] erc20s; // enumerated keys for refAmts
mapping(IERC20 => uint192) refAmts; // {ref/BU}
}
/*
* @title BasketLibP1
*/
library BasketLibP1 {
using BasketLibP1 for Basket;
using FixLib for uint192;
/// Set self to a fresh, empty basket
// self'.erc20s = [] (empty list)
// self'.refAmts = {} (empty map)
function empty(Basket storage self) internal {
uint256 length = self.erc20s.length;
for (uint256 i = 0; i < length; ++i) self.refAmts[self.erc20s[i]] = FIX_ZERO;
delete self.erc20s;
}
/// Set `self` equal to `other`
function setFrom(Basket storage self, Basket storage other) internal {
empty(self);
uint256 length = other.erc20s.length;
for (uint256 i = 0; i < length; ++i) {
self.erc20s.push(other.erc20s[i]);
self.refAmts[other.erc20s[i]] = other.refAmts[other.erc20s[i]];
}
}
/// Add `weight` to the refAmount of collateral token `tok` in the basket `self`
// self'.refAmts[tok] = self.refAmts[tok] + weight
// self'.erc20s is keys(self'.refAmts)
function add(
Basket storage self,
IERC20 tok,
uint192 weight
) internal {
// untestable:
// Both calls to .add() use a weight that has been CEIL rounded in the
// Fixed library div function, so weight will never be 0 here.
// Additionally, setPrimeBasket() enforces prime-basket tokens must have a weight > 0.
if (weight == FIX_ZERO) return;
if (self.refAmts[tok].eq(FIX_ZERO)) {
self.erc20s.push(tok);
self.refAmts[tok] = weight;
} else {
self.refAmts[tok] = self.refAmts[tok].plus(weight);
}
}
}
/**
* @title BasketHandler
* @notice Handles the basket configuration, definition, and evolution over time.
*/
contract BasketHandlerP1 is ComponentP1, IBasketHandler {
using BasketLibP1 for Basket;
using CollateralStatusComparator for CollateralStatus;
using EnumerableSet for EnumerableSet.AddressSet;
using EnumerableSet for EnumerableSet.Bytes32Set;
using FixLib for uint192;
uint192 public constant MAX_TARGET_AMT = 1e3 * FIX_ONE; // {target/BU} max basket weight
// Peer components
IAssetRegistry private assetRegistry;
IBackingManager private backingManager;
IERC20 private rsr;
IRToken private rToken;
IStRSR private stRSR;
// config is the basket configuration, from which basket will be computed in a basket-switch
// event. config is only modified by governance through setPrimeBakset and setBackupConfig
BasketConfig private config;
// basket, disabled, nonce, and timestamp are only ever set by `_switchBasket()`
// basket is the current basket.
Basket private basket;
uint48 public override nonce; // A unique identifier for this basket instance
uint48 public override timestamp; // The timestamp when this basket was last set
// If disabled is true, status() is DISABLED, the basket is invalid,
// and everything except redemption should be paused.
bool private disabled;
// ==== Invariants ====
// basket is a valid Basket:
// basket.erc20s is a valid collateral array and basket.erc20s == keys(basket.refAmts)
// config is a valid BasketConfig:
// erc20s == keys(targetAmts) == keys(targetNames)
// erc20s is a valid collateral array
// for b in vals(backups), b.erc20s is a valid collateral array.
// if basket.erc20s is empty then disabled == true
// BasketHandler.init() just leaves the BasketHandler state zeroed
function init(IMain main_) external initializer {
__Component_init(main_);
assetRegistry = main_.assetRegistry();
backingManager = main_.backingManager();
rsr = main_.rsr();
rToken = main_.rToken();
stRSR = main_.stRSR();
disabled = true;
}
/// Disable the basket in order to schedule a basket refresh
/// @custom:protected
// checks: caller is assetRegistry
// effects: disabled' = true
function disableBasket() external {
require(_msgSender() == address(assetRegistry), "asset registry only");
uint192[] memory refAmts = new uint192[](basket.erc20s.length);
emit BasketSet(nonce, basket.erc20s, refAmts, true);
disabled = true;
}
/// Switch the basket, only callable directly by governance
/// @custom:interaction OR @custom:governance
// checks: either caller has OWNER,
// or (basket is disabled after refresh and we're unpaused and unfrozen)
// actions: calls assetRegistry.refresh(), then _switchBasket()
// effects:
// Either: (basket' is a valid nonempty basket, without DISABLED collateral,
// that satisfies basketConfig) and disabled' = false
// Or no such basket exists and disabled' = true
function refreshBasket() external {
assetRegistry.refresh();
require(
main.hasRole(OWNER, _msgSender()) ||
(status() == CollateralStatus.DISABLED && !main.pausedOrFrozen()),
"basket unrefreshable"
);
_switchBasket();
}
/// Set the prime basket in the basket configuration, in terms of erc20s and target amounts
/// @param erc20s The collateral for the new prime basket
/// @param targetAmts The target amounts (in) {target/BU} for the new prime basket
/// @custom:governance
// checks:
// caller is OWNER
// len(erc20s) == len(targetAmts)
// erc20s is a valid collateral array
// for all i, erc20[i] is in AssetRegistry as collateral
// for all i, 0 < targetAmts[i] <= MAX_TARGET_AMT == 1000
//
// effects:
// config'.erc20s = erc20s
// config'.targetAmts[erc20s[i]] = targetAmts[i], for i from 0 to erc20s.length-1
// config'.targetNames[e] = assetRegistry.toColl(e).targetName, for e in erc20s
function setPrimeBasket(IERC20[] calldata erc20s, uint192[] calldata targetAmts)
external
governance
{
require(erc20s.length > 0, "cannot empty basket");
require(erc20s.length == targetAmts.length, "must be same length");
requireValidCollArray(erc20s);
// Clean up previous basket config
for (uint256 i = 0; i < config.erc20s.length; ++i) {
delete config.targetAmts[config.erc20s[i]];
delete config.targetNames[config.erc20s[i]];
}
delete config.erc20s;
// Set up new config basket
bytes32[] memory names = new bytes32[](erc20s.length);
for (uint256 i = 0; i < erc20s.length; ++i) {
// This is a nice catch to have, but in general it is possible for
// an ERC20 in the prime basket to have its asset unregistered.
require(assetRegistry.toAsset(erc20s[i]).isCollateral(), "token is not collateral");
require(0 < targetAmts[i], "invalid target amount; must be nonzero");
require(targetAmts[i] <= MAX_TARGET_AMT, "invalid target amount; too large");
config.erc20s.push(erc20s[i]);
config.targetAmts[erc20s[i]] = targetAmts[i];
names[i] = assetRegistry.toColl(erc20s[i]).targetName();
config.targetNames[erc20s[i]] = names[i];
}
emit PrimeBasketSet(erc20s, targetAmts, names);
}
/// Set the backup configuration for some target name
/// @custom:governance
// checks:
// caller is OWNER
// erc20s is a valid collateral array
// for all i, erc20[i] is in AssetRegistry as collateral
//
// effects:
// config'.backups[targetName] = {max: max, erc20s: erc20s}
function setBackupConfig(
bytes32 targetName,
uint256 max,
IERC20[] calldata erc20s
) external governance {
requireValidCollArray(erc20s);
BackupConfig storage conf = config.backups[targetName];
conf.max = max;
delete conf.erc20s;
for (uint256 i = 0; i < erc20s.length; ++i) {
// This is a nice catch to have, but in general it is possible for
// an ERC20 in the backup config to have its asset altered.
require(assetRegistry.toAsset(erc20s[i]).isCollateral(), "token is not collateral");
conf.erc20s.push(erc20s[i]);
}
emit BackupConfigSet(targetName, max, erc20s);
}
/// @return Whether this contract owns enough collateral to cover rToken.basketsNeeded() BUs
/// ie, whether the protocol is currently fully collateralized
function fullyCollateralized() external view returns (bool) {
return basketsHeldBy(address(backingManager)) >= rToken.basketsNeeded();
}
/// @return status_ The status of the basket
// returns DISABLED if disabled == true, and worst(status(coll)) otherwise
function status() public view returns (CollateralStatus status_) {
uint256 size = basket.erc20s.length;
// untestable:
// disabled is only set in _switchBasket, and only if size > 0.
if (disabled || size == 0) return CollateralStatus.DISABLED;
for (uint256 i = 0; i < size; ++i) {
CollateralStatus s = assetRegistry.toColl(basket.erc20s[i]).status();
if (s.worseThan(status_)) status_ = s;
}
}
/// @return {tok/BU} The token-quantity of an ERC20 token in the basket.
// Returns 0 if erc20 is not registered, disabled, or not in the basket
// Returns FIX_MAX (in lieu of +infinity) if Collateral.refPerTok() is 0.
// Otherwise returns (token's basket.refAmts / token's Collateral.refPerTok())
function quantity(IERC20 erc20) public view returns (uint192) {
try assetRegistry.toColl(erc20) returns (ICollateral coll) {
if (coll.status() == CollateralStatus.DISABLED) return FIX_ZERO;
uint192 refPerTok = coll.refPerTok(); // {ref/tok}
if (refPerTok > 0) {
// {tok/BU} = {ref/BU} / {ref/tok}
return basket.refAmts[erc20].div(refPerTok, CEIL);
} else {
return FIX_MAX;
}
} catch {
return FIX_ZERO;
}
}
/// Should not revert
/// @return low {UoA/tok} The lower end of the price estimate
/// @return high {UoA/tok} The upper end of the price estimate
// returns sum(quantity(erc20) * price(erc20) for erc20 in basket.erc20s)
function price() external view returns (uint192 low, uint192 high) {
return _price(false);
}
/// Should not revert
/// lowLow should be nonzero when the asset might be worth selling
/// @return lotLow {UoA/tok} The lower end of the lot price estimate
/// @return lotHigh {UoA/tok} The upper end of the lot price estimate
// returns sum(quantity(erc20) * lotPrice(erc20) for erc20 in basket.erc20s)
function lotPrice() external view returns (uint192 lotLow, uint192 lotHigh) {
return _price(true);
}
/// Returns the price of a BU, using the lot prices if `useLotPrice` is true
/// @return low {UoA/tok} The lower end of the lot price estimate
/// @return high {UoA/tok} The upper end of the lot price estimate
function _price(bool useLotPrice) internal view returns (uint192 low, uint192 high) {
uint256 low256;
uint256 high256;
uint256 len = basket.erc20s.length;
for (uint256 i = 0; i < len; ++i) {
uint192 qty = quantity(basket.erc20s[i]);
if (qty == 0) continue;
(uint192 lowP, uint192 highP) = useLotPrice
? assetRegistry.toAsset(basket.erc20s[i]).lotPrice()
: assetRegistry.toAsset(basket.erc20s[i]).price();
low256 += quantityMulPrice(qty, lowP);
high256 += quantityMulPrice(qty, highP);
}
low = low256 >= FIX_MAX ? FIX_MAX : uint192(low256);
high = high256 >= FIX_MAX ? FIX_MAX : uint192(high256);
}
/// Multiply quantity by price, rounding up to FIX_MAX and down to 0
/// @param qty {tok/BU}
/// @param p {UoA/tok}
function quantityMulPrice(uint192 qty, uint192 p) internal pure returns (uint192) {
// untestable:
// qty will never = 0 here because of the check in _price()
if (qty == 0 || p == 0) return 0;
// untestable:
// qty = FIX_MAX iff p = 0
if (qty == FIX_MAX || p == FIX_MAX) return FIX_MAX;
// return FIX_MAX instead of throwing overflow errors.
unchecked {
// p and mul *are* Fix values, so have 18 decimals (D18)
uint256 rawDelta = uint256(p) * qty; // {D36} = {D18} * {D18}
// if we overflowed *, then return FIX_MAX
if (rawDelta / p != qty) return FIX_MAX;
// add in FIX_HALF for rounding
uint256 shiftDelta = rawDelta + (FIX_ONE / 2);
// untestable (here there be dragons):
// A) shiftDelta = rawDelta + (FIX_ONE / 2)
// shiftDelta overflows if:
// B) shiftDelta = MAX_UINT256 - FIX_ONE/2 + 1
// rawDelta + (FIX_ONE/2) = MAX_UINT256 - FIX_ONE/2 + 1
// p * qty = MAX_UINT256 - FIX_ONE + 1
// therefore shiftDelta overflows if:
// C) p = (MAX_UINT256 - FIX_ONE + 1) / qty
// MAX_UINT256 ~= 1e77 , FIX_MAX ~= 6e57 (6e20 difference in magnitude)
// qty <= 1e21 (MAX_TARGET_AMT)
// qty must be between 1e19 & 1e20 in order for p in (C) to be uint192,
// but qty would have to be < 1e18 in order for (A) to overflow
if (shiftDelta < rawDelta) return FIX_MAX;
// return _div(rawDelta, FIX_ONE, ROUND)
return uint192(shiftDelta / FIX_ONE); // {D18} = {D36} / {D18}
}
}
/// Return the current reference basket
/// @return erc20s The erc20s in the reference basket
function basketTokens() external view returns (IERC20[] memory erc20s) {
uint256 len = basket.erc20s.length;
erc20s = new IERC20[](len);
for (uint256 i = 0; i < len; ++i) {
erc20s[i] = basket.erc20s[i];
}
}
/// Return the current issuance/redemption value of `amount` BUs
/// @param amount {BU}
/// @return erc20s The backing collateral erc20s
/// @return quantities {qTok} ERC20 token quantities equal to `amount` BUs
// Returns (erc20s, [quantity(e) * amount {as qTok} for e in erc20s])
function quote(uint192 amount, RoundingMode rounding)
external
view
returns (address[] memory erc20s, uint256[] memory quantities)
{
uint256 length = basket.erc20s.length;
erc20s = new address[](length);
quantities = new uint256[](length);
for (uint256 i = 0; i < length; ++i) {
erc20s[i] = address(basket.erc20s[i]);
// {qTok} = {tok/BU} * {BU} * {tok} * {qTok/tok}
quantities[i] = quantity(basket.erc20s[i]).mul(amount, rounding).shiftl_toUint(
int8(IERC20Metadata(address(basket.erc20s[i])).decimals()),
rounding
);
}
}
/// @return baskets {BU} The balance of basket units held by `account`
/// @dev Returns FIX_ZERO for an empty basket
// Returns:
// 0, if (basket.erc20s is empty) or (disabled is true) or (status() is DISABLED)
// min(e.balanceOf(account) / quantity(e) for e in basket.erc20s if quantity(e) > 0),
function basketsHeldBy(address account) public view returns (uint192 baskets) {
uint256 length = basket.erc20s.length;
if (length == 0 || disabled) return FIX_ZERO;
baskets = FIX_MAX;
for (uint256 i = 0; i < length; ++i) {
ICollateral coll = assetRegistry.toColl(basket.erc20s[i]);
if (coll.status() == CollateralStatus.DISABLED) return FIX_ZERO;
uint192 refPerTok = coll.refPerTok();
// If refPerTok is 0, then we have zero of coll's reference unit.
// We know that basket.refAmts[basket.erc20s[i]] > 0, so we have no baskets.
if (refPerTok == 0) return FIX_ZERO;
// {tok}
uint192 bal = coll.bal(account);
// {tok/BU} = {ref/BU} / {ref/tok}. 0-division averted by condition above.
uint192 q = basket.refAmts[basket.erc20s[i]].div(refPerTok, CEIL);
// {BU} = {tok} / {tok/BU}. q > 0 because q = (n).div(_, CEIL) and n > 0
baskets = fixMin(baskets, bal.div(q));
}
}
/* _switchBasket computes basket' from three inputs:
- the basket configuration (config: BasketConfig)
- the function (isGood: erc20 -> bool), implemented here by goodCollateral()
- the function (targetPerRef: erc20 -> Fix) implemented by the Collateral plugin
==== Definitions ====
We use e:IERC20 to mean any erc20 token address, and tgt:bytes32 to mean any target name
// targetWeight(b, e) is the target-unit weight of token e in basket b
Let targetWeight(b, e) = b.refAmt[e] * targetPerRef(e)
// backups(tgt) is the list of sound backup tokens we plan to use for target `tgt`.
Let backups(tgt) = config.backups[tgt].erc20s
.filter(isGood)
.takeUpTo(config.backups[tgt].max)
Let primeWt(e) = if e in config.erc20s and isGood(e)
then config.targetAmts[e]
else 0
Let backupWt(e) = if e in backups(tgt)
then unsoundPrimeWt(tgt) / len(Backups(tgt))
else 0
Let unsoundPrimeWt(tgt) = sum(config.targetAmts[e]
for e in config.erc20s
where config.targetNames[e] == tgt and !isGood(e))
==== The correctness condition ====
If unsoundPrimeWt(tgt) > 0 and len(backups(tgt)) == 0 for some tgt, then disabled' == true.
Else, disabled' == false and targetWeight(basket', e) == primeWt(e) + backupWt(e) for all e.
==== Higher-level desideratum ====
The resulting total target weights should equal the configured target weight. Formally:
let configTargetWeight(tgt) = sum(config.targetAmts[e]
for e in config.erc20s
where _targetNames[e] == tgt)
let targetWeightSum(b, tgt) = sum(targetWeight(b, e)
for e in config.erc20s
where _targetNames[e] == tgt)
Given all that, if disabled' == false, then for all tgt,
targetWeightSum(basket', tgt) == configTargetWeight(tgt)
==== Usual specs ====
Then, finally, given all that, the effects of _switchBasket() are:
basket' = _newBasket, as defined above
nonce' = nonce + 1
timestamp' = now
*/
// These are effectively local variables of _switchBasket.
// Nothing should use their values from previous transactions.
EnumerableSet.Bytes32Set private _targetNames;
Basket private _newBasket; // Always empty
/// Select and save the next basket, based on the BasketConfig and Collateral statuses
/// (The mutator that actually does all the work in this contract.)
function _switchBasket() private {
disabled = false;
// _targetNames := {}
while (_targetNames.length() > 0) _targetNames.remove(_targetNames.at(0));
// _newBasket := {}
_newBasket.empty();
// _targetNames = set(values(config.targetNames))
// (and this stays true; _targetNames is not touched again in this function)
uint256 basketLength = config.erc20s.length;
for (uint256 i = 0; i < basketLength; ++i) {
_targetNames.add(config.targetNames[config.erc20s[i]]);
}
uint256 targetsLength = _targetNames.length();
// "good" collateral is collateral with any status() other than DISABLED
// goodWeights and totalWeights are in index-correspondence with _targetNames
// As such, they're each interepreted as a map from target name -> target weight
// {target/BU} total target weight of good, prime collateral with target i
// goodWeights := {}
uint192[] memory goodWeights = new uint192[](targetsLength);
// {target/BU} total target weight of all prime collateral with target i
// totalWeights := {}
uint192[] memory totalWeights = new uint192[](targetsLength);
// For each prime collateral token:
for (uint256 i = 0; i < basketLength; ++i) {
IERC20 erc20 = config.erc20s[i];
// Find collateral's targetName index
uint256 targetIndex;
for (targetIndex = 0; targetIndex < targetsLength; ++targetIndex) {
if (_targetNames.at(targetIndex) == config.targetNames[erc20]) break;
}
assert(targetIndex < targetsLength);
// now, _targetNames[targetIndex] == config.targetNames[config.erc20s[i]]
// Set basket weights for good, prime collateral,
// and accumulate the values of goodWeights and targetWeights
uint192 targetWeight = config.targetAmts[erc20];
totalWeights[targetIndex] = totalWeights[targetIndex].plus(targetWeight);
if (goodCollateral(config.targetNames[erc20], erc20) && targetWeight.gt(FIX_ZERO)) {
goodWeights[targetIndex] = goodWeights[targetIndex].plus(targetWeight);
_newBasket.add(
erc20,
targetWeight.div(assetRegistry.toColl(erc20).targetPerRef(), CEIL)
);
// this div is safe: targetPerRef() > 0: goodCollateral check
}
}
// Analysis: at this point:
// for all tgt in target names,
// totalWeights(tgt)
// = sum(config.targetAmts[e] for e in config.erc20s where _targetNames[e] == tgt), and
// goodWeights(tgt)
// = sum(primeWt(e) for e in config.erc20s where _targetNames[e] == tgt)
// for all e in config.erc20s,
// targetWeight(_newBasket, e)
// = sum(primeWt(e) if goodCollateral(e), else 0)
// For each tgt in target names, if we still need more weight for tgt then try to add the
// backup basket for tgt to make up that weight:
for (uint256 i = 0; i < targetsLength; ++i) {
if (totalWeights[i].lte(goodWeights[i])) continue; // Don't need any backup weight
// "tgt" = _targetNames[i]
// Now, unsoundPrimeWt(tgt) > 0
uint256 size = 0; // backup basket size
BackupConfig storage backup = config.backups[_targetNames.at(i)];
// Find the backup basket size: min(backup.max, # of good backup collateral)
uint256 backupLength = backup.erc20s.length;
for (uint256 j = 0; j < backupLength && size < backup.max; ++j) {
if (goodCollateral(_targetNames.at(i), backup.erc20s[j])) size++;
}
// Now, size = len(backups(tgt)). Do the disable check:
// Remove bad collateral and mark basket disabled. Pause most protocol functions
if (size == 0) disabled = true;
// Set backup basket weights...
uint256 assigned = 0;
// needed = unsoundPrimeWt(tgt)
uint192 needed = totalWeights[i].minus(goodWeights[i]);
// Loop: for erc20 in backups(tgt)...
for (uint256 j = 0; j < backupLength && assigned < size; ++j) {
IERC20 erc20 = backup.erc20s[j];
if (goodCollateral(_targetNames.at(i), erc20)) {
// Across this .add(), targetWeight(_newBasket',erc20)
// = targetWeight(_newBasket,erc20) + unsoundPrimeWt(tgt) / len(backups(tgt))
_newBasket.add(
erc20,
needed.div(assetRegistry.toColl(erc20).targetPerRef().mulu(size), CEIL)
// this div is safe: targetPerRef > 0: goodCollateral check
);
assigned++;
}
}
// Here, targetWeight(_newBasket, e) = primeWt(e) + backupWt(e) for all e targeting tgt
}
// Now we've looped through all values of tgt, so for all e,
// targetWeight(_newBasket, e) = primeWt(e) + backupWt(e)
// Notice if basket is actually empty
uint256 newBasketLength = _newBasket.erc20s.length;
if (newBasketLength == 0) disabled = true;
// Update the basket if it's not disabled
if (!disabled) {
basket.setFrom(_newBasket);
nonce += 1;
timestamp = uint48(block.timestamp);
}
// Keep records, emit event
basketLength = basket.erc20s.length;
uint192[] memory refAmts = new uint192[](basketLength);
for (uint256 i = 0; i < basketLength; ++i) {
refAmts[i] = basket.refAmts[basket.erc20s[i]];
}
emit BasketSet(nonce, basket.erc20s, refAmts, disabled);
}
/// Require that erc20s is a valid collateral array
function requireValidCollArray(IERC20[] calldata erc20s) internal view {
IERC20 zero = IERC20(address(0));
for (uint256 i = 0; i < erc20s.length; i++) {
require(erc20s[i] != rsr, "RSR is not valid collateral");
require(erc20s[i] != IERC20(address(rToken)), "RToken is not valid collateral");
require(erc20s[i] != IERC20(address(stRSR)), "stRSR is not valid collateral");
require(erc20s[i] != zero, "address zero is not valid collateral");
}
require(ArrayLib.allUnique(erc20s), "contains duplicates");
}
/// Good collateral is registered, collateral, not DISABLED, has the expected targetName,
/// has nonzero targetPerRef() and refPerTok(), and is not a system token or 0 addr
function goodCollateral(bytes32 targetName, IERC20 erc20) private view returns (bool) {
// untestable:
// All calls to goodCollateral pass an erc20 from the config or the backup.
// Both setPrimeBasket and setBackupConfig must pass a call to requireValidCollArray,
// which runs the 4 checks below.
if (erc20 == IERC20(address(0))) return false;
if (erc20 == rsr) return false;
if (erc20 == IERC20(address(rToken))) return false;
if (erc20 == IERC20(address(stRSR))) return false;
try assetRegistry.toColl(erc20) returns (ICollateral coll) {
return
targetName == coll.targetName() &&
coll.status() != CollateralStatus.DISABLED &&
coll.refPerTok() > 0 &&
coll.targetPerRef() > 0;
} catch {
return false;
}
}
// ==== FacadeRead views ====
// Not used in-protocol
/// Getter pt1 for `config` struct variable
/// @dev Indices are shared across return values
/// @return erc20s The erc20s in the prime basket
/// @return targetNames The bytes32 name identifier of the target unit, per ERC20
/// @return targetAmts {target/BU} The amount of the target unit in the basket, per ERC20
function getPrimeBasket()
external
view
returns (
IERC20[] memory erc20s,
bytes32[] memory targetNames,
uint192[] memory targetAmts
)
{
erc20s = new IERC20[](basket.erc20s.length);
targetNames = new bytes32[](erc20s.length);
targetAmts = new uint192[](erc20s.length);
for (uint256 i = 0; i < erc20s.length; ++i) {
erc20s[i] = config.erc20s[i];
targetNames[i] = config.targetNames[erc20s[i]];
targetAmts[i] = config.targetAmts[erc20s[i]];
}
}
/// Getter pt2 for `config` struct variable
/// @param targetName The name of the target unit to lookup the backup for
/// @return erc20s The backup erc20s for the target unit, in order of most to least desirable
/// @return max The maximum number of tokens from the array to use at a single time
function getBackupConfig(bytes32 targetName)
external
view
returns (IERC20[] memory erc20s, uint256 max)
{
BackupConfig storage backup = config.backups[targetName];
erc20s = new IERC20[](backup.erc20s.length);
for (uint256 i = 0; i < erc20s.length; ++i) {
erc20s[i] = backup.erc20s[i];
}
max = backup.max;
}
// ==== Storage Gap ====
/**
* @dev This empty reserved space is put in place to allow future versions to add new
* variables without shifting down storage in the inheritance chain.
* See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps
*/
uint256[42] private __gap;
}