-
Notifications
You must be signed in to change notification settings - Fork 46
/
VestingLogic.sol
253 lines (219 loc) · 9.45 KB
/
VestingLogic.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
pragma solidity ^0.5.17;
pragma experimental ABIEncoderV2;
import "../../openzeppelin/Ownable.sol";
import "../../interfaces/IERC20.sol";
import "../Staking/interfaces/IStaking.sol";
import "../IFeeSharingCollector.sol";
import "./IVesting.sol";
import "../ApprovalReceiver.sol";
import "./VestingStorage.sol";
import "../../openzeppelin/SafeMath.sol";
/**
* @title Vesting Logic contract.
* @notice Staking, delegating and withdrawal functionality.
* @dev Deployed by a VestingFactory contract.
* */
contract VestingLogic is IVesting, VestingStorage, ApprovalReceiver {
using SafeMath for uint256;
/* Events */
event TokensStaked(address indexed caller, uint256 amount);
event VotesDelegated(address indexed caller, address delegatee);
event TokensWithdrawn(
address indexed caller,
address receiver,
uint256 startFrom,
uint256 end
);
event DividendsCollected(
address indexed caller,
address loanPoolToken,
address receiver,
uint32 maxCheckpoints
);
event MigratedToNewStakingContract(address indexed caller, address newStakingContract);
/* Modifiers */
/**
* @dev Throws if called by any account other than the token owner or the contract owner.
*/
modifier onlyOwners() {
require(msg.sender == tokenOwner || isOwner(), "unauthorized");
_;
}
/**
* @dev Throws if called by any account other than the token owner.
*/
modifier onlyTokenOwner() {
require(msg.sender == tokenOwner, "unauthorized");
_;
}
/* Functions */
/**
* @notice Stakes tokens according to the vesting schedule.
* @param _amount The amount of tokens to stake.
* */
function stakeTokens(uint256 _amount) public {
_stakeTokens(msg.sender, _amount);
}
/**
* @notice Stakes tokens according to the vesting schedule.
* @dev This function will be invoked from receiveApproval.
* @dev SOV.approveAndCall -> this.receiveApproval -> this.stakeTokensWithApproval
* @param _sender The sender of SOV.approveAndCall
* @param _amount The amount of tokens to stake.
* */
function stakeTokensWithApproval(address _sender, uint256 _amount) public onlyThisContract {
_stakeTokens(_sender, _amount);
}
/**
* @notice Stakes tokens according to the vesting schedule. Low level function.
* @dev Once here the allowance of tokens is taken for granted.
* @param _sender The sender of tokens to stake.
* @param _amount The amount of tokens to stake.
* */
function _stakeTokens(address _sender, uint256 _amount) internal {
/// @dev Maybe better to allow staking unil the cliff was reached.
if (startDate == 0) {
startDate = staking.timestampToLockDate(block.timestamp);
}
endDate = staking.timestampToLockDate(block.timestamp + duration);
/// @dev Transfer the tokens to this contract.
bool success = SOV.transferFrom(_sender, address(this), _amount);
require(success);
/// @dev Allow the staking contract to access them.
SOV.approve(address(staking), _amount);
staking.stakeBySchedule(_amount, cliff, duration, FOUR_WEEKS, address(this), tokenOwner);
emit TokensStaked(_sender, _amount);
}
/**
* @notice Delegate votes from `msg.sender` which are locked until lockDate
* to `delegatee`.
* @param _delegatee The address to delegate votes to.
* */
function delegate(address _delegatee) public onlyTokenOwner {
require(_delegatee != address(0), "delegatee address invalid");
/// @dev Withdraw for each unlocked position.
/// @dev Don't change FOUR_WEEKS to TWO_WEEKS, a lot of vestings already deployed with FOUR_WEEKS
/// workaround found, but it doesn't work with TWO_WEEKS
for (uint256 i = startDate + cliff; i <= endDate; i += FOUR_WEEKS) {
staking.delegate(_delegatee, i);
}
emit VotesDelegated(msg.sender, _delegatee);
}
/**
* @notice Withdraws unlocked tokens from the staking contract and
* forwards them to an address specified by the token owner.
* @param receiver The receiving address.
* */
function withdrawTokens(address receiver) public onlyOwners {
uint256 startFrom = startDate + cliff;
_withdrawTokens(receiver, startFrom, block.timestamp);
}
/**
* @notice Withdraws unlocked tokens partially (based on the max withdraw iteration that has been set) from the staking contract and
* forwards them to an address specified by the token owner.
* @param receiver The receiving address.
* @param startFrom The start value for the iterations.
* @param maxWithdrawIterations max withdrawal iteration to work around block gas limit issue.
* */
function withdrawTokensStartingFrom(
address receiver,
uint256 startFrom,
uint256 maxWithdrawIterations
) public onlyOwners {
uint256 defaultStartFrom = startDate + cliff;
startFrom = _timestampToLockDate(startFrom);
startFrom = startFrom < defaultStartFrom ? defaultStartFrom : startFrom;
// @dev max iterations need to be decreased by 1, otherwise the iteration will always be surplus by 1
uint256 maxWithdrawDate = (startFrom + (FOUR_WEEKS * (maxWithdrawIterations.sub(1))));
uint256 endAt = endDate < maxWithdrawDate ? endDate : maxWithdrawDate;
_withdrawTokens(receiver, startFrom, endAt);
}
/**
* @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 receiver The receiving address.
* @param startFrom start withdrawal from date.
* @param endAt end time for regular withdrawal
* or just unlocked tokens (false).
* */
function _withdrawTokens(address receiver, uint256 startFrom, uint256 endAt) internal {
require(receiver != address(0), "receiver address invalid");
uint96 stake;
/// @dev Usually we just need to iterate over the possible dates until now.
uint256 end;
if (staking.allUnlocked()) {
end = endAt < endDate ? endAt : endDate;
} else {
end = endAt < block.timestamp ? endAt : block.timestamp;
if (end > endDate) end = endDate;
}
/// @dev Withdraw for each unlocked position.
/// @dev Don't change FOUR_WEEKS to TWO_WEEKS, a lot of vestings already deployed with FOUR_WEEKS
/// workaround found, but it doesn't work with TWO_WEEKS
for (uint256 i = startFrom; i <= end; i += FOUR_WEEKS) {
/// @dev Read amount to withdraw.
stake = staking.getPriorUserStakeByDate(address(this), i, block.number - 1);
/// @dev Withdraw if > 0
if (stake > 0) {
staking.withdraw(stake, i, receiver);
}
}
emit TokensWithdrawn(msg.sender, receiver, startFrom, end);
}
/**
* @notice Collect dividends from fee sharing proxy.
* @param _loanPoolToken The loan pool token address.
* @param _maxCheckpoints Maximum number of checkpoints to be processed.
* @param _receiver The receiver of tokens or msg.sender
* */
function collectDividends(
address _loanPoolToken,
uint32 _maxCheckpoints,
address _receiver
) public onlyOwners {
require(_receiver != address(0), "receiver address invalid");
/// @dev Invokes the fee sharing proxy.
feeSharingCollector.withdraw(_loanPoolToken, _maxCheckpoints, _receiver);
emit DividendsCollected(msg.sender, _loanPoolToken, _receiver, _maxCheckpoints);
}
/**
* @notice Allows the owners to migrate the positions
* to a new staking contract.
* */
function migrateToNewStakingContract() public onlyOwners {
staking.migrateToNewStakingContract();
staking = IStaking(staking.newStakingContract());
emit MigratedToNewStakingContract(msg.sender, address(staking));
}
/**
* @notice Overrides default ApprovalReceiver._getToken function to
* register SOV token on this contract.
* @return The address of SOV token.
* */
function _getToken() internal view returns (address) {
return address(SOV);
}
/**
* @notice Overrides default ApprovalReceiver._getSelectors function to
* register stakeTokensWithApproval selector on this contract.
* @return The array of registered selectors on this contract.
* */
function _getSelectors() internal pure returns (bytes4[] memory) {
bytes4[] memory selectors = new bytes4[](1);
selectors[0] = this.stakeTokensWithApproval.selector;
return selectors;
}
function _timestampToLockDate(uint256 timestamp) internal view returns (uint256 lockDate) {
// Optimize gas costs by reading kickoffTS from storage only once.
uint256 start = startDate + cliff;
require(timestamp >= start, "timestamp < contract creation"); // WS23
/**
* @dev If staking timestamp does not match any of the unstaking dates
* , set the lockDate to the closest one before the timestamp.
* E.g. Passed timestamps lies 7 weeks after kickoff -> only stake for 6 weeks.
* */
uint256 periodFromKickoff = (timestamp - start) / FOUR_WEEKS;
lockDate = periodFromKickoff * FOUR_WEEKS + start;
}
}