-
Notifications
You must be signed in to change notification settings - Fork 0
/
VRCauldron.sol
487 lines (428 loc) · 17.5 KB
/
VRCauldron.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
// SPDX-License-Identifier: BUSL-1.1
pragma solidity >=0.8.13;
import "../constants/Constants.sol";
import "../interfaces/IOracle.sol";
import "../interfaces/DataTypes.sol";
import "@yield-protocol/utils-v2/src/access/AccessControl.sol";
import "@yield-protocol/utils-v2/src/utils/Math.sol";
import "@yield-protocol/utils-v2/src/utils/Cast.sol";
import { UUPSUpgradeable } from "@openzeppelin/contracts/proxy/utils/UUPSUpgradeable.sol";
library CauldronMath {
/// @dev Add a number (which might be negative) to a positive, and revert if the result is negative.
function add(uint128 x, int128 y) internal pure returns (uint128 z) {
require (y > 0 || x >= uint128(-y), "Result below zero");
z = y > 0 ? x + uint128(y) : x - uint128(-y);
}
}
contract VRCauldron is UUPSUpgradeable, AccessControl, Constants {
using CauldronMath for uint128;
using Math for uint256;
using Cast for uint128;
using Cast for int128;
using Cast for uint256;
event AssetAdded(bytes6 indexed assetId, address indexed asset);
event BaseAdded(bytes6 indexed baseId);
event IlkAdded(bytes6 indexed baseId, bytes6 indexed ilkId);
event SpotOracleAdded(
bytes6 indexed baseId,
bytes6 indexed ilkId,
address indexed oracle,
uint32 ratio
);
event RateOracleAdded(bytes6 indexed baseId, address indexed oracle);
event DebtLimitsSet(
bytes6 indexed baseId,
bytes6 indexed ilkId,
uint96 max,
uint24 min,
uint8 dec
);
event VaultBuilt(
bytes12 indexed vaultId,
address indexed owner,
bytes6 indexed baseId,
bytes6 ilkId
);
event VaultTweaked(
bytes12 indexed vaultId,
bytes6 indexed baseId,
bytes6 indexed ilkId
);
event VaultDestroyed(bytes12 indexed vaultId);
event VaultGiven(bytes12 indexed vaultId, address indexed receiver);
event VaultPoured(
bytes12 indexed vaultId,
bytes6 indexed baseId,
bytes6 indexed ilkId,
int128 ink,
int128 art
);
event VaultStirred(
bytes12 indexed from,
bytes12 indexed to,
uint128 ink,
uint128 art
);
// ==== Upgradability data ====
bool public initialized;
// ==== Configuration data ====
mapping(bytes6 => address) public assets; // Underlyings and collaterals available in Cauldron. 12 bytes still free.
mapping(bytes6 => bool) public bases; // Assets available in Cauldron for borrowing.
mapping(bytes6 => mapping(bytes6 => bool)) public ilks; // [baseId][assetId] Assets that are approved as collateral for the base
mapping(bytes6 => IOracle) public rateOracles; // Variable rate oracle for an underlying
mapping(bytes6 => mapping(bytes6 => DataTypes.SpotOracle)) public spotOracles; // [assetId][assetId] Spot price oracles
// ==== Protocol data ====
mapping(bytes6 => mapping(bytes6 => DataTypes.Debt)) public debt; // [baseId][ilkId] Max and sum of debt per underlying and collateral.
// ==== User data ====
mapping(bytes12 => VRDataTypes.Vault) public vaults; // An user can own one or more Vaults, each one with a bytes12 identifier
mapping(bytes12 => DataTypes.Balances) public balances; // Both debt and assets
constructor() {
// See https://medium.com/immunefi/wormhole-uninitialized-proxy-bugfix-review-90250c41a43a
initialized = true; // Lock the implementation contract
}
// ==== Upgradability ====
/// @dev Give the ROOT role and create a LOCK role with itself as the admin role and no members.
/// Calling setRoleAdmin(msg.sig, LOCK) means no one can grant that msg.sig role anymore.
function initialize (address root_) public {
require(!initialized, "Already initialized");
initialized = true; // On an uninitialized contract, no governance functions can be executed, because no one has permission to do so
_grantRole(ROOT, root_); // Grant ROOT
_setRoleAdmin(LOCK, LOCK); // Create the LOCK role by setting itself as its own admin, creating an independent role tree
}
/// @dev Allow to set a new implementation
function _authorizeUpgrade(address newImplementation) internal override auth {}
// ==== Administration ====
/// @dev Add a new Asset.
/// @notice Only ERC20 support has been tested thoroughly. Any other tokens would have to be tested.
function addAsset(bytes6 assetId, address asset) external auth {
require(assetId != bytes6(0), "Asset id is zero");
require(assets[assetId] == address(0), "Id already used");
assets[assetId] = asset;
emit AssetAdded(assetId, asset);
}
/// @dev Set the maximum and minimum debt for an underlying and ilk pair. Can be reset.
function setDebtLimits(
bytes6 baseId,
bytes6 ilkId,
uint96 max,
uint24 min,
uint8 dec
) external auth {
require(assets[baseId] != address(0), "Base not found");
require(assets[ilkId] != address(0), "Ilk not found");
DataTypes.Debt memory debt_ = debt[baseId][ilkId];
debt_.max = max;
debt_.min = min;
debt_.dec = dec;
debt[baseId][ilkId] = debt_;
emit DebtLimitsSet(baseId, ilkId, max, min, dec);
}
/// @dev Set a rate oracle. Can be reset.
function setRateOracle(bytes6 baseId, IOracle oracle) external auth {
require(assets[baseId] != address(0), "Base not found");
rateOracles[baseId] = oracle;
emit RateOracleAdded(baseId, address(oracle));
}
/// @dev Set a spot oracle and its collateralization ratio. Can be reset.
function setSpotOracle(
bytes6 baseId,
bytes6 ilkId,
IOracle oracle,
uint32 ratio
) external auth {
require(assets[baseId] != address(0), "Base not found");
require(assets[ilkId] != address(0), "Ilk not found");
spotOracles[baseId][ilkId] = DataTypes.SpotOracle({
oracle: oracle,
ratio: ratio // With 6 decimals. 1000000 == 100%
}); // Allows to replace an existing oracle.
emit SpotOracleAdded(baseId, ilkId, address(oracle), ratio);
}
/// @dev Add a new base
function addBase(bytes6 baseId) external auth {
address base = assets[baseId];
require(base != address(0), "Base not found");
require(
rateOracles[baseId] != IOracle(address(0)),
"Rate oracle not found"
);
bases[baseId] = true;
emit BaseAdded(baseId);
}
/// @dev Add a new Ilk (approve an asset as collateral for a base).
function addIlks(bytes6 baseId, bytes6[] calldata ilkIds) external auth {
require(bases[baseId], "Base not found");
for (uint256 i; i < ilkIds.length; i++) {
require(
spotOracles[baseId][ilkIds[i]].oracle != IOracle(address(0)),
"Spot oracle not found"
);
ilks[baseId][ilkIds[i]] = true;
emit IlkAdded(baseId, ilkIds[i]);
}
}
// ==== Vault management ====
/// @dev Create a new vault, linked to a base and a collateral
function build(
address owner,
bytes12 vaultId,
bytes6 baseId,
bytes6 ilkId
) external auth returns (VRDataTypes.Vault memory vault) {
require(vaultId != bytes12(0), "Vault id is zero");
require(baseId != bytes6(0), "Base id is zero");
require(ilkId != bytes6(0), "Ilk id is zero");
require(vaults[vaultId].baseId == bytes6(0), "Vault already exists"); // Base can't take bytes6(0) as their id
require(ilks[baseId][ilkId] == true, "Ilk not added to base");
vault = VRDataTypes.Vault({owner: owner, baseId: baseId, ilkId: ilkId});
vaults[vaultId] = vault;
emit VaultBuilt(vaultId, owner, baseId, ilkId);
}
/// @dev Destroy an empty vault. Used to recover gas costs.
function destroy(bytes12 vaultId) external auth {
require(vaults[vaultId].baseId != bytes6(0), "Vault doesn't exist"); // Bases can't take bytes6(0) as their id
DataTypes.Balances memory balances_ = balances[vaultId];
require(balances_.art == 0 && balances_.ink == 0, "Only empty vaults");
delete vaults[vaultId];
emit VaultDestroyed(vaultId);
}
/// @dev Change a vault base and/or collateral types.
/// We can change the base if there is no debt, or assets if there are no assets
function _tweak(
bytes12 vaultId,
bytes6 baseId,
bytes6 ilkId
) internal returns (VRDataTypes.Vault memory vault) {
require(baseId != bytes6(0), "Base id is zero");
require(ilkId != bytes6(0), "Ilk id is zero");
require(ilks[baseId][ilkId] == true, "Ilk not added to base");
vault = vaults[vaultId];
require(vault.baseId != bytes6(0), "Vault doesn't exist"); // Bases can't take bytes6(0) as their id
DataTypes.Balances memory balances_ = balances[vaultId];
if (baseId != vault.baseId) {
require(balances_.art == 0, "Only with no debt");
vault.baseId = baseId;
}
if (ilkId != vault.ilkId) {
require(balances_.ink == 0, "Only with no collateral");
vault.ilkId = ilkId;
}
vaults[vaultId] = vault;
emit VaultTweaked(vaultId, vault.baseId, vault.ilkId);
}
/// @dev Change a vault base and/or collateral types.
/// We can change the base if there is no debt, or assets if there are no assets
function tweak(
bytes12 vaultId,
bytes6 baseId,
bytes6 ilkId
) external auth returns (VRDataTypes.Vault memory vault) {
vault = _tweak(vaultId, baseId, ilkId);
}
/// @dev Transfer a vault to another user.
function _give(bytes12 vaultId, address receiver)
internal
returns (VRDataTypes.Vault memory vault)
{
require(vaultId != bytes12(0), "Vault id is zero");
require(vaults[vaultId].baseId != bytes6(0), "Vault doesn't exist"); // Base can't take bytes6(0) as their id
vault = vaults[vaultId];
vault.owner = receiver;
vaults[vaultId] = vault;
emit VaultGiven(vaultId, receiver);
}
/// @dev Transfer a vault to another user.
function give(bytes12 vaultId, address receiver)
external
auth
returns (VRDataTypes.Vault memory vault)
{
vault = _give(vaultId, receiver);
}
// ==== Asset and debt management ====
function vaultData(bytes12 vaultId)
internal
view
returns (
VRDataTypes.Vault memory vault_,
DataTypes.Balances memory balances_
)
{
vault_ = vaults[vaultId];
require(vault_.baseId != bytes6(0), "Vault not found");
balances_ = balances[vaultId];
}
/// @dev Convert a base amount to debt terms.
/// @notice Think about rounding up if using, since we are dividing.
function debtFromBase(bytes6 baseId, uint128 base)
external
returns (uint128 art)
{
art = _debtFromBase(baseId, base);
}
/// @dev Convert a base amount to debt terms.
/// @notice Think about rounding up if using, since we are dividing.
function _debtFromBase(bytes6 baseId, uint128 base)
internal
returns (uint128 art)
{
(uint256 rate, ) = rateOracles[baseId].get(baseId, RATE, 0); // The value returned is an accumulator, it doesn't need an input amount
art = uint256(base).wdivup(rate).u128();
}
/// @dev Convert a debt amount for a to base terms
function debtToBase(bytes6 baseId, uint128 art)
external
returns (uint128 base)
{
base = _debtToBase(baseId, art);
}
/// @dev Convert a debt amount for a to base terms
function _debtToBase(bytes6 baseId, uint128 art)
internal
returns (uint128 base)
{
(uint256 rate, ) = rateOracles[baseId].get(baseId, RATE, 0); // The value returned is an accumulator, it doesn't need an input amount
base = uint256(art).wmul(rate).u128();
}
/// @dev Move collateral and debt between vaults.
function stir(
bytes12 from,
bytes12 to,
uint128 ink,
uint128 art
)
external
auth
returns (DataTypes.Balances memory, DataTypes.Balances memory)
{
require(from != to, "Identical vaults");
(
VRDataTypes.Vault memory vaultFrom,
DataTypes.Balances memory balancesFrom
) = vaultData(from);
(
VRDataTypes.Vault memory vaultTo,
DataTypes.Balances memory balancesTo
) = vaultData(to);
if (ink > 0) {
require(vaultFrom.ilkId == vaultTo.ilkId, "Different collateral");
balancesFrom.ink -= ink;
balancesTo.ink += ink;
}
if (art > 0) {
require(vaultFrom.baseId == vaultTo.baseId, "Different base");
balancesFrom.art -= art;
balancesTo.art += art;
}
balances[from] = balancesFrom;
balances[to] = balancesTo;
if (ink > 0)
require(
_level(vaultFrom, balancesFrom) >= 0,
"Undercollateralized at origin"
);
if (art > 0)
require(
_level(vaultTo, balancesTo) >= 0,
"Undercollateralized at destination"
);
emit VaultStirred(from, to, ink, art);
return (balancesFrom, balancesTo);
}
/// @dev Add collateral and rate from vault, pull assets from and push rateed asset to user
/// Or, repay to vault and remove collateral, pull rateed asset from and push assets to user
function _pour(
bytes12 vaultId,
VRDataTypes.Vault memory vault_,
DataTypes.Balances memory balances_,
int128 ink,
int128 art
) internal returns (DataTypes.Balances memory) {
// For now, the collateralization checks are done outside to allow for underwater operation. That might change.
if (ink != 0) {
balances_.ink = balances_.ink.add(ink);
}
// Modify vault and global debt records. If debt increases, check global limit.
if (art != 0) {
DataTypes.Debt memory debt_ = debt[vault_.baseId][vault_.ilkId];
balances_.art = balances_.art.add(art);
debt_.sum = debt_.sum.add(art);
uint128 dust = debt_.min * uint128(10)**debt_.dec;
uint128 line = debt_.max * uint128(10)**debt_.dec;
require(
balances_.art == 0 || balances_.art >= dust,
"Min debt not reached"
);
if (art > 0) require(debt_.sum <= line, "Max debt exceeded");
debt[vault_.baseId][vault_.ilkId] = debt_;
}
balances[vaultId] = balances_;
emit VaultPoured(vaultId, vault_.baseId, vault_.ilkId, ink, art);
return balances_;
}
/// @dev Manipulate a vault, ensuring it is collateralized afterwards.
/// To be used by debt management contracts.
function pour(
bytes12 vaultId,
int128 ink,
int128 base
) external virtual auth returns (DataTypes.Balances memory) {
(
VRDataTypes.Vault memory vault_,
DataTypes.Balances memory balances_
) = vaultData(vaultId);
// Normalize the base amount to debt terms
int128 art = base;
if (base != 0)
art = base > 0
? _debtFromBase(vault_.baseId, base.u128()).i128()
: -_debtFromBase(vault_.baseId, (-base).u128()).i128();
balances_ = _pour(vaultId, vault_, balances_, ink, art);
if (balances_.art > 0 && (ink < 0 || art > 0))
// If there is debt and we are less safe
require(_level(vault_, balances_) >= 0, "Undercollateralized");
return balances_;
}
/// @dev Reduce debt and collateral from a vault, ignoring collateralization checks.
/// To be used by liquidation engines.
function slurp(
bytes12 vaultId,
uint128 ink,
uint128 base
) external auth returns (DataTypes.Balances memory) {
(
VRDataTypes.Vault memory vault_,
DataTypes.Balances memory balances_
) = vaultData(vaultId);
// Normalize the base amount to debt terms
int128 art = _debtFromBase(vault_.baseId, base).i128();
balances_ = _pour(vaultId, vault_, balances_, -(ink.i128()), -art);
return balances_;
}
// ==== Accounting ====
/// @dev Return the collateralization level of a vault. It will be negative if undercollateralized.
function level(bytes12 vaultId) external returns (int256) {
(
VRDataTypes.Vault memory vault_,
DataTypes.Balances memory balances_
) = vaultData(vaultId);
return _level(vault_, balances_);
}
/// @dev Return the collateralization level of a vault. It will be negative if undercollateralized.
function _level(
VRDataTypes.Vault memory vault_,
DataTypes.Balances memory balances_
) internal returns (int256) {
DataTypes.SpotOracle memory spotOracle_ = spotOracles[vault_.baseId][
vault_.ilkId
];
uint256 ratio = uint256(spotOracle_.ratio) * 1e12; // Normalized to 18 decimals
(uint256 inkValue, ) = spotOracle_.oracle.get(
vault_.ilkId,
vault_.baseId,
balances_.ink
); // ink * spot
uint256 baseValue = _debtToBase(vault_.baseId, balances_.art); // art * rate
return inkValue.i256() - baseValue.wmul(ratio).i256();
}
}