-
Notifications
You must be signed in to change notification settings - Fork 7
/
Comptroller.sol
1435 lines (1217 loc) · 61.2 KB
/
Comptroller.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
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// SPDX-License-Identifier: BSD-3-Clause
pragma solidity 0.8.13;
import "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol";
import "./VToken.sol";
import "@venusprotocol/oracle/contracts/PriceOracle.sol";
import "./ComptrollerInterface.sol";
import "./ComptrollerStorage.sol";
import "./Rewards/RewardsDistributor.sol";
import "@venusprotocol/governance-contracts/contracts/Governance/AccessControlManager.sol";
import "./MaxLoopsLimitHelper.sol";
/**
* @title Comptroller Contract
*/
contract Comptroller is
Ownable2StepUpgradeable,
AccessControlledV8,
ComptrollerStorage,
ComptrollerInterface,
ExponentialNoError,
MaxLoopsLimitHelper
{
// PoolRegistry, immutable to save on gas
/// @custom:oz-upgrades-unsafe-allow state-variable-immutable
address public immutable poolRegistry;
/// @notice Emitted when an account enters a market
event MarketEntered(VToken vToken, address account);
/// @notice Emitted when an account exits a market
event MarketExited(VToken vToken, address account);
/// @notice Emitted when close factor is changed by admin
event NewCloseFactor(uint256 oldCloseFactorMantissa, uint256 newCloseFactorMantissa);
/// @notice Emitted when a collateral factor is changed by admin
event NewCollateralFactor(VToken vToken, uint256 oldCollateralFactorMantissa, uint256 newCollateralFactorMantissa);
/// @notice Emitted when liquidation threshold is changed by admin
event NewLiquidationThreshold(
VToken vToken,
uint256 oldLiquidationThresholdMantissa,
uint256 newLiquidationThresholdMantissa
);
/// @notice Emitted when liquidation incentive is changed by admin
event NewLiquidationIncentive(uint256 oldLiquidationIncentiveMantissa, uint256 newLiquidationIncentiveMantissa);
/// @notice Emitted when price oracle is changed
event NewPriceOracle(PriceOracle oldPriceOracle, PriceOracle newPriceOracle);
/// @notice Emitted when an action is paused on a market
event ActionPausedMarket(VToken vToken, Action action, bool pauseState);
/// @notice Emitted when borrow cap for a vToken is changed
event NewBorrowCap(VToken indexed vToken, uint256 newBorrowCap);
/// @notice Emitted when the collateral threshold (in USD) for non-batch liquidations is changed
event NewMinLiquidatableCollateral(uint256 oldMinLiquidatableCollateral, uint256 newMinLiquidatableCollateral);
/// @notice Emitted when supply cap for a vToken is changed
event NewSupplyCap(VToken indexed vToken, uint256 newSupplyCap);
/// @notice Emitted when a rewards distributor is added
event NewRewardsDistributor(address indexed rewardsDistributor);
/// @notice Emitted when a market is supported
event MarketSupported(VToken vToken);
/// @notice Thrown when collateral factor exceeds the upper bound
error InvalidCollateralFactor();
/// @notice Thrown when liquidation threshold exceeds the collateral factor
error InvalidLiquidationThreshold();
/// @notice Thrown when the action is only available to specific sender, but the real sender was different
error UnexpectedSender(address expectedSender, address actualSender);
/// @notice Thrown when the oracle returns an invalid price for some asset
error PriceError(address vToken);
/// @notice Thrown if VToken unexpectedly returned a nonzero error code while trying to get account snapshot
error SnapshotError(address vToken, address user);
/// @notice Thrown when the market is not listed
error MarketNotListed(address market);
/// @notice Thrown when a market has an unexpected comptroller
error ComptrollerMismatch();
/**
* @notice Throwed during the liquidation if user's total collateral amount is lower than
* a predefined threshold. In this case only batch liquidations (either liquidateAccount
* or healAccount) are available.
*/
error MinimalCollateralViolated(uint256 expectedGreaterThan, uint256 actual);
error CollateralExceedsThreshold(uint256 expectedLessThanOrEqualTo, uint256 actual);
error InsufficientCollateral(uint256 collateralToSeize, uint256 availableCollateral);
/// @notice Thrown when the account doesn't have enough liquidity to redeem or borrow
error InsufficientLiquidity();
/// @notice Thrown when trying to liquidate a healthy account
error InsufficientShortfall();
/// @notice Thrown when trying to repay more than allowed by close factor
error TooMuchRepay();
/// @notice Thrown if the user is trying to exit a market in which they have an outstanding debt
error NonzeroBorrowBalance();
/// @notice Thrown when trying to perform an action that is paused
error ActionPaused(address market, Action action);
/// @notice Thrown when trying to add a market that is already listed
error MarketAlreadyListed(address market);
/// @notice Thrown if the supply cap is exceeded
error SupplyCapExceeded(address market, uint256 cap);
/// @notice Thrown if the borrow cap is exceeded
error BorrowCapExceeded(address market, uint256 cap);
/// @custom:oz-upgrades-unsafe-allow constructor
constructor(address poolRegistry_) {
require(poolRegistry_ != address(0), "invalid pool registry address");
poolRegistry = poolRegistry_;
_disableInitializers();
}
/**
* @param loopLimit Limit for the loops can iterate to avoid the DOS
* @param accessControlManager Access control manager contract address
*/
function initialize(uint256 loopLimit, address accessControlManager) external initializer {
__Ownable2Step_init();
__AccessControlled_init_unchained(accessControlManager);
_setMaxLoopsLimit(loopLimit);
}
/**
* @notice Add assets to be included in account liquidity calculation; enabling them to be used as collateral
* @param vTokens The list of addresses of the vToken markets to be enabled
* @return errors An array of NO_ERROR for compatibility with Venus core tooling
* @custom:event MarketEntered is emitted for each market on success
* @custom:error ActionPaused error is thrown if entering any of the markets is paused
* @custom:error MarketNotListed error is thrown if any of the markets is not listed
* @custom:access Not restricted
*/
function enterMarkets(address[] memory vTokens) external override returns (uint256[] memory) {
uint256 len = vTokens.length;
uint256 accountAssetsLen = accountAssets[msg.sender].length;
_ensureMaxLoops(accountAssetsLen + len);
uint256[] memory results = new uint256[](len);
for (uint256 i; i < len; ++i) {
VToken vToken = VToken(vTokens[i]);
_addToMarket(vToken, msg.sender);
results[i] = NO_ERROR;
}
return results;
}
/**
* @notice Removes asset from sender's account liquidity calculation; disabling them as collateral
* @dev Sender must not have an outstanding borrow balance in the asset,
* or be providing necessary collateral for an outstanding borrow.
* @param vTokenAddress The address of the asset to be removed
* @return error Always NO_ERROR for compatibility with Venus core tooling
* @custom:event MarketExited is emitted on success
* @custom:error ActionPaused error is thrown if exiting the market is paused
* @custom:error NonzeroBorrowBalance error is thrown if the user has an outstanding borrow in this market
* @custom:error MarketNotListed error is thrown when the market is not listed
* @custom:error InsufficientLiquidity error is thrown if exiting the market would lead to user's insolvency
* @custom:error SnapshotError is thrown if some vToken fails to return the account's supply and borrows
* @custom:error PriceError is thrown if the oracle returns an incorrect price for some asset
* @custom:access Not restricted
*/
function exitMarket(address vTokenAddress) external override returns (uint256) {
_checkActionPauseState(vTokenAddress, Action.EXIT_MARKET);
VToken vToken = VToken(vTokenAddress);
/* Get sender tokensHeld and amountOwed underlying from the vToken */
(uint256 tokensHeld, uint256 amountOwed, ) = _safeGetAccountSnapshot(vToken, msg.sender);
/* Fail if the sender has a borrow balance */
if (amountOwed != 0) {
revert NonzeroBorrowBalance();
}
/* Fail if the sender is not permitted to redeem all of their tokens */
_checkRedeemAllowed(vTokenAddress, msg.sender, tokensHeld);
Market storage marketToExit = markets[address(vToken)];
/* Return true if the sender is not already ‘in’ the market */
if (!marketToExit.accountMembership[msg.sender]) {
return NO_ERROR;
}
/* Set vToken account membership to false */
delete marketToExit.accountMembership[msg.sender];
/* Delete vToken from the account’s list of assets */
// load into memory for faster iteration
VToken[] memory userAssetList = accountAssets[msg.sender];
uint256 len = userAssetList.length;
uint256 assetIndex = len;
for (uint256 i; i < len; ++i) {
if (userAssetList[i] == vToken) {
assetIndex = i;
break;
}
}
// We *must* have found the asset in the list or our redundant data structure is broken
assert(assetIndex < len);
// copy last item in list to location of item to be removed, reduce length by 1
VToken[] storage storedList = accountAssets[msg.sender];
storedList[assetIndex] = storedList[storedList.length - 1];
storedList.pop();
emit MarketExited(vToken, msg.sender);
return NO_ERROR;
}
/*** Policy Hooks ***/
/**
* @notice Checks if the account should be allowed to mint tokens in the given market
* @param vToken The market to verify the mint against
* @param minter The account which would get the minted tokens
* @param mintAmount The amount of underlying being supplied to the market in exchange for tokens
* @custom:error ActionPaused error is thrown if supplying to this market is paused
* @custom:error MarketNotListed error is thrown when the market is not listed
* @custom:error SupplyCapExceeded error is thrown if the total supply exceeds the cap after minting
* @custom:access Not restricted
*/
function preMintHook(
address vToken,
address minter,
uint256 mintAmount
) external override {
_checkActionPauseState(vToken, Action.MINT);
if (!markets[vToken].isListed) {
revert MarketNotListed(address(vToken));
}
uint256 supplyCap = supplyCaps[vToken];
// Skipping the cap check for uncapped coins to save some gas
if (supplyCap != type(uint256).max) {
uint256 vTokenSupply = VToken(vToken).totalSupply();
Exp memory exchangeRate = Exp({ mantissa: VToken(vToken).exchangeRateStored() });
uint256 nextTotalSupply = mul_ScalarTruncateAddUInt(exchangeRate, vTokenSupply, mintAmount);
if (nextTotalSupply > supplyCap) {
revert SupplyCapExceeded(vToken, supplyCap);
}
}
// Keep the flywheel moving
uint256 rewardDistributorsCount = rewardsDistributors.length;
for (uint256 i; i < rewardDistributorsCount; ++i) {
rewardsDistributors[i].updateRewardTokenSupplyIndex(vToken);
rewardsDistributors[i].distributeSupplierRewardToken(vToken, minter);
}
}
/**
* @notice Checks if the account should be allowed to redeem tokens in the given market
* @param vToken The market to verify the redeem against
* @param redeemer The account which would redeem the tokens
* @param redeemTokens The number of vTokens to exchange for the underlying asset in the market
* @custom:error ActionPaused error is thrown if withdrawals are paused in this market
* @custom:error MarketNotListed error is thrown when the market is not listed
* @custom:error InsufficientLiquidity error is thrown if the withdrawal would lead to user's insolvency
* @custom:error SnapshotError is thrown if some vToken fails to return the account's supply and borrows
* @custom:error PriceError is thrown if the oracle returns an incorrect price for some asset
* @custom:access Not restricted
*/
function preRedeemHook(
address vToken,
address redeemer,
uint256 redeemTokens
) external override {
_checkActionPauseState(vToken, Action.REDEEM);
oracle.updatePrice(vToken);
_checkRedeemAllowed(vToken, redeemer, redeemTokens);
// Keep the flywheel moving
uint256 rewardDistributorsCount = rewardsDistributors.length;
for (uint256 i; i < rewardDistributorsCount; ++i) {
rewardsDistributors[i].updateRewardTokenSupplyIndex(vToken);
rewardsDistributors[i].distributeSupplierRewardToken(vToken, redeemer);
}
}
/**
* @notice Checks if the account should be allowed to borrow the underlying asset of the given market
* @param vToken The market to verify the borrow against
* @param borrower The account which would borrow the asset
* @param borrowAmount The amount of underlying the account would borrow
* @custom:error ActionPaused error is thrown if borrowing is paused in this market
* @custom:error MarketNotListed error is thrown when the market is not listed
* @custom:error InsufficientLiquidity error is thrown if there is not enough collateral to borrow
* @custom:error BorrowCapExceeded is thrown if the borrow cap will be exceeded should this borrow succeed
* @custom:error SnapshotError is thrown if some vToken fails to return the account's supply and borrows
* @custom:error PriceError is thrown if the oracle returns an incorrect price for some asset
* @custom:access Not restricted if vToken is enabled as collateral, otherwise only vToken
*/
/// disable-eslint
function preBorrowHook(
address vToken,
address borrower,
uint256 borrowAmount
) external override {
_checkActionPauseState(vToken, Action.BORROW);
oracle.updatePrice(vToken);
if (!markets[vToken].isListed) {
revert MarketNotListed(address(vToken));
}
if (!markets[vToken].accountMembership[borrower]) {
// only vTokens may call borrowAllowed if borrower not in market
_checkSenderIs(vToken);
// attempt to add borrower to the market or revert
_addToMarket(VToken(msg.sender), borrower);
}
if (oracle.getUnderlyingPrice(vToken) == 0) {
revert PriceError(address(vToken));
}
uint256 borrowCap = borrowCaps[vToken];
// Skipping the cap check for uncapped coins to save some gas
if (borrowCap != type(uint256).max) {
uint256 totalBorrows = VToken(vToken).totalBorrows();
uint256 nextTotalBorrows = totalBorrows + borrowAmount;
if (nextTotalBorrows > borrowCap) {
revert BorrowCapExceeded(vToken, borrowCap);
}
}
AccountLiquiditySnapshot memory snapshot = _getHypotheticalLiquiditySnapshot(
borrower,
VToken(vToken),
0,
borrowAmount,
_getCollateralFactor
);
if (snapshot.shortfall > 0) {
revert InsufficientLiquidity();
}
Exp memory borrowIndex = Exp({ mantissa: VToken(vToken).borrowIndex() });
// Keep the flywheel moving
uint256 rewardDistributorsCount = rewardsDistributors.length;
for (uint256 i; i < rewardDistributorsCount; ++i) {
rewardsDistributors[i].updateRewardTokenBorrowIndex(vToken, borrowIndex);
rewardsDistributors[i].distributeBorrowerRewardToken(vToken, borrower, borrowIndex);
}
}
/**
* @notice Checks if the account should be allowed to repay a borrow in the given market
* @param vToken The market to verify the repay against
* @param borrower The account which would borrowed the asset
* @custom:error ActionPaused error is thrown if repayments are paused in this market
* @custom:error MarketNotListed error is thrown when the market is not listed
* @custom:access Not restricted
*/
function preRepayHook(address vToken, address borrower) external override {
_checkActionPauseState(vToken, Action.REPAY);
oracle.updatePrice(vToken);
if (!markets[vToken].isListed) {
revert MarketNotListed(address(vToken));
}
// Keep the flywheel moving
uint256 rewardDistributorsCount = rewardsDistributors.length;
for (uint256 i; i < rewardDistributorsCount; ++i) {
Exp memory borrowIndex = Exp({ mantissa: VToken(vToken).borrowIndex() });
rewardsDistributors[i].updateRewardTokenBorrowIndex(vToken, borrowIndex);
rewardsDistributors[i].distributeBorrowerRewardToken(vToken, borrower, borrowIndex);
}
}
/**
* @notice Checks if the liquidation should be allowed to occur
* @param vTokenBorrowed Asset which was borrowed by the borrower
* @param vTokenCollateral Asset which was used as collateral and will be seized
* @param borrower The address of the borrower
* @param repayAmount The amount of underlying being repaid
* @param skipLiquidityCheck Allows the borrow to be liquidated regardless of the account liquidity
* @custom:error ActionPaused error is thrown if liquidations are paused in this market
* @custom:error MarketNotListed error is thrown if either collateral or borrowed token is not listed
* @custom:error TooMuchRepay error is thrown if the liquidator is trying to repay more than allowed by close factor
* @custom:error MinimalCollateralViolated is thrown if the users' total collateral is lower than the threshold for non-batch liquidations
* @custom:error InsufficientShortfall is thrown when trying to liquidate a healthy account
* @custom:error SnapshotError is thrown if some vToken fails to return the account's supply and borrows
* @custom:error PriceError is thrown if the oracle returns an incorrect price for some asset
*/
function preLiquidateHook(
address vTokenBorrowed,
address vTokenCollateral,
address borrower,
uint256 repayAmount,
bool skipLiquidityCheck
) external override {
// Pause Action.LIQUIDATE on BORROWED TOKEN to prevent liquidating it.
// If we want to pause liquidating to vTokenCollateral, we should pause
// Action.SEIZE on it
_checkActionPauseState(vTokenBorrowed, Action.LIQUIDATE);
oracle.updatePrice(vTokenBorrowed);
oracle.updatePrice(vTokenCollateral);
if (!markets[vTokenBorrowed].isListed) {
revert MarketNotListed(address(vTokenBorrowed));
}
if (!markets[vTokenCollateral].isListed) {
revert MarketNotListed(address(vTokenCollateral));
}
uint256 borrowBalance = VToken(vTokenBorrowed).borrowBalanceStored(borrower);
/* Allow accounts to be liquidated if the market is deprecated or it is a forced liquidation */
if (skipLiquidityCheck || isDeprecated(VToken(vTokenBorrowed))) {
if (repayAmount > borrowBalance) {
revert TooMuchRepay();
}
return;
}
/* The borrower must have shortfall and collateral > threshold in order to be liquidatable */
AccountLiquiditySnapshot memory snapshot = _getCurrentLiquiditySnapshot(borrower, _getLiquidationThreshold);
if (snapshot.totalCollateral <= minLiquidatableCollateral) {
/* The liquidator should use either liquidateAccount or healAccount */
revert MinimalCollateralViolated(minLiquidatableCollateral, snapshot.totalCollateral);
}
if (snapshot.shortfall == 0) {
revert InsufficientShortfall();
}
/* The liquidator may not repay more than what is allowed by the closeFactor */
uint256 maxClose = mul_ScalarTruncate(Exp({ mantissa: closeFactorMantissa }), borrowBalance);
if (repayAmount > maxClose) {
revert TooMuchRepay();
}
}
/**
* @notice Checks if the seizing of assets should be allowed to occur
* @param vTokenCollateral Asset which was used as collateral and will be seized
* @param seizerContract Contract that tries to seize the asset (either borrowed vToken or Comptroller)
* @param liquidator The address repaying the borrow and seizing the collateral
* @param borrower The address of the borrower
* @custom:error ActionPaused error is thrown if seizing this type of collateral is paused
* @custom:error MarketNotListed error is thrown if either collateral or borrowed token is not listed
* @custom:error ComptrollerMismatch error is when seizer contract or seized asset belong to different pools
* @custom:access Not restricted
*/
function preSeizeHook(
address vTokenCollateral,
address seizerContract,
address liquidator,
address borrower
) external override {
// Pause Action.SEIZE on COLLATERAL to prevent seizing it.
// If we want to pause liquidating vTokenBorrowed, we should pause
// Action.LIQUIDATE on it
_checkActionPauseState(vTokenCollateral, Action.SEIZE);
if (!markets[vTokenCollateral].isListed) {
revert MarketNotListed(vTokenCollateral);
}
if (seizerContract == address(this)) {
// If Comptroller is the seizer, just check if collateral's comptroller
// is equal to the current address
if (address(VToken(vTokenCollateral).comptroller()) != address(this)) {
revert ComptrollerMismatch();
}
} else {
// If the seizer is not the Comptroller, check that the seizer is a
// listed market, and that the markets' comptrollers match
if (!markets[seizerContract].isListed) {
revert MarketNotListed(seizerContract);
}
if (VToken(vTokenCollateral).comptroller() != VToken(seizerContract).comptroller()) {
revert ComptrollerMismatch();
}
}
// Keep the flywheel moving
uint256 rewardDistributorsCount = rewardsDistributors.length;
for (uint256 i; i < rewardDistributorsCount; ++i) {
rewardsDistributors[i].updateRewardTokenSupplyIndex(vTokenCollateral);
rewardsDistributors[i].distributeSupplierRewardToken(vTokenCollateral, borrower);
rewardsDistributors[i].distributeSupplierRewardToken(vTokenCollateral, liquidator);
}
}
/**
* @notice Checks if the account should be allowed to transfer tokens in the given market
* @param vToken The market to verify the transfer against
* @param src The account which sources the tokens
* @param dst The account which receives the tokens
* @param transferTokens The number of vTokens to transfer
* @custom:error ActionPaused error is thrown if withdrawals are paused in this market
* @custom:error MarketNotListed error is thrown when the market is not listed
* @custom:error InsufficientLiquidity error is thrown if the withdrawal would lead to user's insolvency
* @custom:error SnapshotError is thrown if some vToken fails to return the account's supply and borrows
* @custom:error PriceError is thrown if the oracle returns an incorrect price for some asset
* @custom:access Not restricted
*/
function preTransferHook(
address vToken,
address src,
address dst,
uint256 transferTokens
) external override {
_checkActionPauseState(vToken, Action.TRANSFER);
oracle.updatePrice(vToken);
// Currently the only consideration is whether or not
// the src is allowed to redeem this many tokens
_checkRedeemAllowed(vToken, src, transferTokens);
// Keep the flywheel moving
uint256 rewardDistributorsCount = rewardsDistributors.length;
for (uint256 i; i < rewardDistributorsCount; ++i) {
rewardsDistributors[i].updateRewardTokenSupplyIndex(vToken);
rewardsDistributors[i].distributeSupplierRewardToken(vToken, src);
rewardsDistributors[i].distributeSupplierRewardToken(vToken, dst);
}
}
/*** Pool-level operations ***/
/**
* @notice Seizes all the remaining collateral, makes msg.sender repay the existing
* borrows, and treats the rest of the debt as bad debt (for each market).
* The sender has to repay a certain percentage of the debt, computed as
* collateral / (borrows * liquidationIncentive).
* @param user account to heal
* @custom:error CollateralExceedsThreshold error is thrown when the collateral is too big for healing
* @custom:error SnapshotError is thrown if some vToken fails to return the account's supply and borrows
* @custom:error PriceError is thrown if the oracle returns an incorrect price for some asset
* @custom:access Not restricted
*/
function healAccount(address user) external {
VToken[] memory userAssets = accountAssets[user];
uint256 userAssetsCount = userAssets.length;
address liquidator = msg.sender;
// We need all user's markets to be fresh for the computations to be correct
for (uint256 i; i < userAssetsCount; ++i) {
userAssets[i].accrueInterest();
oracle.updatePrice(address(userAssets[i]));
}
AccountLiquiditySnapshot memory snapshot = _getCurrentLiquiditySnapshot(user, _getLiquidationThreshold);
if (snapshot.totalCollateral > minLiquidatableCollateral) {
revert CollateralExceedsThreshold(minLiquidatableCollateral, snapshot.totalCollateral);
}
if (snapshot.shortfall == 0) {
revert InsufficientShortfall();
}
// percentage = collateral / (borrows * liquidation incentive)
Exp memory collateral = Exp({ mantissa: snapshot.totalCollateral });
Exp memory scaledBorrows = mul_(
Exp({ mantissa: snapshot.borrows }),
Exp({ mantissa: liquidationIncentiveMantissa })
);
Exp memory percentage = div_(collateral, scaledBorrows);
if (lessThanExp(Exp({ mantissa: mantissaOne }), percentage)) {
revert CollateralExceedsThreshold(scaledBorrows.mantissa, collateral.mantissa);
}
for (uint256 i; i < userAssetsCount; ++i) {
VToken market = userAssets[i];
(uint256 tokens, uint256 borrowBalance, ) = _safeGetAccountSnapshot(market, user);
uint256 repaymentAmount = mul_ScalarTruncate(percentage, borrowBalance);
// Seize the entire collateral
if (tokens != 0) {
market.seize(liquidator, user, tokens);
}
// Repay a certain percentage of the borrow, forgive the rest
if (borrowBalance != 0) {
market.healBorrow(liquidator, user, repaymentAmount);
}
}
}
/**
* @notice Liquidates all borrows of the borrower. Callable only if the collateral is less than
* a predefined threshold, and the account collateral can be seized to cover all borrows. If
* the collateral is higher than the threshold, use regular liquidations. If the collateral is
* below the threshold, and the account is insolvent, use healAccount.
* @param borrower the borrower address
* @param orders an array of liquidation orders
* @custom:error CollateralExceedsThreshold error is thrown when the collateral is too big for a batch liquidation
* @custom:error InsufficientCollateral error is thrown when there is not enough collateral to cover the debt
* @custom:error SnapshotError is thrown if some vToken fails to return the account's supply and borrows
* @custom:error PriceError is thrown if the oracle returns an incorrect price for some asset
* @custom:access Not restricted
*/
function liquidateAccount(address borrower, LiquidationOrder[] calldata orders) external {
// We will accrue interest and update the oracle prices later during the liquidation
AccountLiquiditySnapshot memory snapshot = _getCurrentLiquiditySnapshot(borrower, _getLiquidationThreshold);
if (snapshot.totalCollateral > minLiquidatableCollateral) {
// You should use the regular vToken.liquidateBorrow(...) call
revert CollateralExceedsThreshold(minLiquidatableCollateral, snapshot.totalCollateral);
}
uint256 collateralToSeize = mul_ScalarTruncate(
Exp({ mantissa: liquidationIncentiveMantissa }),
snapshot.borrows
);
if (collateralToSeize >= snapshot.totalCollateral) {
// There is not enough collateral to seize. Use healBorrow to repay some part of the borrow
// and record bad debt.
revert InsufficientCollateral(collateralToSeize, snapshot.totalCollateral);
}
if (snapshot.shortfall == 0) {
revert InsufficientShortfall();
}
uint256 ordersCount = orders.length;
_ensureMaxLoops(ordersCount);
for (uint256 i; i < ordersCount; ++i) {
if (!markets[address(orders[i].vTokenBorrowed)].isListed) {
revert MarketNotListed(address(orders[i].vTokenBorrowed));
}
if (!markets[address(orders[i].vTokenCollateral)].isListed) {
revert MarketNotListed(address(orders[i].vTokenCollateral));
}
LiquidationOrder calldata order = orders[i];
order.vTokenBorrowed.forceLiquidateBorrow(
msg.sender,
borrower,
order.repayAmount,
order.vTokenCollateral,
true
);
}
VToken[] memory borrowMarkets = accountAssets[borrower];
uint256 marketsCount = borrowMarkets.length;
for (uint256 i; i < marketsCount; ++i) {
(, uint256 borrowBalance, ) = _safeGetAccountSnapshot(borrowMarkets[i], borrower);
require(borrowBalance == 0, "Nonzero borrow balance after liquidation");
}
}
/**
* @notice Sets the closeFactor to use when liquidating borrows
* @param newCloseFactorMantissa New close factor, scaled by 1e18
* @custom:event Emits NewCloseFactor on success
* @custom:access Only Governance
*/
function setCloseFactor(uint256 newCloseFactorMantissa) external {
_checkAccessAllowed("setCloseFactor(uint256)");
require(closeFactorMaxMantissa >= newCloseFactorMantissa, "Close factor greater than maximum close factor");
require(closeFactorMinMantissa <= newCloseFactorMantissa, "Close factor smaller than minimum close factor");
uint256 oldCloseFactorMantissa = closeFactorMantissa;
closeFactorMantissa = newCloseFactorMantissa;
emit NewCloseFactor(oldCloseFactorMantissa, closeFactorMantissa);
}
/**
* @notice Sets the collateralFactor for a market
* @dev This function is restricted by the AccessControlManager
* @param vToken The market to set the factor on
* @param newCollateralFactorMantissa The new collateral factor, scaled by 1e18
* @param newLiquidationThresholdMantissa The new liquidation threshold, scaled by 1e18
* @custom:event Emits NewCollateralFactor when collateral factor is updated
* and NewLiquidationThreshold when liquidation threshold is updated
* @custom:error MarketNotListed error is thrown when the market is not listed
* @custom:error InvalidCollateralFactor error is thrown when collateral factor is too high
* @custom:error InvalidLiquidationThreshold error is thrown when liquidation threshold is lower than collateral factor
* @custom:error PriceError is thrown when the oracle returns an invalid price for the asset
* @custom:access Controlled by AccessControlManager
*/
function setCollateralFactor(
VToken vToken,
uint256 newCollateralFactorMantissa,
uint256 newLiquidationThresholdMantissa
) external {
_checkAccessAllowed("setCollateralFactor(address,uint256,uint256)");
// Verify market is listed
Market storage market = markets[address(vToken)];
if (!market.isListed) {
revert MarketNotListed(address(vToken));
}
// Check collateral factor <= 0.9
if (newCollateralFactorMantissa > collateralFactorMaxMantissa) {
revert InvalidCollateralFactor();
}
// Ensure that liquidation threshold <= 1
if (newLiquidationThresholdMantissa > mantissaOne) {
revert InvalidLiquidationThreshold();
}
// Ensure that liquidation threshold >= CF
if (newLiquidationThresholdMantissa < newCollateralFactorMantissa) {
revert InvalidLiquidationThreshold();
}
// If collateral factor != 0, fail if price == 0
if (newCollateralFactorMantissa != 0 && oracle.getUnderlyingPrice(address(vToken)) == 0) {
revert PriceError(address(vToken));
}
uint256 oldCollateralFactorMantissa = market.collateralFactorMantissa;
if (newCollateralFactorMantissa != oldCollateralFactorMantissa) {
market.collateralFactorMantissa = newCollateralFactorMantissa;
emit NewCollateralFactor(vToken, oldCollateralFactorMantissa, newCollateralFactorMantissa);
}
uint256 oldLiquidationThresholdMantissa = market.liquidationThresholdMantissa;
if (newLiquidationThresholdMantissa != oldLiquidationThresholdMantissa) {
market.liquidationThresholdMantissa = newLiquidationThresholdMantissa;
emit NewLiquidationThreshold(vToken, oldLiquidationThresholdMantissa, newLiquidationThresholdMantissa);
}
}
/**
* @notice Sets liquidationIncentive
* @dev This function is restricted by the AccessControlManager
* @param newLiquidationIncentiveMantissa New liquidationIncentive scaled by 1e18
* @custom:event Emits NewLiquidationIncentive on success
* @custom:access Controlled by AccessControlManager
*/
function setLiquidationIncentive(uint256 newLiquidationIncentiveMantissa) external {
require(newLiquidationIncentiveMantissa >= 1e18, "liquidation incentive should be greater than 1e18");
_checkAccessAllowed("setLiquidationIncentive(uint256)");
// Save current value for use in log
uint256 oldLiquidationIncentiveMantissa = liquidationIncentiveMantissa;
// Set liquidation incentive to new incentive
liquidationIncentiveMantissa = newLiquidationIncentiveMantissa;
// Emit event with old incentive, new incentive
emit NewLiquidationIncentive(oldLiquidationIncentiveMantissa, newLiquidationIncentiveMantissa);
}
/**
* @notice Add the market to the markets mapping and set it as listed
* @dev Only callable by the PoolRegistry
* @param vToken The address of the market (token) to list
* @custom:error MarketAlreadyListed is thrown if the market is already listed in this pool
* @custom:access Only PoolRegistry
*/
function supportMarket(VToken vToken) external {
_checkSenderIs(poolRegistry);
if (markets[address(vToken)].isListed) {
revert MarketAlreadyListed(address(vToken));
}
require(vToken.isVToken(), "Comptroller: Invalid vToken"); // Sanity check to make sure its really a VToken
Market storage newMarket = markets[address(vToken)];
newMarket.isListed = true;
newMarket.collateralFactorMantissa = 0;
newMarket.liquidationThresholdMantissa = 0;
_addMarket(address(vToken));
uint256 rewardDistributorsCount = rewardsDistributors.length;
for (uint256 i; i < rewardDistributorsCount; ++i) {
rewardsDistributors[i].initializeMarket(address(vToken));
}
emit MarketSupported(vToken);
}
/**
* @notice Set the given borrow caps for the given vToken markets. Borrowing that brings total borrows to or above borrow cap will revert.
* @dev This function is restricted by the AccessControlManager
* @dev A borrow cap of -1 corresponds to unlimited borrowing.
* @dev Borrow caps smaller than the current total borrows are accepted. This way, new borrows will not be allowed
until the total borrows amount goes below the new borrow cap
* @param vTokens The addresses of the markets (tokens) to change the borrow caps for
* @param newBorrowCaps The new borrow cap values in underlying to be set. A value of -1 corresponds to unlimited borrowing.
* @custom:access Controlled by AccessControlManager
*/
function setMarketBorrowCaps(VToken[] calldata vTokens, uint256[] calldata newBorrowCaps) external {
_checkAccessAllowed("setMarketBorrowCaps(address[],uint256[])");
uint256 numMarkets = vTokens.length;
uint256 numBorrowCaps = newBorrowCaps.length;
require(numMarkets != 0 && numMarkets == numBorrowCaps, "invalid input");
_ensureMaxLoops(numMarkets);
for (uint256 i; i < numMarkets; ++i) {
borrowCaps[address(vTokens[i])] = newBorrowCaps[i];
emit NewBorrowCap(vTokens[i], newBorrowCaps[i]);
}
}
/**
* @notice Set the given supply caps for the given vToken markets. Supply that brings total Supply to or above supply cap will revert.
* @dev This function is restricted by the AccessControlManager
* @dev A supply cap of -1 corresponds to unlimited supply.
* @dev Supply caps smaller than the current total supplies are accepted. This way, new supplies will not be allowed
until the total supplies amount goes below the new supply cap
* @param vTokens The addresses of the markets (tokens) to change the supply caps for
* @param newSupplyCaps The new supply cap values in underlying to be set. A value of -1 corresponds to unlimited supply.
* @custom:access Controlled by AccessControlManager
*/
function setMarketSupplyCaps(VToken[] calldata vTokens, uint256[] calldata newSupplyCaps) external {
_checkAccessAllowed("setMarketSupplyCaps(address[],uint256[])");
uint256 vTokensCount = vTokens.length;
require(vTokensCount != 0, "invalid number of markets");
require(vTokensCount == newSupplyCaps.length, "invalid number of markets");
_ensureMaxLoops(vTokensCount);
for (uint256 i; i < vTokensCount; ++i) {
supplyCaps[address(vTokens[i])] = newSupplyCaps[i];
emit NewSupplyCap(vTokens[i], newSupplyCaps[i]);
}
}
/**
* @notice Pause/unpause specified actions
* @dev This function is restricted by the AccessControlManager
* @param marketsList Markets to pause/unpause the actions on
* @param actionsList List of action ids to pause/unpause
* @param paused The new paused state (true=paused, false=unpaused)
* @custom:access Controlled by AccessControlManager
*/
function setActionsPaused(
VToken[] calldata marketsList,
Action[] calldata actionsList,
bool paused
) external {
_checkAccessAllowed("setActionsPaused(address[],uint256[],bool)");
uint256 marketsCount = marketsList.length;
uint256 actionsCount = actionsList.length;
_ensureMaxLoops(marketsCount);
for (uint256 marketIdx; marketIdx < marketsCount; ++marketIdx) {
for (uint256 actionIdx; actionIdx < actionsCount; ++actionIdx) {
_setActionPaused(address(marketsList[marketIdx]), actionsList[actionIdx], paused);
}
}
}
/**
* @notice Set the given collateral threshold for non-batch liquidations. Regular liquidations
* will fail if the collateral amount is less than this threshold. Liquidators should use batch
* operations like liquidateAccount or healAccount.
* @dev This function is restricted by the AccessControlManager
* @param newMinLiquidatableCollateral The new min liquidatable collateral (in USD).
* @custom:access Controlled by AccessControlManager
*/
function setMinLiquidatableCollateral(uint256 newMinLiquidatableCollateral) external {
_checkAccessAllowed("setMinLiquidatableCollateral(uint256)");
uint256 oldMinLiquidatableCollateral = minLiquidatableCollateral;
minLiquidatableCollateral = newMinLiquidatableCollateral;
emit NewMinLiquidatableCollateral(oldMinLiquidatableCollateral, newMinLiquidatableCollateral);
}
/**
* @notice Add a new RewardsDistributor and initialize it with all markets
* @dev Only callable by the admin
* @param _rewardsDistributor Address of the RewardDistributor contract to add
* @custom:access Only Governance
* @custom:event Emits NewRewardsDistributor with distributor address
*/
function addRewardsDistributor(RewardsDistributor _rewardsDistributor) external onlyOwner {
require(!rewardsDistributorExists[address(_rewardsDistributor)], "already exists");
uint256 rewardsDistributorsLength = rewardsDistributors.length;
for (uint256 i; i < rewardsDistributorsLength; ++i) {
address rewardToken = address(rewardsDistributors[i].rewardToken());
require(
rewardToken != address(_rewardsDistributor.rewardToken()),
"distributor already exists with this reward"
);
}
uint256 rewardsDistributorsLen = rewardsDistributors.length;
_ensureMaxLoops(rewardsDistributorsLen + 1);
rewardsDistributors.push(_rewardsDistributor);
rewardsDistributorExists[address(_rewardsDistributor)] = true;
uint256 marketsCount = allMarkets.length;
for (uint256 i; i < marketsCount; ++i) {
_rewardsDistributor.initializeMarket(address(allMarkets[i]));
}
emit NewRewardsDistributor(address(_rewardsDistributor));
}
/**
* @notice Sets a new PriceOracle for the Comptroller
* @dev Only callable by the admin
* @param newOracle Address of the new PriceOracle to set
* @custom:event Emits NewPriceOracle on success
*/
function setPriceOracle(PriceOracle newOracle) external onlyOwner {
require(address(newOracle) != address(0), "invalid price oracle address");
PriceOracle oldOracle = oracle;
oracle = newOracle;
emit NewPriceOracle(oldOracle, newOracle);
}
/**
* @notice Set the for loop iteration limit to avoid DOS
* @param limit Limit for the max loops can execute at a time
*/
function setMaxLoopsLimit(uint256 limit) external onlyOwner {
_setMaxLoopsLimit(limit);
}
/**
* @notice Determine the current account liquidity with respect to collateral requirements
* @dev The interface of this function is intentionally kept compatible with Compound and Venus Core
* @param account The account get liquidity for
* @return error Always NO_ERROR for compatibility with Venus core tooling
* @return liquidity Account liquidity in excess of collateral requirements,
* @return shortfall Account shortfall below collateral requirements
*/
function getAccountLiquidity(address account)
external
view
returns (
uint256 error,
uint256 liquidity,
uint256 shortfall
)
{
AccountLiquiditySnapshot memory snapshot = _getCurrentLiquiditySnapshot(account, _getCollateralFactor);
return (NO_ERROR, snapshot.liquidity, snapshot.shortfall);
}
/**
* @notice Determine what the account liquidity would be if the given amounts were redeemed/borrowed
* @dev The interface of this function is intentionally kept compatible with Compound and Venus Core