-
Notifications
You must be signed in to change notification settings - Fork 0
/
RewardsStreamerMP.sol
257 lines (201 loc) · 8.28 KB
/
RewardsStreamerMP.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
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { ReentrancyGuard } from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
// Rewards Streamer with Multiplier Points
contract RewardsStreamerMP is ReentrancyGuard {
error StakingManager__AmountCannotBeZero();
error StakingManager__TransferFailed();
error StakingManager__InsufficientBalance();
error StakingManager__InvalidLockingPeriod();
error StakingManager__CannotRestakeWithLockedFunds();
error StakingManager__TokensAreLocked();
IERC20 public immutable STAKING_TOKEN;
IERC20 public immutable REWARD_TOKEN;
uint256 public constant SCALE_FACTOR = 1e18;
uint256 public constant MP_RATE_PER_YEAR = 1e18;
uint256 public constant MIN_LOCKING_PERIOD = 90 days;
uint256 public constant MAX_LOCKING_PERIOD = 4 * 365 days;
uint256 public constant MAX_MULTIPLIER = 4;
uint256 public totalStaked;
uint256 public totalMP;
uint256 public potentialMP;
uint256 public rewardIndex;
uint256 public accountedRewards;
uint256 public lastMPUpdatedTime;
struct UserInfo {
uint256 stakedBalance;
uint256 userRewardIndex;
uint256 userMP;
uint256 userPotentialMP;
uint256 lastMPUpdateTime;
uint256 lockUntil;
}
mapping(address account => UserInfo data) public users;
constructor(address _stakingToken, address _rewardToken) {
STAKING_TOKEN = IERC20(_stakingToken);
REWARD_TOKEN = IERC20(_rewardToken);
lastMPUpdatedTime = block.timestamp;
}
function stake(uint256 amount, uint256 lockPeriod) external nonReentrant {
if (amount == 0) {
revert StakingManager__AmountCannotBeZero();
}
if (lockPeriod != 0 && (lockPeriod < MIN_LOCKING_PERIOD || lockPeriod > MAX_LOCKING_PERIOD)) {
revert StakingManager__InvalidLockingPeriod();
}
_updateGlobalState();
_updateUserMP(msg.sender);
UserInfo storage user = users[msg.sender];
if (user.lockUntil != 0 && user.lockUntil > block.timestamp) {
revert StakingManager__CannotRestakeWithLockedFunds();
}
uint256 userRewards = calculateUserRewards(msg.sender);
if (userRewards > 0) {
distributeRewards(msg.sender, userRewards);
}
bool success = STAKING_TOKEN.transferFrom(msg.sender, address(this), amount);
if (!success) {
revert StakingManager__TransferFailed();
}
user.stakedBalance += amount;
totalStaked += amount;
uint256 initialMP = amount;
uint256 userPotentialMP = amount * MAX_MULTIPLIER;
if (lockPeriod != 0) {
uint256 lockMultiplier = (lockPeriod * MAX_MULTIPLIER * SCALE_FACTOR) / MAX_LOCKING_PERIOD;
initialMP += amount * lockMultiplier / SCALE_FACTOR;
userPotentialMP += (amount * lockMultiplier / SCALE_FACTOR);
user.lockUntil = block.timestamp + lockPeriod;
} else {
user.lockUntil = 0;
}
user.userMP += initialMP;
totalMP += initialMP;
user.userPotentialMP += userPotentialMP;
potentialMP += userPotentialMP;
user.userRewardIndex = rewardIndex;
user.lastMPUpdateTime = block.timestamp;
}
function unstake(uint256 amount) external nonReentrant {
UserInfo storage user = users[msg.sender];
if (amount > user.stakedBalance) {
revert StakingManager__InsufficientBalance();
}
if (block.timestamp < user.lockUntil) {
revert StakingManager__TokensAreLocked();
}
_updateGlobalState();
_updateUserMP(msg.sender);
uint256 userRewards = calculateUserRewards(msg.sender);
if (userRewards > 0) {
distributeRewards(msg.sender, userRewards);
}
uint256 previousStakedBalance = user.stakedBalance;
uint256 mpToReduce = (user.userMP * amount * SCALE_FACTOR) / (previousStakedBalance * SCALE_FACTOR);
uint256 potentialMPToReduce =
(user.userPotentialMP * amount * SCALE_FACTOR) / (previousStakedBalance * SCALE_FACTOR);
user.stakedBalance -= amount;
user.userMP -= mpToReduce;
user.userPotentialMP -= potentialMPToReduce;
totalMP -= mpToReduce;
potentialMP -= potentialMPToReduce;
totalStaked -= amount;
bool success = STAKING_TOKEN.transfer(msg.sender, amount);
if (!success) {
revert StakingManager__TransferFailed();
}
user.userRewardIndex = rewardIndex;
}
function _updateGlobalState() internal {
updateGlobalMP();
updateRewardIndex();
}
function updateGlobalState() external {
_updateGlobalState();
}
function updateGlobalMP() internal {
if (potentialMP == 0) {
lastMPUpdatedTime = block.timestamp;
return;
}
uint256 currentTime = block.timestamp;
uint256 timeDiff = currentTime - lastMPUpdatedTime;
if (timeDiff == 0) {
return;
}
uint256 accruedMP = (timeDiff * totalStaked * MP_RATE_PER_YEAR) / (365 days * SCALE_FACTOR);
if (accruedMP > potentialMP) {
accruedMP = potentialMP;
}
// Adjust rewardIndex before updating totalMP
uint256 previousTotalWeight = totalStaked + totalMP;
totalMP += accruedMP;
uint256 newTotalWeight = totalStaked + totalMP;
if (previousTotalWeight != 0 && newTotalWeight != previousTotalWeight) {
rewardIndex = (rewardIndex * previousTotalWeight) / newTotalWeight;
}
potentialMP -= accruedMP;
lastMPUpdatedTime = currentTime;
}
function updateRewardIndex() internal {
uint256 totalWeight = totalStaked + totalMP;
if (totalWeight == 0) {
return;
}
uint256 rewardBalance = REWARD_TOKEN.balanceOf(address(this));
uint256 newRewards = rewardBalance > accountedRewards ? rewardBalance - accountedRewards : 0;
if (newRewards > 0) {
rewardIndex += (newRewards * SCALE_FACTOR) / totalWeight;
accountedRewards += newRewards;
}
}
function _updateUserMP(address userAddress) internal {
UserInfo storage user = users[userAddress];
if (user.userPotentialMP == 0 || user.stakedBalance == 0) {
user.lastMPUpdateTime = block.timestamp;
return;
}
uint256 timeDiff = block.timestamp - user.lastMPUpdateTime;
if (timeDiff == 0) {
return;
}
uint256 accruedMP = (timeDiff * user.stakedBalance * MP_RATE_PER_YEAR) / (365 days * SCALE_FACTOR);
if (accruedMP > user.userPotentialMP) {
accruedMP = user.userPotentialMP;
}
user.userPotentialMP -= accruedMP;
user.userMP += accruedMP;
user.lastMPUpdateTime = block.timestamp;
}
function updateUserMP(address userAddress) external {
_updateUserMP(userAddress);
}
function calculateUserRewards(address userAddress) public view returns (uint256) {
UserInfo storage user = users[userAddress];
uint256 userWeight = user.stakedBalance + user.userMP;
uint256 deltaRewardIndex = rewardIndex - user.userRewardIndex;
return (userWeight * deltaRewardIndex) / SCALE_FACTOR;
}
function distributeRewards(address to, uint256 amount) internal {
uint256 rewardBalance = REWARD_TOKEN.balanceOf(address(this));
// If amount is higher than the contract's balance (for rounding error), transfer the balance.
if (amount > rewardBalance) {
amount = rewardBalance;
}
accountedRewards -= amount;
bool success = REWARD_TOKEN.transfer(to, amount);
if (!success) {
revert StakingManager__TransferFailed();
}
}
function getStakedBalance(address userAddress) external view returns (uint256) {
return users[userAddress].stakedBalance;
}
function getPendingRewards(address userAddress) external view returns (uint256) {
return calculateUserRewards(userAddress);
}
function getUserInfo(address userAddress) external view returns (UserInfo memory) {
return users[userAddress];
}
}