/
Staking1155.sol
519 lines (432 loc) · 20.5 KB
/
Staking1155.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
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
// SPDX-License-Identifier: Apache-2.0
pragma solidity ^0.8.11;
import "../openzeppelin-presets/security/ReentrancyGuard.sol";
import "../openzeppelin-presets/utils/math/SafeMath.sol";
import "../eip/interface/IERC1155.sol";
import "./interface/IStaking1155.sol";
/**
* note: This is a Beta release.
*/
abstract contract Staking1155 is ReentrancyGuard, IStaking1155 {
/*///////////////////////////////////////////////////////////////
State variables / Mappings
//////////////////////////////////////////////////////////////*/
///@dev Address of ERC1155 contract -- staked tokens belong to this contract.
address public edition;
/// @dev Flag to check direct transfers of staking tokens.
uint8 internal isStaking = 1;
///@dev Next staking condition Id. Tracks number of conditon updates so far.
uint256 private nextDefaultConditionId;
///@dev List of token-ids ever staked.
uint256[] public indexedTokens;
///@dev Mapping from token-id to whether it is indexed or not.
mapping(uint256 => bool) public isIndexed;
///@dev Mapping from default condition-id to default condition.
mapping(uint256 => StakingCondition) private defaultCondition;
///@dev Mapping from token-id to next staking condition Id for the token. Tracks number of conditon updates so far.
mapping(uint256 => uint256) private nextConditionId;
///@dev Mapping from token-id and staker address to Staker struct. See {struct IStaking1155.Staker}.
mapping(uint256 => mapping(address => Staker)) public stakers;
///@dev Mapping from token-id and condition Id to staking condition. See {struct IStaking1155.StakingCondition}
mapping(uint256 => mapping(uint256 => StakingCondition)) private stakingConditions;
/// @dev Mapping from token-id to list of accounts that have staked that token-id.
mapping(uint256 => address[]) public stakersArray;
constructor(address _edition) ReentrancyGuard() {
require(address(_edition) != address(0), "address 0");
edition = _edition;
}
/*///////////////////////////////////////////////////////////////
External/Public Functions
//////////////////////////////////////////////////////////////*/
/**
* @notice Stake ERC721 Tokens.
*
* @dev See {_stake}. Override that to implement custom logic.
*
* @param _tokenId ERC1155 token-id to stake.
* @param _amount Amount to stake.
*/
function stake(uint256 _tokenId, uint256 _amount) external nonReentrant {
_stake(_tokenId, _amount);
}
/**
* @notice Withdraw staked tokens.
*
* @dev See {_withdraw}. Override that to implement custom logic.
*
* @param _tokenId ERC1155 token-id to withdraw.
* @param _amount Amount to withdraw.
*/
function withdraw(uint256 _tokenId, uint256 _amount) external nonReentrant {
_withdraw(_tokenId, _amount);
}
/**
* @notice Claim accumulated rewards.
*
* @dev See {_claimRewards}. Override that to implement custom logic.
* See {_calculateRewards} for reward-calculation logic.
*
* @param _tokenId Staked token Id.
*/
function claimRewards(uint256 _tokenId) external nonReentrant {
_claimRewards(_tokenId);
}
/**
* @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 _tokenId ERC1155 token Id.
* @param _timeUnit New time unit.
*/
function setTimeUnit(uint256 _tokenId, uint256 _timeUnit) external virtual {
if (!_canSetStakeConditions()) {
revert("Not authorized");
}
uint256 _nextConditionId = nextConditionId[_tokenId];
StakingCondition memory condition = _nextConditionId == 0
? defaultCondition[nextDefaultConditionId - 1]
: stakingConditions[_tokenId][_nextConditionId - 1];
require(_timeUnit != condition.timeUnit, "Time-unit unchanged.");
_setStakingCondition(_tokenId, _timeUnit, condition.rewardsPerUnitTime);
emit UpdatedTimeUnit(_tokenId, condition.timeUnit, _timeUnit);
}
/**
* @notice Set rewards per unit of time.
* Interpreted as x rewards per second/per day/etc based on time-unit.
*
* @dev Only admin/authorized-account can call it.
*
*
* @param _tokenId ERC1155 token Id.
* @param _rewardsPerUnitTime New rewards per unit time.
*/
function setRewardsPerUnitTime(uint256 _tokenId, uint256 _rewardsPerUnitTime) external virtual {
if (!_canSetStakeConditions()) {
revert("Not authorized");
}
uint256 _nextConditionId = nextConditionId[_tokenId];
StakingCondition memory condition = _nextConditionId == 0
? defaultCondition[nextDefaultConditionId - 1]
: stakingConditions[_tokenId][_nextConditionId - 1];
require(_rewardsPerUnitTime != condition.rewardsPerUnitTime, "Reward unchanged.");
_setStakingCondition(_tokenId, condition.timeUnit, _rewardsPerUnitTime);
emit UpdatedRewardsPerUnitTime(_tokenId, condition.rewardsPerUnitTime, _rewardsPerUnitTime);
}
/**
* @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 _defaultTimeUnit New time unit.
*/
function setDefaultTimeUnit(uint256 _defaultTimeUnit) external virtual {
if (!_canSetStakeConditions()) {
revert("Not authorized");
}
StakingCondition memory _defaultCondition = defaultCondition[nextDefaultConditionId - 1];
require(_defaultTimeUnit != _defaultCondition.timeUnit, "Default time-unit unchanged.");
_setDefaultStakingCondition(_defaultTimeUnit, _defaultCondition.rewardsPerUnitTime);
emit UpdatedDefaultTimeUnit(_defaultCondition.timeUnit, _defaultTimeUnit);
}
/**
* @notice Set rewards per unit of time.
* Interpreted as x rewards per second/per day/etc based on time-unit.
*
* @dev Only admin/authorized-account can call it.
*
* @param _defaultRewardsPerUnitTime New rewards per unit time.
*/
function setDefaultRewardsPerUnitTime(uint256 _defaultRewardsPerUnitTime) external virtual {
if (!_canSetStakeConditions()) {
revert("Not authorized");
}
StakingCondition memory _defaultCondition = defaultCondition[nextDefaultConditionId - 1];
require(_defaultRewardsPerUnitTime != _defaultCondition.rewardsPerUnitTime, "Default reward unchanged.");
_setDefaultStakingCondition(_defaultCondition.timeUnit, _defaultRewardsPerUnitTime);
emit UpdatedDefaultRewardsPerUnitTime(_defaultCondition.rewardsPerUnitTime, _defaultRewardsPerUnitTime);
}
/**
* @notice View amount staked and rewards for a user, for a given token-id.
*
* @param _staker Address for which to calculated rewards.
* @return _tokensStaked Amount of tokens staked for given token-id.
* @return _rewards Available reward amount.
*/
function getStakeInfoForToken(uint256 _tokenId, address _staker)
public
view
virtual
returns (uint256 _tokensStaked, uint256 _rewards)
{
_tokensStaked = stakers[_tokenId][_staker].amountStaked;
_rewards = _availableRewards(_tokenId, _staker);
}
/**
* @notice View all tokens staked and total rewards for a user.
*
* @param _staker Address for which to calculated rewards.
* @return _tokensStaked List of token-ids staked.
* @return _tokenAmounts Amount of each token-id staked.
* @return _totalRewards Total rewards available.
*/
function getStakeInfo(address _staker)
public
view
virtual
returns (
uint256[] memory _tokensStaked,
uint256[] memory _tokenAmounts,
uint256 _totalRewards
)
{
uint256[] memory _indexedTokens = indexedTokens;
uint256[] memory _stakedAmounts = new uint256[](_indexedTokens.length);
uint256 indexedTokenCount = _indexedTokens.length;
uint256 stakerTokenCount = 0;
for (uint256 i = 0; i < indexedTokenCount; i++) {
_stakedAmounts[i] = stakers[_indexedTokens[i]][_staker].amountStaked;
if (_stakedAmounts[i] > 0) stakerTokenCount += 1;
}
_tokensStaked = new uint256[](stakerTokenCount);
_tokenAmounts = new uint256[](stakerTokenCount);
uint256 count = 0;
for (uint256 i = 0; i < indexedTokenCount; i++) {
if (_stakedAmounts[i] > 0) {
_tokensStaked[count] = _indexedTokens[i];
_tokenAmounts[count] = _stakedAmounts[i];
_totalRewards += _availableRewards(_indexedTokens[i], _staker);
count += 1;
}
}
}
function getTimeUnit(uint256 _tokenId) public view returns (uint256 _timeUnit) {
uint256 _nextConditionId = nextConditionId[_tokenId];
require(_nextConditionId != 0, "Time unit not set. Check default time unit.");
_timeUnit = stakingConditions[_tokenId][_nextConditionId - 1].timeUnit;
}
function getRewardsPerUnitTime(uint256 _tokenId) public view returns (uint256 _rewardsPerUnitTime) {
uint256 _nextConditionId = nextConditionId[_tokenId];
require(_nextConditionId != 0, "Rewards not set. Check default rewards.");
_rewardsPerUnitTime = stakingConditions[_tokenId][_nextConditionId - 1].rewardsPerUnitTime;
}
function getDefaultTimeUnit() public view returns (uint256 _timeUnit) {
_timeUnit = defaultCondition[nextDefaultConditionId - 1].timeUnit;
}
function getDefaultRewardsPerUnitTime() public view returns (uint256 _rewardsPerUnitTime) {
_rewardsPerUnitTime = defaultCondition[nextDefaultConditionId - 1].rewardsPerUnitTime;
}
/*///////////////////////////////////////////////////////////////
Internal Functions
//////////////////////////////////////////////////////////////*/
/// @dev Staking logic. Override to add custom logic.
function _stake(uint256 _tokenId, uint256 _amount) internal virtual {
require(_amount != 0, "Staking 0 tokens");
address _edition = edition;
if (stakers[_tokenId][_stakeMsgSender()].amountStaked > 0) {
_updateUnclaimedRewardsForStaker(_tokenId, _stakeMsgSender());
} else {
stakersArray[_tokenId].push(_stakeMsgSender());
stakers[_tokenId][_stakeMsgSender()].timeOfLastUpdate = block.timestamp;
uint256 _conditionId = nextConditionId[_tokenId];
stakers[_tokenId][_stakeMsgSender()].conditionIdOflastUpdate = _conditionId == 0
? nextDefaultConditionId - 1
: _conditionId - 1;
}
require(
IERC1155(_edition).balanceOf(_stakeMsgSender(), _tokenId) >= _amount &&
IERC1155(_edition).isApprovedForAll(_stakeMsgSender(), address(this)),
"Not balance or approved"
);
isStaking = 2;
IERC1155(_edition).safeTransferFrom(_stakeMsgSender(), address(this), _tokenId, _amount, "");
isStaking = 1;
// stakerAddress[_tokenIds[i]] = _stakeMsgSender();
stakers[_tokenId][_stakeMsgSender()].amountStaked += _amount;
if (!isIndexed[_tokenId]) {
isIndexed[_tokenId] = true;
indexedTokens.push(_tokenId);
}
emit TokensStaked(_stakeMsgSender(), _tokenId, _amount);
}
/// @dev Withdraw logic. Override to add custom logic.
function _withdraw(uint256 _tokenId, uint256 _amount) internal virtual {
uint256 _amountStaked = stakers[_tokenId][_stakeMsgSender()].amountStaked;
require(_amount != 0, "Withdrawing 0 tokens");
require(_amountStaked >= _amount, "Withdrawing more than staked");
_updateUnclaimedRewardsForStaker(_tokenId, _stakeMsgSender());
if (_amountStaked == _amount) {
address[] memory _stakersArray = stakersArray[_tokenId];
for (uint256 i = 0; i < _stakersArray.length; ++i) {
if (_stakersArray[i] == _stakeMsgSender()) {
stakersArray[_tokenId][i] = stakersArray[_tokenId][_stakersArray.length - 1];
stakersArray[_tokenId].pop();
break;
}
}
}
stakers[_tokenId][_stakeMsgSender()].amountStaked -= _amount;
IERC1155(edition).safeTransferFrom(address(this), _stakeMsgSender(), _tokenId, _amount, "");
emit TokensWithdrawn(_stakeMsgSender(), _tokenId, _amount);
}
/// @dev Logic for claiming rewards. Override to add custom logic.
function _claimRewards(uint256 _tokenId) internal virtual {
uint256 rewards = stakers[_tokenId][_stakeMsgSender()].unclaimedRewards +
_calculateRewards(_tokenId, _stakeMsgSender());
require(rewards != 0, "No rewards");
stakers[_tokenId][_stakeMsgSender()].timeOfLastUpdate = block.timestamp;
stakers[_tokenId][_stakeMsgSender()].unclaimedRewards = 0;
uint256 _conditionId = nextConditionId[_tokenId];
stakers[_tokenId][_stakeMsgSender()].conditionIdOflastUpdate = _conditionId == 0
? nextDefaultConditionId - 1
: _conditionId - 1;
_mintRewards(_stakeMsgSender(), rewards);
emit RewardsClaimed(_stakeMsgSender(), rewards);
}
/// @dev View available rewards for a user.
function _availableRewards(uint256 _tokenId, address _user) internal view virtual returns (uint256 _rewards) {
if (stakers[_tokenId][_user].amountStaked == 0) {
_rewards = stakers[_tokenId][_user].unclaimedRewards;
} else {
_rewards = stakers[_tokenId][_user].unclaimedRewards + _calculateRewards(_tokenId, _user);
}
}
/// @dev Update unclaimed rewards for a users. Called for every state change for a user.
function _updateUnclaimedRewardsForStaker(uint256 _tokenId, address _staker) internal virtual {
uint256 rewards = _calculateRewards(_tokenId, _staker);
stakers[_tokenId][_staker].unclaimedRewards += rewards;
stakers[_tokenId][_staker].timeOfLastUpdate = block.timestamp;
uint256 _conditionId = nextConditionId[_tokenId];
stakers[_tokenId][_staker].conditionIdOflastUpdate = _conditionId == 0
? nextDefaultConditionId - 1
: _conditionId - 1;
}
/// @dev Set staking conditions, for a token-Id.
function _setStakingCondition(
uint256 _tokenId,
uint256 _timeUnit,
uint256 _rewardsPerUnitTime
) internal virtual {
require(_timeUnit != 0, "time-unit can't be 0");
uint256 conditionId = nextConditionId[_tokenId];
if (conditionId == 0) {
uint256 _nextDefaultConditionId = nextDefaultConditionId;
for (; conditionId < _nextDefaultConditionId; conditionId += 1) {
StakingCondition memory _defaultCondition = defaultCondition[conditionId];
stakingConditions[_tokenId][conditionId] = StakingCondition({
timeUnit: _defaultCondition.timeUnit,
rewardsPerUnitTime: _defaultCondition.rewardsPerUnitTime,
startTimestamp: _defaultCondition.startTimestamp,
endTimestamp: _defaultCondition.endTimestamp
});
}
}
stakingConditions[_tokenId][conditionId - 1].endTimestamp = block.timestamp;
stakingConditions[_tokenId][conditionId] = StakingCondition({
timeUnit: _timeUnit,
rewardsPerUnitTime: _rewardsPerUnitTime,
startTimestamp: block.timestamp,
endTimestamp: 0
});
nextConditionId[_tokenId] = conditionId + 1;
}
/// @dev Set default staking conditions.
function _setDefaultStakingCondition(uint256 _timeUnit, uint256 _rewardsPerUnitTime) internal virtual {
require(_timeUnit != 0, "time-unit can't be 0");
uint256 conditionId = nextDefaultConditionId;
nextDefaultConditionId += 1;
defaultCondition[conditionId] = StakingCondition({
timeUnit: _timeUnit,
rewardsPerUnitTime: _rewardsPerUnitTime,
startTimestamp: block.timestamp,
endTimestamp: 0
});
if (conditionId > 0) {
defaultCondition[conditionId - 1].endTimestamp = block.timestamp;
}
}
/// @dev Reward calculation logic. Override to implement custom logic.
function _calculateRewards(uint256 _tokenId, address _staker) internal view virtual returns (uint256 _rewards) {
Staker memory staker = stakers[_tokenId][_staker];
uint256 _stakerConditionId = staker.conditionIdOflastUpdate;
uint256 _nextConditionId = nextConditionId[_tokenId];
if (_nextConditionId == 0) {
_nextConditionId = nextDefaultConditionId;
for (uint256 i = _stakerConditionId; i < _nextConditionId; i += 1) {
StakingCondition memory condition = defaultCondition[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.rewardsPerUnitTime
);
(bool noOverflowSum, uint256 rewardsSum) = SafeMath.tryAdd(
_rewards,
rewardsProduct / condition.timeUnit
);
_rewards = noOverflowProduct && noOverflowSum ? rewardsSum : _rewards;
}
} else {
for (uint256 i = _stakerConditionId; i < _nextConditionId; i += 1) {
StakingCondition memory condition = stakingConditions[_tokenId][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.rewardsPerUnitTime
);
(bool noOverflowSum, uint256 rewardsSum) = SafeMath.tryAdd(
_rewards,
rewardsProduct / condition.timeUnit
);
_rewards = noOverflowProduct && noOverflowSum ? rewardsSum : _rewards;
}
}
}
/*////////////////////////////////////////////////////////////////////
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);
}