-
Notifications
You must be signed in to change notification settings - Fork 18
/
Copy pathTroveManager.sol
1458 lines (1219 loc) · 58.5 KB
/
TroveManager.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: MIT
pragma solidity 0.8.19;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/utils/math/Math.sol";
import "../interfaces/IBorrowerOperations.sol";
import "../interfaces/IDebtToken.sol";
import "../interfaces/ISortedTroves.sol";
import "../interfaces/ITreasury.sol";
import "../interfaces/IPriceFeed.sol";
import "../dependencies/SystemStart.sol";
import "../dependencies/PrismaBase.sol";
import "../dependencies/PrismaMath.sol";
import "../dependencies/PrismaOwnable.sol";
/**
@title Prisma Trove Manager
@notice Based on Liquity's `TroveManager`
https://github.com/liquity/dev/blob/main/packages/contracts/contracts/TroveManager.sol
Prisma's implementation is modified so that multiple `TroveManager` and `SortedTroves`
contracts are deployed in tandem, with each pair managing troves of a single collateral
type.
Functionality related to liquidations has been moved to `LiquidationManager`. This was
necessary to avoid the restriction on deployed bytecode size.
*/
contract TroveManager is PrismaBase, PrismaOwnable, SystemStart {
// --- Connected contract declarations ---
address public immutable borrowerOperationsAddress;
address public immutable liquidationManager;
address immutable gasPoolAddress;
IDebtToken public immutable debtToken;
IPrismaTreasury public immutable treasury;
IPriceFeed public priceFeed;
IERC20 public collateralToken;
// A doubly linked list of Troves, sorted by their collateral ratios
ISortedTroves public sortedTroves;
EmissionId public emissionId;
uint256 constant SECONDS_IN_ONE_MINUTE = 60;
uint256 constant INTEREST_PRECISION = 1e27;
uint256 constant SECONDS_IN_YEAR = 365 days;
uint256 constant REWARD_DURATION = 1 weeks;
// volume-based amounts are divided by this value to allow storing as uint32
uint256 constant VOLUME_MULTIPLIER = 1e20;
// Maximum interest rate must be lower than the minimum LST staking yield
// so that over time the actual TCR becomes greater than the calculated TCR.
uint256 public constant MAX_INTEREST_RATE_IN_BPS = 400; // 4%
uint256 public constant SUNSETTING_INTEREST_RATE = (INTEREST_PRECISION * 5000) / (10000 * SECONDS_IN_YEAR); //50%
// During bootsrap period redemptions are not allowed
uint256 public constant BOOTSTRAP_PERIOD = 14 days;
/*
* BETA: 18 digit decimal. Parameter by which to divide the redeemed fraction, in order to calc the new base rate from a redemption.
* Corresponds to (1 / ALPHA) in the white paper.
*/
uint256 constant BETA = 2;
// commented values are prisma's fixed settings for each parameter
uint256 public minuteDecayFactor; // 999037758833783000;
uint256 public redemptionFeeFloor; // DECIMAL_PRECISION / 1000 * 5; // 0.5%
uint256 public maxRedemptionFee; // DECIMAL_PRECISION; // 100%
uint256 public borrowingFeeFloor; // DECIMAL_PRECISION / 1000 * 5; // 0.5%
uint256 public maxBorrowingFee; // DECIMAL_PRECISION / 100 * 5; // 5%
uint256 public maxSystemDebt;
uint256 public interestRate;
uint256 public activeInterestIndex;
uint256 public lastActiveIndexUpdate;
uint256 public systemDeploymentTime;
bool public sunsetting;
bool public paused;
uint256 public baseRate;
// The timestamp of the latest fee operation (redemption or new debt issuance)
uint256 public lastFeeOperationTime;
uint256 public totalStakes;
// Snapshot of the value of totalStakes, taken immediately after the latest liquidation
uint256 public totalStakesSnapshot;
// Snapshot of the total collateral taken immediately after the latest liquidation.
uint256 public totalCollateralSnapshot;
/*
* L_collateral and L_debt track the sums of accumulated liquidation rewards per unit staked. During its lifetime, each stake earns:
*
* An collateral gain of ( stake * [L_collateral - L_collateral(0)] )
* A debt increase of ( stake * [L_debt - L_debt(0)] )
*
* Where L_collateral(0) and L_debt(0) are snapshots of L_collateral and L_debt for the active Trove taken at the instant the stake was made
*/
uint256 public L_collateral;
uint256 public L_debt;
uint256 public lastDefaultInterestUpdate;
// Error trackers for the trove redistribution calculation
uint256 public lastCollateralError_Redistribution;
uint256 public lastDebtError_Redistribution;
uint256 internal totalActiveCollateral;
uint256 internal totalActiveDebt;
uint256 public interestPayable;
uint256 public defaultedCollateral;
uint256 public defaultedDebt;
uint256 public rewardIntegral;
uint128 public rewardRate;
uint32 public lastUpdate;
uint32 public periodFinish;
mapping(address => uint256) public rewardIntegralFor;
mapping(address => uint256) private pendingRewardFor;
// week -> total available rewards for 1 day within this week
uint256[65535] public dailyMintReward;
// week -> day -> total amount redeemed this day
uint32[7][65535] private totalMints;
// account -> data for latest activity
mapping(address => VolumeData) public accountLatestMint;
mapping(address => Trove) public Troves;
mapping(address => uint256) public surplusBalances;
// Map addresses with active troves to their RewardSnapshot
mapping(address => RewardSnapshot) public rewardSnapshots;
// Array of all active trove addresses - used to to compute an approximate hint off-chain, for the sorted list insertion
address[] TroveOwners;
struct VolumeData {
uint32 amount;
uint32 week;
uint32 day;
}
struct EmissionId {
uint16 debt;
uint16 minting;
}
// Store the necessary data for a trove
struct Trove {
uint256 debt;
uint256 coll;
uint256 stake;
Status status;
uint128 arrayIndex;
uint256 activeInterestIndex;
}
struct RedemptionTotals {
uint256 remainingDebt;
uint256 totalDebtToRedeem;
uint256 totalCollateralDrawn;
uint256 collateralFee;
uint256 collateralToSendToRedeemer;
uint256 decayedBaseRate;
uint256 price;
uint256 totalDebtSupplyAtStart;
}
struct SingleRedemptionValues {
uint256 debtLot;
uint256 collateralLot;
bool cancelledPartial;
}
// Object containing the collateral and debt snapshots for a given active trove
struct RewardSnapshot {
uint256 collateral;
uint256 debt;
}
enum TroveManagerOperation {
applyPendingRewards,
liquidateInNormalMode,
liquidateInRecoveryMode,
redeemCollateral
}
enum Status {
nonExistent,
active,
closedByOwner,
closedByLiquidation,
closedByRedemption
}
event TroveUpdated(
address indexed _borrower,
uint256 _debt,
uint256 _coll,
uint256 _stake,
TroveManagerOperation _operation
);
event Redemption(
uint256 _attemptedDebtAmount,
uint256 _actualDebtAmount,
uint256 _collateralSent,
uint256 _collateralFee
);
event TroveUpdated(address indexed _borrower, uint256 _debt, uint256 _coll, uint256 stake, uint8 operation);
event BaseRateUpdated(uint256 _baseRate);
event LastFeeOpTimeUpdated(uint256 _lastFeeOpTime);
event TotalStakesUpdated(uint256 _newTotalStakes);
event SystemSnapshotsUpdated(uint256 _totalStakesSnapshot, uint256 _totalCollateralSnapshot);
event LTermsUpdated(uint256 _L_collateral, uint256 _L_debt);
event TroveSnapshotsUpdated(uint256 _L_collateral, uint256 _L_debt);
event TroveIndexUpdated(address _borrower, uint256 _newIndex);
event EtherSent(address _to, uint256 _amount);
event RewardClaimed(address indexed account, address indexed recipient, uint256 claimed);
modifier whenNotPaused() {
require(!paused, "Collateral Paused");
_;
}
constructor(
address _prismaCore,
address _gasPoolAddress,
address _debtTokenAddress,
address _borrowerOperationsAddress,
address _treasury,
address _liquidationManager,
uint256 _gasCompensation
) PrismaOwnable(_prismaCore) PrismaBase(_gasCompensation) SystemStart(_prismaCore) {
gasPoolAddress = _gasPoolAddress;
debtToken = IDebtToken(_debtTokenAddress);
borrowerOperationsAddress = _borrowerOperationsAddress;
treasury = IPrismaTreasury(_treasury);
liquidationManager = _liquidationManager;
}
function setAddresses(address _priceFeedAddress, address _sortedTrovesAddress, address _collateralToken) external {
require(address(sortedTroves) == address(0));
priceFeed = IPriceFeed(_priceFeedAddress);
sortedTroves = ISortedTroves(_sortedTrovesAddress);
collateralToken = IERC20(_collateralToken);
systemDeploymentTime = block.timestamp;
sunsetting = false;
activeInterestIndex = INTEREST_PRECISION;
lastActiveIndexUpdate = block.timestamp;
lastDefaultInterestUpdate = block.timestamp;
}
function notifyRegisteredId(uint256[] calldata _assignedIds) external returns (bool) {
require(msg.sender == address(treasury));
require(emissionId.debt == 0, "Already assigned");
uint256 length = _assignedIds.length;
require(length == 2, "Incorrect ID count");
emissionId = EmissionId({ debt: uint16(_assignedIds[0]), minting: uint16(_assignedIds[1]) });
periodFinish = uint32(((block.timestamp / 1 weeks) + 1) * 1 weeks);
return true;
}
/**
* @notice Sets the pause state for this trove manager
* Pausing is used to mitigate risks in exceptional circumstances
* Functionalities affected by pausing are:
* - New borrowing is not possible
* - New collateral deposits are not possible
* @param _paused If true the protocol is paused
*/
function setPaused(bool _paused) external {
require((_paused && msg.sender == guardian()) || msg.sender == owner(), "Unauthorized");
paused = _paused;
}
/**
* @notice Sets a custom price feed for this trove manager
* @param _priceFeedAddress Price feed address
*/
function setPriceFeed(address _priceFeedAddress) external onlyOwner {
priceFeed = IPriceFeed(_priceFeedAddress);
}
/*
When sunsetting we:
1) Disable collateral handoff to SP
2) Greatly Increase interest rate to incentivize redemptions
3) Remove redemptions fees
4) Disable new loans
*/
function startCollateralSunset() external {
require(msg.sender == address(PRISMA_CORE), "Not prisma core");
sunsetting = true;
_accrueActiveInterests();
_redistributeDebtAndColl(0, 0); //Accrue defaults interests
interestRate = SUNSETTING_INTEREST_RATE;
// accrual function doesn't update timestamp if interest was 0
lastActiveIndexUpdate = block.timestamp;
redemptionFeeFloor = 0;
maxSystemDebt = 0;
}
/*
_minuteDecayFactor is calculated as
10**18 * (1/2)**(1/n)
where n = the half-life in minutes
*/
function setParameters(
uint256 _minuteDecayFactor,
uint256 _redemptionFeeFloor,
uint256 _maxRedemptionFee,
uint256 _borrowingFeeFloor,
uint256 _maxBorrowingFee,
uint256 _interestRateInBPS,
uint256 _maxSystemDebt
) public {
require(!sunsetting, "Cannot change after sunset");
if (minuteDecayFactor != 0) {
require(msg.sender == owner(), "Only owner");
}
require(
_minuteDecayFactor >= 977159968434245000 && // half-life of 30 minutes
_minuteDecayFactor <= 999931237762985000 // half-life of 1 week
);
require(_redemptionFeeFloor <= _maxRedemptionFee && _maxRedemptionFee <= DECIMAL_PRECISION);
require(_borrowingFeeFloor <= _maxBorrowingFee && _maxBorrowingFee <= DECIMAL_PRECISION);
_decayBaseRate();
minuteDecayFactor = _minuteDecayFactor;
redemptionFeeFloor = _redemptionFeeFloor;
maxRedemptionFee = _maxRedemptionFee;
borrowingFeeFloor = _borrowingFeeFloor;
maxBorrowingFee = _maxBorrowingFee;
maxSystemDebt = _maxSystemDebt;
require(_interestRateInBPS <= MAX_INTEREST_RATE_IN_BPS, "Interest > Maximum");
uint256 newInterestRate = (INTEREST_PRECISION * _interestRateInBPS) / (10000 * SECONDS_IN_YEAR);
if (newInterestRate != interestRate) {
_accrueActiveInterests();
// accrual function doesn't update timestamp if interest was 0
lastActiveIndexUpdate = block.timestamp;
_redistributeDebtAndColl(0, 0); //Accrue defaults interests
interestRate = newInterestRate;
}
}
function collectInterests() external {
require(interestPayable > 0, "Nothing to collect");
debtToken.mint(PRISMA_CORE.feeReceiver(), interestPayable);
interestPayable = 0;
}
// --- Getters ---
function fetchPrice() public returns (uint256) {
IPriceFeed _priceFeed = priceFeed;
if (address(_priceFeed) == address(0)) {
_priceFeed = IPriceFeed(PRISMA_CORE.priceFeed());
}
return _priceFeed.fetchPrice();
}
function getWeekAndDay() public view returns (uint256, uint256) {
uint256 duration = (block.timestamp - startTime);
uint256 week = duration / 1 weeks;
uint256 day = (duration % 1 weeks) / 1 days;
return (week, day);
}
function getTotalMints(uint256 week) external view returns (uint32[7] memory) {
return totalMints[week];
}
function getTroveOwnersCount() external view returns (uint256) {
return TroveOwners.length;
}
function getTroveFromTroveOwnersArray(uint256 _index) external view returns (address) {
return TroveOwners[_index];
}
function getTroveStatus(address _borrower) external view returns (uint256) {
return uint256(Troves[_borrower].status);
}
function getTroveStake(address _borrower) external view returns (uint256) {
return Troves[_borrower].stake;
}
/**
@notice Get the current total collateral and debt amounts for a trove
@dev Also includes pending rewards from redistribution
*/
function getTroveCollAndDebt(address _borrower) public view returns (uint256 coll, uint256 debt) {
(debt, coll, , ) = getEntireDebtAndColl(_borrower);
return (coll, debt);
}
/**
@notice Get the total and pending collateral and debt amounts for a trove
@dev Used by the liquidation manager
*/
function getEntireDebtAndColl(
address _borrower
) public view returns (uint256 debt, uint256 coll, uint256 pendingDebtReward, uint256 pendingCollateralReward) {
Trove storage t = Troves[_borrower];
debt = t.debt;
coll = t.coll;
(pendingCollateralReward, pendingDebtReward) = getPendingCollAndDebtRewards(_borrower);
// Accrued trove interest for correct liquidation values. This assumes the index to be updated.
uint256 troveInterestIndex = t.activeInterestIndex;
if (troveInterestIndex > 0) {
(uint256 currentIndex, ) = _calculateInterestIndex();
debt = (debt * currentIndex) / troveInterestIndex;
}
debt = debt + pendingDebtReward;
coll = coll + pendingCollateralReward;
}
function getEntireSystemColl() public view returns (uint256) {
return totalActiveCollateral + defaultedCollateral;
}
function getEntireSystemDebt() public view returns (uint256) {
uint256 currentActiveDebt = totalActiveDebt;
uint256 currentDefaultedDebt = defaultedDebt;
(, uint256 interestFactor) = _calculateInterestIndex();
if (interestFactor > 0) {
uint256 activeInterests = Math.mulDiv(currentActiveDebt, interestFactor, INTEREST_PRECISION);
currentActiveDebt = currentActiveDebt + activeInterests;
}
uint256 lastIndexUpdateCached = lastDefaultInterestUpdate;
if (lastIndexUpdateCached < block.timestamp) {
uint256 deltaT = block.timestamp - lastIndexUpdateCached;
interestFactor = deltaT * interestRate;
uint256 accruedInterest = Math.mulDiv(currentDefaultedDebt, interestFactor, INTEREST_PRECISION);
currentDefaultedDebt += accruedInterest;
}
return currentActiveDebt + currentDefaultedDebt;
}
function getEntireSystemBalances() external returns (uint256, uint256, uint256) {
return (getEntireSystemColl(), getEntireSystemDebt(), fetchPrice());
}
// --- Helper functions ---
// Return the nominal collateral ratio (ICR) of a given Trove, without the price. Takes a trove's pending coll and debt rewards from redistributions into account.
function getNominalICR(address _borrower) public view returns (uint256) {
(uint256 currentCollateral, uint256 currentDebt) = getTroveCollAndDebt(_borrower);
uint256 NICR = PrismaMath._computeNominalCR(currentCollateral, currentDebt);
return NICR;
}
// Return the current collateral ratio (ICR) of a given Trove. Takes a trove's pending coll and debt rewards from redistributions into account.
function getCurrentICR(address _borrower, uint256 _price) public view returns (uint256) {
(uint256 currentCollateral, uint256 currentDebt) = getTroveCollAndDebt(_borrower);
uint256 ICR = PrismaMath._computeCR(currentCollateral, currentDebt, _price);
return ICR;
}
function getTCR(uint256 _price) public view returns (uint256 TCR) {
uint256 entireSystemColl = getEntireSystemColl();
uint256 entireSystemDebt = getEntireSystemDebt();
TCR = PrismaMath._computeCR(entireSystemColl, entireSystemDebt, _price);
return TCR;
}
function getTotalActiveCollateral() public view returns (uint256) {
return totalActiveCollateral;
}
function getTotalActiveDebt() public view returns (uint256) {
uint256 currentActiveDebt = totalActiveDebt;
(, uint256 interestFactor) = _calculateInterestIndex();
if (interestFactor > 0) {
uint256 activeInterests = Math.mulDiv(currentActiveDebt, interestFactor, INTEREST_PRECISION);
currentActiveDebt = currentActiveDebt + activeInterests;
}
return currentActiveDebt;
}
// Get the borrower's pending accumulated collateral and debt rewards, earned by their stake
function getPendingCollAndDebtRewards(address _borrower) public view returns (uint256, uint256) {
RewardSnapshot memory snapshot = rewardSnapshots[_borrower];
uint256 coll = L_collateral - snapshot.collateral;
uint256 debt = L_debt - snapshot.debt;
if (coll + debt == 0 || Troves[_borrower].status != Status.active) return (0, 0);
uint256 stake = Troves[_borrower].stake;
return ((stake * coll) / DECIMAL_PRECISION, (stake * debt) / DECIMAL_PRECISION);
}
function hasPendingRewards(address _borrower) public view returns (bool) {
/*
* A Trove has pending rewards if its snapshot is less than the current rewards per-unit-staked sum:
* this indicates that rewards have occured since the snapshot was made, and the user therefore has
* pending rewards
*/
if (Troves[_borrower].status != Status.active) {
return false;
}
return (rewardSnapshots[_borrower].collateral < L_collateral);
}
// --- Redemption fee functions ---
/*
* This function has two impacts on the baseRate state variable:
* 1) decays the baseRate based on time passed since last redemption or debt borrowing operation.
* then,
* 2) increases the baseRate based on the amount redeemed, as a proportion of total supply
*/
function _updateBaseRateFromRedemption(
uint256 _collateralDrawn,
uint256 _price,
uint256 _totalDebtSupply
) internal returns (uint256) {
uint256 decayedBaseRate = _calcDecayedBaseRate();
/* Convert the drawn collateral back to debt at face value rate (1 debt:1 USD), in order to get
* the fraction of total supply that was redeemed at face value. */
uint256 redeemedDebtFraction = (_collateralDrawn * _price) / _totalDebtSupply;
uint256 newBaseRate = decayedBaseRate + (redeemedDebtFraction / BETA);
newBaseRate = PrismaMath._min(newBaseRate, DECIMAL_PRECISION); // cap baseRate at a maximum of 100%
// Update the baseRate state variable
baseRate = newBaseRate;
emit BaseRateUpdated(newBaseRate);
_updateLastFeeOpTime();
return newBaseRate;
}
function getRedemptionRate() public view returns (uint256) {
return _calcRedemptionRate(baseRate);
}
function getRedemptionRateWithDecay() public view returns (uint256) {
return _calcRedemptionRate(_calcDecayedBaseRate());
}
function _calcRedemptionRate(uint256 _baseRate) internal view returns (uint256) {
return
PrismaMath._min(
redemptionFeeFloor + _baseRate,
maxRedemptionFee // cap at a maximum of 100%
);
}
function getRedemptionFeeWithDecay(uint256 _collateralDrawn) external view returns (uint256) {
return _calcRedemptionFee(getRedemptionRateWithDecay(), _collateralDrawn);
}
function _calcRedemptionFee(uint256 _redemptionRate, uint256 _collateralDrawn) internal pure returns (uint256) {
uint256 redemptionFee = (_redemptionRate * _collateralDrawn) / DECIMAL_PRECISION;
require(redemptionFee < _collateralDrawn, "Fee exceeds returned collateral");
return redemptionFee;
}
// --- Borrowing fee functions ---
function getBorrowingRate() public view returns (uint256) {
return _calcBorrowingRate(baseRate);
}
function getBorrowingRateWithDecay() public view returns (uint256) {
return _calcBorrowingRate(_calcDecayedBaseRate());
}
function _calcBorrowingRate(uint256 _baseRate) internal view returns (uint256) {
return PrismaMath._min(borrowingFeeFloor + _baseRate, maxBorrowingFee);
}
function getBorrowingFee(uint256 _debt) external view returns (uint256) {
return _calcBorrowingFee(getBorrowingRate(), _debt);
}
function getBorrowingFeeWithDecay(uint256 _debt) external view returns (uint256) {
return _calcBorrowingFee(getBorrowingRateWithDecay(), _debt);
}
function _calcBorrowingFee(uint256 _borrowingRate, uint256 _debt) internal pure returns (uint256) {
return (_borrowingRate * _debt) / DECIMAL_PRECISION;
}
// --- Internal fee functions ---
// Update the last fee operation time only if time passed >= decay interval. This prevents base rate griefing.
function _updateLastFeeOpTime() internal {
uint256 timePassed = block.timestamp - lastFeeOperationTime;
if (timePassed >= SECONDS_IN_ONE_MINUTE) {
lastFeeOperationTime = block.timestamp;
emit LastFeeOpTimeUpdated(block.timestamp);
}
}
function _calcDecayedBaseRate() internal view returns (uint256) {
uint256 minutesPassed = (block.timestamp - lastFeeOperationTime) / SECONDS_IN_ONE_MINUTE;
uint256 decayFactor = PrismaMath._decPow(minuteDecayFactor, minutesPassed);
return (baseRate * decayFactor) / DECIMAL_PRECISION;
}
// --- Redemption functions ---
/* Send _debtAmount debt to the system and redeem the corresponding amount of collateral from as many Troves as are needed to fill the redemption
* request. Applies pending rewards to a Trove before reducing its debt and coll.
*
* Note that if _amount is very large, this function can run out of gas, specially if traversed troves are small. This can be easily avoided by
* splitting the total _amount in appropriate chunks and calling the function multiple times.
*
* Param `_maxIterations` can also be provided, so the loop through Troves is capped (if it’s zero, it will be ignored).This makes it easier to
* avoid OOG for the frontend, as only knowing approximately the average cost of an iteration is enough, without needing to know the “topology”
* of the trove list. It also avoids the need to set the cap in stone in the contract, nor doing gas calculations, as both gas price and opcode
* costs can vary.
*
* All Troves that are redeemed from -- with the likely exception of the last one -- will end up with no debt left, therefore they will be closed.
* If the last Trove does have some remaining debt, it has a finite ICR, and the reinsertion could be anywhere in the list, therefore it requires a hint.
* A frontend should use getRedemptionHints() to calculate what the ICR of this Trove will be after redemption, and pass a hint for its position
* in the sortedTroves list along with the ICR value that the hint was found for.
*
* If another transaction modifies the list between calling getRedemptionHints() and passing the hints to redeemCollateral(), it
* is very likely that the last (partially) redeemed Trove would end up with a different ICR than what the hint is for. In this case the
* redemption will stop after the last completely redeemed Trove and the sender will keep the remaining debt amount, which they can attempt
* to redeem later.
*/
function redeemCollateral(
uint256 _debtAmount,
address _firstRedemptionHint,
address _upperPartialRedemptionHint,
address _lowerPartialRedemptionHint,
uint256 _partialRedemptionHintNICR,
uint256 _maxIterations,
uint256 _maxFeePercentage
) external {
ISortedTroves _sortedTrovesCached = sortedTroves;
RedemptionTotals memory totals;
require(
_maxFeePercentage >= redemptionFeeFloor && _maxFeePercentage <= maxRedemptionFee,
"Max fee 0.5% to 100%"
);
require(block.timestamp >= systemDeploymentTime + BOOTSTRAP_PERIOD, "BOOTSTRAP_PERIOD");
totals.price = fetchPrice();
require(getTCR(totals.price) >= MCR, "Cannot redeem when TCR < MCR");
require(_debtAmount > 0, "Amount must be greater than zero");
require(debtToken.balanceOf(msg.sender) >= _debtAmount, "Insufficient balance");
_updateBalances();
totals.totalDebtSupplyAtStart = getEntireSystemDebt();
totals.remainingDebt = _debtAmount;
address currentBorrower;
if (_isValidFirstRedemptionHint(_sortedTrovesCached, _firstRedemptionHint, totals.price)) {
currentBorrower = _firstRedemptionHint;
} else {
currentBorrower = _sortedTrovesCached.getLast();
// Find the first trove with ICR >= MCR
while (currentBorrower != address(0) && getCurrentICR(currentBorrower, totals.price) < MCR) {
currentBorrower = _sortedTrovesCached.getPrev(currentBorrower);
}
}
// Loop through the Troves starting from the one with lowest collateral ratio until _amount of debt is exchanged for collateral
if (_maxIterations == 0) {
_maxIterations = type(uint256).max;
}
while (currentBorrower != address(0) && totals.remainingDebt > 0 && _maxIterations > 0) {
_maxIterations--;
// Save the address of the Trove preceding the current one, before potentially modifying the list
address nextUserToCheck = _sortedTrovesCached.getPrev(currentBorrower);
_applyPendingRewards(currentBorrower);
SingleRedemptionValues memory singleRedemption = _redeemCollateralFromTrove(
_sortedTrovesCached,
currentBorrower,
totals.remainingDebt,
totals.price,
_upperPartialRedemptionHint,
_lowerPartialRedemptionHint,
_partialRedemptionHintNICR
);
if (singleRedemption.cancelledPartial) break; // Partial redemption was cancelled (out-of-date hint, or new net debt < minimum), therefore we could not redeem from the last Trove
totals.totalDebtToRedeem = totals.totalDebtToRedeem + singleRedemption.debtLot;
totals.totalCollateralDrawn = totals.totalCollateralDrawn + singleRedemption.collateralLot;
totals.remainingDebt = totals.remainingDebt - singleRedemption.debtLot;
currentBorrower = nextUserToCheck;
}
require(totals.totalCollateralDrawn > 0, "Unable to redeem any amount");
// Decay the baseRate due to time passed, and then increase it according to the size of this redemption.
// Use the saved total debt supply value, from before it was reduced by the redemption.
_updateBaseRateFromRedemption(totals.totalCollateralDrawn, totals.price, totals.totalDebtSupplyAtStart);
// Calculate the collateral fee
totals.collateralFee = sunsetting ? 0 : _calcRedemptionFee(getRedemptionRate(), totals.totalCollateralDrawn);
_requireUserAcceptsFee(totals.collateralFee, totals.totalCollateralDrawn, _maxFeePercentage);
_sendCollateral(PRISMA_CORE.feeReceiver(), totals.collateralFee);
totals.collateralToSendToRedeemer = totals.totalCollateralDrawn - totals.collateralFee;
emit Redemption(_debtAmount, totals.totalDebtToRedeem, totals.totalCollateralDrawn, totals.collateralFee);
// Burn the total debt that is cancelled with debt, and send the redeemed collateral to msg.sender
debtToken.burn(msg.sender, totals.totalDebtToRedeem);
// Update Trove Manager debt, and send collateral to account
totalActiveDebt = totalActiveDebt - totals.totalDebtToRedeem;
_sendCollateral(msg.sender, totals.collateralToSendToRedeemer);
_resetState();
}
// Redeem as much collateral as possible from _borrower's Trove in exchange for debt up to _maxDebtAmount
function _redeemCollateralFromTrove(
ISortedTroves _sortedTrovesCached,
address _borrower,
uint256 _maxDebtAmount,
uint256 _price,
address _upperPartialRedemptionHint,
address _lowerPartialRedemptionHint,
uint256 _partialRedemptionHintNICR
) internal returns (SingleRedemptionValues memory singleRedemption) {
Trove storage t = Troves[_borrower];
// Determine the remaining amount (lot) to be redeemed, capped by the entire debt of the Trove minus the liquidation reserve
singleRedemption.debtLot = PrismaMath._min(_maxDebtAmount, t.debt - DEBT_GAS_COMPENSATION);
// Get the CollateralLot of equivalent value in USD
singleRedemption.collateralLot = (singleRedemption.debtLot * DECIMAL_PRECISION) / _price;
// Decrease the debt and collateral of the current Trove according to the debt lot and corresponding collateral to send
uint256 newDebt = (t.debt) - singleRedemption.debtLot;
uint256 newColl = (t.coll) - singleRedemption.collateralLot;
if (newDebt == DEBT_GAS_COMPENSATION) {
// No debt left in the Trove (except for the liquidation reserve), therefore the trove gets closed
_removeStake(_borrower);
_closeTrove(_borrower, Status.closedByRedemption);
_redeemCloseTrove(_borrower, DEBT_GAS_COMPENSATION, newColl);
emit TroveUpdated(_borrower, 0, 0, 0, TroveManagerOperation.redeemCollateral);
} else {
uint256 newNICR = PrismaMath._computeNominalCR(newColl, newDebt);
/*
* If the provided hint is out of date, we bail since trying to reinsert without a good hint will almost
* certainly result in running out of gas.
*
* If the resultant net debt of the partial is less than the minimum, net debt we bail.
*/
{
// We check if the ICR hint is reasonable up to date, with continuous interest there might be slight differences (<1bps)
uint256 icrError = _partialRedemptionHintNICR > newNICR
? _partialRedemptionHintNICR - newNICR
: newNICR - _partialRedemptionHintNICR;
if (
icrError > 5e14 ||
_getNetDebt(newDebt) < IBorrowerOperations(borrowerOperationsAddress).minNetDebt()
) {
singleRedemption.cancelledPartial = true;
return singleRedemption;
}
}
_sortedTrovesCached.reInsert(_borrower, newNICR, _upperPartialRedemptionHint, _lowerPartialRedemptionHint);
t.debt = newDebt;
t.coll = newColl;
_updateStakeAndTotalStakes(t);
emit TroveUpdated(_borrower, newDebt, newColl, t.stake, TroveManagerOperation.redeemCollateral);
}
return singleRedemption;
}
/*
* Called when a full redemption occurs, and closes the trove.
* The redeemer swaps (debt - liquidation reserve) debt for (debt - liquidation reserve) worth of collateral, so the debt liquidation reserve left corresponds to the remaining debt.
* In order to close the trove, the debt liquidation reserve is burned, and the corresponding debt is removed.
* The debt recorded on the trove's struct is zero'd elswhere, in _closeTrove.
* Any surplus collateral left in the trove, is sent to the Coll surplus pool, and can be later claimed by the borrower.
*/
function _redeemCloseTrove(address _borrower, uint256 _debt, uint256 _collateral) internal {
debtToken.burn(gasPoolAddress, _debt);
// Update Trove Manager debt, and send collateral to account
totalActiveDebt = totalActiveDebt - _debt;
// send collateral from Trove Manager to CollSurplus Pool
surplusBalances[_borrower] += _collateral;
totalActiveCollateral -= _collateral;
}
function _isValidFirstRedemptionHint(
ISortedTroves _sortedTroves,
address _firstRedemptionHint,
uint256 _price
) internal view returns (bool) {
if (
_firstRedemptionHint == address(0) ||
!_sortedTroves.contains(_firstRedemptionHint) ||
getCurrentICR(_firstRedemptionHint, _price) < MCR
) {
return false;
}
address nextTrove = _sortedTroves.getNext(_firstRedemptionHint);
return nextTrove == address(0) || getCurrentICR(nextTrove, _price) < MCR;
}
/**
* Claim remaining collateral from a redemption or from a liquidation with ICR > MCR in Recovery Mode
*/
function claimCollateral(address _receiver) external {
uint256 claimableColl = surplusBalances[msg.sender];
require(claimableColl > 0, "No collateral available to claim");
surplusBalances[msg.sender] = 0;
require(collateralToken.transfer(_receiver, claimableColl), "Sending surplus collateral failed");
}
// --- Reward Claim functions ---
function claimReward(address receiver) external returns (uint256) {
uint256 amount = _claimReward(msg.sender);
if (amount > 0) {
treasury.transferAllocatedTokens(msg.sender, receiver, amount);
}
emit RewardClaimed(msg.sender, receiver, amount);
return amount;
}
function treasuryClaimReward(address claimant, address) external returns (uint256) {
require(msg.sender == address(treasury));
return _claimReward(claimant);
}
function _claimReward(address account) internal returns (uint256) {
require(emissionId.debt > 0, "Rewards not active");
// update active debt rewards
_applyPendingRewards(account);
uint256 amount = pendingRewardFor[account];
if (amount > 0) pendingRewardFor[account] = 0;
// add pending mint awards
uint256 mintAmount = _getPendingMintReward(account);
if (mintAmount > 0) {
amount += mintAmount;
delete accountLatestMint[account];
}
return amount;
}
function claimableReward(address account) external view returns (uint256) {
// previously calculated rewards
uint256 amount = pendingRewardFor[account];
// pending active debt rewards
uint256 updated = periodFinish;
if (updated > block.timestamp) updated = block.timestamp;
uint256 duration = updated - lastUpdate;
uint256 integral = rewardIntegral;
if (duration > 0) {
uint256 supply = totalActiveDebt;
if (supply > 0) {
integral += (duration * rewardRate * 1e18) / supply;
}
}
uint256 integralFor = rewardIntegralFor[account];
if (integral > integralFor) {
amount += (Troves[account].debt * (integral - integralFor)) / 1e18;
}
// pending mint rewards
amount += _getPendingMintReward(account);
return amount;
}
function _getPendingMintReward(address account) internal view returns (uint256 amount) {
VolumeData memory data = accountLatestMint[account];
if (data.amount > 0) {
(uint256 week, uint256 day) = getWeekAndDay();
if (data.day != day || data.week != week) {
return (dailyMintReward[data.week] * data.amount) / totalMints[data.week][data.day];
}
}
}
function _updateIntegrals(address account, uint256 balance, uint256 supply) internal {
uint256 integral = _updateRewardIntegral(supply);
_updateIntegralForAccount(account, balance, integral);
}
function _updateIntegralForAccount(address account, uint256 balance, uint256 currentIntegral) internal {
uint256 integralFor = rewardIntegralFor[account];
if (currentIntegral > integralFor) {
pendingRewardFor[account] += (balance * (currentIntegral - integralFor)) / 1e18;
rewardIntegralFor[account] = currentIntegral;
}
}
function _updateRewardIntegral(uint256 supply) internal returns (uint256 integral) {
uint256 _periodFinish = periodFinish;
uint256 updated = _periodFinish;
if (updated > block.timestamp) updated = block.timestamp;
uint256 duration = updated - lastUpdate;
integral = rewardIntegral;
if (duration > 0) {
lastUpdate = uint32(updated);
if (supply > 0) {
integral += (duration * rewardRate * 1e18) / supply;
rewardIntegral = integral;
}
}
_fetchRewards(_periodFinish);
return integral;
}
function _fetchRewards(uint256 _periodFinish) internal {
EmissionId memory id = emissionId;
if (id.debt == 0) return;
uint256 currentWeek = getWeek();
if (currentWeek < (_periodFinish - startTime) / 1 weeks) return;
uint256 previousWeek = (_periodFinish - startTime) / 1 weeks - 1;
// active debt rewards
uint256 amount = treasury.allocateNewEmissions(id.debt);
if (block.timestamp < _periodFinish) {
uint256 remaining = _periodFinish - block.timestamp;
amount += remaining * rewardRate;
}
rewardRate = uint128(amount / REWARD_DURATION);
lastUpdate = uint32(block.timestamp);
periodFinish = uint32(block.timestamp + REWARD_DURATION);
// minting rewards
amount = treasury.allocateNewEmissions(id.minting);
uint256 reward = dailyMintReward[previousWeek];
if (reward > 0) {
uint32[7] memory totals = totalMints[previousWeek];
for (uint256 i = 0; i < 7; i++) {
if (totals[i] == 0) {
amount += reward;
}
}
}
dailyMintReward[currentWeek] = amount / 7;
}
// --- Trove Adjustment functions ---
function openTrove(
address _borrower,
uint256 _collateralAmount,
uint256 _compositeDebt,
uint256 NICR,
address _upperHint,
address _lowerHint
) external whenNotPaused returns (uint256 stake, uint256 arrayIndex) {
_requireCallerIsBO();
require(!sunsetting, "Cannot open while sunsetting");
uint256 supply = totalActiveDebt;