/
BasketLib.sol
308 lines (262 loc) · 12.9 KB
/
BasketLib.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
// SPDX-License-Identifier: BlueOak-1.0.0
pragma solidity 0.8.17;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/utils/structs/EnumerableSet.sol";
import "../../interfaces/IAssetRegistry.sol";
import "../../libraries/Fixed.sol";
// A "valid collateral array" is a IERC20[] array without rtoken/rsr/stRSR/zero address/duplicates
// 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.
//
// 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
* @notice A helper library that implements a `nextBasket()` function for selecting a reference
* basket from the current basket config in combination with collateral statuses/exchange rates.
*/
library BasketLibP1 {
using BasketLibP1 for Basket;
using EnumerableSet for EnumerableSet.AddressSet;
using EnumerableSet for EnumerableSet.Bytes32Set;
using FixLib for uint192;
// === Basket Algebra ===
/// 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);
}
}
// === Basket Selection ===
/* nextBasket() 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 return false.
Else, return true 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 nextBasket() returns true, then for all tgt,
targetWeightSum(basket', tgt) == configTargetWeight(tgt)
*/
/// Select next reference basket from basket config
/// Works in-place on `newBasket`
/// @param targetNames Scratch space for computation; initial value unused
/// @param newBasket Scratch space for computation; initial value unused
/// @param config The current basket configuration
/// @return success result; i.e newBasket can be expected to contain a valid reference basket
function nextBasket(
Basket storage newBasket,
EnumerableSet.Bytes32Set storage targetNames,
BasketConfig storage config,
IAssetRegistry assetRegistry
) external returns (bool) {
// 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)
for (uint256 i = 0; i < config.erc20s.length; ++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 < config.erc20s.length; ++i) {
// Find collateral's targetName index
uint256 targetIndex;
for (targetIndex = 0; targetIndex < targetsLength; ++targetIndex) {
if (targetNames.at(targetIndex) == config.targetNames[config.erc20s[i]]) break;
}
assert(targetIndex < targetsLength);
// now, targetNames[targetIndex] == config.targetNames[erc20]
// Set basket weights for good, prime collateral,
// and accumulate the values of goodWeights and targetWeights
uint192 targetWeight = config.targetAmts[config.erc20s[i]];
totalWeights[targetIndex] = totalWeights[targetIndex].plus(targetWeight);
if (
goodCollateral(
config.targetNames[config.erc20s[i]],
config.erc20s[i],
assetRegistry
) && targetWeight.gt(FIX_ZERO)
) {
goodWeights[targetIndex] = goodWeights[targetIndex].plus(targetWeight);
newBasket.add(
config.erc20s[i],
targetWeight.div(
// this div is safe: targetPerRef() > 0: goodCollateral check
assetRegistry.toColl(config.erc20s[i]).targetPerRef(),
CEIL
)
);
}
}
// 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)
for (uint256 j = 0; j < backup.erc20s.length && size < backup.max; ++j) {
if (goodCollateral(targetNames.at(i), backup.erc20s[j], assetRegistry)) size++;
}
// Now, size = len(backups(tgt)). If empty, fail.
if (size == 0) return false;
// Set backup basket weights...
uint256 assigned = 0;
// Loop: for erc20 in backups(tgt)...
for (uint256 j = 0; j < backup.erc20s.length && assigned < size; ++j) {
if (goodCollateral(targetNames.at(i), backup.erc20s[j], assetRegistry)) {
// Across this .add(), targetWeight(newBasket',erc20)
// = targetWeight(newBasket,erc20) + unsoundPrimeWt(tgt) / len(backups(tgt))
newBasket.add(
backup.erc20s[j],
totalWeights[i].minus(goodWeights[i]).div(
// this div is safe: targetPerRef > 0: goodCollateral check
assetRegistry.toColl(backup.erc20s[j]).targetPerRef().mulu(size),
CEIL
)
);
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)
return newBasket.erc20s.length > 0;
}
// === Private ===
/// Good collateral is registered, collateral, SOUND, has the expected targetName,
/// has nonzero targetPerRef() and refPerTok(), and is not a system token or 0 addr
function goodCollateral(
bytes32 targetName,
IERC20 erc20,
IAssetRegistry assetRegistry
) private view returns (bool) {
// untestable:
// ERC20 is not address(0), validated when setting prime/backup baskets
if (address(erc20) == address(0)) return false;
// P1 gas optimization
// We do not need to check that the ERC20 is not a system token
// BasketHandlerP1.requireValidCollArray() has been run on all ERC20s already
try assetRegistry.toColl(erc20) returns (ICollateral coll) {
return
targetName == coll.targetName() &&
coll.status() == CollateralStatus.SOUND &&
coll.refPerTok() > 0 &&
coll.targetPerRef() > 0;
} catch {
return false;
}
}
}