/
Staking20Upgradeable.sol
347 lines (285 loc) · 12.7 KB
/
Staking20Upgradeable.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
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
// SPDX-License-Identifier: Apache-2.0
pragma solidity ^0.8.11;
import "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol";
import "../openzeppelin-presets/utils/math/SafeMath.sol";
import "../eip/interface/IERC20.sol";
import "../lib/CurrencyTransferLib.sol";
import "./interface/IStaking20.sol";
/**
* note: This is a Beta release.
*/
abstract contract Staking20Upgradeable is ReentrancyGuardUpgradeable, IStaking20 {
/*///////////////////////////////////////////////////////////////
State variables / Mappings
//////////////////////////////////////////////////////////////*/
///@dev Address of ERC20 contract -- staked tokens belong to this contract.
address public token;
/// @dev Decimals of staking token.
uint256 public stakingTokenDecimals;
/// @dev Decimals of reward token.
uint256 public rewardTokenDecimals;
/// @dev List of accounts that have staked that token-id.
address[] public stakersArray;
///@dev Next staking condition Id. Tracks number of conditon updates so far.
uint256 private nextConditionId;
///@dev Mapping staker address to Staker struct. See {struct IStaking20.Staker}.
mapping(address => Staker) public stakers;
///@dev Mapping from condition Id to staking condition. See {struct IStaking721.StakingCondition}
mapping(uint256 => StakingCondition) private stakingConditions;
function __Staking20_init(
address _token,
uint256 _stakingTokenDecimals,
uint256 _rewardTokenDecimals
) internal onlyInitializing {
__ReentrancyGuard_init();
require(address(_token) != address(0), "token address 0");
require(_stakingTokenDecimals != 0 && _rewardTokenDecimals != 0, "decimals 0");
token = _token;
stakingTokenDecimals = _stakingTokenDecimals;
rewardTokenDecimals = _rewardTokenDecimals;
}
/*///////////////////////////////////////////////////////////////
External/Public Functions
//////////////////////////////////////////////////////////////*/
/**
* @notice Stake ERC20 Tokens.
*
* @dev See {_stake}. Override that to implement custom logic.
*
* @param _amount Amount to stake.
*/
function stake(uint256 _amount) external nonReentrant {
_stake(_amount);
}
/**
* @notice Withdraw staked ERC20 tokens.
*
* @dev See {_withdraw}. Override that to implement custom logic.
*
* @param _amount Amount to withdraw.
*/
function withdraw(uint256 _amount) external nonReentrant {
_withdraw(_amount);
}
/**
* @notice Claim accumulated rewards.
*
* @dev See {_claimRewards}. Override that to implement custom logic.
* See {_calculateRewards} for reward-calculation logic.
*/
function claimRewards() external nonReentrant {
_claimRewards();
}
/**
* @notice Set time unit. Set as a number of seconds.
* Could be specified as -- x * 1 hours, x * 1 days, etc.
*
* @dev Only admin/authorized-account can call it.
*
* @param _timeUnit New time unit.
*/
function setTimeUnit(uint256 _timeUnit) external virtual {
if (!_canSetStakeConditions()) {
revert("Not authorized");
}
StakingCondition memory condition = stakingConditions[nextConditionId - 1];
require(_timeUnit != condition.timeUnit, "Time-unit unchanged.");
_setStakingCondition(_timeUnit, condition.rewardRatioNumerator, condition.rewardRatioDenominator);
emit UpdatedTimeUnit(condition.timeUnit, _timeUnit);
}
/**
* @notice Set rewards per unit of time.
* Interpreted as (numerator/denominator) rewards per second/per day/etc based on time-unit.
*
* For e.g., ratio of 1/20 would mean 1 reward token for every 20 tokens staked.
*
* @dev Only admin/authorized-account can call it.
*
* @param _numerator Reward ratio numerator.
* @param _denominator Reward ratio denominator.
*/
function setRewardRatio(uint256 _numerator, uint256 _denominator) external virtual {
if (!_canSetStakeConditions()) {
revert("Not authorized");
}
StakingCondition memory condition = stakingConditions[nextConditionId - 1];
require(
_numerator != condition.rewardRatioNumerator || _denominator != condition.rewardRatioDenominator,
"Reward ratio unchanged."
);
_setStakingCondition(condition.timeUnit, _numerator, _denominator);
emit UpdatedRewardRatio(
condition.rewardRatioNumerator,
_numerator,
condition.rewardRatioDenominator,
_denominator
);
}
/**
* @notice View amount staked and rewards for a user.
*
* @param _staker Address for which to calculated rewards.
* @return _tokensStaked Amount of tokens staked.
* @return _rewards Available reward amount.
*/
function getStakeInfo(address _staker) public view virtual returns (uint256 _tokensStaked, uint256 _rewards) {
_tokensStaked = stakers[_staker].amountStaked;
_rewards = _availableRewards(_staker);
}
function getTimeUnit() public view returns (uint256 _timeUnit) {
_timeUnit = stakingConditions[nextConditionId - 1].timeUnit;
}
function getRewardRatio() public view returns (uint256 _numerator, uint256 _denominator) {
_numerator = stakingConditions[nextConditionId - 1].rewardRatioNumerator;
_denominator = stakingConditions[nextConditionId - 1].rewardRatioDenominator;
}
/*///////////////////////////////////////////////////////////////
Internal Functions
//////////////////////////////////////////////////////////////*/
/// @dev Staking logic. Override to add custom logic.
function _stake(uint256 _amount) internal virtual {
require(_amount != 0, "Staking 0 tokens");
address _token = token;
if (stakers[_stakeMsgSender()].amountStaked > 0) {
_updateUnclaimedRewardsForStaker(_stakeMsgSender());
} else {
stakersArray.push(_stakeMsgSender());
stakers[_stakeMsgSender()].timeOfLastUpdate = block.timestamp;
stakers[_stakeMsgSender()].conditionIdOflastUpdate = nextConditionId - 1;
}
CurrencyTransferLib.transferCurrency(_token, _stakeMsgSender(), address(this), _amount);
stakers[_stakeMsgSender()].amountStaked += _amount;
emit TokensStaked(_stakeMsgSender(), _amount);
}
/// @dev Withdraw logic. Override to add custom logic.
function _withdraw(uint256 _amount) internal virtual {
uint256 _amountStaked = stakers[_stakeMsgSender()].amountStaked;
require(_amount != 0, "Withdrawing 0 tokens");
require(_amountStaked >= _amount, "Withdrawing more than staked");
_updateUnclaimedRewardsForStaker(_stakeMsgSender());
if (_amountStaked == _amount) {
address[] memory _stakersArray = stakersArray;
for (uint256 i = 0; i < _stakersArray.length; ++i) {
if (_stakersArray[i] == _stakeMsgSender()) {
stakersArray[i] = stakersArray[_stakersArray.length - 1];
stakersArray.pop();
break;
}
}
}
stakers[_stakeMsgSender()].amountStaked -= _amount;
CurrencyTransferLib.transferCurrency(token, address(this), _stakeMsgSender(), _amount);
emit TokensWithdrawn(_stakeMsgSender(), _amount);
}
/// @dev Logic for claiming rewards. Override to add custom logic.
function _claimRewards() internal virtual {
uint256 rewards = stakers[_stakeMsgSender()].unclaimedRewards + _calculateRewards(_stakeMsgSender());
require(rewards != 0, "No rewards");
stakers[_stakeMsgSender()].timeOfLastUpdate = block.timestamp;
stakers[_stakeMsgSender()].unclaimedRewards = 0;
stakers[_stakeMsgSender()].conditionIdOflastUpdate = nextConditionId - 1;
_mintRewards(_stakeMsgSender(), rewards);
emit RewardsClaimed(_stakeMsgSender(), rewards);
}
/// @dev View available rewards for a user.
function _availableRewards(address _staker) internal view virtual returns (uint256 _rewards) {
if (stakers[_staker].amountStaked == 0) {
_rewards = stakers[_staker].unclaimedRewards;
} else {
_rewards = stakers[_staker].unclaimedRewards + _calculateRewards(_staker);
}
}
/// @dev Update unclaimed rewards for a users. Called for every state change for a user.
function _updateUnclaimedRewardsForStaker(address _staker) internal virtual {
uint256 rewards = _calculateRewards(_staker);
stakers[_staker].unclaimedRewards += rewards;
stakers[_staker].timeOfLastUpdate = block.timestamp;
stakers[_staker].conditionIdOflastUpdate = nextConditionId - 1;
}
/// @dev Set staking conditions.
function _setStakingCondition(
uint256 _timeUnit,
uint256 _numerator,
uint256 _denominator
) internal virtual {
require(_denominator != 0, "divide by 0");
require(_timeUnit != 0, "time-unit can't be 0");
uint256 conditionId = nextConditionId;
nextConditionId += 1;
stakingConditions[conditionId] = StakingCondition({
timeUnit: _timeUnit,
rewardRatioNumerator: _numerator,
rewardRatioDenominator: _denominator,
startTimestamp: block.timestamp,
endTimestamp: 0
});
if (conditionId > 0) {
stakingConditions[conditionId - 1].endTimestamp = block.timestamp;
}
}
/// @dev Calculate rewards for a staker.
function _calculateRewards(address _staker) internal view virtual returns (uint256 _rewards) {
Staker memory staker = stakers[_staker];
uint256 _stakerConditionId = staker.conditionIdOflastUpdate;
uint256 _nextConditionId = nextConditionId;
for (uint256 i = _stakerConditionId; i < _nextConditionId; i += 1) {
StakingCondition memory condition = stakingConditions[i];
uint256 startTime = i != _stakerConditionId ? condition.startTimestamp : staker.timeOfLastUpdate;
uint256 endTime = condition.endTimestamp != 0 ? condition.endTimestamp : block.timestamp;
(bool noOverflowProduct, uint256 rewardsProduct) = SafeMath.tryMul(
(endTime - startTime) * staker.amountStaked,
condition.rewardRatioNumerator
);
(bool noOverflowSum, uint256 rewardsSum) = SafeMath.tryAdd(
_rewards,
(rewardsProduct / condition.timeUnit) / condition.rewardRatioDenominator
);
_rewards = noOverflowProduct && noOverflowSum ? rewardsSum : _rewards;
}
(, _rewards) = SafeMath.tryMul(_rewards, 10**rewardTokenDecimals);
_rewards /= (10**stakingTokenDecimals);
}
/*////////////////////////////////////////////////////////////////////
Optional hooks that can be implemented in the derived contract
///////////////////////////////////////////////////////////////////*/
/// @dev Exposes the ability to override the msg sender -- support ERC2771.
function _stakeMsgSender() internal virtual returns (address) {
return msg.sender;
}
/*///////////////////////////////////////////////////////////////
Virtual functions: to be implemented in derived contract
//////////////////////////////////////////////////////////////*/
/**
* @dev Mint/Transfer ERC20 rewards to the staker. Must override.
*
* @param _staker Address for which to calculated rewards.
* @param _rewards Amount of tokens to be given out as reward.
*
* For example, override as below to mint ERC20 rewards:
*
* ```
* function _mintRewards(address _staker, uint256 _rewards) internal override {
*
* TokenERC20(rewardTokenAddress).mintTo(_staker, _rewards);
*
* }
* ```
*/
function _mintRewards(address _staker, uint256 _rewards) internal virtual;
/**
* @dev Returns whether staking restrictions can be set in given execution context.
* Must override.
*
*
* For example, override as below to restrict access to admin:
*
* ```
* function _canSetStakeConditions() internal override {
*
* return msg.sender == adminAddress;
*
* }
* ```
*/
function _canSetStakeConditions() internal view virtual returns (bool);
}