-
Notifications
You must be signed in to change notification settings - Fork 53
Expand file tree
/
Copy pathStakingWithdrawModule.sol
More file actions
402 lines (350 loc) · 17 KB
/
StakingWithdrawModule.sol
File metadata and controls
402 lines (350 loc) · 17 KB
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
pragma solidity ^0.5.17;
pragma experimental ABIEncoderV2;
import "../../../proxy/modules/interfaces/IFunctionsList.sol";
import "./shared/CheckpointsShared.sol";
import "../../../rsk/RSKAddrValidator.sol";
import "../../Vesting/ITeamVesting.sol";
import "../../Vesting/IVesting.sol";
import "./shared/StakingShared.sol";
/**
* @title Staking withdrawal functionality module
**/
contract StakingWithdrawModule is IFunctionsList, StakingShared, CheckpointsShared {
using SafeMath for uint256;
event MaxVestingWithdrawIterationsUpdated(uint256 oldMaxIterations, uint256 newMaxIterations);
/// @dev Struct for direct withdraw function -- to avoid stack too deep issue
struct VestingConfig {
address vestingAddress;
uint256 startDate;
uint256 endDate;
uint256 cliff;
uint256 duration;
address tokenOwner;
}
/// @notice An event emitted when staked tokens get withdrawn.
event StakingWithdrawn(
address indexed staker,
uint256 amount,
uint256 until,
address indexed receiver,
bool isGovernance
);
/// @notice An event emitted when vesting tokens get withdrawn.
event VestingTokensWithdrawn(address vesting, address receiver);
/// @notice An event emitted when the owner unlocks all tokens.
event TokensUnlocked(uint256 amount);
/**
* @notice Withdraw the given amount of tokens if they are unlocked.
* @param amount The number of tokens to withdraw.
* @param until The date until which the tokens were staked.
* @param receiver The receiver of the tokens. If not specified, send to the msg.sender
* @dev If until is not a valid lock date, the next lock date after until is used.
* */
function withdraw(uint96 amount, uint256 until, address receiver) external whenNotFrozen {
// adjust until here to avoid adjusting multiple times, and to make sure an adjusted date is passed to
// _notSameBlockAsStakingCheckpoint
until = _adjustDateForOrigin(until);
_notSameBlockAsStakingCheckpoint(until, msg.sender);
_withdraw(amount, until, receiver, false);
// @dev withdraws tokens for lock date 2 weeks later than given lock date if sender is a contract
// we need to check block.timestamp here
_withdrawNext(until, receiver, false);
}
/**
* @notice Governance withdraw vesting directly through staking contract.
* This direct withdraw vesting solves the out of gas issue when there are too many iterations when withdrawing.
* This function only allows cancelling vesting contract of the TeamVesting type.
*
* @param vesting The vesting address.
* @param receiver The receiving address.
* @param startFrom The start value for the iterations.
*/
function cancelTeamVesting(
address vesting,
address receiver,
uint256 startFrom
) external onlyAuthorized whenNotFrozen {
/// require the caller only for team vesting contract.
require(vestingRegistryLogic.isTeamVesting(vesting), "Only team vesting allowed");
_cancelTeamVesting(vesting, receiver, startFrom);
}
/**
* @notice Withdraws tokens from the staking contract and forwards them
* to an address specified by the token owner. Low level function.
* @dev Once here the caller permission is taken for granted.
* @param _vesting The vesting address.
* @param _receiver The receiving address.
* @param _startFrom The start value for the iterations.
* or just unlocked tokens (false).
*
* @return nextStartFrom is a timestamp to be used for next withdrawal.
* @return notCompleted flag that indicates that the cancel team vesting is not completely done.
* */
function _cancelTeamVesting(
address _vesting,
address _receiver,
uint256 _startFrom
) private returns (uint256 nextStartFrom, bool notCompleted) {
require(_receiver != address(0), "receiver address invalid");
ITeamVesting teamVesting = ITeamVesting(_vesting);
VestingConfig memory vestingConfig = VestingConfig(
_vesting,
teamVesting.startDate(),
teamVesting.endDate(),
teamVesting.cliff(),
teamVesting.duration(),
teamVesting.tokenOwner()
);
/// @dev In the unlikely case that all tokens have been unlocked early,
/// allow to withdraw all of them, as long as the itrations less than maxVestingWithdrawIterations.
uint256 end = vestingConfig.endDate;
uint256 defaultStart = vestingConfig.startDate + vestingConfig.cliff;
_startFrom = _startFrom >= defaultStart ? _startFrom : defaultStart;
/// @dev max iterations need to be decreased by 1, otherwise the iteration will always be surplus by 1
uint256 totalIterationValue = (_startFrom +
(TWO_WEEKS * (maxVestingWithdrawIterations - 1)));
uint256 adjustedEnd = end < totalIterationValue ? end : totalIterationValue;
/// @dev Withdraw for each unlocked position.
for (uint256 i = _startFrom; i <= adjustedEnd; i += TWO_WEEKS) {
/// @dev Read amount to withdraw.
uint96 tempStake = _getPriorUserStakeByDate(_vesting, i, block.number - 1);
if (tempStake > 0) {
/// @dev do governance direct withdraw for team vesting
_withdrawFromTeamVesting(tempStake, i, _receiver, vestingConfig);
}
}
if (adjustedEnd < end) {
nextStartFrom = adjustedEnd + TWO_WEEKS;
emit TeamVestingPartiallyCancelled(msg.sender, _receiver, nextStartFrom);
return (nextStartFrom, true);
} else {
emit TeamVestingCancelled(msg.sender, _receiver);
return (end, false);
}
}
/**
* @notice Send user' staked tokens to a receiver taking into account punishments.
* Sovryn encourages long-term commitment and thinking. When/if you unstake before
* the end of the staking period, a percentage of the original staking amount will
* be slashed. This amount is also added to the reward pool and is distributed
* between all other stakers.
*
* @param amount The number of tokens to withdraw.
* @param until The date until which the tokens were staked.
* Needs to be adjusted to the next valid lock date before calling this function.
* @param receiver The receiver of the tokens. If not specified, send to the msg.sender
* @param isGovernance Whether all tokens (true)
* or just unlocked tokens (false).
* */
function _withdraw(
uint96 amount,
uint256 until,
address receiver,
bool isGovernance
) internal {
// @dev it's very unlikely some one will have 1/10**18 SOV staked in Vesting contract
// this check is a part of workaround for Vesting.withdrawTokens issue
if (amount == 1 && _isVestingContract(msg.sender)) {
return;
}
_validateWithdrawParams(msg.sender, amount, until);
/// @dev Determine the receiver.
if (receiver == address(0)) receiver = msg.sender;
/// @dev Update the checkpoints.
_decreaseDailyStake(until, amount);
_decreaseUserStake(msg.sender, until, amount);
if (_isVestingContract(msg.sender)) _decreaseVestingStake(until, amount);
_decreaseDelegateStake(delegates[msg.sender][until], until, amount);
/// @dev Early unstaking should be punished.
if (block.timestamp < until && !allUnlocked && !isGovernance) {
uint96 punishedAmount = _getPunishedAmount(amount, until);
amount -= punishedAmount;
/// @dev punishedAmount can be 0 if block.timestamp are very close to 'until'
if (punishedAmount > 0) {
require(address(feeSharing) != address(0), "FeeSharing address wasn't set"); // S08
/// @dev Move punished amount to fee sharing.
/// @dev Approve transfer here and let feeSharing do transfer and write checkpoint.
SOVToken.approve(address(feeSharing), punishedAmount);
feeSharing.transferTokens(address(SOVToken), punishedAmount);
}
}
/// @dev transferFrom
bool success = SOVToken.transfer(receiver, amount);
require(success, "Token transfer failed"); // S09
emit StakingWithdrawn(msg.sender, amount, until, receiver, isGovernance);
}
/**
* @notice Send user' staked tokens to a receiver.
* This function is dedicated only for direct withdrawal from staking contract.
* Currently only being used by cancelTeamVesting()
*
* @param amount The number of tokens to withdraw.
* @param until The date until which the tokens were staked.
* @param receiver The receiver of the tokens. If not specified, send to the msg.sender.
* @param vestingConfig The vesting config.
* @dev VestingConfig struct intended to avoid stack too deep issue, and it contains this properties:
address vestingAddress; // vesting contract address
uint256 startDate; //start date of vesting
uint256 endDate; // end date of vesting
uint256 cliff; // after this time period the tokens begin to unlock
uint256 duration; // after this period all the tokens will be unlocked
address tokenOwner; // owner of the vested tokens
* */
function _withdrawFromTeamVesting(
uint96 amount,
uint256 until,
address receiver,
VestingConfig memory vestingConfig
) internal {
address vesting = vestingConfig.vestingAddress;
until = _timestampToLockDate(until);
_validateWithdrawParams(vesting, amount, until);
/// @dev Update the checkpoints.
_decreaseDailyStake(until, amount);
_decreaseUserStake(vesting, until, amount);
_decreaseVestingStake(until, amount);
_decreaseDelegateStake(delegates[vesting][until], until, amount);
/// @dev transferFrom
bool success = SOVToken.transfer(receiver, amount);
require(success, "Token transfer failed"); // S09
emit StakingWithdrawn(vesting, amount, until, receiver, true);
}
// @dev withdraws tokens for lock date 2 weeks later than given lock date
function _withdrawNext(uint256 until, address receiver, bool isGovernance) internal {
if (_isVestingContract(msg.sender)) {
// nextLock needs to be adjusted to the next valid lock date to make sure we don't accidentally
// withdraw stakes that are in the future and would get slashed (if until is not
// a valid lock date). but until is already handled in the withdraw function
uint256 nextLock = until.add(TWO_WEEKS);
if (isGovernance || block.timestamp >= nextLock) {
uint96 stakes = _getPriorUserStakeByDate(msg.sender, nextLock, block.number - 1);
if (stakes > 0) {
_withdraw(stakes, nextLock, receiver, isGovernance);
}
}
}
}
/**
* @notice Get available and punished amount for withdrawing.
* @param amount The number of tokens to withdraw.
* @param until The date until which the tokens were staked. Adjusted to the next valid lock date, if necessary.
* @return Amount to withraw and penalty amount
* */
function getWithdrawAmounts(
uint96 amount,
uint256 until
) external view returns (uint96, uint96) {
until = _adjustDateForOrigin(until);
_validateWithdrawParams(msg.sender, amount, until);
uint96 punishedAmount = _getPunishedAmount(amount, until);
return (amount - punishedAmount, punishedAmount);
}
/**
* @notice Get punished amount for withdrawing.
* @param amount The number of tokens to withdraw.
* @param until The date until which the tokens were staked.
* */
function _getPunishedAmount(uint96 amount, uint256 until) internal view returns (uint96) {
uint256 date = _timestampToLockDate(block.timestamp);
uint96 weight = _computeWeightByDate(until, date); /// @dev (10 - 1) * WEIGHT_FACTOR
weight = weight * weightScaling;
return (amount * weight) / WEIGHT_FACTOR / 100;
}
/**
* @notice Validate withdraw parameters.
* @param account Address to be validated.
* @param amount The number of tokens to withdraw.
* @param until The date until which the tokens were staked.
* */
function _validateWithdrawParams(address account, uint96 amount, uint256 until) internal view {
require(amount > 0, "Amount of tokens to withdraw must be > 0"); // S10
uint96 balance = _getPriorUserStakeByDate(account, until, block.number - 1);
require(amount <= balance, "Staking::withdraw: not enough balance"); // S11
}
/**
* @notice Allow the owner to unlock all tokens in case the staking contract
* is going to be replaced
* Note: Not reversible on purpose. once unlocked, everything is unlocked.
* The owner should not be able to just quickly unlock to withdraw his own
* tokens and lock again.
* @dev Last resort.
* */
function unlockAllTokens() external onlyOwner whenNotFrozen {
allUnlocked = true;
emit TokensUnlocked(SOVToken.balanceOf(address(this)));
}
/**
* @dev set max withdraw iterations.
*
* @param newMaxIterations new max iterations value.
*/
function setMaxVestingWithdrawIterations(
uint256 newMaxIterations
) external onlyAuthorized whenNotFrozen {
require(newMaxIterations > 0, "Invalid max iterations");
emit MaxVestingWithdrawIterationsUpdated(maxVestingWithdrawIterations, newMaxIterations);
maxVestingWithdrawIterations = newMaxIterations;
}
/**
* @notice Withdraw tokens for vesting contract.
* @param vesting The address of Vesting contract.
* @param receiver The receiver of the tokens. If not specified, send to the msg.sender
* @dev This function is dedicated only to support backward compatibility for sovryn ecosystem that has been implementing this staking contract.
* @dev Sovryn protocol will use the cancelTeamVesting function for the withdrawal moving forward.
* https://github.com/DistributedCollective/Sovryn-smart-contracts/blob/4bbfe5bd0311ca71e4ef0e3af810d3791d8e4061/contracts/governance/Staking/modules/StakingWithdrawModule.sol#L78
* */
function governanceWithdrawVesting(
address vesting,
address receiver
) public onlyAuthorized whenNotFrozen {
require(vestingRegistryLogic.isTeamVesting(vesting), "Only team vesting allowed");
ITeamVesting teamVesting = ITeamVesting(vesting);
uint256 teamVestingStartDate = teamVesting.startDate();
uint256 teamVestingCliff = teamVesting.cliff();
uint256 nextStartFrom = teamVestingStartDate + teamVestingCliff;
bool withdrawFlag = true;
bool notCompleted;
/**
* The withdrawal is limited to certain iterations (set in maxVestingWithdrawIterations), so in order to withdraw all, we need to iterate until it is fully withdrawn.
*/
while (withdrawFlag) {
/**
* notCompleted is the flag whether the withdrawal is fully withdrawn or not.
* As long as the notCompleted is true, we will keep the iteration using the nextStartFrom.
*/
(nextStartFrom, notCompleted) = _cancelTeamVesting(vesting, receiver, nextStartFrom);
withdrawFlag = notCompleted ? true : false;
}
emit VestingTokensWithdrawn(vesting, receiver);
}
/**
* @notice Withdraw the given amount of tokens.
* @param amount The number of tokens to withdraw.
* @param until The date until which the tokens were staked.
* @param receiver The receiver of the tokens. If not specified, send to the msg.sender
* @dev Can be invoked only by whitelisted contract passed to governanceWithdrawVesting
* */
function governanceWithdraw(
uint96 amount,
uint256 until,
address receiver
) external whenNotFrozen {
require(vestingWhitelist[msg.sender], "unauthorized"); // S07
_notSameBlockAsStakingCheckpoint(until, msg.sender);
_withdraw(amount, until, receiver, true);
// @dev withdraws tokens for lock date 2 weeks later than given lock date if sender is a contract
// we don't need to check block.timestamp here
_withdrawNext(until, receiver, true);
}
function getFunctionsList() external pure returns (bytes4[] memory) {
bytes4[] memory functionsList = new bytes4[](7);
functionsList[0] = this.withdraw.selector;
functionsList[1] = this.cancelTeamVesting.selector;
functionsList[2] = this.getWithdrawAmounts.selector;
functionsList[3] = this.unlockAllTokens.selector;
functionsList[4] = this.setMaxVestingWithdrawIterations.selector;
functionsList[5] = this.governanceWithdraw.selector;
functionsList[6] = this.governanceWithdrawVesting.selector;
return functionsList;
}
}