-
Notifications
You must be signed in to change notification settings - Fork 162
/
Prime.sol
884 lines (729 loc) · 33 KB
/
Prime.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
pragma solidity 0.8.13;
import { SafeERC20Upgradeable, IERC20Upgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC20/utils/SafeERC20Upgradeable.sol";
import { AccessControlledV8 } from "@venusprotocol/governance-contracts/contracts/Governance/AccessControlledV8.sol";
import { ResilientOracleInterface } from "@venusprotocol/oracle/contracts/interfaces/OracleInterface.sol";
import { PausableUpgradeable } from "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol";
import { MaxLoopsLimitHelper } from "@venusprotocol/isolated-pools/contracts/MaxLoopsLimitHelper.sol";
import { PrimeStorageV1 } from "./PrimeStorage.sol";
import { Scores } from "./libs/Scores.sol";
import { IPrimeLiquidityProvider } from "./Interfaces/IPrimeLiquidityProvider.sol";
import { IXVSVault } from "./Interfaces/IXVSVault.sol";
import { IVToken } from "./Interfaces/IVToken.sol";
import { IProtocolShareReserve } from "./Interfaces/IProtocolShareReserve.sol";
import { IIncomeDestination } from "./Interfaces/IIncomeDestination.sol";
error MarketNotSupported();
error InvalidLimit();
error IneligibleToClaim();
error WaitMoreTime();
error UserHasNoPrimeToken();
error InvalidCaller();
error InvalidComptroller();
error NoScoreUpdatesRequired();
error MarketAlreadyExists();
error InvalidAddress();
error InvalidBlocksPerYear();
contract Prime is IIncomeDestination, AccessControlledV8, PausableUpgradeable, MaxLoopsLimitHelper, PrimeStorageV1 {
using SafeERC20Upgradeable for IERC20Upgradeable;
/// @notice total blocks per year
/// @custom:oz-upgrades-unsafe-allow state-variable-immutable
uint256 public immutable BLOCKS_PER_YEAR;
/// @notice address of WBNB contract
/// @custom:oz-upgrades-unsafe-allow state-variable-immutable
address public immutable WBNB;
/// @notice address of vBNB contract
/// @custom:oz-upgrades-unsafe-allow state-variable-immutable
address public immutable vBNB;
/// @notice Emitted when prime token is minted
event Mint(address indexed user, bool isIrrevocable);
/// @notice Emitted when prime token is burned
event Burn(address indexed user);
/// @notice Emitted asset state is update by protocol share reserve
event UpdatedAssetsState(address indexed comptroller, address indexed asset);
/// @notice Emitted when a market is added to prime program
event MarketAdded(address indexed market, uint256 indexed supplyMultiplier, uint256 indexed borrowMultiplier);
/// @notice Emitted when mint limits are updated
event MintLimitsUpdated(uint256 indexed oldIrrevocableLimit, uint256 indexed oldRevocableLimit, uint256 indexed newIrrevocableLimit, uint256 newRevocableLimit);
/// @notice Emitted when user score is updated
event UserScoreUpdated(address indexed user);
/// @notice Emitted when alpha is updated
event AlphaUpdated(uint128 indexed oldNumerator, uint128 indexed oldDenominator, uint128 indexed newNumerator, uint128 newDenominator);
/// @notice Emitted when multiplier is updated
event MultiplierUpdated(address indexed market, uint256 indexed oldSupplyMultiplier, uint256 indexed oldBorrowMultiplier, uint256 newSupplyMultiplier, uint256 newBorrowMultiplier);
/// @notice Emitted when interest is claimed
event InterestClaimed(address indexed user, address indexed market, uint256 amount);
/// @custom:oz-upgrades-unsafe-allow constructor
constructor(address _wbnb, address _vbnb, uint256 _blocksPerYear) {
if (_wbnb == address(0)) revert InvalidAddress();
if (_vbnb == address(0)) revert InvalidAddress();
if (_blocksPerYear == 0) revert InvalidBlocksPerYear();
WBNB = _wbnb;
vBNB = _vbnb;
BLOCKS_PER_YEAR = _blocksPerYear;
// Note that the contract is upgradeable. Use initialize() or reinitializers
// to set the state variables.
_disableInitializers();
}
function initialize(
address _xvsVault,
address _xvsVaultRewardToken,
uint256 _xvsVaultPoolId,
uint128 _alphaNumerator,
uint128 _alphaDenominator,
address _accessControlManager,
address _protocolShareReserve,
address _primeLiquidityProvider,
address _comptroller,
address _oracle,
uint256 _loopsLimit
) external virtual initializer {
if (_xvsVault == address(0)) revert InvalidAddress();
if (_xvsVaultRewardToken == address(0)) revert InvalidAddress();
if (_protocolShareReserve == address(0)) revert InvalidAddress();
if (_comptroller == address(0)) revert InvalidAddress();
if (_oracle == address(0)) revert InvalidAddress();
if (_primeLiquidityProvider == address(0)) revert InvalidAddress();
alphaNumerator = _alphaNumerator;
alphaDenominator = _alphaDenominator;
xvsVaultRewardToken = _xvsVaultRewardToken;
xvsVaultPoolId = _xvsVaultPoolId;
xvsVault = _xvsVault;
nextScoreUpdateRoundId = 0;
protocolShareReserve = _protocolShareReserve;
primeLiquidityProvider = _primeLiquidityProvider;
comptroller = _comptroller;
oracle = ResilientOracleInterface(_oracle);
__AccessControlled_init(_accessControlManager);
__Pausable_init();
_setMaxLoopsLimit(_loopsLimit);
_pause();
}
/**
* @notice Update value of alpha
* @param _alphaNumerator numerator of alpha. If alpha is 0.5 then numerator is 1
* @param _alphaDenominator denominator of alpha. If alpha is 0.5 then denominator is 2
*/
function updateAlpha(uint128 _alphaNumerator, uint128 _alphaDenominator) external {
_checkAccessAllowed("updateAlpha(uint128,uint128)");
emit AlphaUpdated(alphaNumerator, alphaDenominator, _alphaNumerator, _alphaDenominator);
alphaNumerator = _alphaNumerator;
alphaDenominator = _alphaDenominator;
for (uint i = 0; i < allMarkets.length;) {
accrueInterest(allMarkets[i]);
unchecked {
i++;
}
}
_startScoreUpdateRound();
}
/**
* @notice Update multipliers for a market
* @param _supplyMultiplier new supply multiplier for the market
* @param _borrowMultiplier new borrow multiplier for the market
*/
function updateMultipliers(address market, uint256 _supplyMultiplier, uint256 _borrowMultiplier) external {
_checkAccessAllowed("updateMultipliers(address,uint256,uint256)");
if (!markets[market].exists) revert MarketNotSupported();
accrueInterest(market);
emit MultiplierUpdated(market, markets[market].supplyMultiplier, markets[market].borrowMultiplier, _supplyMultiplier, _borrowMultiplier);
markets[market].supplyMultiplier = _supplyMultiplier;
markets[market].borrowMultiplier = _borrowMultiplier;
_startScoreUpdateRound();
}
/**
* @notice Add a market to prime program
* @param vToken address of the market vToken
* @param supplyMultiplier the multiplier for supply cap. It should be converted to 1e18
* @param borrowMultiplier the multiplier for borrow cap. It should be converted to 1e18
*/
function addMarket(address vToken, uint256 supplyMultiplier, uint256 borrowMultiplier) external {
_checkAccessAllowed("addMarket(address,uint256,uint256)");
if (markets[vToken].exists) revert MarketAlreadyExists();
markets[vToken].rewardIndex = 0;
markets[vToken].supplyMultiplier = supplyMultiplier;
markets[vToken].borrowMultiplier = borrowMultiplier;
markets[vToken].sumOfMembersScore = 0;
markets[vToken].exists = true;
vTokenForAsset[_getUnderlying(vToken)] = vToken;
allMarkets.push(vToken);
_startScoreUpdateRound();
_ensureMaxLoops(allMarkets.length);
emit MarketAdded(vToken, supplyMultiplier, borrowMultiplier);
}
/**
* @notice Set limits for total tokens that can be mined
* @param irrevocableLimit total number of irrevocable tokens that can be minted
* @param revocableLimit total number of revocable tokens that can be minted
*/
function setLimit(uint256 irrevocableLimit, uint256 revocableLimit) external {
_checkAccessAllowed("setLimit(uint256,uint256)");
if (irrevocableLimit < _totalIrrevocable || revocableLimit < _totalRevocable) revert InvalidLimit();
emit MintLimitsUpdated(_irrevocableLimit, _revocableLimit, irrevocableLimit, revocableLimit);
_revocableLimit = revocableLimit;
_irrevocableLimit = irrevocableLimit;
}
/**
* @notice Directly issue prime tokens to users
* @param isIrrevocable is the tokens being issued is irrevocable
* @param users list of address to issue tokens to
*/
function issue(bool isIrrevocable, address[] calldata users) external {
_checkAccessAllowed("issue(bool,address[])");
if (isIrrevocable) {
for (uint i = 0; i < users.length;) {
_mint(true, users[i]);
_initializeMarkets(users[i]);
unchecked {
i++;
}
}
} else {
for (uint i = 0; i < users.length;) {
_mint(false, users[i]);
_initializeMarkets(users[i]);
delete stakedAt[users[i]];
unchecked {
i++;
}
}
}
}
/**
* @notice Executed by XVSVault whenever user's XVSVault balance changes
* @param user the account address whose balance was updated
*/
function xvsUpdated(address user) external {
uint256 totalStaked = _xvsBalanceOfUser(user);
bool isAccountEligible = isEligible(totalStaked);
if (tokens[user].exists && !isAccountEligible) {
if (tokens[user].isIrrevocable) {
_accrueInterestAndUpdateScore(user);
} else {
_burn(user);
}
} else if (!isAccountEligible && !tokens[user].exists && stakedAt[user] > 0) {
stakedAt[user] = 0;
} else if (stakedAt[user] == 0 && isAccountEligible && !tokens[user].exists) {
stakedAt[user] = block.timestamp;
} else if (tokens[user].exists && isAccountEligible) {
_accrueInterestAndUpdateScore(user);
}
}
/**
* @notice Retrieves an array of all available markets
* @return An array of addresses representing all available markets
*/
function getAllMarkets() external view returns (address[] memory) {
return allMarkets;
}
function _accrueInterestAndUpdateScore(address user) internal {
address[] storage _allMarkets = allMarkets;
for (uint i = 0; i < _allMarkets.length;) {
_executeBoost(user, _allMarkets[i]);
_updateScore(user, _allMarkets[i]);
unchecked {
i++;
}
}
}
function accrueInterestAndUpdateScore(address user, address market) public {
_executeBoost(user, market);
_updateScore(user, market);
}
/**
* @notice For claiming prime token when staking period is completed
*/
function claim() external {
if (stakedAt[msg.sender] == 0) revert IneligibleToClaim();
if (block.timestamp - stakedAt[msg.sender] < STAKING_PERIOD) revert WaitMoreTime();
stakedAt[msg.sender] = 0;
_mint(false, msg.sender);
_initializeMarkets(msg.sender);
}
/**
* @notice For burning any prime token
* @param user the account address for which the prime token will be burned
*/
function burn(address user) external {
_checkAccessAllowed("burn(address)");
_burn(user);
}
/**
* @notice fetch the numbers of seconds remaining for staking period to complete
* @param user the account address for which we are checking the remaining time
* @return timeRemaining the number of seconds the user needs to wait to claim prime token
*/
function claimTimeRemaining(address user) external view returns (uint256) {
if (stakedAt[user] == 0) revert IneligibleToClaim();
uint256 totalTimeStaked = block.timestamp - stakedAt[user];
if (totalTimeStaked < STAKING_PERIOD) {
return STAKING_PERIOD - totalTimeStaked;
} else {
return 0;
}
}
/**
* @notice Initializes all the markets for the user when a prime token is minted
* @param account the account address for which markets needs to be initialized
*/
function _initializeMarkets(address account) internal {
address[] storage _allMarkets = allMarkets;
for (uint i = 0; i < _allMarkets.length;) {
address market = _allMarkets[i];
accrueInterest(market);
interests[market][account].rewardIndex = markets[market].rewardIndex;
uint score = _calculateScore(market, account);
interests[market][account].score = score;
markets[market].sumOfMembersScore = markets[market].sumOfMembersScore + score;
unchecked {
i++;
}
}
}
/**
* @notice fetch the current XVS balance of user in the XVSVault
* @param user the account address for which markets needs to be initialized
* @return xvsBalance the XVS balance of user
*/
function _xvsBalanceOfUser(address user) internal view returns (uint256) {
(uint256 xvs, , uint256 pendingWithdrawals) = IXVSVault(xvsVault).getUserInfo(
xvsVaultRewardToken,
xvsVaultPoolId,
user
);
return (xvs - pendingWithdrawals);
}
/**
* @notice calculate the current score of user
* @param market the market for which to calculate the score
* @param user the account for which to calculate the score
* @return score the score of the user
*/
function _calculateScore(address market, address user) internal returns (uint256) {
uint256 xvsBalanceForScore = _xvsBalanceForScore(_xvsBalanceOfUser(user));
IVToken vToken = IVToken(market);
uint256 borrow = vToken.borrowBalanceStored(user);
uint256 exchangeRate = vToken.exchangeRateStored();
uint256 balanceOfAccount = vToken.balanceOf(user);
uint256 supply = (exchangeRate * balanceOfAccount) / EXP_SCALE;
address xvsToken = IXVSVault(xvsVault).xvsAddress();
oracle.updateAssetPrice(xvsToken);
oracle.updatePrice(market);
(uint256 capital, , ) = _capitalForScore(xvsBalanceForScore, borrow, supply, market);
capital = capital * (10 ** (18 - vToken.decimals()));
return Scores.calculateScore(xvsBalanceForScore, capital, alphaNumerator, alphaDenominator);
}
/**
* @notice calcukate the current XVS balance that will be used in calculation of score
* @param xvs the actual XVS balance of user
* @return xvsBalanceForScore the XVS balance to use in score
*/
function _xvsBalanceForScore(uint256 xvs) internal view returns (uint256) {
if (xvs > MAXIMUM_XVS_CAP) {
return MAXIMUM_XVS_CAP;
} else {
return xvs;
}
}
/**
* @notice calculate the capital for calculation of score
* @param xvs the actual XVS balance of user
* @param borrow the borrow balance of user
* @param supply the supply balance of user
* @param market the market vToken address
* @return capital the capital to use in calculation of score
* @return cappedSupply the capped supply of user
* @return cappedBorrow the capped borrow of user
*/
function _capitalForScore(
uint256 xvs,
uint256 borrow,
uint256 supply,
address market
) internal view returns (uint256, uint256, uint256) {
address xvsToken = IXVSVault(xvsVault).xvsAddress();
uint256 xvsPrice = oracle.getPrice(xvsToken);
uint256 borrowCapUSD = (xvsPrice * ((xvs * markets[market].borrowMultiplier) / EXP_SCALE)) / EXP_SCALE;
uint256 supplyCapUSD = (xvsPrice * ((xvs * markets[market].supplyMultiplier) / EXP_SCALE)) / EXP_SCALE;
uint256 tokenPrice = oracle.getUnderlyingPrice(market);
uint256 supplyUSD = (tokenPrice * supply) / EXP_SCALE;
uint256 borrowUSD = (tokenPrice * borrow) / EXP_SCALE;
if (supplyUSD >= supplyCapUSD) {
supply = supplyUSD > 0 ? (supply * supplyCapUSD) / supplyUSD : 0;
}
if (borrowUSD >= borrowCapUSD) {
borrow = borrowUSD > 0 ? (borrow * borrowCapUSD) / borrowUSD : 0;
}
return ((supply + borrow), supply, borrow);
}
/**
* @notice Used to mint a new prime token
* @param isIrrevocable is the tokens being issued is irrevocable
* @param user token owner
*/
function _mint(bool isIrrevocable, address user) internal {
if (tokens[user].exists) revert IneligibleToClaim();
tokens[user].exists = true;
tokens[user].isIrrevocable = isIrrevocable;
if (isIrrevocable) {
_totalIrrevocable++;
} else {
_totalRevocable++;
}
if (_totalIrrevocable > _irrevocableLimit || _totalRevocable > _revocableLimit) revert InvalidLimit();
emit Mint(user, isIrrevocable);
}
/**
* @notice Used to burn a new prime token
* @param user owner whose prime token to burn
*/
function _burn(address user) internal {
if (!tokens[user].exists) revert UserHasNoPrimeToken();
address[] storage _allMarkets = allMarkets;
for (uint i = 0; i < _allMarkets.length;) {
_executeBoost(user, _allMarkets[i]);
markets[_allMarkets[i]].sumOfMembersScore =
markets[_allMarkets[i]].sumOfMembersScore -
interests[_allMarkets[i]][user].score;
interests[_allMarkets[i]][user].score = 0;
interests[_allMarkets[i]][user].rewardIndex = 0;
unchecked {
i++;
}
}
if (tokens[user].isIrrevocable) {
_totalIrrevocable--;
} else {
_totalRevocable--;
}
tokens[user].exists = false;
tokens[user].isIrrevocable = false;
_updateRoundAfterTokenBurned(user);
emit Burn(user);
}
/**
* @notice Used to get if the XVS balance is eligible for prime token
* @param amount amount of XVS
*/
function isEligible(uint256 amount) internal view returns (bool) {
if (amount >= MINIMUM_STAKED_XVS) {
return true;
}
return false;
}
/**
* @notice Accrue rewards for the user. Must be called by Comptroller before changing account's borrow or supply balance.
* @param user account for which we need to accrue rewards
* @param vToken the market for which we need to accrue rewards
*/
function _executeBoost(address user, address vToken) internal {
if (!markets[vToken].exists || !tokens[user].exists) {
return;
}
accrueInterest(vToken);
interests[vToken][user].accrued += _interestAccrued(vToken, user);
interests[vToken][user].rewardIndex = markets[vToken].rewardIndex;
}
/**
* @notice Update total score of user and market. Must be called after changing account's borrow or supply balance.
* @param user account for which we need to update score
* @param market the market for which we need to score
*/
function _updateScore(address user, address market) internal {
if (!markets[market].exists) {
return;
}
if (!tokens[user].exists) {
return;
}
uint score = _calculateScore(market, user);
markets[market].sumOfMembersScore = markets[market].sumOfMembersScore - interests[market][user].score + score;
interests[market][user].score = score;
}
/**
* @notice Distributes income from market since last distribution
* @param vToken the market for which to distribute the income
*/
function accrueInterest(address vToken) public {
if (!markets[vToken].exists) revert MarketNotSupported();
address underlying = _getUnderlying(vToken);
IPrimeLiquidityProvider _primeLiquidityProvider = IPrimeLiquidityProvider(primeLiquidityProvider);
uint totalIncomeUnreleased = IProtocolShareReserve(protocolShareReserve).getUnreleasedFunds(
comptroller,
IProtocolShareReserve.Schema.SPREAD_PRIME_CORE,
address(this),
underlying
);
uint256 distributionIncome = totalIncomeUnreleased - unreleasedPSRIncome[underlying];
_primeLiquidityProvider.accrueTokens(underlying);
uint256 totalAccruedInPLP = _primeLiquidityProvider.tokenAmountAccrued(underlying);
uint256 unreleasedPLPAccruedInterest = totalAccruedInPLP - unreleasedPLPIncome[underlying];
distributionIncome += unreleasedPLPAccruedInterest;
if (distributionIncome == 0) {
return;
}
unreleasedPSRIncome[underlying] = totalIncomeUnreleased;
unreleasedPLPIncome[underlying] = totalAccruedInPLP;
uint256 delta;
if (markets[vToken].sumOfMembersScore > 0) {
delta = ((distributionIncome * EXP_SCALE) / markets[vToken].sumOfMembersScore);
}
markets[vToken].rewardIndex = markets[vToken].rewardIndex + delta;
}
/**
* @notice Returns boosted interest accrued for a user
* @param vToken the market for which to fetch the accrued interest
* @param user the account for which to get the accrued interest
*/
function getInterestAccrued(address vToken, address user) public returns (uint256) {
accrueInterest(vToken);
return _interestAccrued(vToken, user);
}
function _interestAccrued(address vToken, address user) internal returns (uint256) {
uint256 index = markets[vToken].rewardIndex - interests[vToken][user].rewardIndex;
uint256 score = interests[vToken][user].score;
return (index * score) / EXP_SCALE;
}
/**
* @notice For user to claim boosted yield
* @param vToken the market for which claim the accrued interest
*/
function claimInterest(address vToken) external whenNotPaused {
_claimInterest(vToken, msg.sender);
}
/**
* @notice For user to claim boosted yield
* @param vToken the market for which claim the accrued interest
*/
function claimInterest(address vToken, address user) external whenNotPaused {
_claimInterest(vToken, user);
}
/**
* @notice To transfer the accrued interest to user
* @param vToken the market for which claim the accrued interest
* @param user the account for which to get the accrued interest
*/
function _claimInterest(address vToken, address user) internal {
uint256 amount = getInterestAccrued(vToken, user);
amount += interests[vToken][user].accrued;
interests[vToken][user].rewardIndex = markets[vToken].rewardIndex;
interests[vToken][user].accrued = 0;
address underlying = _getUnderlying(vToken);
IERC20Upgradeable asset = IERC20Upgradeable(underlying);
if (amount > asset.balanceOf(address(this))) {
address[] memory assets = new address[](1);
assets[0] = address(asset);
IProtocolShareReserve(protocolShareReserve).releaseFunds(comptroller, assets);
if (amount > asset.balanceOf(address(this))) {
IPrimeLiquidityProvider(primeLiquidityProvider).releaseFunds(address(asset));
unreleasedPLPIncome[underlying] = 0;
}
}
asset.safeTransfer(user, amount);
emit InterestClaimed(user, vToken, amount);
}
/**
* @notice Callback by ProtocolShareReserve to update assets state when funds are released to this contract
* @param _comptroller The address of the Comptroller whose income is distributed
* @param asset The address of the asset whose income is distributed
*/
function updateAssetsState(address _comptroller, address asset) external {
if (msg.sender != protocolShareReserve) revert InvalidCaller();
if (comptroller != _comptroller) revert InvalidComptroller();
address vToken = vTokenForAsset[asset];
if (vToken == address(0)) revert MarketNotSupported();
IVToken market = IVToken(vToken);
unreleasedPSRIncome[_getUnderlying(address(market))] = 0;
emit UpdatedAssetsState(comptroller, asset);
}
function _getUnderlying(address vToken) internal view returns (address) {
if (vToken == vBNB) {
return WBNB;
} else {
return IVToken(vToken).underlying();
}
}
//////////////////////////////////////////////////
/////// Update Scores after Config Change ///////
////////////////////////////////////////////////
/**
* @notice Update total score of multiple users and market
* @param users accounts for which we need to update score
*/
function updateScores(address[] memory users) external {
if (pendingScoreUpdates == 0) revert NoScoreUpdatesRequired();
if (nextScoreUpdateRoundId == 0) revert NoScoreUpdatesRequired();
for (uint256 i = 0; i < users.length;) {
address user = users[i];
if (!tokens[user].exists) revert UserHasNoPrimeToken();
if (isScoreUpdated[nextScoreUpdateRoundId][user]) continue;
address[] storage _allMarkets = allMarkets;
for (uint j = 0; j < _allMarkets.length;) {
address market = _allMarkets[j];
accrueInterestAndUpdateScore(user, market);
unchecked {
j++;
}
}
pendingScoreUpdates--;
isScoreUpdated[nextScoreUpdateRoundId][user] = true;
unchecked {
i++;
}
emit UserScoreUpdated(user);
}
}
/**
* @notice starts round to update scores of a particular or all markets
*/
function _startScoreUpdateRound() internal {
nextScoreUpdateRoundId++;
totalScoreUpdatesRequired = _totalIrrevocable + _totalRevocable;
pendingScoreUpdates = totalScoreUpdatesRequired;
}
/**
* @notice update the required score updates when token is burned before round is completed
*/
function _updateRoundAfterTokenBurned(address user) internal {
if (totalScoreUpdatesRequired > 0) totalScoreUpdatesRequired--;
if (pendingScoreUpdates > 0 && !isScoreUpdated[nextScoreUpdateRoundId][user]) {
pendingScoreUpdates--;
}
}
//////////////////////////////////////////////////
//////////////// APR Calculation ////////////////
////////////////////////////////////////////////
/**
* @notice Returns the income the market generates per block
* @param vToken the market for which to fetch the income per block
* @return income the amount of tokens generated as income per block
*/
function _incomePerBlock(address vToken) internal view returns (uint256) {
IVToken market = IVToken(vToken);
return ((((market.totalBorrows() * market.borrowRatePerBlock()) / EXP_SCALE) * market.reserveFactorMantissa()) /
EXP_SCALE);
}
/**
* @notice the percentage of income we distribute among the prime token holders
* @return percentage the percentage returned without mantissa
*/
function _distributionPercentage() internal view returns (uint256) {
return
IProtocolShareReserve(protocolShareReserve).getPercentageDistribution(
address(this),
IProtocolShareReserve.Schema.SPREAD_PRIME_CORE
);
}
/**
* @notice the total income that's going to be distributed in a year to prime token holders
* @param vToken the market for which to fetch the total income that's going to distributed in a year
* @return amount the total income
*/
function _incomeDistributionYearly(address vToken) internal view returns (uint256 amount) {
uint256 totalIncomePerBlockFromMarket = _incomePerBlock(vToken);
uint256 incomePerBlockForDistributionFromMarket = (totalIncomePerBlockFromMarket * _distributionPercentage()) /
IProtocolShareReserve(protocolShareReserve).MAX_PERCENT();
amount += BLOCKS_PER_YEAR * incomePerBlockForDistributionFromMarket;
uint256 totalIncomePerBlockFromPLP = IPrimeLiquidityProvider(primeLiquidityProvider)
.getEffectiveDistributionSpeed(_getUnderlying(vToken));
amount += BLOCKS_PER_YEAR * totalIncomePerBlockFromPLP;
}
/**
* @notice used to calculate the supply and borrow APR of the user
* @param vToken the market for which to fetch the APR
* @param user the user whose APR we need to calculate
* @param totalSupply the total token supply of the user
* @param totalBorrow the total tokens borrowed by the user
* @param totalCappedSupply the total token capped supply of the user
* @param totalCappedBorrow the total capped tokens borrowed by the user
* @param userScore the score of the user
* @param totalScore the total market score
* @return supplyAPR the supply APR of the user
* @return borrowAPR the borrow APR of the user
*/
function _calculateUserAPR(
address vToken,
address user,
uint256 totalSupply,
uint256 totalBorrow,
uint256 totalCappedSupply,
uint256 totalCappedBorrow,
uint256 userScore,
uint256 totalScore
) internal view returns (uint256 supplyAPR, uint256 borrowAPR) {
if (totalScore == 0) return (0, 0);
uint256 userYearlyIncome = (userScore * _incomeDistributionYearly(vToken)) / totalScore;
uint256 totalCappedValue = totalCappedSupply + totalCappedBorrow;
if (totalCappedValue == 0) return (0, 0);
uint256 userSupplyIncomeYearly = (userYearlyIncome * totalCappedSupply) / totalCappedValue;
uint256 userBorrowIncomeYearly = (userYearlyIncome * totalCappedBorrow) / totalCappedValue;
supplyAPR = totalSupply == 0 ? 0 : ((userSupplyIncomeYearly * MAXIMUM_BPS) / totalSupply);
borrowAPR = totalBorrow == 0 ? 0 : ((userBorrowIncomeYearly * MAXIMUM_BPS) / totalBorrow);
}
/**
* @notice Returns supply and borrow APR for user for a given market
* @param market the market for which to fetch the APR
* @param user the account for which to get the APR
* @return supplyAPR supply APR of the user
* @return borrowAPR borrow APR of the user
*/
function calculateAPR(address market, address user) external view returns (uint256 supplyAPR, uint256 borrowAPR) {
IVToken vToken = IVToken(market);
uint256 borrow = vToken.borrowBalanceStored(user);
uint256 exchangeRate = vToken.exchangeRateStored();
uint256 balanceOfAccount = vToken.balanceOf(user);
uint256 supply = (exchangeRate * balanceOfAccount) / EXP_SCALE;
uint256 userScore = interests[market][user].score;
uint256 totalScore = markets[market].sumOfMembersScore;
uint256 xvsBalanceForScore = _xvsBalanceForScore(_xvsBalanceOfUser(user));
(, uint256 cappedSupply, uint256 cappedBorrow) = _capitalForScore(
xvsBalanceForScore,
borrow,
supply,
address(vToken)
);
return _calculateUserAPR(market, user, supply, borrow, cappedSupply, cappedBorrow, userScore, totalScore);
}
/**
* @notice Returns supply and borrow APR for estimated supply, borrow and XVS staked
* @param market the market for which to fetch the APR
* @param user the account for which to get the APR
* @param borrow hypothetical borrow amount
* @param supply hypothetical supply amount
* @param xvsStaked hypothetical staked XVS amount
* @return supplyAPR supply APR of the user
* @return borrowAPR borrow APR of the user
*/
function estimateAPR(
address market,
address user,
uint256 borrow,
uint256 supply,
uint256 xvsStaked
) external view returns (uint256 supplyAPR, uint256 borrowAPR) {
uint256 totalScore = markets[market].sumOfMembersScore - interests[market][user].score;
uint256 xvsBalanceForScore = _xvsBalanceForScore(xvsStaked);
(uint256 capital, uint256 cappedSupply, uint256 cappedBorrow) = _capitalForScore(
xvsBalanceForScore,
borrow,
supply,
market
);
uint256 userScore = Scores.calculateScore(xvsBalanceForScore, capital, alphaNumerator, alphaDenominator);
totalScore = totalScore + userScore;
return _calculateUserAPR(market, user, supply, borrow, cappedSupply, cappedBorrow, userScore, totalScore);
}
//////////////////////////////////////////////////
//////////////// (Un)Pause Claim ////////////////
////////////////////////////////////////////////
/**
* @notice To pause or unpuase claiming of interest
*/
function togglePause() external {
_checkAccessAllowed("togglePause()");
if (paused()) {
_unpause();
} else {
_pause();
}
}
}