-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathMultiRewardDistributor.sol
1248 lines (1079 loc) · 47.5 KB
/
MultiRewardDistributor.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.17;
import {ReentrancyGuard} from "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import {Initializable} from "@openzeppelin/contracts/proxy/utils/Initializable.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {Pausable} from "@openzeppelin/contracts/security/Pausable.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {MToken} from "@protocol/core/MToken.sol";
import {Comptroller} from "@protocol/core/Comptroller.sol";
import {MTokenInterface} from "@protocol/core/MTokenInterfaces.sol";
import {ExponentialNoError} from "@protocol/core/ExponentialNoError.sol";
import {MultiRewardDistributorCommon} from "@protocol/core/MultiRewardDistributor/MultiRewardDistributorCommon.sol";
/**
@title A multi-asset distributor that tracks mTokens supply/borrows
@author Octavius - [email protected]
This contract integrates with the Moonwell Comptroller and manages all reward disbursal and index
calculations both for the global market indices as well as individual user indices on those markets.
It is largely the same logic that compound uses, just generalized (meaning that transfers will not
fail if things can't be sent out, but the excess is accrued on the books to be sent later).
Each market has an array of configs, each with a unique emission token owned by a specific team/user.
That owner can adjust supply and borrow emissions, end times, and
This emitter also supports native assets, but keep in mind that things get complicated with multiple
owners managing a native asset emitter - one owner can drain the contract by increasing their own
Delegates admin control to the comptroller's admin (no internal admin controls).
There is a hard rule that each market should only have 1 config with a specific emission token.
Emission configs are non-removable because they hold the supplier/borrower indices and that would
cause rewards to not be disbursed properly when a config is removed.
There is a pause guardian in this contract that can immediately stop all token emissions. Accruals
still happen but no tokens will be sent out when the circuit breaker is popped. Much like the pause
guardians on the Comptroller, only the comptroller's admin can actually unpause things.
*/
contract MultiRewardDistributor is
Pausable,
ReentrancyGuard,
Initializable,
MultiRewardDistributorCommon,
ExponentialNoError
{
using SafeERC20 for IERC20;
/// @notice The main data storage for this contract, holds a mapping of mToken to array
// of market configs
mapping(address => MarketEmissionConfig[]) public marketConfigs;
/// @notice Comptroller this distributor is bound to
Comptroller public comptroller; /// we can't make this immutable because we are using proxies
/// @notice The pause guardian for this contract
address public pauseGuardian;
/// @notice The initialIndexConstant, used to initialize indexes, and taken from the Comptroller
uint224 public constant initialIndexConstant = 1e36;
/// @notice The emission cap dictates an upper limit for reward speed emission speed configs
/// @dev By default, is set to 100 1e18 token emissions / sec to avoid unbounded
/// computation/multiplication overflows
uint256 public emissionCap;
// Some structs we can't move to the interface
struct CurrentMarketData {
uint256 totalMTokens;
uint256 totalBorrows;
Exp marketBorrowIndex;
}
struct CalculatedData {
CurrentMarketData marketData;
MTokenData mTokenInfo;
}
/// construct the logic contract and initialize so that the initialize function is uncallable
/// from the implementation and only callable from the proxy
constructor() {
_disableInitializers();
}
function initialize(
address _comptroller,
address _pauseGuardian
) external initializer {
// Sanity check the params
require(
_comptroller != address(0),
"Comptroller can't be the 0 address!"
);
require(
_pauseGuardian != address(0),
"Pause Guardian can't be the 0 address!"
);
comptroller = Comptroller(payable(_comptroller));
require(
comptroller.isComptroller(),
"Can't bind to something that's not a comptroller!"
);
pauseGuardian = _pauseGuardian;
emissionCap = 100e18;
}
/*
====================================================================================================
ACL Modifiers
all modifiers allow for the admin to call in to take actions within this contract, the idea being that
the timelock can act like an owner of the config to set parameters, and act like the comptroller to
kick the reward index updates, and act like a pause guardian to pause things.
====================================================================================================
*/
/// @notice Only allow the comptroller's admin to take an action, usually the timelock
modifier onlyComptrollersAdmin() {
require(
msg.sender == address(comptroller.admin()),
"Only the comptroller's administrator can do this!"
);
_;
}
/// @notice Only allow the comptroller OR the comptroller's admin to take an action
modifier onlyComptrollerOrAdmin() {
require(
msg.sender == address(comptroller) ||
msg.sender == comptroller.admin(),
"Only the comptroller or comptroller admin can call this function"
);
_;
}
/// @notice Only allow the emission config owner OR the comptroller's admin to take an action
modifier onlyEmissionConfigOwnerOrAdmin(
MToken _mToken,
address emissionToken
) {
MarketEmissionConfig
storage emissionConfig = fetchConfigByEmissionToken(
_mToken,
emissionToken
);
require(
msg.sender == emissionConfig.config.owner ||
msg.sender == comptroller.admin(),
"Only the config owner or comptroller admin can call this function"
);
_;
}
/// @notice Only allow the pause guardian OR the comptroller's admin to take an action
modifier onlyPauseGuardianOrAdmin() {
require(
msg.sender == pauseGuardian || msg.sender == comptroller.admin(),
"Only the pause guardian or comptroller admin can call this function"
);
_;
}
/*
====================================================================================================
External/publicly accessible API
The main public API for the contract, generally focused on getting a user's outstanding rewards or
pulling down specific configs. Users should call `claimRewards` on the comptroller as usual to recv
their rewards.
====================================================================================================
*/
/**
* @notice Get the current owner of a config
* @param _mToken The market to get a config for
* @param _emissionToken The reward token address
*/
function getCurrentOwner(
MToken _mToken,
address _emissionToken
) external view returns (address) {
MarketEmissionConfig
storage emissionConfig = fetchConfigByEmissionToken(
_mToken,
_emissionToken
);
return emissionConfig.config.owner;
}
/// @notice A view to enumerate all configs for a given market, does not include index data
function getAllMarketConfigs(
MToken _mToken
) external view returns (MarketConfig[] memory) {
MarketEmissionConfig[] storage configs = marketConfigs[
address(_mToken)
];
MarketConfig[] memory outputMarketConfigs = new MarketConfig[](
configs.length
);
// Pop out the MarketConfigs to return them
for (uint256 index = 0; index < configs.length; index++) {
MarketEmissionConfig storage emissionConfig = configs[index];
outputMarketConfigs[index] = emissionConfig.config;
}
return outputMarketConfigs;
}
/// @notice A view to get a config for a specific market/emission token pair
function getConfigForMarket(
MToken _mToken,
address _emissionToken
) external view returns (MarketConfig memory) {
MarketEmissionConfig
storage emissionConfig = fetchConfigByEmissionToken(
_mToken,
_emissionToken
);
return emissionConfig.config;
}
/// @notice A view to enumerate a user's rewards across all markets and all emission tokens
function getOutstandingRewardsForUser(
address _user
) external view returns (RewardWithMToken[] memory) {
MToken[] memory markets = comptroller.getAllMarkets();
RewardWithMToken[] memory outputData = new RewardWithMToken[](
markets.length
);
for (uint256 index = 0; index < markets.length; index++) {
RewardInfo[] memory rewardInfo = getOutstandingRewardsForUser(
markets[index],
_user
);
outputData[index] = RewardWithMToken(
address(markets[index]),
rewardInfo
);
}
return outputData;
}
/// @notice A view to enumerate a user's rewards across a specified market and all emission tokens for that market
function getOutstandingRewardsForUser(
MToken _mToken,
address _user
) public view returns (RewardInfo[] memory) {
// Global config for this mToken
MarketEmissionConfig[] storage configs = marketConfigs[
address(_mToken)
];
// Output var
RewardInfo[] memory outputRewardData = new RewardInfo[](configs.length);
// Code golf to avoid too many local vars :rolling-eyes:
CalculatedData memory calcData = CalculatedData({
marketData: CurrentMarketData({
totalMTokens: _mToken.totalSupply(),
totalBorrows: _mToken.totalBorrows(),
marketBorrowIndex: Exp({mantissa: _mToken.borrowIndex()})
}),
mTokenInfo: MTokenData({
mTokenBalance: _mToken.balanceOf(_user),
borrowBalanceStored: _mToken.borrowBalanceStored(_user)
})
});
for (uint256 index = 0; index < configs.length; index++) {
MarketEmissionConfig storage emissionConfig = configs[index];
// Calculate our new global supply index
IndexUpdate memory supplyUpdate = calculateNewIndex(
emissionConfig.config.supplyEmissionsPerSec,
emissionConfig.config.supplyGlobalTimestamp,
emissionConfig.config.supplyGlobalIndex,
emissionConfig.config.endTime,
calcData.marketData.totalMTokens
);
// Calculate our new global borrow index
IndexUpdate memory borrowUpdate = calculateNewIndex(
emissionConfig.config.borrowEmissionsPerSec,
emissionConfig.config.borrowGlobalTimestamp,
emissionConfig.config.borrowGlobalIndex,
emissionConfig.config.endTime,
div_(
calcData.marketData.totalBorrows,
calcData.marketData.marketBorrowIndex
)
);
// Calculate outstanding supplier side rewards
uint256 supplierRewardsAccrued = calculateSupplyRewardsForUser(
emissionConfig,
supplyUpdate.newIndex,
calcData.mTokenInfo.mTokenBalance,
_user
);
uint256 borrowerRewardsAccrued = calculateBorrowRewardsForUser(
emissionConfig,
borrowUpdate.newIndex,
calcData.marketData.marketBorrowIndex,
calcData.mTokenInfo,
_user
);
outputRewardData[index] = RewardInfo({
emissionToken: emissionConfig.config.emissionToken,
totalAmount: borrowerRewardsAccrued + supplierRewardsAccrued,
supplySide: supplierRewardsAccrued,
borrowSide: borrowerRewardsAccrued
});
}
return outputRewardData;
}
/// @notice A view to get the current emission caps
function getCurrentEmissionCap() external view returns (uint256) {
return emissionCap;
}
/// @notice view to get the cached global supply index for an mToken and emission index
/// @param mToken The market to get a config for
/// @param index The index of the config to get
function getGlobalSupplyIndex(
address mToken,
uint256 index
) public view returns (uint256) {
MarketEmissionConfig storage emissionConfig = marketConfigs[mToken][
index
];
// Set the new values in storage
return emissionConfig.config.supplyGlobalIndex;
}
/// @notice view to get the cached global borrow index for an mToken and emission index
/// @param mToken The market to get a config for
/// @param index The index of the config to get
function getGlobalBorrowIndex(
address mToken,
uint256 index
) public view returns (uint256) {
MarketEmissionConfig storage emissionConfig = marketConfigs[mToken][
index
];
// Set the new values in storage
return emissionConfig.config.borrowGlobalIndex;
}
/*
====================================================================================================
Administrative API
Should be only callable by the comptroller's admin (usually the timelock), this is the only way
to add new configurations to the markets. There's also a rescue assets function that will sweep
tokens out of this contract and to the timelock, the thought being that rescuing accidentally sent
funds or sweeping existing tokens to a new distributor is possible.
====================================================================================================
*/
/**
* @notice Add a new emission configuration for a specific market
* @dev Emission config must not already exist for the specified market (unique to the emission token)
*/
function _addEmissionConfig(
MToken _mToken,
address _owner,
address _emissionToken,
uint256 _supplyEmissionPerSec,
uint256 _borrowEmissionsPerSec,
uint256 _endTime
) external onlyComptrollersAdmin {
// Ensure market is listed in the comptroller before accepting a config for it (should always be checked
// in the comptroller first, but never hurts to codify that assertion/requirement here.
(bool tokenIsListed, ) = comptroller.markets(address(_mToken));
require(
tokenIsListed,
"The market requested to be added is un-listed!"
);
// Sanity check emission speeds are below emissionCap
require(
_supplyEmissionPerSec < emissionCap,
"Cannot set a supply reward speed higher than the emission cap!"
);
require(
_borrowEmissionsPerSec < emissionCap,
"Cannot set a borrow reward speed higher than the emission cap!"
);
// Sanity check end time is some time in the future
require(
_endTime > block.timestamp + 1,
"The _endTime parameter must be in the future!"
);
MarketEmissionConfig[] storage configs = marketConfigs[
address(_mToken)
];
// Sanity check to ensure that the emission token doesn't already exist in a config
for (uint256 index = 0; index < configs.length; index++) {
MarketEmissionConfig storage mTokenConfig = configs[index];
require(
mTokenConfig.config.emissionToken != _emissionToken,
"Emission token already listed!"
);
}
// Things look good, create a config
MarketConfig memory config = MarketConfig({
// Set the owner of the reward distributor config
owner: _owner,
// Set the emission token address
emissionToken: _emissionToken,
// Set the time that the emission campaign should end at
endTime: _endTime,
// Initialize the global supply
supplyGlobalTimestamp: safe32(
block.timestamp,
"block timestamp exceeds 32 bits"
),
supplyGlobalIndex: initialIndexConstant,
// Initialize the global borrow index + timestamp
borrowGlobalTimestamp: safe32(
block.timestamp,
"block timestamp exceeds 32 bits"
),
borrowGlobalIndex: initialIndexConstant,
// Set supply and reward borrow speeds
supplyEmissionsPerSec: _supplyEmissionPerSec,
borrowEmissionsPerSec: _borrowEmissionsPerSec
});
emit NewConfigCreated(
_mToken,
_owner,
_emissionToken,
_supplyEmissionPerSec,
_borrowEmissionsPerSec,
_endTime
);
// Go push in our new config
MarketEmissionConfig storage newConfig = configs.push();
newConfig.config = config;
}
/**
* @notice Sweep ERC-20 tokens from the comptroller to the admin
* @param _tokenAddress The address of the token to transfer
* @param _amount The amount of tokens to sweep, uint256.max means everything
*/
function _rescueFunds(
address _tokenAddress,
uint256 _amount
) external onlyComptrollersAdmin {
IERC20 token = IERC20(_tokenAddress);
// Similar to mTokens, if this is uint256.max that means "transfer everything"
if (_amount == type(uint256).max) {
token.safeTransfer(
comptroller.admin(),
token.balanceOf(address(this))
);
} else {
token.safeTransfer(comptroller.admin(), _amount);
}
emit FundsRescued(_tokenAddress, _amount);
}
/**
* @notice Sets a new pause guardian, callable by the CURRENT pause guardian or comptroller's admin
* @param _newPauseGuardian The new pause guardian
*/
function _setPauseGuardian(
address _newPauseGuardian
) external onlyPauseGuardianOrAdmin {
require(
_newPauseGuardian != address(0),
"Pause Guardian can't be the 0 address!"
);
address currentPauseGuardian = pauseGuardian;
pauseGuardian = _newPauseGuardian;
emit NewPauseGuardian(currentPauseGuardian, _newPauseGuardian);
}
/**
* @notice Sets a new emission cap for supply/borrow speeds
* @param _newEmissionCap The new emission cap
*/
function _setEmissionCap(
uint256 _newEmissionCap
) external onlyComptrollersAdmin {
uint256 oldEmissionCap = emissionCap;
emissionCap = _newEmissionCap;
emit NewEmissionCap(oldEmissionCap, _newEmissionCap);
}
/*
====================================================================================================
Comptroller specific API
This is the main integration points with the Moonwell Comptroller. Within the `allowMint`/`allowBorrow`/etc
hooks, the comptroller will reach out to kick the global index update (updateMarketIndex) as well as update
the supplier's/borrower's token specific distribution indices for that market
====================================================================================================
*/
/**
* @notice Updates the supply indices for a given market
* @param _mToken The market to update
*/
function updateMarketSupplyIndex(
MToken _mToken
) external onlyComptrollerOrAdmin {
updateMarketSupplyIndexInternal(_mToken);
}
/**
* @notice Calculate the deltas in indices between this user's index and the global supplier index for all configs,
* and accrue any owed emissions to their supplierRewardsAccrued for this market's configs
* @param _mToken The market to update
* @param _supplier The supplier whose index will be updated
* @param _sendTokens Whether to send tokens as part of calculating owed rewards
*/
function disburseSupplierRewards(
MToken _mToken,
address _supplier,
bool _sendTokens
) external onlyComptrollerOrAdmin {
disburseSupplierRewardsInternal(_mToken, _supplier, _sendTokens);
}
/**
* @notice Combine the above 2 functions into one that will update the global and user supplier indexes and
* disburse rewards
* @param _mToken The market to update
* @param _supplier The supplier whose index will be updated
* @param _sendTokens Whether to send tokens as part of calculating owed rewards
*/
function updateMarketSupplyIndexAndDisburseSupplierRewards(
MToken _mToken,
address _supplier,
bool _sendTokens
) external onlyComptrollerOrAdmin {
updateMarketSupplyIndexInternal(_mToken);
disburseSupplierRewardsInternal(_mToken, _supplier, _sendTokens);
}
/**
* @notice Updates the borrow indices for a given market
* @param _mToken The market to update
*/
function updateMarketBorrowIndex(
MToken _mToken
) external onlyComptrollerOrAdmin {
updateMarketBorrowIndexInternal(_mToken);
}
/**
* @notice Calculate the deltas in indices between this user's index and the global borrower index for all configs,
* and accrue any owed emissions to their borrowerRewardsAccrued for this market's configs
* @param _mToken The market to update
* @param _borrower The borrower whose index will be updated
* @param _sendTokens Whether to send tokens as part of calculating owed rewards
*/
function disburseBorrowerRewards(
MToken _mToken,
address _borrower,
bool _sendTokens
) external onlyComptrollerOrAdmin {
disburseBorrowerRewardsInternal(_mToken, _borrower, _sendTokens);
}
/**
* @notice Combine the above 2 functions into one that will update the global and user borrower indexes and
* disburse rewards
* @param _mToken The market to update
* @param _borrower The borrower whose index will be updated
* @param _sendTokens Whether to send tokens as part of calculating owed rewards
*/
function updateMarketBorrowIndexAndDisburseBorrowerRewards(
MToken _mToken,
address _borrower,
bool _sendTokens
) external onlyComptrollerOrAdmin {
updateMarketBorrowIndexInternal(_mToken);
disburseBorrowerRewardsInternal(_mToken, _borrower, _sendTokens);
}
/*
====================================================================================================
Pause Guardian API
The pause guardian tooling is responsible for toggling on and off actual reward emissions. Things
will still be accrued as normal, but the `sendRewards` function will simply not attempt to transfer
any tokens out.
Similarly to the pause guardians in the Comptroller, when the pause guardian pops this circuit
breaker, only the comptroller's admin is able to unpause things and get tokens emitting again.
====================================================================================================
*/
/// @notice Pauses reward sending *but not accrual*
function _pauseRewards() external onlyPauseGuardianOrAdmin {
_pause();
emit RewardsPaused();
}
/// @notice Unpauses and allows reward sending once again
function _unpauseRewards() external onlyComptrollersAdmin {
_unpause();
emit RewardsUnpaused();
}
/*
====================================================================================================
Configuration Owner API
This is a set of APIs for external parties/emission config owners to update their configs. They're
able to transfer ownership, update emission speeds, and update the end time for a campaign. Worth
noting, if the endTime is hit, no more rewards will be accrued, BUT you can call `_updateEndTime`
to extend the specified campaign - if the campaign has ended already, then rewards will start
accruing from the time of reactivation.
====================================================================================================
*/
/**
* @notice Update the supply emissions for a given mToken + emission token pair.
* @param _mToken The market to change a config for
* @param _emissionToken The underlying reward token address
* @param _newSupplySpeed The supply side emission speed denoted in the underlying emission token's decimals
*/
function _updateSupplySpeed(
MToken _mToken,
address _emissionToken,
uint256 _newSupplySpeed
) external onlyEmissionConfigOwnerOrAdmin(_mToken, _emissionToken) {
MarketEmissionConfig
storage emissionConfig = fetchConfigByEmissionToken(
_mToken,
_emissionToken
);
uint256 currentSupplySpeed = emissionConfig
.config
.supplyEmissionsPerSec;
require(
_newSupplySpeed != currentSupplySpeed,
"Can't set new supply emissions to be equal to current!"
);
require(
_newSupplySpeed < emissionCap,
"Cannot set a supply reward speed higher than the emission cap!"
);
// Make sure we update our indices before setting the new speed
updateMarketSupplyIndexInternal(_mToken);
// Update supply speed
emissionConfig.config.supplyEmissionsPerSec = _newSupplySpeed;
emit NewSupplyRewardSpeed(
_mToken,
_emissionToken,
currentSupplySpeed,
_newSupplySpeed
);
}
/**
* @notice Update the borrow emissions for a given mToken + emission token pair.
* @param _mToken The market to change a config for
* @param _emissionToken The underlying reward token address
* @param _newBorrowSpeed The borrow side emission speed denoted in the underlying emission token's decimals
*/
function _updateBorrowSpeed(
MToken _mToken,
address _emissionToken,
uint256 _newBorrowSpeed
) external onlyEmissionConfigOwnerOrAdmin(_mToken, _emissionToken) {
MarketEmissionConfig
storage emissionConfig = fetchConfigByEmissionToken(
_mToken,
_emissionToken
);
uint256 currentBorrowSpeed = emissionConfig
.config
.borrowEmissionsPerSec;
require(
_newBorrowSpeed != currentBorrowSpeed,
"Can't set new borrow emissions to be equal to current!"
);
require(
_newBorrowSpeed < emissionCap,
"Cannot set a borrow reward speed higher than the emission cap!"
);
// Make sure we update our indices before setting the new speed
updateMarketBorrowIndexInternal(_mToken);
// Update borrow speed
emissionConfig.config.borrowEmissionsPerSec = _newBorrowSpeed;
emit NewBorrowRewardSpeed(
_mToken,
_emissionToken,
currentBorrowSpeed,
_newBorrowSpeed
);
}
/**
* @notice Update the owner of a config
* @param _mToken The market to change a config for
* @param _emissionToken The underlying reward token address
* @param _newOwner The new owner for this config
*/
function _updateOwner(
MToken _mToken,
address _emissionToken,
address _newOwner
) external onlyEmissionConfigOwnerOrAdmin(_mToken, _emissionToken) {
MarketEmissionConfig
storage emissionConfig = fetchConfigByEmissionToken(
_mToken,
_emissionToken
);
address currentOwner = emissionConfig.config.owner;
emissionConfig.config.owner = _newOwner;
emit NewEmissionConfigOwner(
_mToken,
_emissionToken,
currentOwner,
_newOwner
);
}
/**
* @notice Update the end time for an emission campaign, must be in the future
* @param _mToken The market to change a config for
* @param _emissionToken The underlying reward token address
* @param _newEndTime The new desired end time
*/
function _updateEndTime(
MToken _mToken,
address _emissionToken,
uint256 _newEndTime
) external onlyEmissionConfigOwnerOrAdmin(_mToken, _emissionToken) {
MarketEmissionConfig
storage emissionConfig = fetchConfigByEmissionToken(
_mToken,
_emissionToken
);
uint256 currentEndTime = emissionConfig.config.endTime;
// Must be older than our existing end time AND the current block
require(
_newEndTime > currentEndTime,
"_newEndTime MUST be > currentEndTime"
);
require(
_newEndTime > block.timestamp,
"_newEndTime MUST be > block.timestamp"
);
// Update both global indices before setting the new end time. If rewards are off this just updates the
// global block timestamp to the current second
updateMarketBorrowIndexInternal(_mToken);
updateMarketSupplyIndexInternal(_mToken);
emissionConfig.config.endTime = _newEndTime;
emit NewRewardEndTime(
_mToken,
_emissionToken,
currentEndTime,
_newEndTime
);
}
/*
====================================================================================================
Internal functions
Internal functions used by other parts of this contract, views first then mutation functions
====================================================================================================
*/
/**
* @notice An internal view to calculate the total owed supplier rewards for a given supplier address
* @param _emissionConfig The emission config to read index data from
* @param _globalSupplyIndex The global supply index for a market
* @param _supplierTokens The amount of this market's mTokens owned by a user
* @param _supplier The address of the supplier
*/
function calculateSupplyRewardsForUser(
MarketEmissionConfig storage _emissionConfig,
uint224 _globalSupplyIndex,
uint256 _supplierTokens,
address _supplier
) internal view returns (uint256) {
uint256 userSupplyIndex = _emissionConfig.supplierIndices[_supplier];
// If our user's index isn't set yet, set to the current global supply index
if (
userSupplyIndex == 0 && _globalSupplyIndex >= initialIndexConstant
) {
userSupplyIndex = initialIndexConstant; //_globalSupplyIndex;
}
// Calculate change in the cumulative sum of the reward per cToken accrued
Double memory deltaIndex = Double({
mantissa: sub_(_globalSupplyIndex, userSupplyIndex)
});
// Calculate reward accrued: cTokenAmount * accruedPerCToken
uint256 supplierDelta = mul_(_supplierTokens, deltaIndex);
return
add_(
_emissionConfig.supplierRewardsAccrued[_supplier],
supplierDelta
);
}
/**
* @notice An internal view to calculate the total owed borrower rewards for a given borrower address
* @param _emissionConfig The emission config to read index data from
* @param _globalBorrowIndex The global borrow index for a market
* @param _marketBorrowIndex The mToken's borrowIndex
* @param _mTokenData A struct holding a borrower's
* @param _borrower The address of the supplier mToken balance and borrowed balance
*/
function calculateBorrowRewardsForUser(
MarketEmissionConfig storage _emissionConfig,
uint224 _globalBorrowIndex,
Exp memory _marketBorrowIndex,
MTokenData memory _mTokenData,
address _borrower
) internal view returns (uint256) {
uint256 userBorrowIndex = _emissionConfig.borrowerIndices[_borrower];
// If our user's index isn't set yet, set to the current global borrow index
if (
userBorrowIndex == 0 && _globalBorrowIndex >= initialIndexConstant
) {
userBorrowIndex = initialIndexConstant; //userBorrowIndex = _globalBorrowIndex;
}
// Calculate change in the cumulative sum of the reward per cToken accrued
Double memory deltaIndex = Double({
mantissa: sub_(_globalBorrowIndex, userBorrowIndex)
});
uint borrowerAmount = div_(
_mTokenData.borrowBalanceStored,
_marketBorrowIndex
);
// Calculate reward accrued: mTokenAmount * accruedPerMToken
uint borrowerDelta = mul_(borrowerAmount, deltaIndex);
return
add_(
_emissionConfig.borrowerRewardsAccrued[_borrower],
borrowerDelta
);
}
/**
* @notice An internal view to calculate the global reward indices while taking into account emissions end times.
* @dev Denominator here is whatever fractional denominator is used to calculate the index. On the supply side
* it's simply mToken.totalSupply(), while on the borrow side it's (mToken.totalBorrows() / mToken.borrowIndex())
* @param _emissionsPerSecond The configured emissions per second for this index
* @param _currentTimestamp The current index timestamp
* @param _currentIndex The current index
* @param _rewardEndTime The end time for this reward config
* @param _denominator The denominator used in the calculation (supply side == mToken.totalSupply,
* borrow side is (mToken.totalBorrows() / mToken.borrowIndex()).
*/
function calculateNewIndex(
uint256 _emissionsPerSecond,
uint32 _currentTimestamp,
uint224 _currentIndex,
uint256 _rewardEndTime,
uint256 _denominator
) internal view returns (IndexUpdate memory) {
uint32 blockTimestamp = safe32(
block.timestamp,
"block timestamp exceeds 32 bits"
);
uint256 deltaTimestamps = sub_(
blockTimestamp,
uint256(_currentTimestamp)
);
// If our current block timestamp is newer than our emission end time, we need to halt
// reward emissions by stinting the growth of the global index, but importantly not
// the global timestamp. Should not be gte because the equivalent case makes a
// 0 deltaTimestamp which doesn't accrue the last bit of rewards properly.
if (blockTimestamp > _rewardEndTime) {
// If our current index timestamp is less than our end time it means this
// is the first time the endTime threshold has been breached, and we have
// some left over rewards to accrue, so clamp deltaTimestamps to the whatever
// window of rewards still remains.
if (_currentTimestamp < _rewardEndTime) {
deltaTimestamps = sub_(_rewardEndTime, _currentTimestamp);
} else {
// Otherwise just set deltaTimestamps to 0 to ensure that we short circuit
// in the next step
deltaTimestamps = 0;
}
}
// Short circuit to update the timestamp but *not* the index if there's nothing
// to calculate
if (deltaTimestamps == 0 || _emissionsPerSecond == 0) {
return
IndexUpdate({
newIndex: _currentIndex,
newTimestamp: blockTimestamp
});
}
// At this point we know we have to calculate a new index, so do so
uint256 tokenAccrued = mul_(deltaTimestamps, _emissionsPerSecond);
Double memory ratio = _denominator > 0
? fraction(tokenAccrued, _denominator)
: Double({mantissa: 0});
uint224 newIndex = safe224(
add_(Double({mantissa: _currentIndex}), ratio).mantissa,
"new index exceeds 224 bits"
);
return IndexUpdate({newIndex: newIndex, newTimestamp: blockTimestamp});
}
/**
* @notice An internal view to find a config for a given market given a specific emission token
* @dev Reverts if the mtoken + emission token combo could not be found.
* @param _mToken The market to fetch a config for
* @param _emissionToken The emission token to fetch a config for
*/
function fetchConfigByEmissionToken(
MToken _mToken,
address _emissionToken
) internal view returns (MarketEmissionConfig storage) {
MarketEmissionConfig[] storage configs = marketConfigs[
address(_mToken)
];
for (uint256 index = 0; index < configs.length; index++) {
MarketEmissionConfig storage emissionConfig = configs[index];
if (emissionConfig.config.emissionToken == _emissionToken) {
return emissionConfig;
}
}
revert("Unable to find emission token in mToken configs");
}
//
// Internal mutable functions
//
/**
* @notice An internal function to update the global supply index for a given mToken
* @param _mToken The market to update the global supply index for
*/