/
BoostedVault.sol
658 lines (589 loc) 路 24.3 KB
/
BoostedVault.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
// SPDX-License-Identifier: AGPL-3.0-or-later
pragma solidity 0.8.6;
// Internal
import { IBoostedVaultWithLockup } from "../../interfaces/IBoostedVaultWithLockup.sol";
import { ISavingsContractV4 } from "../../interfaces/ISavingsContract.sol";
import { InitializableRewardsDistributionRecipient } from "../InitializableRewardsDistributionRecipient.sol";
import { BoostedTokenWrapper } from "./BoostedTokenWrapper.sol";
import { Initializable } from "../../shared/@openzeppelin-2.5/Initializable.sol";
// Libs
import { IERC20, SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import { SafeCast } from "@openzeppelin/contracts/utils/math/SafeCast.sol";
import { StableMath } from "../../shared/StableMath.sol";
/**
* @title BoostedVault
* @author mStable
* @notice Accrues rewards second by second, based on a users boosted balance
* @dev Forked from rewards/staking/StakingRewards.sol
* Changes:
* - Lockup implemented in `updateReward` hook (20% unlock immediately, 80% locked for 6 months)
* - `updateBoost` hook called after every external action to reset a users boost
* - Struct packing of common data
* - Searching for and claiming of unlocked rewards
*/
contract BoostedVault is
IBoostedVaultWithLockup,
Initializable,
InitializableRewardsDistributionRecipient,
BoostedTokenWrapper
{
using SafeERC20 for IERC20;
using StableMath for uint256;
using SafeCast for uint256;
event RewardAdded(uint256 reward);
event Staked(address indexed user, uint256 amount, address payer);
event Withdrawn(address indexed user, uint256 amount);
event Poked(address indexed user);
event RewardPaid(address indexed user, uint256 reward);
/// @notice token the rewards are distributed in. eg MTA
IERC20 public immutable rewardsToken;
/// @notice length of each staking period in seconds. 7 days = 604,800; 3 months = 7,862,400
uint64 public constant DURATION = 7 days;
/// @notice Length of token lockup, after rewards are earned
uint256 public constant LOCKUP = 26 weeks;
/// @notice Percentage of earned tokens unlocked immediately
uint64 public constant UNLOCK = 33e16;
/// @notice Timestamp for current period finish
uint256 public periodFinish;
/// @notice RewardRate for the rest of the period
uint256 public rewardRate;
/// @notice Last time any user took action
uint256 public lastUpdateTime;
/// @notice Ever increasing rewardPerToken rate, based on % of total supply
uint256 public rewardPerTokenStored;
mapping(address => UserData) public userData;
/// @notice Locked reward tracking
mapping(address => Reward[]) public userRewards;
mapping(address => uint64) public userClaim;
struct UserData {
uint128 rewardPerTokenPaid;
uint128 rewards;
uint64 lastAction;
uint64 rewardCount;
}
struct Reward {
uint64 start;
uint64 finish;
uint128 rate;
}
/**
* @param _nexus mStable system Nexus address
* @param _stakingToken token that is beinf rewarded for being staked. eg MTA, imUSD or fPmUSD/GUSD
* @param _boostDirector vMTA boost director
* @param _priceCoeff Rough price of a given LP token, to be used in boost calculations, where $1 = 1e18
* @param _boostCoeff Boost coefficent using the the boost formula
* @param _rewardsToken first token that is being distributed as a reward. eg MTA
*/
constructor(
address _nexus,
address _stakingToken,
address _boostDirector,
uint256 _priceCoeff,
uint256 _boostCoeff,
address _rewardsToken
)
InitializableRewardsDistributionRecipient(_nexus)
BoostedTokenWrapper(_stakingToken, _boostDirector, _priceCoeff, _boostCoeff)
{
rewardsToken = IERC20(_rewardsToken);
}
/**
* @dev Initialization function for upgradable proxy contract.
* This function should be called via Proxy just after contract deployment.
* To avoid variable shadowing appended `Arg` after arguments name.
* @param _rewardsDistributorArg mStable Reward Distributor contract address
* @param _nameArg token name. eg imUSD Vault or GUSD Feeder Pool Vault
* @param _symbolArg token symbol. eg v-imUSD or v-fPmUSD/GUSD
*/
function initialize(
address _rewardsDistributorArg,
string calldata _nameArg,
string calldata _symbolArg
) external initializer {
InitializableRewardsDistributionRecipient._initialize(_rewardsDistributorArg);
BoostedTokenWrapper._initialize(_nameArg, _symbolArg);
}
/**
* @dev Updates the reward for a given address, before executing function.
* Locks 80% of new rewards up for 6 months, vesting linearly from (time of last action + 6 months) to
* (now + 6 months). This allows rewards to be distributed close to how they were accrued, as opposed
* to locking up for a flat 6 months from the time of this fn call (allowing more passive accrual).
*/
modifier updateReward(address _account) {
uint256 currentTime = block.timestamp;
uint64 currentTime64 = SafeCast.toUint64(currentTime);
// Setting of global vars
(uint256 newRewardPerToken, uint256 lastApplicableTime) = _rewardPerToken();
// If statement protects against loss in initialisation case
if (newRewardPerToken > 0) {
rewardPerTokenStored = newRewardPerToken;
lastUpdateTime = lastApplicableTime;
// Setting of personal vars based on new globals
if (_account != address(0)) {
UserData memory data = userData[_account];
uint256 earned_ = _earned(_account, data.rewardPerTokenPaid, newRewardPerToken);
// If earned == 0, then it must either be the initial stake, or an action in the
// same block, since new rewards unlock after each block.
if (earned_ > 0) {
uint256 unlocked = earned_.mulTruncate(UNLOCK);
uint256 locked = earned_ - unlocked;
userRewards[_account].push(
Reward({
start: SafeCast.toUint64(LOCKUP + data.lastAction),
finish: SafeCast.toUint64(LOCKUP + currentTime),
rate: SafeCast.toUint128(locked / (currentTime - data.lastAction))
})
);
userData[_account] = UserData({
rewardPerTokenPaid: SafeCast.toUint128(newRewardPerToken),
rewards: SafeCast.toUint128(unlocked + data.rewards),
lastAction: currentTime64,
rewardCount: data.rewardCount + 1
});
} else {
userData[_account] = UserData({
rewardPerTokenPaid: SafeCast.toUint128(newRewardPerToken),
rewards: data.rewards,
lastAction: currentTime64,
rewardCount: data.rewardCount
});
}
}
} else if (_account != address(0)) {
// This should only be hit once, for first staker in initialisation case
userData[_account].lastAction = currentTime64;
}
_;
}
/** @dev Updates the boost for a given address, after the rest of the function has executed */
modifier updateBoost(address _account) {
_;
_setBoost(_account);
}
/***************************************
ACTIONS - EXTERNAL
****************************************/
/**
* @notice Stakes a given amount of the StakingToken for the sender
* @param _amount Units of StakingToken
*/
function stake(uint256 _amount)
external
override
updateReward(msg.sender)
updateBoost(msg.sender)
{
_stake(msg.sender, _amount);
}
/**
* @notice Stakes a given amount of the StakingToken for a given beneficiary
* @param _beneficiary Staked tokens are credited to this address
* @param _amount Units of StakingToken
*/
function stake(address _beneficiary, uint256 _amount)
external
override
updateReward(_beneficiary)
updateBoost(_beneficiary)
{
_stake(_beneficiary, _amount);
}
/**
* @notice Withdraws stake from pool and claims any unlocked rewards.
* Note, this function is costly - the args for _claimRewards
* should be determined off chain and then passed to other fn
*/
function exit() external override updateReward(msg.sender) updateBoost(msg.sender) {
_withdraw(rawBalanceOf(msg.sender));
(uint256 first, uint256 last) = _unclaimedEpochs(msg.sender);
_claimRewards(first, last);
}
/**
* @notice Withdraws stake from pool and claims any unlocked rewards.
* @param _first Index of the first array element to claim
* @param _last Index of the last array element to claim
*/
function exit(uint256 _first, uint256 _last)
external
override
updateReward(msg.sender)
updateBoost(msg.sender)
{
_withdraw(rawBalanceOf(msg.sender));
_claimRewards(_first, _last);
}
/**
* @notice Withdraws given stake amount from the pool
* @param _amount Units of the staked token to withdraw
*/
function withdraw(uint256 _amount)
external
override
updateReward(msg.sender)
updateBoost(msg.sender)
{
_withdraw(_amount);
}
/**
* @notice Redeems staked interest-bearing asset tokens for either bAsset or fAsset tokens.
* Withdraws a given staked amount of interest-bearing assets from the vault,
* redeems the interest-bearing asset for the underlying mAsset and either
* 1. Redeems the underlying mAsset tokens for bAsset tokens.
* 2. Swaps the underlying mAsset tokens for fAsset tokens in a Feeder Pool.
* @param _amount Units of the staked interest-bearing asset tokens to withdraw. eg imUSD or imBTC.
* @param _minAmountOut Minimum units of `output` tokens to be received by the beneficiary. This is to the same decimal places as the `output` token.
* @param _output Asset to receive in exchange for the redeemed mAssets. This can be a bAsset or a fAsset. For example:
- bAssets (USDC, DAI, sUSD or USDT) or fAssets (GUSD, BUSD, alUSD, FEI or RAI) for mainnet imUSD Vault.
- bAssets (USDC, DAI or USDT) or fAsset FRAX for Polygon imUSD Vault.
- bAssets (WBTC, sBTC or renBTC) or fAssets (HBTC or TBTCV2) for mainnet imBTC Vault.
* @param _beneficiary Address to send `output` tokens to.
* @param _router mAsset address if the `output` is a bAsset. Feeder Pool address if the `output` is a fAsset.
* @param _isBassetOut `true` if `output` is a bAsset. `false` if `output` is a fAsset.
* @return outputQuantity Units of `output` tokens sent to the beneficiary. This is to the same decimal places as the `output` token.
*/
function withdrawAndUnwrap(
uint256 _amount,
uint256 _minAmountOut,
address _output,
address _beneficiary,
address _router,
bool _isBassetOut
)
external
override
updateReward(msg.sender)
updateBoost(msg.sender)
returns (uint256 outputQuantity)
{
require(_amount > 0, "Cannot withdraw 0");
// Reduce raw balance (but do not transfer `stakingToken`)
_reduceRaw(_amount);
// Unwrap `stakingToken` into `output` and send to `beneficiary`
(, , outputQuantity) = ISavingsContractV4(address(stakingToken)).redeemAndUnwrap(
_amount,
true,
_minAmountOut,
_output,
_beneficiary,
_router,
_isBassetOut
);
emit Withdrawn(msg.sender, _amount);
}
/**
* @notice Claims only the tokens that have been immediately unlocked, not including
* those that are in the lockers.
*/
function claimReward() external override updateReward(msg.sender) updateBoost(msg.sender) {
uint256 unlocked = userData[msg.sender].rewards;
userData[msg.sender].rewards = 0;
if (unlocked > 0) {
rewardsToken.safeTransfer(msg.sender, unlocked);
emit RewardPaid(msg.sender, unlocked);
}
}
/**
* @notice Claims all unlocked rewards for sender.
* Note, this function is costly - the args for _claimRewards
* should be determined off chain and then passed to other fn
*/
function claimRewards() external override updateReward(msg.sender) updateBoost(msg.sender) {
(uint256 first, uint256 last) = _unclaimedEpochs(msg.sender);
_claimRewards(first, last);
}
/**
* @notice Claims all unlocked rewards for sender. Both immediately unlocked
* rewards and also locked rewards past their time lock.
* @param _first Index of the first array element to claim
* @param _last Index of the last array element to claim
*/
function claimRewards(uint256 _first, uint256 _last)
external
override
updateReward(msg.sender)
updateBoost(msg.sender)
{
_claimRewards(_first, _last);
}
/**
* @notice Pokes a given account to reset the boost
*/
function pokeBoost(address _account)
external
override
updateReward(_account)
updateBoost(_account)
{
emit Poked(_account);
}
/***************************************
ACTIONS - INTERNAL
****************************************/
/**
* @dev Claims all unlocked rewards for sender. Both immediately unlocked
* rewards and also locked rewards past their time lock.
* @param _first Index of the first array element to claim
* @param _last Index of the last array element to claim
*/
function _claimRewards(uint256 _first, uint256 _last) internal {
(uint256 unclaimed, uint256 lastTimestamp) = _unclaimedRewards(msg.sender, _first, _last);
userClaim[msg.sender] = uint64(lastTimestamp);
uint256 unlocked = userData[msg.sender].rewards;
userData[msg.sender].rewards = 0;
uint256 total = unclaimed + unlocked;
if (total > 0) {
rewardsToken.safeTransfer(msg.sender, total);
emit RewardPaid(msg.sender, total);
}
}
/**
* @dev Internally stakes an amount by depositing from sender,
* and crediting to the specified beneficiary
* @param _beneficiary Staked tokens are credited to this address
* @param _amount Units of StakingToken
*/
function _stake(address _beneficiary, uint256 _amount) internal {
require(_amount > 0, "Cannot stake 0");
require(_beneficiary != address(0), "Invalid beneficiary address");
_stakeRaw(_beneficiary, _amount);
emit Staked(_beneficiary, _amount, msg.sender);
}
/**
* @dev Withdraws raw units from the sender
* @param _amount Units of StakingToken
*/
function _withdraw(uint256 _amount) internal {
require(_amount > 0, "Cannot withdraw 0");
_withdrawRaw(_amount);
emit Withdrawn(msg.sender, _amount);
}
/***************************************
GETTERS
****************************************/
/**
* @notice Gets the RewardsToken
*/
function getRewardToken() external view override returns (IERC20) {
return rewardsToken;
}
/**
* @notice Gets the last applicable timestamp for this reward period
*/
function lastTimeRewardApplicable() public view override returns (uint256) {
return StableMath.min(block.timestamp, periodFinish);
}
/**
* @notice Calculates the amount of unclaimed rewards per token since last update,
* and sums with stored to give the new cumulative reward per token
* @return 'Reward' per staked token
*/
function rewardPerToken() public view override returns (uint256) {
(uint256 rewardPerToken_, ) = _rewardPerToken();
return rewardPerToken_;
}
function _rewardPerToken()
internal
view
returns (uint256 rewardPerToken_, uint256 lastTimeRewardApplicable_)
{
uint256 lastApplicableTime = lastTimeRewardApplicable(); // + 1 SLOAD
uint256 timeDelta = lastApplicableTime - lastUpdateTime; // + 1 SLOAD
// If this has been called twice in the same block, shortcircuit to reduce gas
if (timeDelta == 0) {
return (rewardPerTokenStored, lastApplicableTime);
}
// new reward units to distribute = rewardRate * timeSinceLastUpdate
uint256 rewardUnitsToDistribute = rewardRate * timeDelta; // + 1 SLOAD
uint256 supply = totalSupply(); // + 1 SLOAD
// If there is no StakingToken liquidity, avoid div(0)
// If there is nothing to distribute, short circuit
if (supply == 0 || rewardUnitsToDistribute == 0) {
return (rewardPerTokenStored, lastApplicableTime);
}
// new reward units per token = (rewardUnitsToDistribute * 1e18) / totalTokens
uint256 unitsToDistributePerToken = rewardUnitsToDistribute.divPrecisely(supply);
// return summed rate
return (rewardPerTokenStored + unitsToDistributePerToken, lastApplicableTime); // + 1 SLOAD
}
/**
* @notice Returned the units of IMMEDIATELY claimable rewards a user has to receive. Note - this
* does NOT include the majority of rewards which will be locked up.
* @param _account User address
* @return Total reward amount earned
*/
function earned(address _account) public view override returns (uint256) {
uint256 newEarned = _earned(
_account,
userData[_account].rewardPerTokenPaid,
rewardPerToken()
);
uint256 immediatelyUnlocked = newEarned.mulTruncate(UNLOCK);
return immediatelyUnlocked + userData[_account].rewards;
}
/**
* @notice Calculates all unclaimed reward data, finding both immediately unlocked rewards
* and those that have passed their time lock.
* @param _account User address
* @return amount Total units of unclaimed rewards
* @return first Index of the first userReward that has unlocked
* @return last Index of the last userReward that has unlocked
*/
function unclaimedRewards(address _account)
external
view
override
returns (
uint256 amount,
uint256 first,
uint256 last
)
{
(first, last) = _unclaimedEpochs(_account);
(uint256 unlocked, ) = _unclaimedRewards(_account, first, last);
amount = unlocked + earned(_account);
}
/** @dev Returns only the most recently earned rewards */
function _earned(
address _account,
uint256 _userRewardPerTokenPaid,
uint256 _currentRewardPerToken
) internal view returns (uint256) {
// current rate per token - rate user previously received
uint256 userRewardDelta = _currentRewardPerToken - _userRewardPerTokenPaid; // + 1 SLOAD
// Short circuit if there is nothing new to distribute
if (userRewardDelta == 0) {
return 0;
}
// new reward = staked tokens * difference in rate
uint256 userNewReward = balanceOf(_account).mulTruncate(userRewardDelta); // + 1 SLOAD
// add to previous rewards
return userNewReward;
}
/**
* @dev Gets the first and last indexes of array elements containing unclaimed rewards
*/
function _unclaimedEpochs(address _account)
internal
view
returns (uint256 first, uint256 last)
{
uint64 lastClaim = userClaim[_account];
uint256 firstUnclaimed = _findFirstUnclaimed(lastClaim, _account);
uint256 lastUnclaimed = _findLastUnclaimed(_account);
return (firstUnclaimed, lastUnclaimed);
}
/**
* @dev Sums the cumulative rewards from a valid range
*/
function _unclaimedRewards(
address _account,
uint256 _first,
uint256 _last
) internal view returns (uint256 amount, uint256 latestTimestamp) {
uint256 currentTime = block.timestamp;
uint64 lastClaim = userClaim[_account];
// Check for no rewards unlocked
uint256 totalLen = userRewards[_account].length;
if (_first == 0 && _last == 0) {
if (totalLen == 0 || currentTime <= userRewards[_account][0].start) {
return (0, currentTime);
}
}
// If there are previous unlocks, check for claims that would leave them untouchable
if (_first > 0) {
require(
lastClaim >= userRewards[_account][_first - 1].finish,
"Invalid _first arg: Must claim earlier entries"
);
}
uint256 count = _last - _first + 1;
for (uint256 i = 0; i < count; i++) {
uint256 id = _first + i;
Reward memory rwd = userRewards[_account][id];
require(currentTime >= rwd.start && lastClaim <= rwd.finish, "Invalid epoch");
uint256 endTime = StableMath.min(rwd.finish, currentTime);
uint256 startTime = StableMath.max(rwd.start, lastClaim);
uint256 unclaimed = (endTime - startTime) * rwd.rate;
amount += unclaimed;
}
// Calculate last relevant timestamp here to allow users to avoid issue of OOG errors
// by claiming rewards in batches.
latestTimestamp = StableMath.min(currentTime, userRewards[_account][_last].finish);
}
/**
* @dev Uses binarysearch to find the unclaimed lockups for a given account
*/
function _findFirstUnclaimed(uint64 _lastClaim, address _account)
internal
view
returns (uint256 first)
{
uint256 len = userRewards[_account].length;
if (len == 0) return 0;
// Binary search
uint256 min = 0;
uint256 max = len - 1;
// Will be always enough for 128-bit numbers
for (uint256 i = 0; i < 128; i++) {
if (min >= max) break;
uint256 mid = (min + max + 1) / 2;
if (_lastClaim > userRewards[_account][mid].start) {
min = mid;
} else {
max = mid - 1;
}
}
return min;
}
/**
* @dev Uses binarysearch to find the unclaimed lockups for a given account
*/
function _findLastUnclaimed(address _account) internal view returns (uint256 first) {
uint256 len = userRewards[_account].length;
if (len == 0) return 0;
// Binary search
uint256 min = 0;
uint256 max = len - 1;
// Will be always enough for 128-bit numbers
for (uint256 i = 0; i < 128; i++) {
if (min >= max) break;
uint256 mid = (min + max + 1) / 2;
if (block.timestamp > userRewards[_account][mid].start) {
min = mid;
} else {
max = mid - 1;
}
}
return min;
}
/***************************************
ADMIN
****************************************/
/**
* @notice Notifies the contract that new rewards have been added.
* Calculates an updated rewardRate based on the rewards in period.
* @param _reward Units of RewardToken that have been added to the pool
*/
function notifyRewardAmount(uint256 _reward)
external
override
onlyRewardsDistributor
updateReward(address(0))
{
require(_reward < 1e24, "Cannot notify with more than a million units");
uint256 currentTime = block.timestamp;
// If previous period over, reset rewardRate
if (currentTime >= periodFinish) {
rewardRate = _reward / DURATION;
}
// If additional reward to existing period, calc sum
else {
uint256 remaining = periodFinish - currentTime;
uint256 leftover = remaining * rewardRate;
rewardRate = (_reward + leftover) / DURATION;
}
lastUpdateTime = currentTime;
periodFinish = currentTime + DURATION;
emit RewardAdded(_reward);
}
}