-
Notifications
You must be signed in to change notification settings - Fork 1
/
ERC20VestedMine.sol
414 lines (339 loc) · 13.3 KB
/
ERC20VestedMine.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
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.11;
import "openzeppelin/token/ERC20/ERC20.sol";
import "./AbstractRewardMine.sol";
import "../interfaces/IDistributor.sol";
import "../interfaces/IBonding.sol";
import "../StabilizedPoolExtensions/BondingExtension.sol";
struct SharesAndDebt {
uint256 totalImpliedReward;
uint256 totalDebt;
uint256 perShareReward;
uint256 perShareDebt;
}
/// @title ERC20 Vested Mine
/// @author 0xScotch <scotch@malt.money>
/// @notice An implementation of AbstractRewardMine to handle rewards being vested by the RewardDistributor
contract ERC20VestedMine is AbstractRewardMine, BondingExtension {
IVestingDistributor public vestingDistributor;
uint256 internal shareUnity;
mapping(uint256 => SharesAndDebt) internal focalSharesAndDebt;
mapping(uint256 => mapping(address => SharesAndDebt))
internal accountFocalSharesAndDebt;
constructor(
address timelock,
address repository,
address poolFactory,
uint256 _poolId
) AbstractRewardMine(timelock, repository, poolFactory) {
poolId = _poolId;
_grantRole(REWARD_PROVIDER_ROLE, timelock);
}
function setupContracts(
address _miningService,
address _vestingDistributor,
address _bonding,
address _collateralToken,
address pool
) external onlyRoleMalt(POOL_FACTORY_ROLE, "Must have pool factory role") {
require(!contractActive, "VestedMine: Already setup");
require(_miningService != address(0), "VestedMine: MiningSvc addr(0)");
require(
_vestingDistributor != address(0),
"VestedMine: Distributor addr(0)"
);
require(_bonding != address(0), "VestedMine: Bonding addr(0)");
require(_collateralToken != address(0), "VestedMine: RewardToken addr(0)");
contractActive = true;
vestingDistributor = IVestingDistributor(_vestingDistributor);
bonding = IBonding(_bonding);
shareUnity = 10**bonding.stakeTokenDecimals();
_initialSetup(_collateralToken, _miningService, _vestingDistributor);
(, address updater, ) = poolFactory.getPool(pool);
_setPoolUpdater(updater);
}
function onUnbond(address account, uint256 amount)
external
override
onlyRoleMalt(MINING_SERVICE_ROLE, "Must having mining service privilege")
{
// Withdraw all current rewards
// Done now before we change stake padding below
uint256 rewardEarned = earned(account);
_handleWithdrawForAccount(account, rewardEarned, account);
uint256 bondedBalance = balanceOfBonded(account);
if (bondedBalance == 0) {
return;
}
_checkForForfeit(account, amount, bondedBalance);
uint256 lessStakePadding = (balanceOfStakePadding(account) * amount) /
bondedBalance;
_reconcileWithdrawn(account, amount, bondedBalance);
_removeFromStakePadding(account, lessStakePadding);
}
function totalBonded() public view override returns (uint256) {
return bonding.totalBondedByPool(poolId);
}
function valueOfBonded() public view override returns (uint256) {
return bonding.valueOfBonded(poolId);
}
function balanceOfBonded(address account)
public
view
override
returns (uint256)
{
return bonding.balanceOfBonded(poolId, account);
}
/*
* totalReleasedReward and totalDeclaredReward will often be the same. However, in the case
* of vesting rewards they are different. In that case totalDeclaredReward is total
* reward, including unvested. totalReleasedReward is just the rewards that have completed
* the vesting schedule.
*/
function totalDeclaredReward() public view override returns (uint256) {
return vestingDistributor.totalDeclaredReward();
}
function declareReward(uint256 amount)
external
virtual
onlyRoleMalt(REWARD_PROVIDER_ROLE, "Only reward provider role")
{
uint256 bonded = totalBonded();
if (amount == 0 || bonded == 0) {
return;
}
uint256 focalId = vestingDistributor.focalID();
uint256 localShareUnity = shareUnity; // gas saving
SharesAndDebt storage globalActiveFocalShares = focalSharesAndDebt[focalId];
/*
* normReward is normalizing the reward as if the reward was declared
* at the very start of the focal period.
* Eg if $100 reward comes in 33% towards the end of the vesting period
* then that will look the same as $150 of rewards vesting from the very
* beginning of the vesting period. However, to ensure that only $100
* rewards are actual given out we accrue $50 of 'vesting debt'.
*
* To calculate how much has vested you first calculate what %
* of the vesting period has elapsed. Then take that % of the
* normReward and then subtract of normDebt.
*
* Using the above $100 at 33% into the vesting period as an example.
* If we are 50% through the vesting period then 50% of the $150
* normReward has vested = $75. Now subtract the $50 debt and
* we are left with $25 of rewards.
* This is correct as the $100 came in at 33.33% and we are now
* 50% in, so we have moved 16.66% towards the 66.66% of the
* remaining time. 16.66 is 25% of 66.66 so 25% of the $100 should
* have vested.
*
* By normalizing rewards to always start and end vesting at the start
* and end of the focal periods the math becomes significantly easier.
* We also normalize the full normReward and normDebt to be per share
* currently bonded which makes other math easier down the line.
*/
uint256 unvestedBps = vestingDistributor.getFocalUnvestedBps(focalId);
if (unvestedBps == 0) {
return;
}
uint256 normReward = (amount * 10000) / unvestedBps;
uint256 normDebt = normReward - amount;
uint256 normRewardPerShare = (normReward * localShareUnity) / bonded;
uint256 normDebtPerShare = (normDebt * localShareUnity) / bonded;
focalSharesAndDebt[focalId].totalImpliedReward += normReward;
focalSharesAndDebt[focalId].totalDebt += normDebt;
focalSharesAndDebt[focalId].perShareReward += normRewardPerShare;
focalSharesAndDebt[focalId].perShareDebt += normDebtPerShare;
}
function earned(address account)
public
view
override
returns (uint256 earnedReward)
{
uint256 totalAccountReward = balanceOfRewards(account);
uint256 unvested = _getAccountUnvested(account);
uint256 vested;
if (totalAccountReward > unvested) {
vested = totalAccountReward - unvested;
}
if (vested > _userWithdrawn[account]) {
earnedReward = vested - _userWithdrawn[account];
}
uint256 balance = collateralToken.balanceOf(address(this));
if (earnedReward > balance) {
earnedReward = balance;
}
}
function accountUnvested(address account) public view returns (uint256) {
return _getAccountUnvested(account);
}
function getFocalShares(uint256 focalId)
external
view
returns (
uint256 totalImpliedReward,
uint256 totalDebt,
uint256 perShareReward,
uint256 perShareDebt
)
{
SharesAndDebt storage focalShares = focalSharesAndDebt[focalId];
return (
focalShares.totalImpliedReward,
focalShares.totalDebt,
focalShares.perShareReward,
focalShares.perShareDebt
);
}
function getAccountFocalDebt(address account, uint256 focalId)
external
view
returns (uint256, uint256)
{
SharesAndDebt storage accountFocalDebt = accountFocalSharesAndDebt[focalId][
account
];
return (accountFocalDebt.perShareReward, accountFocalDebt.perShareDebt);
}
/*
* INTERNAL FUNCTIONS
*/
function _getAccountUnvested(address account)
internal
view
returns (uint256 unvested)
{
// focalID starts at 1 so vesting can't underflow
uint256 activeFocalId = vestingDistributor.focalID();
uint256 vestingFocalId = activeFocalId - 1;
uint256 userBonded = balanceOfBonded(account);
uint256 activeUnvestedPerShare = _getFocalUnvestedPerShare(
activeFocalId,
account
);
uint256 vestingUnvestedPerShare = _getFocalUnvestedPerShare(
vestingFocalId,
account
);
unvested =
((activeUnvestedPerShare + vestingUnvestedPerShare) * userBonded) /
shareUnity;
}
function _getFocalUnvestedPerShare(uint256 focalId, address account)
internal
view
returns (uint256 unvestedPerShare)
{
SharesAndDebt storage globalActiveFocalShares = focalSharesAndDebt[focalId];
SharesAndDebt storage accountActiveFocalShares = accountFocalSharesAndDebt[
focalId
][account];
uint256 bonded = totalBonded();
if (globalActiveFocalShares.perShareReward == 0 || bonded == 0) {
return 0;
}
uint256 unvestedBps = vestingDistributor.getFocalUnvestedBps(focalId);
uint256 vestedBps = 10000 - unvestedBps;
uint256 totalRewardPerShare = globalActiveFocalShares.perShareReward -
globalActiveFocalShares.perShareDebt;
uint256 totalUserDebtPerShare = accountActiveFocalShares.perShareReward -
accountActiveFocalShares.perShareDebt;
uint256 rewardPerShare = ((globalActiveFocalShares.perShareReward *
vestedBps) / 10000) - globalActiveFocalShares.perShareDebt;
uint256 userDebtPerShare = ((accountActiveFocalShares.perShareReward *
vestedBps) / 10000) - accountActiveFocalShares.perShareDebt;
uint256 userTotalPerShare = totalRewardPerShare - totalUserDebtPerShare;
uint256 userVestedPerShare = rewardPerShare - userDebtPerShare;
if (userTotalPerShare > userVestedPerShare) {
unvestedPerShare = userTotalPerShare - userVestedPerShare;
}
}
function _afterBond(address account, uint256 amount) internal override {
uint256 focalId = vestingDistributor.focalID();
uint256 vestingFocalId = focalId - 1;
uint256 initialUserBonded = balanceOfBonded(account);
uint256 userTotalBonded = initialUserBonded + amount;
SharesAndDebt memory currentShares = focalSharesAndDebt[focalId];
SharesAndDebt memory vestingShares = focalSharesAndDebt[vestingFocalId];
uint256 perShare = accountFocalSharesAndDebt[focalId][account]
.perShareReward;
uint256 vestingPerShare = accountFocalSharesAndDebt[vestingFocalId][account]
.perShareReward;
if (
currentShares.perShareReward == 0 && vestingShares.perShareReward == 0
) {
return;
}
uint256 debt = accountFocalSharesAndDebt[focalId][account].perShareDebt;
uint256 vestingDebt = accountFocalSharesAndDebt[vestingFocalId][account]
.perShareDebt;
// Pro-rata it down according to old bonded value
perShare = (perShare * initialUserBonded) / userTotalBonded;
debt = (debt * initialUserBonded) / userTotalBonded;
vestingPerShare = (vestingPerShare * initialUserBonded) / userTotalBonded;
vestingDebt = (vestingDebt * initialUserBonded) / userTotalBonded;
// Now add on the new pro-ratad perShare values
perShare += (currentShares.perShareReward * amount) / userTotalBonded;
debt += (currentShares.perShareDebt * amount) / userTotalBonded;
vestingPerShare +=
(vestingShares.perShareReward * amount) /
userTotalBonded;
vestingDebt += (vestingShares.perShareDebt * amount) / userTotalBonded;
accountFocalSharesAndDebt[focalId][account].perShareReward = perShare;
accountFocalSharesAndDebt[focalId][account].perShareDebt = debt;
accountFocalSharesAndDebt[vestingFocalId][account]
.perShareReward = vestingPerShare;
accountFocalSharesAndDebt[vestingFocalId][account]
.perShareDebt = vestingDebt;
}
function _checkForForfeit(
address account,
uint256 amount,
uint256 bondedBalance
) internal {
// The user is unbonding so we should reduce declaredReward
// proportional to the unbonded amount
// At any given point in time, every user has rewards allocated
// to them. balanceOfRewards(account) will tell you this value.
// If a user unbonds x% of their LP then declaredReward should
// reduce by exactly x% of that user's allocated rewards
// However, this has to be done in 2 parts. First forfeit x%
// Of unvested rewards. This decrements declaredReward automatically.
// Then we call decrementRewards using x% of rewards that have
// already been released. The net effect is declaredReward decreases
// by x% of the users allocated reward
uint256 unvested = _getAccountUnvested(account);
uint256 forfeitReward = (unvested * amount) / bondedBalance;
// A full withdrawn happens before this method is called.
// So we can safely say _userWithdrawn is in fact all of the
// currently vested rewards for the bonded LP
uint256 declaredRewardDecrease = (_userWithdrawn[account] * amount) /
bondedBalance;
if (forfeitReward > 0) {
vestingDistributor.forfeit(forfeitReward);
}
if (declaredRewardDecrease > 0) {
vestingDistributor.decrementRewards(declaredRewardDecrease);
}
}
function _beforeWithdraw(address account, uint256 amount) internal override {
// Vest rewards before withdrawing to make sure all capital is available
vestingDistributor.vest();
}
/*
* PRIVILEDGED FUNCTIONS
*/
function setVestingDistributor(address _vestingDistributor)
external
onlyRoleMalt(ADMIN_ROLE, "Must have admin privs")
{
vestingDistributor = IVestingDistributor(_vestingDistributor);
}
function _accessControl()
internal
override(MiningServiceExtension, BondingExtension)
{
_onlyRoleMalt(POOL_UPDATER_ROLE, "Must have pool updater role");
}
}