/
WhitelistedCauldronV3.sol
633 lines (542 loc) · 28.6 KB
/
WhitelistedCauldronV3.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
// SPDX-License-Identifier: UNLICENSED
// Cauldron
// ( ( (
// )\ ) ( )\ )\ ) (
// (((_) ( /( ))\ ((_)(()/( )( ( (
// )\___ )(_)) /((_) _ ((_))(()\ )\ )\ )
// ((/ __|((_)_ (_))( | | _| | ((_) ((_) _(_/(
// | (__ / _` || || || |/ _` | | '_|/ _ \| ' \))
// \___|\__,_| \_,_||_|\__,_| |_| \___/|_||_|
// Copyright (c) 2021 BoringCrypto - All rights reserved
// Twitter: @Boring_Crypto
// Special thanks to:
// @0xKeno - for all his invaluable contributions
// @burger_crypto - for the idea of trying to let the LPs benefit from liquidations
pragma solidity 0.6.12;
pragma experimental ABIEncoderV2;
import "@boringcrypto/boring-solidity/contracts/libraries/BoringMath.sol";
import "@boringcrypto/boring-solidity/contracts/BoringOwnable.sol";
import "@boringcrypto/boring-solidity/contracts/ERC20.sol";
import "@boringcrypto/boring-solidity/contracts/interfaces/IMasterContract.sol";
import "@boringcrypto/boring-solidity/contracts/libraries/BoringRebase.sol";
import "@boringcrypto/boring-solidity/contracts/libraries/BoringERC20.sol";
import "@sushiswap/bentobox-sdk/contracts/IBentoBoxV1.sol";
import "./MagicInternetMoney.sol";
import "./interfaces/IOracle.sol";
import "./interfaces/ISwapper.sol";
import "./interfaces/IWhitelister.sol";
// solhint-disable avoid-low-level-calls
// solhint-disable no-inline-assembly
/// @title Cauldron
/// @dev This contract allows contract calls to any contract (except BentoBox)
/// from arbitrary callers thus, don't trust calls from this contract in any circumstances.
contract WhitelistedCauldronV3 is BoringOwnable, IMasterContract {
using BoringMath for uint256;
using BoringMath128 for uint128;
using RebaseLibrary for Rebase;
using BoringERC20 for IERC20;
event LogExchangeRate(uint256 rate);
event LogAccrue(uint128 accruedAmount);
event LogAddCollateral(address indexed from, address indexed to, uint256 share);
event LogRemoveCollateral(address indexed from, address indexed to, uint256 share);
event LogBorrow(address indexed from, address indexed to, uint256 amount, uint256 part);
event LogRepay(address indexed from, address indexed to, uint256 amount, uint256 part);
event LogFeeTo(address indexed newFeeTo);
event LogWithdrawFees(address indexed feeTo, uint256 feesEarnedFraction);
event LogInterestChange(uint64 oldInterestRate, uint64 newInterestRate);
event LogChangeBorrowLimit(uint128 newLimit, uint128 perAddressPart);
event LogChangeWhitelister(IWhitelister indexed newWhiteLister);
event LogLiquidation(
address indexed from,
address indexed user,
address indexed to,
uint256 collateralShare,
uint256 borrowAmount,
uint256 borrowPart
);
// Immutables (for MasterContract and all clones)
IBentoBoxV1 public immutable bentoBox;
WhitelistedCauldronV3 public immutable masterContract;
IERC20 public immutable magicInternetMoney;
// MasterContract variables
address public feeTo;
// Per clone variables
// Clone init settings
IERC20 public collateral;
IOracle public oracle;
bytes public oracleData;
struct BorrowCap {
uint128 total;
uint128 borrowPartPerAddress;
}
BorrowCap public borrowLimit;
// whitelister
IWhitelister public whitelister;
// Total amounts
uint256 public totalCollateralShare; // Total collateral supplied
Rebase public totalBorrow; // elastic = Total token amount to be repayed by borrowers, base = Total parts of the debt held by borrowers
// User balances
mapping(address => uint256) public userCollateralShare;
mapping(address => uint256) public userBorrowPart;
/// @notice Exchange and interest rate tracking.
/// This is 'cached' here because calls to Oracles can be very expensive.
uint256 public exchangeRate;
struct AccrueInfo {
uint64 lastAccrued;
uint128 feesEarned;
uint64 INTEREST_PER_SECOND;
}
AccrueInfo public accrueInfo;
uint64 private constant ONE_PERCENT_RATE = 317097920;
/// @notice tracking of last interest update
uint256 private lastInterestUpdate;
// Settings
uint256 public COLLATERIZATION_RATE;
uint256 private constant COLLATERIZATION_RATE_PRECISION = 1e5; // Must be less than EXCHANGE_RATE_PRECISION (due to optimization in math)
uint256 private constant EXCHANGE_RATE_PRECISION = 1e18;
uint256 public LIQUIDATION_MULTIPLIER;
uint256 private constant LIQUIDATION_MULTIPLIER_PRECISION = 1e5;
uint256 public BORROW_OPENING_FEE;
uint256 private constant BORROW_OPENING_FEE_PRECISION = 1e5;
uint256 private constant DISTRIBUTION_PART = 10;
uint256 private constant DISTRIBUTION_PRECISION = 100;
modifier onlyMasterContractOwner() {
require(msg.sender == masterContract.owner(), "Caller is not the owner");
_;
}
/// @notice The constructor is only used for the initial master contract. Subsequent clones are initialised via `init`.
constructor(IBentoBoxV1 bentoBox_, IERC20 magicInternetMoney_) public {
bentoBox = bentoBox_;
magicInternetMoney = magicInternetMoney_;
masterContract = this;
}
/// @notice Serves as the constructor for clones, as clones can't have a regular constructor
/// @dev `data` is abi encoded in the format: (IERC20 collateral, IERC20 asset, IOracle oracle, bytes oracleData)
function init(bytes calldata data) public payable override {
require(address(collateral) == address(0), "Cauldron: already initialized");
(collateral, oracle, oracleData, accrueInfo.INTEREST_PER_SECOND, LIQUIDATION_MULTIPLIER, COLLATERIZATION_RATE, BORROW_OPENING_FEE) = abi.decode(data, (IERC20, IOracle, bytes, uint64, uint256, uint256, uint256));
borrowLimit = BorrowCap(type(uint128).max, type(uint128).max);
require(address(collateral) != address(0), "Cauldron: bad pair");
(, exchangeRate) = oracle.get(oracleData);
}
/// @notice Accrues the interest on the borrowed tokens and handles the accumulation of fees.
function accrue() public {
AccrueInfo memory _accrueInfo = accrueInfo;
// Number of seconds since accrue was called
uint256 elapsedTime = block.timestamp - _accrueInfo.lastAccrued;
if (elapsedTime == 0) {
return;
}
_accrueInfo.lastAccrued = uint64(block.timestamp);
Rebase memory _totalBorrow = totalBorrow;
if (_totalBorrow.base == 0) {
accrueInfo = _accrueInfo;
return;
}
// Accrue interest
uint128 extraAmount = (uint256(_totalBorrow.elastic).mul(_accrueInfo.INTEREST_PER_SECOND).mul(elapsedTime) / 1e18).to128();
_totalBorrow.elastic = _totalBorrow.elastic.add(extraAmount);
_accrueInfo.feesEarned = _accrueInfo.feesEarned.add(extraAmount);
totalBorrow = _totalBorrow;
accrueInfo = _accrueInfo;
emit LogAccrue(extraAmount);
}
/// @notice Concrete implementation of `isSolvent`. Includes a third parameter to allow caching `exchangeRate`.
/// @param _exchangeRate The exchange rate. Used to cache the `exchangeRate` between calls.
function _isSolvent(address user, uint256 _exchangeRate) internal view returns (bool) {
// accrue must have already been called!
uint256 borrowPart = userBorrowPart[user];
if (borrowPart == 0) return true;
uint256 collateralShare = userCollateralShare[user];
if (collateralShare == 0) return false;
Rebase memory _totalBorrow = totalBorrow;
return
bentoBox.toAmount(
collateral,
collateralShare.mul(EXCHANGE_RATE_PRECISION / COLLATERIZATION_RATE_PRECISION).mul(COLLATERIZATION_RATE),
false
) >=
// Moved exchangeRate here instead of dividing the other side to preserve more precision
borrowPart.mul(_totalBorrow.elastic).mul(_exchangeRate) / _totalBorrow.base;
}
/// @dev Checks if the user is solvent in the closed liquidation case at the end of the function body.
modifier solvent() {
_;
(, uint256 _exchangeRate) = updateExchangeRate();
require(_isSolvent(msg.sender, _exchangeRate), "Cauldron: user insolvent");
}
/// @notice Gets the exchange rate. I.e how much collateral to buy 1e18 asset.
/// This function is supposed to be invoked if needed because Oracle queries can be expensive.
/// @return updated True if `exchangeRate` was updated.
/// @return rate The new exchange rate.
function updateExchangeRate() public returns (bool updated, uint256 rate) {
(updated, rate) = oracle.get(oracleData);
if (updated) {
exchangeRate = rate;
emit LogExchangeRate(rate);
} else {
// Return the old rate if fetching wasn't successful
rate = exchangeRate;
}
}
/// @dev Helper function to move tokens.
/// @param token The ERC-20 token.
/// @param share The amount in shares to add.
/// @param total Grand total amount to deduct from this contract's balance. Only applicable if `skim` is True.
/// Only used for accounting checks.
/// @param skim If True, only does a balance check on this contract.
/// False if tokens from msg.sender in `bentoBox` should be transferred.
function _addTokens(
IERC20 token,
uint256 share,
uint256 total,
bool skim
) internal {
if (skim) {
require(share <= bentoBox.balanceOf(token, address(this)).sub(total), "Cauldron: Skim too much");
} else {
bentoBox.transfer(token, msg.sender, address(this), share);
}
}
/// @notice Adds `collateral` from msg.sender to the account `to`.
/// @param to The receiver of the tokens.
/// @param skim True if the amount should be skimmed from the deposit balance of msg.sender.x
/// False if tokens from msg.sender in `bentoBox` should be transferred.
/// @param share The amount of shares to add for `to`.
function addCollateral(
address to,
bool skim,
uint256 share
) public {
userCollateralShare[to] = userCollateralShare[to].add(share);
uint256 oldTotalCollateralShare = totalCollateralShare;
totalCollateralShare = oldTotalCollateralShare.add(share);
_addTokens(collateral, share, oldTotalCollateralShare, skim);
emit LogAddCollateral(skim ? address(bentoBox) : msg.sender, to, share);
}
/// @dev Concrete implementation of `removeCollateral`.
function _removeCollateral(address to, uint256 share) internal {
userCollateralShare[msg.sender] = userCollateralShare[msg.sender].sub(share);
totalCollateralShare = totalCollateralShare.sub(share);
emit LogRemoveCollateral(msg.sender, to, share);
bentoBox.transfer(collateral, address(this), to, share);
}
/// @notice Removes `share` amount of collateral and transfers it to `to`.
/// @param to The receiver of the shares.
/// @param share Amount of shares to remove.
function removeCollateral(address to, uint256 share) public solvent {
// accrue must be called because we check solvency
accrue();
_removeCollateral(to, share);
}
/// @dev Concrete implementation of `borrow`.
function _borrow(address to, uint256 amount) internal returns (uint256 part, uint256 share) {
uint256 feeAmount = amount.mul(BORROW_OPENING_FEE) / BORROW_OPENING_FEE_PRECISION; // A flat % fee is charged for any borrow
(totalBorrow, part) = totalBorrow.add(amount.add(feeAmount), true);
BorrowCap memory cap = borrowLimit;
require(totalBorrow.elastic <= cap.total, "Borrow Limit reached");
accrueInfo.feesEarned = accrueInfo.feesEarned.add(uint128(feeAmount));
uint256 newBorrowPart = userBorrowPart[msg.sender].add(part);
require(newBorrowPart <= cap.borrowPartPerAddress, "Borrow Limit reached");
require(whitelister == IWhitelister(0) || whitelister.getBorrowStatus(msg.sender, newBorrowPart), "Whitelisted borrow exceeded");
userBorrowPart[msg.sender] = newBorrowPart;
// As long as there are tokens on this contract you can 'mint'... this enables limiting borrows
share = bentoBox.toShare(magicInternetMoney, amount, false);
bentoBox.transfer(magicInternetMoney, address(this), to, share);
emit LogBorrow(msg.sender, to, amount.add(feeAmount), part);
}
/// @notice Sender borrows `amount` and transfers it to `to`.
/// @return part Total part of the debt held by borrowers.
/// @return share Total amount in shares borrowed.
function borrow(address to, uint256 amount) public solvent returns (uint256 part, uint256 share) {
accrue();
(part, share) = _borrow(to, amount);
}
/// @dev Concrete implementation of `repay`.
function _repay(
address to,
bool skim,
uint256 part
) internal returns (uint256 amount) {
(totalBorrow, amount) = totalBorrow.sub(part, true);
userBorrowPart[to] = userBorrowPart[to].sub(part);
uint256 share = bentoBox.toShare(magicInternetMoney, amount, true);
bentoBox.transfer(magicInternetMoney, skim ? address(bentoBox) : msg.sender, address(this), share);
emit LogRepay(skim ? address(bentoBox) : msg.sender, to, amount, part);
}
/// @notice Repays a loan.
/// @param to Address of the user this payment should go.
/// @param skim True if the amount should be skimmed from the deposit balance of msg.sender.
/// False if tokens from msg.sender in `bentoBox` should be transferred.
/// @param part The amount to repay. See `userBorrowPart`.
/// @return amount The total amount repayed.
function repay(
address to,
bool skim,
uint256 part
) public returns (uint256 amount) {
accrue();
amount = _repay(to, skim, part);
}
// Functions that need accrue to be called
uint8 internal constant ACTION_REPAY = 2;
uint8 internal constant ACTION_REMOVE_COLLATERAL = 4;
uint8 internal constant ACTION_BORROW = 5;
uint8 internal constant ACTION_GET_REPAY_SHARE = 6;
uint8 internal constant ACTION_GET_REPAY_PART = 7;
uint8 internal constant ACTION_ACCRUE = 8;
// Functions that don't need accrue to be called
uint8 internal constant ACTION_ADD_COLLATERAL = 10;
uint8 internal constant ACTION_UPDATE_EXCHANGE_RATE = 11;
// Function on BentoBox
uint8 internal constant ACTION_BENTO_DEPOSIT = 20;
uint8 internal constant ACTION_BENTO_WITHDRAW = 21;
uint8 internal constant ACTION_BENTO_TRANSFER = 22;
uint8 internal constant ACTION_BENTO_TRANSFER_MULTIPLE = 23;
uint8 internal constant ACTION_BENTO_SETAPPROVAL = 24;
// Any external call (except to BentoBox)
uint8 internal constant ACTION_CALL = 30;
uint8 internal constant ACTION_SET_MAX_BORROW = 31;
int256 internal constant USE_VALUE1 = -1;
int256 internal constant USE_VALUE2 = -2;
/// @dev Helper function for choosing the correct value (`value1` or `value2`) depending on `inNum`.
function _num(
int256 inNum,
uint256 value1,
uint256 value2
) internal pure returns (uint256 outNum) {
outNum = inNum >= 0 ? uint256(inNum) : (inNum == USE_VALUE1 ? value1 : value2);
}
/// @dev Helper function for depositing into `bentoBox`.
function _bentoDeposit(
bytes memory data,
uint256 value,
uint256 value1,
uint256 value2
) internal returns (uint256, uint256) {
(IERC20 token, address to, int256 amount, int256 share) = abi.decode(data, (IERC20, address, int256, int256));
amount = int256(_num(amount, value1, value2)); // Done this way to avoid stack too deep errors
share = int256(_num(share, value1, value2));
return bentoBox.deposit{value: value}(token, msg.sender, to, uint256(amount), uint256(share));
}
/// @dev Helper function to withdraw from the `bentoBox`.
function _bentoWithdraw(
bytes memory data,
uint256 value1,
uint256 value2
) internal returns (uint256, uint256) {
(IERC20 token, address to, int256 amount, int256 share) = abi.decode(data, (IERC20, address, int256, int256));
return bentoBox.withdraw(token, msg.sender, to, _num(amount, value1, value2), _num(share, value1, value2));
}
/// @dev Helper function to perform a contract call and eventually extracting revert messages on failure.
/// Calls to `bentoBox` are not allowed for obvious security reasons.
/// This also means that calls made from this contract shall *not* be trusted.
function _call(
uint256 value,
bytes memory data,
uint256 value1,
uint256 value2
) internal returns (bytes memory, uint8) {
(address callee, bytes memory callData, bool useValue1, bool useValue2, uint8 returnValues) =
abi.decode(data, (address, bytes, bool, bool, uint8));
if (useValue1 && !useValue2) {
callData = abi.encodePacked(callData, value1);
} else if (!useValue1 && useValue2) {
callData = abi.encodePacked(callData, value2);
} else if (useValue1 && useValue2) {
callData = abi.encodePacked(callData, value1, value2);
}
require(callee != address(bentoBox) && callee != address(this), "Cauldron: can't call");
(bool success, bytes memory returnData) = callee.call{value: value}(callData);
require(success, "Cauldron: call failed");
return (returnData, returnValues);
}
struct CookStatus {
bool needsSolvencyCheck;
bool hasAccrued;
}
/// @notice Executes a set of actions and allows composability (contract calls) to other contracts.
/// @param actions An array with a sequence of actions to execute (see ACTION_ declarations).
/// @param values A one-to-one mapped array to `actions`. ETH amounts to send along with the actions.
/// Only applicable to `ACTION_CALL`, `ACTION_BENTO_DEPOSIT`.
/// @param datas A one-to-one mapped array to `actions`. Contains abi encoded data of function arguments.
/// @return value1 May contain the first positioned return value of the last executed action (if applicable).
/// @return value2 May contain the second positioned return value of the last executed action which returns 2 values (if applicable).
function cook(
uint8[] calldata actions,
uint256[] calldata values,
bytes[] calldata datas
) external payable returns (uint256 value1, uint256 value2) {
CookStatus memory status;
for (uint256 i = 0; i < actions.length; i++) {
uint8 action = actions[i];
if (!status.hasAccrued && action < 10) {
accrue();
status.hasAccrued = true;
}
if (action == ACTION_ADD_COLLATERAL) {
(int256 share, address to, bool skim) = abi.decode(datas[i], (int256, address, bool));
addCollateral(to, skim, _num(share, value1, value2));
} else if (action == ACTION_REPAY) {
(int256 part, address to, bool skim) = abi.decode(datas[i], (int256, address, bool));
_repay(to, skim, _num(part, value1, value2));
} else if (action == ACTION_REMOVE_COLLATERAL) {
(int256 share, address to) = abi.decode(datas[i], (int256, address));
_removeCollateral(to, _num(share, value1, value2));
status.needsSolvencyCheck = true;
} else if (action == ACTION_BORROW) {
(int256 amount, address to) = abi.decode(datas[i], (int256, address));
(value1, value2) = _borrow(to, _num(amount, value1, value2));
status.needsSolvencyCheck = true;
} else if (action == ACTION_UPDATE_EXCHANGE_RATE) {
(bool must_update, uint256 minRate, uint256 maxRate) = abi.decode(datas[i], (bool, uint256, uint256));
(bool updated, uint256 rate) = updateExchangeRate();
require((!must_update || updated) && rate > minRate && (maxRate == 0 || rate > maxRate), "Cauldron: rate not ok");
} else if (action == ACTION_BENTO_SETAPPROVAL) {
(address user, address _masterContract, bool approved, uint8 v, bytes32 r, bytes32 s) =
abi.decode(datas[i], (address, address, bool, uint8, bytes32, bytes32));
bentoBox.setMasterContractApproval(user, _masterContract, approved, v, r, s);
} else if (action == ACTION_BENTO_DEPOSIT) {
(value1, value2) = _bentoDeposit(datas[i], values[i], value1, value2);
} else if (action == ACTION_BENTO_WITHDRAW) {
(value1, value2) = _bentoWithdraw(datas[i], value1, value2);
} else if (action == ACTION_BENTO_TRANSFER) {
(IERC20 token, address to, int256 share) = abi.decode(datas[i], (IERC20, address, int256));
bentoBox.transfer(token, msg.sender, to, _num(share, value1, value2));
} else if (action == ACTION_BENTO_TRANSFER_MULTIPLE) {
(IERC20 token, address[] memory tos, uint256[] memory shares) = abi.decode(datas[i], (IERC20, address[], uint256[]));
bentoBox.transferMultiple(token, msg.sender, tos, shares);
} else if (action == ACTION_CALL) {
(bytes memory returnData, uint8 returnValues) = _call(values[i], datas[i], value1, value2);
if (returnValues == 1) {
(value1) = abi.decode(returnData, (uint256));
} else if (returnValues == 2) {
(value1, value2) = abi.decode(returnData, (uint256, uint256));
}
} else if (action == ACTION_SET_MAX_BORROW) {
(address user, uint256 maxBorrow, bytes32[] memory merkleProof) = abi.decode(datas[i], (address, uint256, bytes32[]));
whitelister.setMaxBorrow(user, maxBorrow, merkleProof);
} else if (action == ACTION_GET_REPAY_SHARE) {
int256 part = abi.decode(datas[i], (int256));
value1 = bentoBox.toShare(magicInternetMoney, totalBorrow.toElastic(_num(part, value1, value2), true), true);
} else if (action == ACTION_GET_REPAY_PART) {
int256 amount = abi.decode(datas[i], (int256));
value1 = totalBorrow.toBase(_num(amount, value1, value2), false);
}
}
if (status.needsSolvencyCheck) {
(, uint256 _exchangeRate) = updateExchangeRate();
require(_isSolvent(msg.sender, _exchangeRate), "Cauldron: user insolvent");
}
}
/// @notice Handles the liquidation of users' balances, once the users' amount of collateral is too low.
/// @param users An array of user addresses.
/// @param maxBorrowParts A one-to-one mapping to `users`, contains maximum (partial) borrow amounts (to liquidate) of the respective user.
/// @param to Address of the receiver in open liquidations if `swapper` is zero.
function liquidate(
address[] calldata users,
uint256[] calldata maxBorrowParts,
address to,
ISwapper swapper
) public {
// Oracle can fail but we still need to allow liquidations
(, uint256 _exchangeRate) = updateExchangeRate();
accrue();
uint256 allCollateralShare;
uint256 allBorrowAmount;
uint256 allBorrowPart;
Rebase memory _totalBorrow = totalBorrow;
Rebase memory bentoBoxTotals = bentoBox.totals(collateral);
for (uint256 i = 0; i < users.length; i++) {
address user = users[i];
if (!_isSolvent(user, _exchangeRate)) {
uint256 borrowPart;
{
uint256 availableBorrowPart = userBorrowPart[user];
borrowPart = maxBorrowParts[i] > availableBorrowPart ? availableBorrowPart : maxBorrowParts[i];
userBorrowPart[user] = availableBorrowPart.sub(borrowPart);
}
uint256 borrowAmount = _totalBorrow.toElastic(borrowPart, false);
uint256 collateralShare =
bentoBoxTotals.toBase(
borrowAmount.mul(LIQUIDATION_MULTIPLIER).mul(_exchangeRate) /
(LIQUIDATION_MULTIPLIER_PRECISION * EXCHANGE_RATE_PRECISION),
false
);
userCollateralShare[user] = userCollateralShare[user].sub(collateralShare);
emit LogRemoveCollateral(user, to, collateralShare);
emit LogRepay(msg.sender, user, borrowAmount, borrowPart);
emit LogLiquidation(msg.sender, user, to, collateralShare, borrowAmount, borrowPart);
// Keep totals
allCollateralShare = allCollateralShare.add(collateralShare);
allBorrowAmount = allBorrowAmount.add(borrowAmount);
allBorrowPart = allBorrowPart.add(borrowPart);
}
}
require(allBorrowAmount != 0, "Cauldron: all are solvent");
_totalBorrow.elastic = _totalBorrow.elastic.sub(allBorrowAmount.to128());
_totalBorrow.base = _totalBorrow.base.sub(allBorrowPart.to128());
totalBorrow = _totalBorrow;
totalCollateralShare = totalCollateralShare.sub(allCollateralShare);
// Apply a percentual fee share to sSpell holders
{
uint256 distributionAmount = (allBorrowAmount.mul(LIQUIDATION_MULTIPLIER) / LIQUIDATION_MULTIPLIER_PRECISION).sub(allBorrowAmount).mul(DISTRIBUTION_PART) / DISTRIBUTION_PRECISION; // Distribution Amount
allBorrowAmount = allBorrowAmount.add(distributionAmount);
accrueInfo.feesEarned = accrueInfo.feesEarned.add(distributionAmount.to128());
}
uint256 allBorrowShare = bentoBox.toShare(magicInternetMoney, allBorrowAmount, true);
// Swap using a swapper freely chosen by the caller
// Open (flash) liquidation: get proceeds first and provide the borrow after
bentoBox.transfer(collateral, address(this), to, allCollateralShare);
if (swapper != ISwapper(0)) {
swapper.swap(collateral, magicInternetMoney, msg.sender, allBorrowShare, allCollateralShare);
}
allBorrowShare = bentoBox.toShare(magicInternetMoney, allBorrowAmount, true);
bentoBox.transfer(magicInternetMoney, msg.sender, address(this), allBorrowShare);
}
/// @notice Withdraws the fees accumulated.
function withdrawFees() public {
accrue();
address _feeTo = masterContract.feeTo();
uint256 _feesEarned = accrueInfo.feesEarned;
uint256 share = bentoBox.toShare(magicInternetMoney, _feesEarned, false);
bentoBox.transfer(magicInternetMoney, address(this), _feeTo, share);
accrueInfo.feesEarned = 0;
emit LogWithdrawFees(_feeTo, _feesEarned);
}
/// @notice Sets the beneficiary of interest accrued.
/// MasterContract Only Admin function.
/// @param newFeeTo The address of the receiver.
function setFeeTo(address newFeeTo) public onlyOwner {
feeTo = newFeeTo;
emit LogFeeTo(newFeeTo);
}
/// @notice reduces the supply of MIM
/// @param amount amount to reduce supply by
function reduceSupply(uint256 amount) public onlyMasterContractOwner {
bentoBox.withdraw(magicInternetMoney, address(this), masterContract.owner(), amount, 0);
}
/// @notice allows to change the interest rate
/// @param newInterestRate new interest rate
function changeInterestRate(uint64 newInterestRate) public onlyMasterContractOwner {
uint64 oldInterestRate = accrueInfo.INTEREST_PER_SECOND;
require(newInterestRate < oldInterestRate + oldInterestRate * 3 / 4 || newInterestRate <= ONE_PERCENT_RATE, "Interest rate increase > 75%");
require(lastInterestUpdate + 3 days < block.timestamp, "Update only every 3 days");
lastInterestUpdate = block.timestamp;
accrueInfo.INTEREST_PER_SECOND = newInterestRate;
emit LogInterestChange(oldInterestRate, newInterestRate);
}
/// @notice allows to change the borrow limit
/// @param newBorrowLimit new borrow limit
/// @param perAddressPart new borrow limit per address
function changeBorrowLimit(uint128 newBorrowLimit, uint128 perAddressPart) public onlyMasterContractOwner {
borrowLimit = BorrowCap(newBorrowLimit, perAddressPart);
emit LogChangeBorrowLimit(newBorrowLimit, perAddressPart);
}
/// @notice allows to change the whitelister
/// @param newWhiteLister new whitelisting address
function changeWhitelister(IWhitelister newWhiteLister) public onlyMasterContractOwner {
whitelister = newWhiteLister;
emit LogChangeWhitelister(newWhiteLister);
}
}