-
Notifications
You must be signed in to change notification settings - Fork 6
/
GPToke.sol
324 lines (250 loc) · 11.5 KB
/
GPToke.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
// solhint-disable not-rely-on-time
// SPDX-License-Identifier: UNLICENSED
// Copyright (c) 2023 Tokemak Foundation. All rights reserved.
pragma solidity 0.8.17;
import { SafeERC20 } from "openzeppelin-contracts/token/ERC20/utils/SafeERC20.sol";
import { IERC20Metadata } from "openzeppelin-contracts/token/ERC20/extensions/IERC20Metadata.sol";
import { ERC20 } from "openzeppelin-contracts/token/ERC20/ERC20.sol";
import { ERC20Votes } from "openzeppelin-contracts/token/ERC20/extensions/ERC20Votes.sol";
import { ERC20Permit } from "openzeppelin-contracts/token/ERC20/extensions/draft-ERC20Permit.sol";
import { Ownable } from "openzeppelin-contracts/access/Ownable.sol";
import { Pausable } from "openzeppelin-contracts/security/Pausable.sol";
import { PRBMathUD60x18 } from "prb-math/contracts/PRBMathUD60x18.sol";
import { IWETH9 } from "src/interfaces/utils/IWETH9.sol";
import { IGPToke } from "src/interfaces/staking/IGPToke.sol";
import { ISystemRegistry } from "src/interfaces/ISystemRegistry.sol";
import { SecurityBase } from "src/security/SecurityBase.sol";
import { Errors } from "src/utils/Errors.sol";
import { SystemComponent } from "src/SystemComponent.sol";
contract GPToke is IGPToke, ERC20Votes, Pausable, SystemComponent, SecurityBase {
using SafeERC20 for IERC20Metadata;
using SafeERC20 for IWETH9;
// variables
uint256 public immutable startEpoch;
uint256 public immutable minStakeDuration;
// solhint-disable-next-line const-name-snakecase
uint256 public maxStakeDuration = 1461 days; // default 4 years
uint256 public constant MIN_STAKE_AMOUNT = 10_000;
uint256 public constant MAX_STAKE_AMOUNT = 100e6 * 1e18; // default 100m toke
mapping(address => Lockup[]) public lockups;
uint256 private constant YEAR_BASE_BOOST = 18e17;
IERC20Metadata public immutable toke;
//
// Reward Vars
//
IWETH9 private immutable weth;
uint256 public constant REWARD_FACTOR = 1e12;
// tracks user's checkpointed reward debt per share
mapping(address => uint256) public rewardDebtPerShare;
// keeps track of rewards checkpointed / offloaded but not yet transferred
mapping(address => uint256) private unclaimedRewards;
// total current accumulated reward per share
uint256 public accRewardPerShare;
// See {IGPToke-totalRewardsEarned}
uint256 public totalRewardsEarned;
// See {IGPToke-totalRewardsClaimed}
uint256 public totalRewardsClaimed;
// See {IGPToke-rewardsClaimed}
mapping(address => uint256) public rewardsClaimed;
constructor(
ISystemRegistry _systemRegistry,
uint256 _startEpoch,
uint256 _minStakeDuration
)
SystemComponent(_systemRegistry)
ERC20("Staked Toke", "accToke")
ERC20Permit("accToke")
SecurityBase(address(_systemRegistry.accessController()))
{
startEpoch = _startEpoch;
minStakeDuration = _minStakeDuration;
toke = systemRegistry.toke();
weth = systemRegistry.weth();
}
// @dev short-circuit transfers
function transfer(address, uint256) public pure override returns (bool) {
revert TransfersDisabled();
}
// @dev short-circuit transfers
function transferFrom(address, address, uint256) public pure override returns (bool) {
revert TransfersDisabled();
}
/// @inheritdoc IGPToke
function stake(uint256 amount, uint256 duration, address to) external {
_stake(amount, duration, to);
}
/// @inheritdoc IGPToke
function stake(uint256 amount, uint256 duration) external {
_stake(amount, duration, msg.sender);
}
function _stake(uint256 amount, uint256 duration, address to) internal whenNotPaused {
//
// validation checks
//
if (to == address(0)) revert ZeroAddress();
if (amount < MIN_STAKE_AMOUNT) revert StakingAmountInsufficient();
if (amount > MAX_STAKE_AMOUNT) revert StakingAmountExceeded();
// duration checked inside previewPoints
(uint256 points, uint256 end) = previewPoints(amount, duration);
if (points + totalSupply() > type(uint192).max) {
revert StakingPointsExceeded();
}
// checkpoint rewards for caller
_collectRewards(to, false);
// save information for current lockup
lockups[to].push(Lockup({ amount: uint128(amount), end: uint128(end), points: points }));
// create points for user
_mint(to, points);
emit Stake(to, lockups[to].length - 1, amount, end, points);
// transfer staked toke in
toke.safeTransferFrom(msg.sender, address(this), amount);
}
/// @inheritdoc IGPToke
function unstake(uint256 lockupId) external whenNotPaused {
if (lockupId >= lockups[msg.sender].length) revert LockupDoesNotExist();
// get staking information
Lockup memory lockup = lockups[msg.sender][lockupId];
uint256 amount = lockup.amount;
uint256 end = lockup.end;
uint256 points = lockup.points;
// slither-disable-next-line timestamp
if (block.timestamp < end) revert NotUnlockableYet();
if (end == 0) revert AlreadyUnlocked();
// checkpoint rewards
_collectRewards(msg.sender, false);
// remove stake
delete lockups[msg.sender][lockupId];
// wipe points
_burn(msg.sender, points);
emit Unstake(msg.sender, lockupId, amount, end, points);
// send staked toke back to user
toke.safeTransfer(msg.sender, amount);
}
/// @inheritdoc IGPToke
function extend(uint256 lockupId, uint256 duration) external whenNotPaused {
if (lockupId >= lockups[msg.sender].length) revert LockupDoesNotExist();
// before doing anything, make sure the rewards checkpoints are updated!
_collectRewards(msg.sender, false);
// duration checked inside previewPoints
Lockup storage lockup = lockups[msg.sender][lockupId];
uint256 oldAmount = lockup.amount;
uint256 oldEnd = lockup.end;
uint256 oldPoints = lockup.points;
(uint256 newPoints, uint256 newEnd) = previewPoints(oldAmount, duration);
if (newEnd <= oldEnd) revert ExtendDurationTooShort();
lockup.end = uint128(newEnd);
lockup.points = newPoints;
lockups[msg.sender][lockupId] = lockup;
// issue extra points for extension
_mint(msg.sender, newPoints - oldPoints);
emit Extend(msg.sender, lockupId, oldAmount, oldEnd, newEnd, oldPoints, newPoints);
}
/// @inheritdoc IGPToke
function previewPoints(uint256 amount, uint256 duration) public view returns (uint256 points, uint256 end) {
if (duration < minStakeDuration) revert StakingDurationTooShort();
if (duration > maxStakeDuration) revert StakingDurationTooLong();
// slither-disable-next-line timestamp
uint256 start = block.timestamp > startEpoch ? block.timestamp : startEpoch;
end = start + duration;
// calculate points based on duration from staking end date
uint256 endYearpoc = ((end - startEpoch) * 1e18) / 365 days;
uint256 multiplier = PRBMathUD60x18.pow(YEAR_BASE_BOOST, endYearpoc);
points = (amount * multiplier) / 1e18;
}
/// @inheritdoc IGPToke
function getLockups(address user) external view returns (Lockup[] memory) {
return lockups[user];
}
/// @notice Update max stake duration allowed
function setMaxStakeDuration(uint256 _maxStakeDuration) external onlyOwner {
uint256 old = maxStakeDuration;
maxStakeDuration = _maxStakeDuration;
emit SetMaxStakeDuration(old, _maxStakeDuration);
}
function pause() external onlyOwner {
_pause();
}
function unpause() external onlyOwner {
_unpause();
}
/* **************************************************************************** */
/* */
/* Rewards */
/* */
/* **************************************************************************** */
/// @notice Allows an actor to deposit ETH as staking reward to be distributed to all staked participants
/// @param amount Amount of `WETH` to take from caller and deposit as reward for the stakers
function addWETHRewards(uint256 amount) external whenNotPaused {
Errors.verifyNotZero(amount, "amount");
uint256 supply = totalSupply();
Errors.verifyNotZero(supply, "supply");
totalRewardsEarned += amount;
accRewardPerShare += amount * REWARD_FACTOR / supply;
emit RewardsAdded(amount);
weth.safeTransferFrom(msg.sender, address(this), amount);
}
/// @inheritdoc IGPToke
function previewRewards() external view returns (uint256 amount) {
return previewRewards(msg.sender);
}
/// @inheritdoc IGPToke
function previewRewards(address user) public view returns (uint256 amount) {
uint256 supply = totalSupply();
if (supply == 0) {
return unclaimedRewards[user];
}
// calculate reward per share by taking the current reward per share and subtracting what user already claimed
uint256 _netRewardsPerShare = accRewardPerShare - rewardDebtPerShare[user];
// calculate full reward user is entitled to by taking their recently earned and adding unclaimed checkpointed
return ((balanceOf(user) * _netRewardsPerShare) / REWARD_FACTOR) + unclaimedRewards[user];
}
/// @inheritdoc IGPToke
function collectRewards() external returns (uint256) {
return _collectRewards(msg.sender, true);
}
/// @dev See {IGPToke-collectRewards}.
function _collectRewards(address user, bool distribute) internal returns (uint256) {
// calculate user's new rewards per share (current minus claimed)
uint256 netRewardsPerShare = accRewardPerShare - rewardDebtPerShare[user];
// calculate amount of actual rewards
uint256 netRewards = (balanceOf(user) * netRewardsPerShare) / REWARD_FACTOR;
// get reference to user's pending (sandboxed) rewards
uint256 pendingRewards = unclaimedRewards[user];
// update checkpoint to current
rewardDebtPerShare[user] = accRewardPerShare;
// if nothing to claim, bail
if (netRewards == 0 && pendingRewards == 0) {
return 0;
}
if (distribute) {
//
// if asked for actual distribution, transfer all earnings
//
// reset sandboxed rewards
unclaimedRewards[user] = 0;
// get total amount by adding new rewards and previously sandboxed
uint256 totalClaiming = netRewards + pendingRewards;
// update running totals
totalRewardsClaimed += totalClaiming;
rewardsClaimed[user] += totalClaiming;
emit RewardsClaimed(user, totalClaiming);
// send rewards to user
weth.safeTransfer(user, totalClaiming);
// return total amount claimed
return totalClaiming;
}
if (netRewards > 0) {
// Save (sandbox) to their account for later transfer
unclaimedRewards[user] += netRewards;
emit RewardsCollected(user, netRewards);
}
// nothing collected
return 0;
}
/// @notice Catch-all. If any eth is sent, wrap and add to rewards
receive() external payable {
// appreciate the ETH! wrap and add as rewards
weth.deposit{ value: msg.value }();
GPToke(payable(this)).addWETHRewards(msg.value);
}
}