forked from indexed-finance/dividends
-
Notifications
You must be signed in to change notification settings - Fork 7
/
Copy pathSharesTimeLock.sol
296 lines (246 loc) · 9.81 KB
/
SharesTimeLock.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
// SPDX-License-Identifier: MIT
pragma solidity ^0.7.0;
pragma abicoder v2;
import {OwnableUpgradeable as Ownable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
import "./ERC20NonTransferableRewardsOwned.sol";
import "./libraries/LowGasSafeMath.sol";
contract SharesTimeLock is Ownable() {
using LowGasSafeMath for uint256;
using TransferHelper for address;
address public depositToken;
ERC20NonTransferableRewardsOwned public rewardsToken;
// min amount in
uint32 public minLockDuration;
uint32 public maxLockDuration;
uint256 public minLockAmount;
uint256 private constant AVG_SECONDS_MONTH = 2628000;
bool public emergencyUnlockTriggered;
/*
Mapping of coefficient for the staking curve
y=x/k*log(x)
where `x` is the staking time
and `k` is a constant 56.0268900276223
the period of staking here is calculated in months.
*/
uint256[37] public maxRatioArray;
event MinLockAmountChanged(uint256 newLockAmount);
event WhitelistedChanged(address indexed user, bool indexed whitelisted);
event Deposited(uint256 indexed lockId, uint256 amount, uint32 lockDuration, address indexed owner);
event Withdrawn(uint256 indexed lockId, uint256 amount, address indexed owner);
event Ejected(uint256 indexed lockId, uint256 amount, address indexed owner);
event BoostedToMax(uint256 indexed oldLockId, uint256 indexed newLockId, uint256 amount, address indexed owner);
event EjectBufferUpdated(uint256 newEjectBuffer);
struct Lock {
uint256 amount;
uint32 lockedAt;
uint32 lockDuration;
}
struct StakingData {
uint256 totalStaked;
uint256 veTokenTotalSupply;
uint256 accountVeTokenBalance;
uint256 accountWithdrawableRewards;
uint256 accountWithdrawnRewards;
uint256 accountDepositTokenBalance;
uint256 accountDepositTokenAllowance;
Lock[] accountLocks;
}
mapping(address => Lock[]) public locksOf;
mapping(address => bool) public whitelisted;
uint256 public ejectBuffer;
function getLocksOfLength(address account) external view returns (uint256) {
return locksOf[account].length;
}
/**
* @dev Returns the rewards multiplier for `duration` expressed as a fraction of 1e18.
*/
function getRewardsMultiplier(uint32 duration) public view returns (uint256 multiplier) {
require(duration >= minLockDuration && duration <= maxLockDuration, "getRewardsMultiplier: Duration not correct");
uint256 month = uint256(duration) / secondsPerMonth();
multiplier = maxRatioArray[month];
return multiplier;
}
function initialize(
address depositToken_,
ERC20NonTransferableRewardsOwned rewardsToken_,
uint32 minLockDuration_,
uint32 maxLockDuration_,
uint256 minLockAmount_
) public {
__Ownable_init();
rewardsToken = rewardsToken_;
depositToken = depositToken_;
require(minLockDuration_ < maxLockDuration_, "min>=max");
minLockDuration = minLockDuration_;
maxLockDuration = maxLockDuration_;
minLockAmount = minLockAmount_;
ejectBuffer = 7 days;
maxRatioArray = [
1,
2,
3,
4,
5,
6,
83333333333300000, // 6
105586554548800000, // 7
128950935744800000, // 8
153286798191400000, // 9
178485723463700000, // 10
204461099502300000, // 11
231142134539100000, // 12
258469880674300000, // 13
286394488282000000, // 14
314873248847800000, // 15
343869161986300000, // 16
373349862059400000, // 17
403286798191400000, // 18
433654597035900000, // 19
464430560048100000, // 20
495594261536300000, // 21
527127223437300000, // 22
559012649336100000, // 23
591235204823000000, // 24
623780834516600000, // 25
656636608405400000, // 26
689790591861100000, // 27
723231734933100000, // 28
756949777475800000, // 29
790935167376600000, // 30
825178989697100000, // 31
859672904965600000, // 32
894409095191000000, // 33
929380216424000000, // 34
964579356905500000, // 35
1000000000000000000 // 36
];
}
function depositByMonths(uint256 amount, uint256 months, address receiver) external {
// only allow whitelisted contracts or EOAS
require(
tx.origin == _msgSender() || whitelisted[_msgSender()],
"Not EOA or whitelisted"
);
// only allow whitelisted addresses to deposit to another address
require(
_msgSender() == receiver || whitelisted[_msgSender()],
"Only whitelised address can deposit to another address"
);
uint32 duration = uint32( months.mul(secondsPerMonth()) );
deposit(amount, duration, receiver);
}
function deposit(uint256 amount, uint32 duration, address receiver) internal {
require(amount >= minLockAmount, "Deposit: amount too small");
require(!emergencyUnlockTriggered, "Deposit: deposits locked");
depositToken.safeTransferFrom(_msgSender(), address(this), amount);
uint256 multiplier = getRewardsMultiplier(duration);
uint256 rewardShares = amount.mul(multiplier) / 1e18;
rewardsToken.mint(receiver, rewardShares);
locksOf[receiver].push(Lock({
amount: amount,
lockedAt: uint32(block.timestamp),
lockDuration: duration
}));
emit Deposited(locksOf[receiver].length - 1, amount, duration, receiver);
}
function withdraw(uint256 lockId) external {
Lock memory lock = locksOf[_msgSender()][lockId];
uint256 unlockAt = lock.lockedAt + lock.lockDuration;
require(block.timestamp > unlockAt || emergencyUnlockTriggered, "Withdraw: lock not expired and timelock not in emergency mode");
delete locksOf[_msgSender()][lockId];
uint256 multiplier = getRewardsMultiplier(lock.lockDuration);
uint256 rewardShares = lock.amount.mul(multiplier) / 1e18;
rewardsToken.burn(_msgSender(), rewardShares);
depositToken.safeTransfer(_msgSender(), lock.amount);
emit Withdrawn(lockId, lock.amount, _msgSender());
}
function boostToMax(uint256 lockId) external {
require(!emergencyUnlockTriggered, "BoostToMax: emergency unlock triggered");
Lock memory lock = locksOf[_msgSender()][lockId];
delete locksOf[_msgSender()][lockId];
uint256 multiplier = getRewardsMultiplier(lock.lockDuration);
uint256 rewardShares = lock.amount.mul(multiplier) / 1e18;
require(rewardsToken.balanceOf(_msgSender()) >= rewardShares, "boostToMax: Wrong shares number");
uint256 newMultiplier = getRewardsMultiplier(maxLockDuration);
uint256 newRewardShares = lock.amount.mul(newMultiplier) / 1e18;
rewardsToken.mint(_msgSender(), newRewardShares.sub(rewardShares));
locksOf[_msgSender()].push(Lock({
amount: lock.amount,
lockedAt: uint32(block.timestamp),
lockDuration: maxLockDuration
}));
emit BoostedToMax(lockId, locksOf[_msgSender()].length - 1, lock.amount, _msgSender());
}
// Eject expired locks
function eject(address[] memory lockAccounts, uint256[] memory lockIds) external {
require(lockAccounts.length == lockIds.length, "Array length mismatch");
for(uint256 i = 0; i < lockIds.length; i ++) {
//skip if lockId is invalid
if(locksOf[lockAccounts[i]].length - 1 < lockIds[i]) {
continue;
}
Lock memory lock = locksOf[lockAccounts[i]][lockIds[i]];
//skip if lock not expired or locked amount is zero
if(lock.lockedAt + lock.lockDuration + ejectBuffer > block.timestamp || lock.amount == 0) {
continue;
}
delete locksOf[lockAccounts[i]][lockIds[i]];
uint256 multiplier = getRewardsMultiplier(lock.lockDuration);
uint256 rewardShares = lock.amount.mul(multiplier) / 1e18;
rewardsToken.burn(lockAccounts[i], rewardShares);
depositToken.safeTransfer(lockAccounts[i], lock.amount);
emit Ejected(lockIds[i], lock.amount, lockAccounts[i]);
}
}
/**
* Setters
*/
function setMinLockAmount(uint256 minLockAmount_) external onlyOwner {
minLockAmount = minLockAmount_;
emit MinLockAmountChanged(minLockAmount_);
}
function setWhitelisted(address user, bool isWhitelisted) external onlyOwner {
whitelisted[user] = isWhitelisted;
emit WhitelistedChanged(user, isWhitelisted);
}
function triggerEmergencyUnlock() external onlyOwner {
require(!emergencyUnlockTriggered, "TriggerEmergencyUnlock: already triggered");
emergencyUnlockTriggered = true;
}
function setEjectBuffer(uint256 buffer) external onlyOwner {
ejectBuffer = buffer;
emit EjectBufferUpdated(buffer);
}
/**
* Getters
*/
function getStakingData(address account) external view returns (StakingData memory data) {
data.totalStaked = IERC20(depositToken).balanceOf(address(this));
data.veTokenTotalSupply = rewardsToken.totalSupply();
data.accountVeTokenBalance = rewardsToken.balanceOf(account);
data.accountWithdrawableRewards = rewardsToken.withdrawableRewardsOf(account);
data.accountWithdrawnRewards = rewardsToken.withdrawnRewardsOf(account);
data.accountDepositTokenBalance = IERC20(depositToken).balanceOf(account);
data.accountDepositTokenAllowance = IERC20(depositToken).allowance(account, address(this));
data.accountLocks = new Lock[](locksOf[account].length);
for(uint256 i = 0; i < locksOf[account].length; i ++) {
data.accountLocks[i] = locksOf[account][i];
}
}
// Used to overwrite in testing situations
function secondsPerMonth() internal view virtual returns(uint256) {
return AVG_SECONDS_MONTH;
}
function canEject(address account, uint256 lockId) external view returns(bool) {
//cannot eject non existing locks
if(locksOf[account].length - 1 < lockId) {
return false;
}
Lock memory lock = locksOf[account][lockId];
// if lock is already removed it cannot be ejected
if(lock.lockedAt == 0) {
return false;
}
return lock.lockedAt + lock.lockDuration + ejectBuffer <= block.timestamp;
}
}