From 3d04ee715e9ffd510c53597cf16adee49b6e8626 Mon Sep 17 00:00:00 2001 From: Yash <72552910+kumaryash90@users.noreply.github.com> Date: Mon, 12 Dec 2022 02:20:07 +0530 Subject: [PATCH] Staking updates (#294) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * more efficient way of storing state for Staking contracts * [M-1] Block gas limit can be exceeded during setTimeUnit() and setRewardsPerUnit() when staker count grows * [C-1] Contract admins can lock staked tokens in the contract * [M-1] revised fix for Staking1155 * [H-1] TokenStake.sol rewards can be over- or under-awarded when the staking and reward tokens have different decimals * [M-2] ERC721 and ERC1155 tokens safe-transferred directly to contract will be locked and unrecoverable * [C-1] revised fix for large rewardsPerUnitTime * [L-1] Incorrect ERC165 implementation for NFTStake and EditionStake * [Q-2] Normalize support for ERC2771 trusted forwarder * [Q-3] Reentrancy init called twice * [Q-5] unitTime and rewardsPerUnitTime setter functions don’t check for new input data * [Q-6] getStakeInfo should be marked as external * [G-1] Halt array iteration after staker removed during withdraw() * [G-2] Loop reading from storage array length * [Q-7] Missing reward balance information * [M-3] TokenStake.sol: Double entry-point ERC20 tokens could be drained from the staking contract * [H-2] TokenStake.sol: Tokens with a tax on transfer will account for inaccurate amounts * virtual functions for bases * docs * v3.2.9 Co-authored-by: Krishang --- contracts/base/Staking1155Base.sol | 10 +- contracts/base/Staking20Base.sol | 13 +- contracts/base/Staking721Base.sol | 14 +- contracts/extension/Staking1155.sol | 291 +++++++++---- .../extension/Staking1155Upgradeable.sol | 291 +++++++++---- contracts/extension/Staking20.sol | 207 +++++++--- contracts/extension/Staking20Upgradeable.sol | 207 +++++++--- contracts/extension/Staking721.sol | 181 +++++--- contracts/extension/Staking721Upgradeable.sol | 181 +++++--- .../extension/interface/IStaking1155.sol | 27 +- contracts/extension/interface/IStaking20.sol | 32 +- contracts/extension/interface/IStaking721.sol | 27 +- .../utils/math/SafeMath.sol | 227 +++++++++++ contracts/package.json | 2 +- contracts/staking/EditionStake.sol | 36 +- contracts/staking/NFTStake.sol | 31 +- contracts/staking/TokenStake.sol | 30 +- docs/EditionStake.md | 146 ++++--- docs/NFTStake.md | 108 +++-- docs/SafeMath.md | 12 + docs/Staking1155.md | 114 +++--- docs/Staking1155Base.md | 126 +++--- docs/Staking1155Upgradeable.md | 114 +++--- docs/Staking20.md | 75 +++- docs/Staking20Base.md | 105 +++-- docs/Staking20Upgradeable.md | 75 +++- docs/Staking721.md | 88 ++-- docs/Staking721Base.md | 88 ++-- docs/Staking721Upgradeable.md | 88 ++-- docs/TokenStake.md | 121 ++++-- lib/forge-std | 2 +- src/test/mocks/MockERC20.sol | 19 + src/test/sdk/extension/StakingExtension.t.sol | 39 +- src/test/staking/EditionStake.t.sol | 170 +++++++- src/test/staking/NFTStake.t.sol | 109 ++++- src/test/staking/TokenStake.t.sol | 385 +++++++++++++++++- 36 files changed, 2853 insertions(+), 938 deletions(-) create mode 100644 contracts/openzeppelin-presets/utils/math/SafeMath.sol create mode 100644 docs/SafeMath.md diff --git a/contracts/base/Staking1155Base.sol b/contracts/base/Staking1155Base.sol index afa3db96f..bbe6fed39 100644 --- a/contracts/base/Staking1155Base.sol +++ b/contracts/base/Staking1155Base.sol @@ -43,12 +43,16 @@ contract Staking1155Base is ContractMetadata, Multicall, Ownable, Staking1155 { address _rewardToken ) Staking1155(_edition) { _setupOwner(msg.sender); - _setDefaultTimeUnit(_defaultTimeUnit); - _setDefaultRewardsPerUnitTime(_defaultRewardsPerUnitTime); + _setDefaultStakingCondition(_defaultTimeUnit, _defaultRewardsPerUnitTime); rewardToken = _rewardToken; } + /// @notice View total rewards available in the staking contract. + function getRewardTokenBalance() external view virtual override returns (uint256 _rewardsAvailableInContract) { + return IERC20(rewardToken).balanceOf(address(this)); + } + /*////////////////////////////////////////////////////////////// Minting logic //////////////////////////////////////////////////////////////*/ @@ -79,7 +83,7 @@ contract Staking1155Base is ContractMetadata, Multicall, Ownable, Staking1155 { //////////////////////////////////////////////////////////////*/ /// @dev Returns whether staking restrictions can be set in given execution context. - function _canSetStakeConditions() internal view override returns (bool) { + function _canSetStakeConditions() internal view virtual override returns (bool) { return msg.sender == owner(); } diff --git a/contracts/base/Staking20Base.sol b/contracts/base/Staking20Base.sol index f88a026ca..04d6bece5 100644 --- a/contracts/base/Staking20Base.sol +++ b/contracts/base/Staking20Base.sol @@ -7,6 +7,7 @@ import "../extension/Ownable.sol"; import "../extension/Staking20.sol"; import "../eip/interface/IERC20.sol"; +import "../eip/interface/IERC20Metadata.sol"; /** * note: This is a Beta release. @@ -42,15 +43,19 @@ contract Staking20Base is ContractMetadata, Multicall, Ownable, Staking20 { uint256 _rewardRatioDenominator, address _stakingToken, address _rewardToken - ) Staking20(_stakingToken) { + ) Staking20(_stakingToken, IERC20Metadata(_stakingToken).decimals(), IERC20Metadata(_rewardToken).decimals()) { _setupOwner(msg.sender); - _setTimeUnit(_timeUnit); - _setRewardRatio(_rewardRatioNumerator, _rewardRatioDenominator); + _setStakingCondition(_timeUnit, _rewardRatioNumerator, _rewardRatioDenominator); require(_rewardToken != _stakingToken, "Reward Token and Staking Token can't be same."); rewardToken = _rewardToken; } + /// @notice View total rewards available in the staking contract. + function getRewardTokenBalance() external view virtual override returns (uint256 _rewardsAvailableInContract) { + return IERC20(rewardToken).balanceOf(address(this)); + } + /*////////////////////////////////////////////////////////////// Minting logic //////////////////////////////////////////////////////////////*/ @@ -81,7 +86,7 @@ contract Staking20Base is ContractMetadata, Multicall, Ownable, Staking20 { //////////////////////////////////////////////////////////////*/ /// @dev Returns whether staking restrictions can be set in given execution context. - function _canSetStakeConditions() internal view override returns (bool) { + function _canSetStakeConditions() internal view virtual override returns (bool) { return msg.sender == owner(); } diff --git a/contracts/base/Staking721Base.sol b/contracts/base/Staking721Base.sol index 419578499..648587b51 100644 --- a/contracts/base/Staking721Base.sol +++ b/contracts/base/Staking721Base.sol @@ -37,18 +37,22 @@ contract Staking721Base is ContractMetadata, Multicall, Ownable, Staking721 { address public rewardToken; constructor( - uint256 _timeUnit, - uint256 _rewardsPerUnitTime, + uint128 _timeUnit, + uint128 _rewardsPerUnitTime, address _nftCollection, address _rewardToken ) Staking721(_nftCollection) { _setupOwner(msg.sender); - _setTimeUnit(_timeUnit); - _setRewardsPerUnitTime(_rewardsPerUnitTime); + _setStakingCondition(_timeUnit, _rewardsPerUnitTime); rewardToken = _rewardToken; } + /// @notice View total rewards available in the staking contract. + function getRewardTokenBalance() external view virtual override returns (uint256 _rewardsAvailableInContract) { + return IERC20(rewardToken).balanceOf(address(this)); + } + /*////////////////////////////////////////////////////////////// Minting logic //////////////////////////////////////////////////////////////*/ @@ -79,7 +83,7 @@ contract Staking721Base is ContractMetadata, Multicall, Ownable, Staking721 { //////////////////////////////////////////////////////////////*/ /// @dev Returns whether staking restrictions can be set in given execution context. - function _canSetStakeConditions() internal view override returns (bool) { + function _canSetStakeConditions() internal view virtual override returns (bool) { return msg.sender == owner(); } diff --git a/contracts/extension/Staking1155.sol b/contracts/extension/Staking1155.sol index 26277231e..b55e1d344 100644 --- a/contracts/extension/Staking1155.sol +++ b/contracts/extension/Staking1155.sol @@ -2,6 +2,7 @@ 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"; @@ -18,11 +19,11 @@ abstract contract Staking1155 is ReentrancyGuard, IStaking1155 { ///@dev Address of ERC1155 contract -- staked tokens belong to this contract. address public edition; - /// @dev Default unit of time specified in number of seconds. Can be set as 1 seconds, 1 days, 1 hours, etc. - uint256 public defaultTimeUnit; + /// @dev Flag to check direct transfers of staking tokens. + uint8 internal isStaking = 1; - ///@dev Default rewards accumulated per unit of time. - uint256 public defaultRewardsPerUnitTime; + ///@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; @@ -30,15 +31,18 @@ abstract contract Staking1155 is ReentrancyGuard, IStaking1155 { ///@dev Mapping from token-id to whether it is indexed or not. mapping(uint256 => bool) public isIndexed; - /// @dev Mapping from token-id to unit of time specified in number of seconds. Can be set as 1 seconds, 1 days, 1 hours, etc. - mapping(uint256 => uint256) public timeUnit; + ///@dev Mapping from default condition-id to default condition. + mapping(uint256 => StakingCondition) private defaultCondition; - ///@dev Mapping from token-id to rewards accumulated per unit of time. - mapping(uint256 => uint256) public rewardsPerUnitTime; + ///@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; @@ -102,12 +106,15 @@ abstract contract Staking1155 is ReentrancyGuard, IStaking1155 { revert("Not authorized"); } - _updateUnclaimedRewardsForAll(_tokenId); + uint256 _nextConditionId = nextConditionId[_tokenId]; + StakingCondition memory condition = _nextConditionId == 0 + ? defaultCondition[nextDefaultConditionId - 1] + : stakingConditions[_tokenId][_nextConditionId - 1]; + require(_timeUnit != condition.timeUnit, "Time-unit unchanged."); - uint256 currentTimeUnit = timeUnit[_tokenId]; - _setTimeUnit(_tokenId, _timeUnit); + _setStakingCondition(_tokenId, _timeUnit, condition.rewardsPerUnitTime); - emit UpdatedTimeUnit(_tokenId, currentTimeUnit, _timeUnit); + emit UpdatedTimeUnit(_tokenId, condition.timeUnit, _timeUnit); } /** @@ -125,12 +132,15 @@ abstract contract Staking1155 is ReentrancyGuard, IStaking1155 { revert("Not authorized"); } - _updateUnclaimedRewardsForAll(_tokenId); + uint256 _nextConditionId = nextConditionId[_tokenId]; + StakingCondition memory condition = _nextConditionId == 0 + ? defaultCondition[nextDefaultConditionId - 1] + : stakingConditions[_tokenId][_nextConditionId - 1]; + require(_rewardsPerUnitTime != condition.rewardsPerUnitTime, "Reward unchanged."); - uint256 currentRewardsPerUnitTime = rewardsPerUnitTime[_tokenId]; - _setRewardsPerUnitTime(_tokenId, _rewardsPerUnitTime); + _setStakingCondition(_tokenId, condition.timeUnit, _rewardsPerUnitTime); - emit UpdatedRewardsPerUnitTime(_tokenId, currentRewardsPerUnitTime, _rewardsPerUnitTime); + emit UpdatedRewardsPerUnitTime(_tokenId, condition.rewardsPerUnitTime, _rewardsPerUnitTime); } /** @@ -146,16 +156,12 @@ abstract contract Staking1155 is ReentrancyGuard, IStaking1155 { revert("Not authorized"); } - uint256[] memory _indexedTokens = indexedTokens; - - for (uint256 i = 0; i < _indexedTokens.length; i++) { - _updateUnclaimedRewardsForAll(_indexedTokens[i]); - } + StakingCondition memory _defaultCondition = defaultCondition[nextDefaultConditionId - 1]; + require(_defaultTimeUnit != _defaultCondition.timeUnit, "Default time-unit unchanged."); - uint256 currentDefaultTimeUnit = defaultTimeUnit; - _setDefaultTimeUnit(_defaultTimeUnit); + _setDefaultStakingCondition(_defaultTimeUnit, _defaultCondition.rewardsPerUnitTime); - emit UpdatedDefaultTimeUnit(currentDefaultTimeUnit, _defaultTimeUnit); + emit UpdatedDefaultTimeUnit(_defaultCondition.timeUnit, _defaultTimeUnit); } /** @@ -171,16 +177,12 @@ abstract contract Staking1155 is ReentrancyGuard, IStaking1155 { revert("Not authorized"); } - uint256[] memory _indexedTokens = indexedTokens; - - for (uint256 i = 0; i < _indexedTokens.length; i++) { - _updateUnclaimedRewardsForAll(_indexedTokens[i]); - } + StakingCondition memory _defaultCondition = defaultCondition[nextDefaultConditionId - 1]; + require(_defaultRewardsPerUnitTime != _defaultCondition.rewardsPerUnitTime, "Default reward unchanged."); - uint256 currentDefaultRewardsPerUnitTime = defaultRewardsPerUnitTime; - _setDefaultRewardsPerUnitTime(_defaultRewardsPerUnitTime); + _setDefaultStakingCondition(_defaultCondition.timeUnit, _defaultRewardsPerUnitTime); - emit UpdatedDefaultRewardsPerUnitTime(currentDefaultRewardsPerUnitTime, _defaultRewardsPerUnitTime); + emit UpdatedDefaultRewardsPerUnitTime(_defaultCondition.rewardsPerUnitTime, _defaultRewardsPerUnitTime); } /** @@ -191,7 +193,7 @@ abstract contract Staking1155 is ReentrancyGuard, IStaking1155 { * @return _rewards Available reward amount. */ function getStakeInfoForToken(uint256 _tokenId, address _staker) - public + external view virtual returns (uint256 _tokensStaked, uint256 _rewards) @@ -209,7 +211,7 @@ abstract contract Staking1155 is ReentrancyGuard, IStaking1155 { * @return _totalRewards Total rewards available. */ function getStakeInfo(address _staker) - public + external view virtual returns ( @@ -241,6 +243,26 @@ abstract contract Staking1155 is ReentrancyGuard, IStaking1155 { } } + 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 //////////////////////////////////////////////////////////////*/ @@ -250,67 +272,80 @@ abstract contract Staking1155 is ReentrancyGuard, IStaking1155 { require(_amount != 0, "Staking 0 tokens"); address _edition = edition; - if (stakers[_tokenId][msg.sender].amountStaked > 0) { - _updateUnclaimedRewardsForStaker(_tokenId, msg.sender); + if (stakers[_tokenId][_stakeMsgSender()].amountStaked > 0) { + _updateUnclaimedRewardsForStaker(_tokenId, _stakeMsgSender()); } else { - stakersArray[_tokenId].push(msg.sender); - stakers[_tokenId][msg.sender].timeOfLastUpdate = block.timestamp; + 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(msg.sender, _tokenId) >= _amount && - IERC1155(_edition).isApprovedForAll(msg.sender, address(this)), + IERC1155(_edition).balanceOf(_stakeMsgSender(), _tokenId) >= _amount && + IERC1155(_edition).isApprovedForAll(_stakeMsgSender(), address(this)), "Not balance or approved" ); - IERC1155(_edition).safeTransferFrom(msg.sender, address(this), _tokenId, _amount, ""); - // stakerAddress[_tokenIds[i]] = msg.sender; - stakers[_tokenId][msg.sender].amountStaked += _amount; + 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(msg.sender, _tokenId, _amount); + 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][msg.sender].amountStaked; + uint256 _amountStaked = stakers[_tokenId][_stakeMsgSender()].amountStaked; require(_amount != 0, "Withdrawing 0 tokens"); require(_amountStaked >= _amount, "Withdrawing more than staked"); - _updateUnclaimedRewardsForStaker(_tokenId, msg.sender); + _updateUnclaimedRewardsForStaker(_tokenId, _stakeMsgSender()); if (_amountStaked == _amount) { address[] memory _stakersArray = stakersArray[_tokenId]; for (uint256 i = 0; i < _stakersArray.length; ++i) { - if (_stakersArray[i] == msg.sender) { - stakersArray[_tokenId][i] = stakersArray[_tokenId][_stakersArray.length - 1]; + if (_stakersArray[i] == _stakeMsgSender()) { + stakersArray[_tokenId][i] = _stakersArray[_stakersArray.length - 1]; stakersArray[_tokenId].pop(); break; } } } - stakers[_tokenId][msg.sender].amountStaked -= _amount; + stakers[_tokenId][_stakeMsgSender()].amountStaked -= _amount; - IERC1155(edition).safeTransferFrom(address(this), msg.sender, _tokenId, _amount, ""); + IERC1155(edition).safeTransferFrom(address(this), _stakeMsgSender(), _tokenId, _amount, ""); - emit TokensWithdrawn(msg.sender, _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][msg.sender].unclaimedRewards + _calculateRewards(_tokenId, msg.sender); + uint256 rewards = stakers[_tokenId][_stakeMsgSender()].unclaimedRewards + + _calculateRewards(_tokenId, _stakeMsgSender()); require(rewards != 0, "No rewards"); - stakers[_tokenId][msg.sender].timeOfLastUpdate = block.timestamp; - stakers[_tokenId][msg.sender].unclaimedRewards = 0; + 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(msg.sender, rewards); + _mintRewards(_stakeMsgSender(), rewards); - emit RewardsClaimed(msg.sender, rewards); + emit RewardsClaimed(_stakeMsgSender(), rewards); } /// @dev View available rewards for a user. @@ -322,59 +357,137 @@ abstract contract Staking1155 is ReentrancyGuard, IStaking1155 { } } - /// @dev Update unclaimed rewards for all users. Called when setting timeUnit or rewardsPerUnitTime. - function _updateUnclaimedRewardsForAll(uint256 _tokenId) internal virtual { - address[] memory _stakers = stakersArray[_tokenId]; - uint256 len = _stakers.length; - for (uint256 i = 0; i < len; ++i) { - address user = _stakers[i]; - - uint256 rewards = _calculateRewards(_tokenId, user); - stakers[_tokenId][user].unclaimedRewards += rewards; - stakers[_tokenId][user].timeOfLastUpdate = block.timestamp; - } - } - /// @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; - } - /// @dev Set time unit in seconds. - function _setTimeUnit(uint256 _tokenId, uint256 _timeUnit) internal virtual { - timeUnit[_tokenId] = _timeUnit; + uint256 _conditionId = nextConditionId[_tokenId]; + stakers[_tokenId][_staker].conditionIdOflastUpdate = _conditionId == 0 + ? nextDefaultConditionId - 1 + : _conditionId - 1; } - /// @dev Set rewards per unit time. - function _setRewardsPerUnitTime(uint256 _tokenId, uint256 _rewardsPerUnitTime) internal virtual { - rewardsPerUnitTime[_tokenId] = _rewardsPerUnitTime; - } + /// @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; - /// @dev Set time unit in seconds. - function _setDefaultTimeUnit(uint256 _defaultTimeUnit) internal virtual { - defaultTimeUnit = _defaultTimeUnit; + stakingConditions[_tokenId][conditionId] = StakingCondition({ + timeUnit: _timeUnit, + rewardsPerUnitTime: _rewardsPerUnitTime, + startTimestamp: block.timestamp, + endTimestamp: 0 + }); + + nextConditionId[_tokenId] = conditionId + 1; } - /// @dev Set rewards per unit time. - function _setDefaultRewardsPerUnitTime(uint256 _defaultRewardsPerUnitTime) internal virtual { - defaultRewardsPerUnitTime = _defaultRewardsPerUnitTime; + /// @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 _timeUnit = timeUnit[_tokenId]; - uint256 _rewardsPerUnitTime = rewardsPerUnitTime[_tokenId]; + 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; + } + } + } - if (_timeUnit == 0) _timeUnit = defaultTimeUnit; - if (_rewardsPerUnitTime == 0) _rewardsPerUnitTime = defaultRewardsPerUnitTime; + /*//////////////////////////////////////////////////////////////////// + Optional hooks that can be implemented in the derived contract + ///////////////////////////////////////////////////////////////////*/ - _rewards = ((((block.timestamp - staker.timeOfLastUpdate) * staker.amountStaked) * _rewardsPerUnitTime) / - _timeUnit); + /// @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 + //////////////////////////////////////////////////////////////*/ + + /** + * @notice View total rewards available in the staking contract. + * + */ + function getRewardTokenBalance() external view virtual returns (uint256 _rewardsAvailableInContract); + /** * @dev Mint/Transfer ERC20 rewards to the staker. Must override. * diff --git a/contracts/extension/Staking1155Upgradeable.sol b/contracts/extension/Staking1155Upgradeable.sol index 3dc53ba34..f0bf5a6d1 100644 --- a/contracts/extension/Staking1155Upgradeable.sol +++ b/contracts/extension/Staking1155Upgradeable.sol @@ -2,6 +2,7 @@ pragma solidity ^0.8.11; import "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol"; +import "../openzeppelin-presets/utils/math/SafeMath.sol"; import "../eip/interface/IERC1155.sol"; import "./interface/IStaking1155.sol"; @@ -18,11 +19,11 @@ abstract contract Staking1155Upgradeable is ReentrancyGuardUpgradeable, IStaking ///@dev Address of ERC1155 contract -- staked tokens belong to this contract. address public edition; - /// @dev Default unit of time specified in number of seconds. Can be set as 1 seconds, 1 days, 1 hours, etc. - uint256 public defaultTimeUnit; + /// @dev Flag to check direct transfers of staking tokens. + uint8 internal isStaking = 1; - ///@dev Default rewards accumulated per unit of time. - uint256 public defaultRewardsPerUnitTime; + ///@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; @@ -30,15 +31,18 @@ abstract contract Staking1155Upgradeable is ReentrancyGuardUpgradeable, IStaking ///@dev Mapping from token-id to whether it is indexed or not. mapping(uint256 => bool) public isIndexed; - /// @dev Mapping from token-id to unit of time specified in number of seconds. Can be set as 1 seconds, 1 days, 1 hours, etc. - mapping(uint256 => uint256) public timeUnit; + ///@dev Mapping from default condition-id to default condition. + mapping(uint256 => StakingCondition) private defaultCondition; - ///@dev Mapping from token-id to rewards accumulated per unit of time. - mapping(uint256 => uint256) public rewardsPerUnitTime; + ///@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; @@ -104,12 +108,15 @@ abstract contract Staking1155Upgradeable is ReentrancyGuardUpgradeable, IStaking revert("Not authorized"); } - _updateUnclaimedRewardsForAll(_tokenId); + uint256 _nextConditionId = nextConditionId[_tokenId]; + StakingCondition memory condition = _nextConditionId == 0 + ? defaultCondition[nextDefaultConditionId - 1] + : stakingConditions[_tokenId][_nextConditionId - 1]; + require(_timeUnit != condition.timeUnit, "Time-unit unchanged."); - uint256 currentTimeUnit = timeUnit[_tokenId]; - _setTimeUnit(_tokenId, _timeUnit); + _setStakingCondition(_tokenId, _timeUnit, condition.rewardsPerUnitTime); - emit UpdatedTimeUnit(_tokenId, currentTimeUnit, _timeUnit); + emit UpdatedTimeUnit(_tokenId, condition.timeUnit, _timeUnit); } /** @@ -127,12 +134,15 @@ abstract contract Staking1155Upgradeable is ReentrancyGuardUpgradeable, IStaking revert("Not authorized"); } - _updateUnclaimedRewardsForAll(_tokenId); + uint256 _nextConditionId = nextConditionId[_tokenId]; + StakingCondition memory condition = _nextConditionId == 0 + ? defaultCondition[nextDefaultConditionId - 1] + : stakingConditions[_tokenId][_nextConditionId - 1]; + require(_rewardsPerUnitTime != condition.rewardsPerUnitTime, "Reward unchanged."); - uint256 currentRewardsPerUnitTime = rewardsPerUnitTime[_tokenId]; - _setRewardsPerUnitTime(_tokenId, _rewardsPerUnitTime); + _setStakingCondition(_tokenId, condition.timeUnit, _rewardsPerUnitTime); - emit UpdatedRewardsPerUnitTime(_tokenId, currentRewardsPerUnitTime, _rewardsPerUnitTime); + emit UpdatedRewardsPerUnitTime(_tokenId, condition.rewardsPerUnitTime, _rewardsPerUnitTime); } /** @@ -148,16 +158,12 @@ abstract contract Staking1155Upgradeable is ReentrancyGuardUpgradeable, IStaking revert("Not authorized"); } - uint256[] memory _indexedTokens = indexedTokens; - - for (uint256 i = 0; i < _indexedTokens.length; i++) { - _updateUnclaimedRewardsForAll(_indexedTokens[i]); - } + StakingCondition memory _defaultCondition = defaultCondition[nextDefaultConditionId - 1]; + require(_defaultTimeUnit != _defaultCondition.timeUnit, "Default time-unit unchanged."); - uint256 currentDefaultTimeUnit = defaultTimeUnit; - _setDefaultTimeUnit(_defaultTimeUnit); + _setDefaultStakingCondition(_defaultTimeUnit, _defaultCondition.rewardsPerUnitTime); - emit UpdatedDefaultTimeUnit(currentDefaultTimeUnit, _defaultTimeUnit); + emit UpdatedDefaultTimeUnit(_defaultCondition.timeUnit, _defaultTimeUnit); } /** @@ -173,16 +179,12 @@ abstract contract Staking1155Upgradeable is ReentrancyGuardUpgradeable, IStaking revert("Not authorized"); } - uint256[] memory _indexedTokens = indexedTokens; - - for (uint256 i = 0; i < _indexedTokens.length; i++) { - _updateUnclaimedRewardsForAll(_indexedTokens[i]); - } + StakingCondition memory _defaultCondition = defaultCondition[nextDefaultConditionId - 1]; + require(_defaultRewardsPerUnitTime != _defaultCondition.rewardsPerUnitTime, "Default reward unchanged."); - uint256 currentDefaultRewardsPerUnitTime = defaultRewardsPerUnitTime; - _setDefaultRewardsPerUnitTime(_defaultRewardsPerUnitTime); + _setDefaultStakingCondition(_defaultCondition.timeUnit, _defaultRewardsPerUnitTime); - emit UpdatedDefaultRewardsPerUnitTime(currentDefaultRewardsPerUnitTime, _defaultRewardsPerUnitTime); + emit UpdatedDefaultRewardsPerUnitTime(_defaultCondition.rewardsPerUnitTime, _defaultRewardsPerUnitTime); } /** @@ -193,7 +195,7 @@ abstract contract Staking1155Upgradeable is ReentrancyGuardUpgradeable, IStaking * @return _rewards Available reward amount. */ function getStakeInfoForToken(uint256 _tokenId, address _staker) - public + external view virtual returns (uint256 _tokensStaked, uint256 _rewards) @@ -211,7 +213,7 @@ abstract contract Staking1155Upgradeable is ReentrancyGuardUpgradeable, IStaking * @return _totalRewards Total rewards available. */ function getStakeInfo(address _staker) - public + external view virtual returns ( @@ -243,6 +245,26 @@ abstract contract Staking1155Upgradeable is ReentrancyGuardUpgradeable, IStaking } } + 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 //////////////////////////////////////////////////////////////*/ @@ -252,67 +274,80 @@ abstract contract Staking1155Upgradeable is ReentrancyGuardUpgradeable, IStaking require(_amount != 0, "Staking 0 tokens"); address _edition = edition; - if (stakers[_tokenId][msg.sender].amountStaked > 0) { - _updateUnclaimedRewardsForStaker(_tokenId, msg.sender); + if (stakers[_tokenId][_stakeMsgSender()].amountStaked > 0) { + _updateUnclaimedRewardsForStaker(_tokenId, _stakeMsgSender()); } else { - stakersArray[_tokenId].push(msg.sender); - stakers[_tokenId][msg.sender].timeOfLastUpdate = block.timestamp; + 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(msg.sender, _tokenId) >= _amount && - IERC1155(_edition).isApprovedForAll(msg.sender, address(this)), + IERC1155(_edition).balanceOf(_stakeMsgSender(), _tokenId) >= _amount && + IERC1155(_edition).isApprovedForAll(_stakeMsgSender(), address(this)), "Not balance or approved" ); - IERC1155(_edition).safeTransferFrom(msg.sender, address(this), _tokenId, _amount, ""); - // stakerAddress[_tokenIds[i]] = msg.sender; - stakers[_tokenId][msg.sender].amountStaked += _amount; + 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(msg.sender, _tokenId, _amount); + 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][msg.sender].amountStaked; + uint256 _amountStaked = stakers[_tokenId][_stakeMsgSender()].amountStaked; require(_amount != 0, "Withdrawing 0 tokens"); require(_amountStaked >= _amount, "Withdrawing more than staked"); - _updateUnclaimedRewardsForStaker(_tokenId, msg.sender); + _updateUnclaimedRewardsForStaker(_tokenId, _stakeMsgSender()); if (_amountStaked == _amount) { address[] memory _stakersArray = stakersArray[_tokenId]; for (uint256 i = 0; i < _stakersArray.length; ++i) { - if (_stakersArray[i] == msg.sender) { - stakersArray[_tokenId][i] = stakersArray[_tokenId][_stakersArray.length - 1]; + if (_stakersArray[i] == _stakeMsgSender()) { + stakersArray[_tokenId][i] = _stakersArray[_stakersArray.length - 1]; stakersArray[_tokenId].pop(); break; } } } - stakers[_tokenId][msg.sender].amountStaked -= _amount; + stakers[_tokenId][_stakeMsgSender()].amountStaked -= _amount; - IERC1155(edition).safeTransferFrom(address(this), msg.sender, _tokenId, _amount, ""); + IERC1155(edition).safeTransferFrom(address(this), _stakeMsgSender(), _tokenId, _amount, ""); - emit TokensWithdrawn(msg.sender, _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][msg.sender].unclaimedRewards + _calculateRewards(_tokenId, msg.sender); + uint256 rewards = stakers[_tokenId][_stakeMsgSender()].unclaimedRewards + + _calculateRewards(_tokenId, _stakeMsgSender()); require(rewards != 0, "No rewards"); - stakers[_tokenId][msg.sender].timeOfLastUpdate = block.timestamp; - stakers[_tokenId][msg.sender].unclaimedRewards = 0; + 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(msg.sender, rewards); + _mintRewards(_stakeMsgSender(), rewards); - emit RewardsClaimed(msg.sender, rewards); + emit RewardsClaimed(_stakeMsgSender(), rewards); } /// @dev View available rewards for a user. @@ -324,59 +359,137 @@ abstract contract Staking1155Upgradeable is ReentrancyGuardUpgradeable, IStaking } } - /// @dev Update unclaimed rewards for all users. Called when setting timeUnit or rewardsPerUnitTime. - function _updateUnclaimedRewardsForAll(uint256 _tokenId) internal virtual { - address[] memory _stakers = stakersArray[_tokenId]; - uint256 len = _stakers.length; - for (uint256 i = 0; i < len; ++i) { - address user = _stakers[i]; - - uint256 rewards = _calculateRewards(_tokenId, user); - stakers[_tokenId][user].unclaimedRewards += rewards; - stakers[_tokenId][user].timeOfLastUpdate = block.timestamp; - } - } - /// @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; - } - /// @dev Set time unit in seconds. - function _setTimeUnit(uint256 _tokenId, uint256 _timeUnit) internal virtual { - timeUnit[_tokenId] = _timeUnit; + uint256 _conditionId = nextConditionId[_tokenId]; + stakers[_tokenId][_staker].conditionIdOflastUpdate = _conditionId == 0 + ? nextDefaultConditionId - 1 + : _conditionId - 1; } - /// @dev Set rewards per unit time. - function _setRewardsPerUnitTime(uint256 _tokenId, uint256 _rewardsPerUnitTime) internal virtual { - rewardsPerUnitTime[_tokenId] = _rewardsPerUnitTime; - } + /// @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; - /// @dev Set time unit in seconds. - function _setDefaultTimeUnit(uint256 _defaultTimeUnit) internal virtual { - defaultTimeUnit = _defaultTimeUnit; + stakingConditions[_tokenId][conditionId] = StakingCondition({ + timeUnit: _timeUnit, + rewardsPerUnitTime: _rewardsPerUnitTime, + startTimestamp: block.timestamp, + endTimestamp: 0 + }); + + nextConditionId[_tokenId] = conditionId + 1; } - /// @dev Set rewards per unit time. - function _setDefaultRewardsPerUnitTime(uint256 _defaultRewardsPerUnitTime) internal virtual { - defaultRewardsPerUnitTime = _defaultRewardsPerUnitTime; + /// @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 _timeUnit = timeUnit[_tokenId]; - uint256 _rewardsPerUnitTime = rewardsPerUnitTime[_tokenId]; + 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; + } + } + } - if (_timeUnit == 0) _timeUnit = defaultTimeUnit; - if (_rewardsPerUnitTime == 0) _rewardsPerUnitTime = defaultRewardsPerUnitTime; + /*//////////////////////////////////////////////////////////////////// + Optional hooks that can be implemented in the derived contract + ///////////////////////////////////////////////////////////////////*/ - _rewards = ((((block.timestamp - staker.timeOfLastUpdate) * staker.amountStaked) * _rewardsPerUnitTime) / - _timeUnit); + /// @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 + //////////////////////////////////////////////////////////////*/ + + /** + * @notice View total rewards available in the staking contract. + * + */ + function getRewardTokenBalance() external view virtual returns (uint256 _rewardsAvailableInContract); + /** * @dev Mint/Transfer ERC20 rewards to the staker. Must override. * diff --git a/contracts/extension/Staking20.sol b/contracts/extension/Staking20.sol index b1ad45759..ca7a6d645 100644 --- a/contracts/extension/Staking20.sol +++ b/contracts/extension/Staking20.sol @@ -2,6 +2,7 @@ pragma solidity ^0.8.11; import "../openzeppelin-presets/security/ReentrancyGuard.sol"; +import "../openzeppelin-presets/utils/math/SafeMath.sol"; import "../eip/interface/IERC20.sol"; import "../lib/CurrencyTransferLib.sol"; @@ -19,24 +20,38 @@ abstract contract Staking20 is ReentrancyGuard, IStaking20 { ///@dev Address of ERC20 contract -- staked tokens belong to this contract. address public token; - /// @dev Unit of time specified in number of seconds. Can be set as 1 seconds, 1 days, 1 hours, etc. - uint256 public timeUnit; + /// @dev Decimals of staking token. + uint256 public stakingTokenDecimals; - ///@dev Rewards ratio is the number of reward tokens for a number of staked tokens, per unit of time. - uint256 public rewardRatioNumerator; + /// @dev Decimals of reward token. + uint256 public rewardTokenDecimals; - ///@dev Rewards ratio is the number of reward tokens for a number of staked tokens, per unit of time. - uint256 public rewardRatioDenominator; + /// @dev List of accounts that have staked that token-id. + address[] public stakersArray; + + /// @dev Total amount of tokens staked in the contract. + uint256 public stakingTokenBalance; + + ///@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 List of accounts that have staked that token-id. - address[] public stakersArray; + ///@dev Mapping from condition Id to staking condition. See {struct IStaking721.StakingCondition} + mapping(uint256 => StakingCondition) private stakingConditions; - constructor(address _token) ReentrancyGuard() { + constructor( + address _token, + uint256 _stakingTokenDecimals, + uint256 _rewardTokenDecimals + ) ReentrancyGuard() { require(address(_token) != address(0), "address 0"); + require(_stakingTokenDecimals != 0 && _rewardTokenDecimals != 0, "decimals 0"); + token = _token; + stakingTokenDecimals = _stakingTokenDecimals; + rewardTokenDecimals = _rewardTokenDecimals; } /*/////////////////////////////////////////////////////////////// @@ -88,12 +103,12 @@ abstract contract Staking20 is ReentrancyGuard, IStaking20 { revert("Not authorized"); } - _updateUnclaimedRewardsForAll(); + StakingCondition memory condition = stakingConditions[nextConditionId - 1]; + require(_timeUnit != condition.timeUnit, "Time-unit unchanged."); - uint256 currentTimeUnit = timeUnit; - _setTimeUnit(_timeUnit); + _setStakingCondition(_timeUnit, condition.rewardRatioNumerator, condition.rewardRatioDenominator); - emit UpdatedTimeUnit(currentTimeUnit, _timeUnit); + emit UpdatedTimeUnit(condition.timeUnit, _timeUnit); } /** @@ -112,13 +127,19 @@ abstract contract Staking20 is ReentrancyGuard, IStaking20 { revert("Not authorized"); } - _updateUnclaimedRewardsForAll(); - - uint256 currentNumerator = rewardRatioNumerator; - uint256 currentDenominator = rewardRatioDenominator; - _setRewardRatio(_numerator, _denominator); - - emit UpdatedRewardRatio(currentNumerator, _numerator, currentDenominator, _denominator); + 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 + ); } /** @@ -128,11 +149,20 @@ abstract contract Staking20 is ReentrancyGuard, IStaking20 { * @return _tokensStaked Amount of tokens staked. * @return _rewards Available reward amount. */ - function getStakeInfo(address _staker) public view virtual returns (uint256 _tokensStaked, uint256 _rewards) { + function getStakeInfo(address _staker) external 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 //////////////////////////////////////////////////////////////*/ @@ -142,57 +172,63 @@ abstract contract Staking20 is ReentrancyGuard, IStaking20 { require(_amount != 0, "Staking 0 tokens"); address _token = token; - if (stakers[msg.sender].amountStaked > 0) { - _updateUnclaimedRewardsForStaker(msg.sender); + if (stakers[_stakeMsgSender()].amountStaked > 0) { + _updateUnclaimedRewardsForStaker(_stakeMsgSender()); } else { - stakersArray.push(msg.sender); - stakers[msg.sender].timeOfLastUpdate = block.timestamp; + stakersArray.push(_stakeMsgSender()); + stakers[_stakeMsgSender()].timeOfLastUpdate = block.timestamp; + stakers[_stakeMsgSender()].conditionIdOflastUpdate = nextConditionId - 1; } - CurrencyTransferLib.transferCurrency(_token, msg.sender, address(this), _amount); + uint256 balanceBefore = IERC20(_token).balanceOf(address(this)); + CurrencyTransferLib.transferCurrency(_token, _stakeMsgSender(), address(this), _amount); + uint256 actualAmount = IERC20(_token).balanceOf(address(this)) - balanceBefore; - stakers[msg.sender].amountStaked += _amount; + stakers[_stakeMsgSender()].amountStaked += actualAmount; + stakingTokenBalance += actualAmount; - emit TokensStaked(msg.sender, _amount); + emit TokensStaked(_stakeMsgSender(), actualAmount); } /// @dev Withdraw logic. Override to add custom logic. function _withdraw(uint256 _amount) internal virtual { - uint256 _amountStaked = stakers[msg.sender].amountStaked; + uint256 _amountStaked = stakers[_stakeMsgSender()].amountStaked; require(_amount != 0, "Withdrawing 0 tokens"); require(_amountStaked >= _amount, "Withdrawing more than staked"); - _updateUnclaimedRewardsForStaker(msg.sender); + _updateUnclaimedRewardsForStaker(_stakeMsgSender()); if (_amountStaked == _amount) { address[] memory _stakersArray = stakersArray; for (uint256 i = 0; i < _stakersArray.length; ++i) { - if (_stakersArray[i] == msg.sender) { - stakersArray[i] = stakersArray[_stakersArray.length - 1]; + if (_stakersArray[i] == _stakeMsgSender()) { + stakersArray[i] = _stakersArray[_stakersArray.length - 1]; stakersArray.pop(); break; } } } - stakers[msg.sender].amountStaked -= _amount; + stakers[_stakeMsgSender()].amountStaked -= _amount; + stakingTokenBalance -= _amount; - CurrencyTransferLib.transferCurrency(token, address(this), msg.sender, _amount); + CurrencyTransferLib.transferCurrency(token, address(this), _stakeMsgSender(), _amount); - emit TokensWithdrawn(msg.sender, _amount); + emit TokensWithdrawn(_stakeMsgSender(), _amount); } /// @dev Logic for claiming rewards. Override to add custom logic. function _claimRewards() internal virtual { - uint256 rewards = stakers[msg.sender].unclaimedRewards + _calculateRewards(msg.sender); + uint256 rewards = stakers[_stakeMsgSender()].unclaimedRewards + _calculateRewards(_stakeMsgSender()); require(rewards != 0, "No rewards"); - stakers[msg.sender].timeOfLastUpdate = block.timestamp; - stakers[msg.sender].unclaimedRewards = 0; + stakers[_stakeMsgSender()].timeOfLastUpdate = block.timestamp; + stakers[_stakeMsgSender()].unclaimedRewards = 0; + stakers[_stakeMsgSender()].conditionIdOflastUpdate = nextConditionId - 1; - _mintRewards(msg.sender, rewards); + _mintRewards(_stakeMsgSender(), rewards); - emit RewardsClaimed(msg.sender, rewards); + emit RewardsClaimed(_stakeMsgSender(), rewards); } /// @dev View available rewards for a user. @@ -204,46 +240,87 @@ abstract contract Staking20 is ReentrancyGuard, IStaking20 { } } - /// @dev Update unclaimed rewards for all users. Called when setting timeUnit or rewardsPerUnitTime. - function _updateUnclaimedRewardsForAll() internal virtual { - address[] memory _stakers = stakersArray; - uint256 len = _stakers.length; - for (uint256 i = 0; i < len; ++i) { - address _staker = _stakers[i]; - - uint256 rewards = _calculateRewards(_staker); - stakers[_staker].unclaimedRewards += rewards; - stakers[_staker].timeOfLastUpdate = block.timestamp; - } - } - /// @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 time unit in seconds. - function _setTimeUnit(uint256 _timeUnit) internal virtual { - timeUnit = _timeUnit; - } - - /// @dev Set reward ratio per unit time. - function _setRewardRatio(uint256 _numerator, uint256 _denominator) internal virtual { + /// @dev Set staking conditions. + function _setStakingCondition( + uint256 _timeUnit, + uint256 _numerator, + uint256 _denominator + ) internal virtual { require(_denominator != 0, "divide by 0"); - rewardRatioNumerator = _numerator; - rewardRatioDenominator = _denominator; + 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 Reward calculation logic. Override to implement custom logic. + /// @dev Calculate rewards for a staker. function _calculateRewards(address _staker) internal view virtual returns (uint256 _rewards) { Staker memory staker = stakers[_staker]; - _rewards = (((((block.timestamp - staker.timeOfLastUpdate) * staker.amountStaked) * rewardRatioNumerator) / - timeUnit) / rewardRatioDenominator); + 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 + //////////////////////////////////////////////////////////////*/ + + /** + * @notice View total rewards available in the staking contract. + * + */ + function getRewardTokenBalance() external view virtual returns (uint256 _rewardsAvailableInContract); + /** * @dev Mint/Transfer ERC20 rewards to the staker. Must override. * diff --git a/contracts/extension/Staking20Upgradeable.sol b/contracts/extension/Staking20Upgradeable.sol index 489bed518..bfa282fe7 100644 --- a/contracts/extension/Staking20Upgradeable.sol +++ b/contracts/extension/Staking20Upgradeable.sol @@ -2,6 +2,7 @@ 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"; @@ -19,26 +20,40 @@ abstract contract Staking20Upgradeable is ReentrancyGuardUpgradeable, IStaking20 ///@dev Address of ERC20 contract -- staked tokens belong to this contract. address public token; - /// @dev Unit of time specified in number of seconds. Can be set as 1 seconds, 1 days, 1 hours, etc. - uint256 public timeUnit; + /// @dev Decimals of staking token. + uint256 public stakingTokenDecimals; - ///@dev Rewards ratio is the number of reward tokens for a number of staked tokens, per unit of time. - uint256 public rewardRatioNumerator; + /// @dev Decimals of reward token. + uint256 public rewardTokenDecimals; - ///@dev Rewards ratio is the number of reward tokens for a number of staked tokens, per unit of time. - uint256 public rewardRatioDenominator; + /// @dev List of accounts that have staked that token-id. + address[] public stakersArray; + + /// @dev Total amount of tokens staked in the contract. + uint256 public stakingTokenBalance; + + ///@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 List of accounts that have staked that token-id. - address[] public stakersArray; + ///@dev Mapping from condition Id to staking condition. See {struct IStaking721.StakingCondition} + mapping(uint256 => StakingCondition) private stakingConditions; - function __Staking20_init(address _token) internal onlyInitializing { + 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; } /*/////////////////////////////////////////////////////////////// @@ -90,12 +105,12 @@ abstract contract Staking20Upgradeable is ReentrancyGuardUpgradeable, IStaking20 revert("Not authorized"); } - _updateUnclaimedRewardsForAll(); + StakingCondition memory condition = stakingConditions[nextConditionId - 1]; + require(_timeUnit != condition.timeUnit, "Time-unit unchanged."); - uint256 currentTimeUnit = timeUnit; - _setTimeUnit(_timeUnit); + _setStakingCondition(_timeUnit, condition.rewardRatioNumerator, condition.rewardRatioDenominator); - emit UpdatedTimeUnit(currentTimeUnit, _timeUnit); + emit UpdatedTimeUnit(condition.timeUnit, _timeUnit); } /** @@ -114,13 +129,19 @@ abstract contract Staking20Upgradeable is ReentrancyGuardUpgradeable, IStaking20 revert("Not authorized"); } - _updateUnclaimedRewardsForAll(); - - uint256 currentNumerator = rewardRatioNumerator; - uint256 currentDenominator = rewardRatioDenominator; - _setRewardRatio(_numerator, _denominator); - - emit UpdatedRewardRatio(currentNumerator, _numerator, currentDenominator, _denominator); + 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 + ); } /** @@ -130,11 +151,20 @@ abstract contract Staking20Upgradeable is ReentrancyGuardUpgradeable, IStaking20 * @return _tokensStaked Amount of tokens staked. * @return _rewards Available reward amount. */ - function getStakeInfo(address _staker) public view virtual returns (uint256 _tokensStaked, uint256 _rewards) { + function getStakeInfo(address _staker) external 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 //////////////////////////////////////////////////////////////*/ @@ -144,57 +174,63 @@ abstract contract Staking20Upgradeable is ReentrancyGuardUpgradeable, IStaking20 require(_amount != 0, "Staking 0 tokens"); address _token = token; - if (stakers[msg.sender].amountStaked > 0) { - _updateUnclaimedRewardsForStaker(msg.sender); + if (stakers[_stakeMsgSender()].amountStaked > 0) { + _updateUnclaimedRewardsForStaker(_stakeMsgSender()); } else { - stakersArray.push(msg.sender); - stakers[msg.sender].timeOfLastUpdate = block.timestamp; + stakersArray.push(_stakeMsgSender()); + stakers[_stakeMsgSender()].timeOfLastUpdate = block.timestamp; + stakers[_stakeMsgSender()].conditionIdOflastUpdate = nextConditionId - 1; } - CurrencyTransferLib.transferCurrency(_token, msg.sender, address(this), _amount); + uint256 balanceBefore = IERC20(_token).balanceOf(address(this)); + CurrencyTransferLib.transferCurrency(_token, _stakeMsgSender(), address(this), _amount); + uint256 actualAmount = IERC20(_token).balanceOf(address(this)) - balanceBefore; - stakers[msg.sender].amountStaked += _amount; + stakers[_stakeMsgSender()].amountStaked += actualAmount; + stakingTokenBalance += actualAmount; - emit TokensStaked(msg.sender, _amount); + emit TokensStaked(_stakeMsgSender(), actualAmount); } /// @dev Withdraw logic. Override to add custom logic. function _withdraw(uint256 _amount) internal virtual { - uint256 _amountStaked = stakers[msg.sender].amountStaked; + uint256 _amountStaked = stakers[_stakeMsgSender()].amountStaked; require(_amount != 0, "Withdrawing 0 tokens"); require(_amountStaked >= _amount, "Withdrawing more than staked"); - _updateUnclaimedRewardsForStaker(msg.sender); + _updateUnclaimedRewardsForStaker(_stakeMsgSender()); if (_amountStaked == _amount) { address[] memory _stakersArray = stakersArray; for (uint256 i = 0; i < _stakersArray.length; ++i) { - if (_stakersArray[i] == msg.sender) { - stakersArray[i] = stakersArray[_stakersArray.length - 1]; + if (_stakersArray[i] == _stakeMsgSender()) { + stakersArray[i] = _stakersArray[_stakersArray.length - 1]; stakersArray.pop(); break; } } } - stakers[msg.sender].amountStaked -= _amount; + stakers[_stakeMsgSender()].amountStaked -= _amount; + stakingTokenBalance -= _amount; - CurrencyTransferLib.transferCurrency(token, address(this), msg.sender, _amount); + CurrencyTransferLib.transferCurrency(token, address(this), _stakeMsgSender(), _amount); - emit TokensWithdrawn(msg.sender, _amount); + emit TokensWithdrawn(_stakeMsgSender(), _amount); } /// @dev Logic for claiming rewards. Override to add custom logic. function _claimRewards() internal virtual { - uint256 rewards = stakers[msg.sender].unclaimedRewards + _calculateRewards(msg.sender); + uint256 rewards = stakers[_stakeMsgSender()].unclaimedRewards + _calculateRewards(_stakeMsgSender()); require(rewards != 0, "No rewards"); - stakers[msg.sender].timeOfLastUpdate = block.timestamp; - stakers[msg.sender].unclaimedRewards = 0; + stakers[_stakeMsgSender()].timeOfLastUpdate = block.timestamp; + stakers[_stakeMsgSender()].unclaimedRewards = 0; + stakers[_stakeMsgSender()].conditionIdOflastUpdate = nextConditionId - 1; - _mintRewards(msg.sender, rewards); + _mintRewards(_stakeMsgSender(), rewards); - emit RewardsClaimed(msg.sender, rewards); + emit RewardsClaimed(_stakeMsgSender(), rewards); } /// @dev View available rewards for a user. @@ -206,46 +242,87 @@ abstract contract Staking20Upgradeable is ReentrancyGuardUpgradeable, IStaking20 } } - /// @dev Update unclaimed rewards for all users. Called when setting timeUnit or rewardsPerUnitTime. - function _updateUnclaimedRewardsForAll() internal virtual { - address[] memory _stakers = stakersArray; - uint256 len = _stakers.length; - for (uint256 i = 0; i < len; ++i) { - address _staker = _stakers[i]; - - uint256 rewards = _calculateRewards(_staker); - stakers[_staker].unclaimedRewards += rewards; - stakers[_staker].timeOfLastUpdate = block.timestamp; - } - } - /// @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 time unit in seconds. - function _setTimeUnit(uint256 _timeUnit) internal virtual { - timeUnit = _timeUnit; - } - - /// @dev Set reward ratio per unit time. - function _setRewardRatio(uint256 _numerator, uint256 _denominator) internal virtual { + /// @dev Set staking conditions. + function _setStakingCondition( + uint256 _timeUnit, + uint256 _numerator, + uint256 _denominator + ) internal virtual { require(_denominator != 0, "divide by 0"); - rewardRatioNumerator = _numerator; - rewardRatioDenominator = _denominator; + 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 Reward calculation logic. Override to implement custom logic. + /// @dev Calculate rewards for a staker. function _calculateRewards(address _staker) internal view virtual returns (uint256 _rewards) { Staker memory staker = stakers[_staker]; - _rewards = (((((block.timestamp - staker.timeOfLastUpdate) * staker.amountStaked) * rewardRatioNumerator) / - timeUnit) / rewardRatioDenominator); + 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 + //////////////////////////////////////////////////////////////*/ + + /** + * @notice View total rewards available in the staking contract. + * + */ + function getRewardTokenBalance() external view virtual returns (uint256 _rewardsAvailableInContract); + /** * @dev Mint/Transfer ERC20 rewards to the staker. Must override. * diff --git a/contracts/extension/Staking721.sol b/contracts/extension/Staking721.sol index ed886a427..bdb3b52b9 100644 --- a/contracts/extension/Staking721.sol +++ b/contracts/extension/Staking721.sol @@ -2,6 +2,7 @@ pragma solidity ^0.8.11; import "../openzeppelin-presets/security/ReentrancyGuard.sol"; +import "../openzeppelin-presets/utils/math/SafeMath.sol"; import "../eip/interface/IERC721.sol"; import "./interface/IStaking721.sol"; @@ -18,15 +19,18 @@ abstract contract Staking721 is ReentrancyGuard, IStaking721 { ///@dev Address of ERC721 NFT contract -- staked tokens belong to this contract. address public nftCollection; - /// @dev Unit of time specified in number of seconds. Can be set as 1 seconds, 1 days, 1 hours, etc. - uint256 public timeUnit; - - ///@dev Rewards accumulated per unit of time. - uint256 public rewardsPerUnitTime; - ///@dev List of token-ids ever staked. uint256[] public indexedTokens; + /// @dev List of accounts that have staked their NFTs. + address[] public stakersArray; + + /// @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 nextConditionId; + ///@dev Mapping from token-id to whether it is indexed or not. mapping(uint256 => bool) public isIndexed; @@ -36,8 +40,8 @@ abstract contract Staking721 is ReentrancyGuard, IStaking721 { /// @dev Mapping from staked token-id to staker address. mapping(uint256 => address) public stakerAddress; - /// @dev List of accounts that have staked their NFTs. - address[] public stakersArray; + ///@dev Mapping from condition Id to staking condition. See {struct IStaking721.StakingCondition} + mapping(uint256 => StakingCondition) private stakingConditions; constructor(address _nftCollection) ReentrancyGuard() { require(address(_nftCollection) != address(0), "collection address 0"); @@ -94,12 +98,12 @@ abstract contract Staking721 is ReentrancyGuard, IStaking721 { revert("Not authorized"); } - _updateUnclaimedRewardsForAll(); + StakingCondition memory condition = stakingConditions[nextConditionId - 1]; + require(_timeUnit != condition.timeUnit, "Time-unit unchanged."); - uint256 currentTimeUnit = timeUnit; - _setTimeUnit(_timeUnit); + _setStakingCondition(_timeUnit, condition.rewardsPerUnitTime); - emit UpdatedTimeUnit(currentTimeUnit, _timeUnit); + emit UpdatedTimeUnit(condition.timeUnit, _timeUnit); } /** @@ -116,12 +120,12 @@ abstract contract Staking721 is ReentrancyGuard, IStaking721 { revert("Not authorized"); } - _updateUnclaimedRewardsForAll(); + StakingCondition memory condition = stakingConditions[nextConditionId - 1]; + require(_rewardsPerUnitTime != condition.rewardsPerUnitTime, "Reward unchanged."); - uint256 currentRewardsPerUnitTime = rewardsPerUnitTime; - _setRewardsPerUnitTime(_rewardsPerUnitTime); + _setStakingCondition(condition.timeUnit, _rewardsPerUnitTime); - emit UpdatedRewardsPerUnitTime(currentRewardsPerUnitTime, _rewardsPerUnitTime); + emit UpdatedRewardsPerUnitTime(condition.rewardsPerUnitTime, _rewardsPerUnitTime); } /** @@ -132,7 +136,7 @@ abstract contract Staking721 is ReentrancyGuard, IStaking721 { * @return _rewards Available reward amount. */ function getStakeInfo(address _staker) - public + external view virtual returns (uint256[] memory _tokensStaked, uint256 _rewards) @@ -159,6 +163,14 @@ abstract contract Staking721 is ReentrancyGuard, IStaking721 { _rewards = _availableRewards(_staker); } + function getTimeUnit() public view returns (uint256 _timeUnit) { + _timeUnit = stakingConditions[nextConditionId - 1].timeUnit; + } + + function getRewardsPerUnitTime() public view returns (uint256 _rewardsPerUnitTime) { + _rewardsPerUnitTime = stakingConditions[nextConditionId - 1].rewardsPerUnitTime; + } + /*/////////////////////////////////////////////////////////////// Internal Functions //////////////////////////////////////////////////////////////*/ @@ -170,74 +182,82 @@ abstract contract Staking721 is ReentrancyGuard, IStaking721 { address _nftCollection = nftCollection; - if (stakers[msg.sender].amountStaked > 0) { - _updateUnclaimedRewardsForStaker(msg.sender); + if (stakers[_stakeMsgSender()].amountStaked > 0) { + _updateUnclaimedRewardsForStaker(_stakeMsgSender()); } else { - stakersArray.push(msg.sender); - stakers[msg.sender].timeOfLastUpdate = block.timestamp; + stakersArray.push(_stakeMsgSender()); + stakers[_stakeMsgSender()].timeOfLastUpdate = block.timestamp; + stakers[_stakeMsgSender()].conditionIdOflastUpdate = nextConditionId - 1; } for (uint256 i = 0; i < len; ++i) { require( - IERC721(_nftCollection).ownerOf(_tokenIds[i]) == msg.sender && + IERC721(_nftCollection).ownerOf(_tokenIds[i]) == _stakeMsgSender() && (IERC721(_nftCollection).getApproved(_tokenIds[i]) == address(this) || - IERC721(_nftCollection).isApprovedForAll(msg.sender, address(this))), + IERC721(_nftCollection).isApprovedForAll(_stakeMsgSender(), address(this))), "Not owned or approved" ); - IERC721(_nftCollection).transferFrom(msg.sender, address(this), _tokenIds[i]); - stakerAddress[_tokenIds[i]] = msg.sender; + + isStaking = 2; + IERC721(_nftCollection).safeTransferFrom(_stakeMsgSender(), address(this), _tokenIds[i]); + isStaking = 1; + + stakerAddress[_tokenIds[i]] = _stakeMsgSender(); if (!isIndexed[_tokenIds[i]]) { isIndexed[_tokenIds[i]] = true; indexedTokens.push(_tokenIds[i]); } } - stakers[msg.sender].amountStaked += len; + stakers[_stakeMsgSender()].amountStaked += len; - emit TokensStaked(msg.sender, _tokenIds); + emit TokensStaked(_stakeMsgSender(), _tokenIds); } /// @dev Withdraw logic. Override to add custom logic. function _withdraw(uint256[] calldata _tokenIds) internal virtual { - uint256 _amountStaked = stakers[msg.sender].amountStaked; + uint256 _amountStaked = stakers[_stakeMsgSender()].amountStaked; uint256 len = _tokenIds.length; require(len != 0, "Withdrawing 0 tokens"); require(_amountStaked >= len, "Withdrawing more than staked"); address _nftCollection = nftCollection; - _updateUnclaimedRewardsForStaker(msg.sender); + _updateUnclaimedRewardsForStaker(_stakeMsgSender()); if (_amountStaked == len) { - for (uint256 i = 0; i < stakersArray.length; ++i) { - if (stakersArray[i] == msg.sender) { - stakersArray[i] = stakersArray[stakersArray.length - 1]; + 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[msg.sender].amountStaked -= len; + stakers[_stakeMsgSender()].amountStaked -= len; for (uint256 i = 0; i < len; ++i) { - require(stakerAddress[_tokenIds[i]] == msg.sender, "Not staker"); + require(stakerAddress[_tokenIds[i]] == _stakeMsgSender(), "Not staker"); stakerAddress[_tokenIds[i]] = address(0); - IERC721(_nftCollection).transferFrom(address(this), msg.sender, _tokenIds[i]); + IERC721(_nftCollection).safeTransferFrom(address(this), _stakeMsgSender(), _tokenIds[i]); } - emit TokensWithdrawn(msg.sender, _tokenIds); + emit TokensWithdrawn(_stakeMsgSender(), _tokenIds); } /// @dev Logic for claiming rewards. Override to add custom logic. function _claimRewards() internal virtual { - uint256 rewards = stakers[msg.sender].unclaimedRewards + _calculateRewards(msg.sender); + uint256 rewards = stakers[_stakeMsgSender()].unclaimedRewards + _calculateRewards(_stakeMsgSender()); require(rewards != 0, "No rewards"); - stakers[msg.sender].timeOfLastUpdate = block.timestamp; - stakers[msg.sender].unclaimedRewards = 0; + stakers[_stakeMsgSender()].timeOfLastUpdate = block.timestamp; + stakers[_stakeMsgSender()].unclaimedRewards = 0; + stakers[_stakeMsgSender()].conditionIdOflastUpdate = nextConditionId - 1; - _mintRewards(msg.sender, rewards); + _mintRewards(_stakeMsgSender(), rewards); - emit RewardsClaimed(msg.sender, rewards); + emit RewardsClaimed(_stakeMsgSender(), rewards); } /// @dev View available rewards for a user. @@ -249,43 +269,74 @@ abstract contract Staking721 is ReentrancyGuard, IStaking721 { } } - /// @dev Update unclaimed rewards for all users. Called when setting timeUnit or rewardsPerUnitTime. - function _updateUnclaimedRewardsForAll() internal virtual { - address[] memory _stakers = stakersArray; - uint256 len = _stakers.length; - for (uint256 i = 0; i < len; ++i) { - address user = _stakers[i]; - - uint256 rewards = _calculateRewards(user); - stakers[user].unclaimedRewards += rewards; - stakers[user].timeOfLastUpdate = block.timestamp; - } - } - /// @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 time unit in seconds. - function _setTimeUnit(uint256 _timeUnit) internal virtual { - timeUnit = _timeUnit; - } - - /// @dev Set rewards per unit time. - function _setRewardsPerUnitTime(uint256 _rewardsPerUnitTime) internal virtual { - rewardsPerUnitTime = _rewardsPerUnitTime; + /// @dev Set staking conditions. + function _setStakingCondition(uint256 _timeUnit, uint256 _rewardsPerUnitTime) internal virtual { + require(_timeUnit != 0, "time-unit can't be 0"); + uint256 conditionId = nextConditionId; + nextConditionId += 1; + + stakingConditions[conditionId] = StakingCondition({ + timeUnit: _timeUnit, + rewardsPerUnitTime: _rewardsPerUnitTime, + startTimestamp: block.timestamp, + endTimestamp: 0 + }); + + if (conditionId > 0) { + stakingConditions[conditionId - 1].endTimestamp = block.timestamp; + } } - /// @dev Reward calculation logic. Override to implement custom logic. + /// @dev Calculate rewards for a staker. function _calculateRewards(address _staker) internal view virtual returns (uint256 _rewards) { Staker memory staker = stakers[_staker]; - _rewards = ((((block.timestamp - staker.timeOfLastUpdate) * staker.amountStaked) * rewardsPerUnitTime) / - timeUnit); + + 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.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 + //////////////////////////////////////////////////////////////*/ + + /** + * @notice View total rewards available in the staking contract. + * + */ + function getRewardTokenBalance() external view virtual returns (uint256 _rewardsAvailableInContract); + /** * @dev Mint/Transfer ERC20 rewards to the staker. Must override. * diff --git a/contracts/extension/Staking721Upgradeable.sol b/contracts/extension/Staking721Upgradeable.sol index c23878a80..a58fae586 100644 --- a/contracts/extension/Staking721Upgradeable.sol +++ b/contracts/extension/Staking721Upgradeable.sol @@ -2,6 +2,7 @@ pragma solidity ^0.8.11; import "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol"; +import "../openzeppelin-presets/utils/math/SafeMath.sol"; import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; import "./interface/IStaking721.sol"; @@ -18,15 +19,18 @@ abstract contract Staking721Upgradeable is ReentrancyGuardUpgradeable, IStaking7 ///@dev Address of ERC721 NFT contract -- staked tokens belong to this contract. address public nftCollection; - /// @dev Unit of time specified in number of seconds. Can be set as 1 seconds, 1 days, 1 hours, etc. - uint256 public timeUnit; - - ///@dev Rewards accumulated per unit of time. - uint256 public rewardsPerUnitTime; - ///@dev List of token-ids ever staked. uint256[] public indexedTokens; + /// @dev List of accounts that have staked their NFTs. + address[] public stakersArray; + + /// @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 nextConditionId; + ///@dev Mapping from token-id to whether it is indexed or not. mapping(uint256 => bool) public isIndexed; @@ -36,8 +40,8 @@ abstract contract Staking721Upgradeable is ReentrancyGuardUpgradeable, IStaking7 /// @dev Mapping from staked token-id to staker address. mapping(uint256 => address) public stakerAddress; - /// @dev List of accounts that have staked their NFTs. - address[] public stakersArray; + ///@dev Mapping from condition Id to staking condition. See {struct IStaking721.StakingCondition} + mapping(uint256 => StakingCondition) private stakingConditions; function __Staking721_init(address _nftCollection) internal onlyInitializing { __ReentrancyGuard_init(); @@ -96,12 +100,12 @@ abstract contract Staking721Upgradeable is ReentrancyGuardUpgradeable, IStaking7 revert("Not authorized"); } - _updateUnclaimedRewardsForAll(); + StakingCondition memory condition = stakingConditions[nextConditionId - 1]; + require(_timeUnit != condition.timeUnit, "Time-unit unchanged."); - uint256 currentTimeUnit = timeUnit; - _setTimeUnit(_timeUnit); + _setStakingCondition(_timeUnit, condition.rewardsPerUnitTime); - emit UpdatedTimeUnit(currentTimeUnit, _timeUnit); + emit UpdatedTimeUnit(condition.timeUnit, _timeUnit); } /** @@ -118,12 +122,12 @@ abstract contract Staking721Upgradeable is ReentrancyGuardUpgradeable, IStaking7 revert("Not authorized"); } - _updateUnclaimedRewardsForAll(); + StakingCondition memory condition = stakingConditions[nextConditionId - 1]; + require(_rewardsPerUnitTime != condition.rewardsPerUnitTime, "Reward unchanged."); - uint256 currentRewardsPerUnitTime = rewardsPerUnitTime; - _setRewardsPerUnitTime(_rewardsPerUnitTime); + _setStakingCondition(condition.timeUnit, _rewardsPerUnitTime); - emit UpdatedRewardsPerUnitTime(currentRewardsPerUnitTime, _rewardsPerUnitTime); + emit UpdatedRewardsPerUnitTime(condition.rewardsPerUnitTime, _rewardsPerUnitTime); } /** @@ -134,7 +138,7 @@ abstract contract Staking721Upgradeable is ReentrancyGuardUpgradeable, IStaking7 * @return _rewards Available reward amount. */ function getStakeInfo(address _staker) - public + external view virtual returns (uint256[] memory _tokensStaked, uint256 _rewards) @@ -161,6 +165,14 @@ abstract contract Staking721Upgradeable is ReentrancyGuardUpgradeable, IStaking7 _rewards = _availableRewards(_staker); } + function getTimeUnit() public view returns (uint256 _timeUnit) { + _timeUnit = stakingConditions[nextConditionId - 1].timeUnit; + } + + function getRewardsPerUnitTime() public view returns (uint256 _rewardsPerUnitTime) { + _rewardsPerUnitTime = stakingConditions[nextConditionId - 1].rewardsPerUnitTime; + } + /*/////////////////////////////////////////////////////////////// Internal Functions //////////////////////////////////////////////////////////////*/ @@ -172,74 +184,82 @@ abstract contract Staking721Upgradeable is ReentrancyGuardUpgradeable, IStaking7 address _nftCollection = nftCollection; - if (stakers[msg.sender].amountStaked > 0) { - _updateUnclaimedRewardsForStaker(msg.sender); + if (stakers[_stakeMsgSender()].amountStaked > 0) { + _updateUnclaimedRewardsForStaker(_stakeMsgSender()); } else { - stakersArray.push(msg.sender); - stakers[msg.sender].timeOfLastUpdate = block.timestamp; + stakersArray.push(_stakeMsgSender()); + stakers[_stakeMsgSender()].timeOfLastUpdate = block.timestamp; + stakers[_stakeMsgSender()].conditionIdOflastUpdate = nextConditionId - 1; } for (uint256 i = 0; i < len; ++i) { require( - IERC721(_nftCollection).ownerOf(_tokenIds[i]) == msg.sender && + IERC721(_nftCollection).ownerOf(_tokenIds[i]) == _stakeMsgSender() && (IERC721(_nftCollection).getApproved(_tokenIds[i]) == address(this) || - IERC721(_nftCollection).isApprovedForAll(msg.sender, address(this))), + IERC721(_nftCollection).isApprovedForAll(_stakeMsgSender(), address(this))), "Not owned or approved" ); - IERC721(_nftCollection).transferFrom(msg.sender, address(this), _tokenIds[i]); - stakerAddress[_tokenIds[i]] = msg.sender; + + isStaking = 2; + IERC721(_nftCollection).safeTransferFrom(_stakeMsgSender(), address(this), _tokenIds[i]); + isStaking = 1; + + stakerAddress[_tokenIds[i]] = _stakeMsgSender(); if (!isIndexed[_tokenIds[i]]) { isIndexed[_tokenIds[i]] = true; indexedTokens.push(_tokenIds[i]); } } - stakers[msg.sender].amountStaked += len; + stakers[_stakeMsgSender()].amountStaked += len; - emit TokensStaked(msg.sender, _tokenIds); + emit TokensStaked(_stakeMsgSender(), _tokenIds); } /// @dev Withdraw logic. Override to add custom logic. function _withdraw(uint256[] calldata _tokenIds) internal virtual { - uint256 _amountStaked = stakers[msg.sender].amountStaked; + uint256 _amountStaked = stakers[_stakeMsgSender()].amountStaked; uint256 len = _tokenIds.length; require(len != 0, "Withdrawing 0 tokens"); require(_amountStaked >= len, "Withdrawing more than staked"); address _nftCollection = nftCollection; - _updateUnclaimedRewardsForStaker(msg.sender); + _updateUnclaimedRewardsForStaker(_stakeMsgSender()); if (_amountStaked == len) { - for (uint256 i = 0; i < stakersArray.length; ++i) { - if (stakersArray[i] == msg.sender) { - stakersArray[i] = stakersArray[stakersArray.length - 1]; + 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[msg.sender].amountStaked -= len; + stakers[_stakeMsgSender()].amountStaked -= len; for (uint256 i = 0; i < len; ++i) { - require(stakerAddress[_tokenIds[i]] == msg.sender, "Not staker"); + require(stakerAddress[_tokenIds[i]] == _stakeMsgSender(), "Not staker"); stakerAddress[_tokenIds[i]] = address(0); - IERC721(_nftCollection).transferFrom(address(this), msg.sender, _tokenIds[i]); + IERC721(_nftCollection).safeTransferFrom(address(this), _stakeMsgSender(), _tokenIds[i]); } - emit TokensWithdrawn(msg.sender, _tokenIds); + emit TokensWithdrawn(_stakeMsgSender(), _tokenIds); } /// @dev Logic for claiming rewards. Override to add custom logic. function _claimRewards() internal virtual { - uint256 rewards = stakers[msg.sender].unclaimedRewards + _calculateRewards(msg.sender); + uint256 rewards = stakers[_stakeMsgSender()].unclaimedRewards + _calculateRewards(_stakeMsgSender()); require(rewards != 0, "No rewards"); - stakers[msg.sender].timeOfLastUpdate = block.timestamp; - stakers[msg.sender].unclaimedRewards = 0; + stakers[_stakeMsgSender()].timeOfLastUpdate = block.timestamp; + stakers[_stakeMsgSender()].unclaimedRewards = 0; + stakers[_stakeMsgSender()].conditionIdOflastUpdate = nextConditionId - 1; - _mintRewards(msg.sender, rewards); + _mintRewards(_stakeMsgSender(), rewards); - emit RewardsClaimed(msg.sender, rewards); + emit RewardsClaimed(_stakeMsgSender(), rewards); } /// @dev View available rewards for a user. @@ -251,43 +271,74 @@ abstract contract Staking721Upgradeable is ReentrancyGuardUpgradeable, IStaking7 } } - /// @dev Update unclaimed rewards for all users. Called when setting timeUnit or rewardsPerUnitTime. - function _updateUnclaimedRewardsForAll() internal virtual { - address[] memory _stakers = stakersArray; - uint256 len = _stakers.length; - for (uint256 i = 0; i < len; ++i) { - address user = _stakers[i]; - - uint256 rewards = _calculateRewards(user); - stakers[user].unclaimedRewards += rewards; - stakers[user].timeOfLastUpdate = block.timestamp; - } - } - /// @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 time unit in seconds. - function _setTimeUnit(uint256 _timeUnit) internal virtual { - timeUnit = _timeUnit; - } - - /// @dev Set rewards per unit time. - function _setRewardsPerUnitTime(uint256 _rewardsPerUnitTime) internal virtual { - rewardsPerUnitTime = _rewardsPerUnitTime; + /// @dev Set staking conditions. + function _setStakingCondition(uint256 _timeUnit, uint256 _rewardsPerUnitTime) internal virtual { + require(_timeUnit != 0, "time-unit can't be 0"); + uint256 conditionId = nextConditionId; + nextConditionId += 1; + + stakingConditions[conditionId] = StakingCondition({ + timeUnit: _timeUnit, + rewardsPerUnitTime: _rewardsPerUnitTime, + startTimestamp: block.timestamp, + endTimestamp: 0 + }); + + if (conditionId > 0) { + stakingConditions[conditionId - 1].endTimestamp = block.timestamp; + } } - /// @dev Reward calculation logic. Override to implement custom logic. + /// @dev Calculate rewards for a staker. function _calculateRewards(address _staker) internal view virtual returns (uint256 _rewards) { Staker memory staker = stakers[_staker]; - _rewards = ((((block.timestamp - staker.timeOfLastUpdate) * staker.amountStaked) * rewardsPerUnitTime) / - timeUnit); + + 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.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 + //////////////////////////////////////////////////////////////*/ + + /** + * @notice View total rewards available in the staking contract. + * + */ + function getRewardTokenBalance() external view virtual returns (uint256 _rewardsAvailableInContract); + /** * @dev Mint/Transfer ERC20 rewards to the staker. Must override. * diff --git a/contracts/extension/interface/IStaking1155.sol b/contracts/extension/interface/IStaking1155.sol index eadb26ba8..2c30693bc 100644 --- a/contracts/extension/interface/IStaking1155.sol +++ b/contracts/extension/interface/IStaking1155.sol @@ -30,16 +30,37 @@ interface IStaking1155 { /** * @notice Staker Info. * - * @param amountStaked Total number of tokens staked by the staker. + * @param amountStaked Total number of tokens staked by the staker. * - * @param timeOfLastUpdate Last reward-update timestamp. + * @param timeOfLastUpdate Last reward-update timestamp. * - * @param unclaimedRewards Rewards accumulated but not claimed by user yet. + * @param unclaimedRewards Rewards accumulated but not claimed by user yet. + * + * @param conditionIdOflastUpdate Condition-Id when rewards were last updated for user. */ struct Staker { uint256 amountStaked; uint256 timeOfLastUpdate; uint256 unclaimedRewards; + uint256 conditionIdOflastUpdate; + } + + /** + * @notice Staking Condition. + * + * @param timeUnit Unit of time specified in number of seconds. Can be set as 1 seconds, 1 days, 1 hours, etc. + * + * @param rewardsPerUnitTime Rewards accumulated per unit of time. + * + * @param startTimestamp Condition start timestamp. + * + * @param endTimestamp Condition end timestamp. + */ + struct StakingCondition { + uint256 timeUnit; + uint256 rewardsPerUnitTime; + uint256 startTimestamp; + uint256 endTimestamp; } /** diff --git a/contracts/extension/interface/IStaking20.sol b/contracts/extension/interface/IStaking20.sol index ee6a4ff8e..88c985138 100644 --- a/contracts/extension/interface/IStaking20.sol +++ b/contracts/extension/interface/IStaking20.sol @@ -28,16 +28,42 @@ interface IStaking20 { /** * @notice Staker Info. * - * @param amountStaked Total number of tokens staked by the staker. + * @param amountStaked Total number of tokens staked by the staker. * - * @param timeOfLastUpdate Last reward-update timestamp. + * @param timeOfLastUpdate Last reward-update timestamp. * - * @param unclaimedRewards Rewards accumulated but not claimed by user yet. + * @param unclaimedRewards Rewards accumulated but not claimed by user yet. + * + * @param conditionIdOflastUpdate Condition-Id when rewards were last updated for user. */ struct Staker { uint256 amountStaked; uint256 timeOfLastUpdate; uint256 unclaimedRewards; + uint256 conditionIdOflastUpdate; + } + + /** + * @notice Staking Condition. + * + * @param timeUnit Unit of time specified in number of seconds. Can be set as 1 seconds, 1 days, 1 hours, etc. + * + * @param rewardRatioNumerator Rewards ratio is the number of reward tokens for a number of staked tokens, + * per unit of time. + * + * @param rewardRatioDenominator Rewards ratio is the number of reward tokens for a number of staked tokens, + * per unit of time. + * + * @param startTimestamp Condition start timestamp. + * + * @param endTimestamp Condition end timestamp. + */ + struct StakingCondition { + uint256 timeUnit; + uint256 rewardRatioNumerator; + uint256 rewardRatioDenominator; + uint256 startTimestamp; + uint256 endTimestamp; } /** diff --git a/contracts/extension/interface/IStaking721.sol b/contracts/extension/interface/IStaking721.sol index 6b3de28ec..f0d702c65 100644 --- a/contracts/extension/interface/IStaking721.sol +++ b/contracts/extension/interface/IStaking721.sol @@ -20,16 +20,37 @@ interface IStaking721 { /** * @notice Staker Info. * - * @param amountStaked Total number of tokens staked by the staker. + * @param amountStaked Total number of tokens staked by the staker. * - * @param timeOfLastUpdate Last reward-update timestamp. + * @param timeOfLastUpdate Last reward-update timestamp. * - * @param unclaimedRewards Rewards accumulated but not claimed by user yet. + * @param unclaimedRewards Rewards accumulated but not claimed by user yet. + * + * @param conditionIdOflastUpdate Condition-Id when rewards were last updated for user. */ struct Staker { uint256 amountStaked; uint256 timeOfLastUpdate; uint256 unclaimedRewards; + uint256 conditionIdOflastUpdate; + } + + /** + * @notice Staking Condition. + * + * @param timeUnit Unit of time specified in number of seconds. Can be set as 1 seconds, 1 days, 1 hours, etc. + * + * @param rewardsPerUnitTime Rewards accumulated per unit of time. + * + * @param startTimestamp Condition start timestamp. + * + * @param endTimestamp Condition end timestamp. + */ + struct StakingCondition { + uint256 timeUnit; + uint256 rewardsPerUnitTime; + uint256 startTimestamp; + uint256 endTimestamp; } /** diff --git a/contracts/openzeppelin-presets/utils/math/SafeMath.sol b/contracts/openzeppelin-presets/utils/math/SafeMath.sol new file mode 100644 index 000000000..550f0e779 --- /dev/null +++ b/contracts/openzeppelin-presets/utils/math/SafeMath.sol @@ -0,0 +1,227 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v4.6.0) (utils/math/SafeMath.sol) + +pragma solidity ^0.8.0; + +// CAUTION +// This version of SafeMath should only be used with Solidity 0.8 or later, +// because it relies on the compiler's built in overflow checks. + +/** + * @dev Wrappers over Solidity's arithmetic operations. + * + * NOTE: `SafeMath` is generally not needed starting with Solidity 0.8, since the compiler + * now has built in overflow checking. + */ +library SafeMath { + /** + * @dev Returns the addition of two unsigned integers, with an overflow flag. + * + * _Available since v3.4._ + */ + function tryAdd(uint256 a, uint256 b) internal pure returns (bool, uint256) { + unchecked { + uint256 c = a + b; + if (c < a) return (false, 0); + return (true, c); + } + } + + /** + * @dev Returns the subtraction of two unsigned integers, with an overflow flag. + * + * _Available since v3.4._ + */ + function trySub(uint256 a, uint256 b) internal pure returns (bool, uint256) { + unchecked { + if (b > a) return (false, 0); + return (true, a - b); + } + } + + /** + * @dev Returns the multiplication of two unsigned integers, with an overflow flag. + * + * _Available since v3.4._ + */ + function tryMul(uint256 a, uint256 b) internal pure returns (bool, uint256) { + unchecked { + // Gas optimization: this is cheaper than requiring 'a' not being zero, but the + // benefit is lost if 'b' is also tested. + // See: https://github.com/OpenZeppelin/openzeppelin-contracts/pull/522 + if (a == 0) return (true, 0); + uint256 c = a * b; + if (c / a != b) return (false, 0); + return (true, c); + } + } + + /** + * @dev Returns the division of two unsigned integers, with a division by zero flag. + * + * _Available since v3.4._ + */ + function tryDiv(uint256 a, uint256 b) internal pure returns (bool, uint256) { + unchecked { + if (b == 0) return (false, 0); + return (true, a / b); + } + } + + /** + * @dev Returns the remainder of dividing two unsigned integers, with a division by zero flag. + * + * _Available since v3.4._ + */ + function tryMod(uint256 a, uint256 b) internal pure returns (bool, uint256) { + unchecked { + if (b == 0) return (false, 0); + return (true, a % b); + } + } + + /** + * @dev Returns the addition of two unsigned integers, reverting on + * overflow. + * + * Counterpart to Solidity's `+` operator. + * + * Requirements: + * + * - Addition cannot overflow. + */ + function add(uint256 a, uint256 b) internal pure returns (uint256) { + return a + b; + } + + /** + * @dev Returns the subtraction of two unsigned integers, reverting on + * overflow (when the result is negative). + * + * Counterpart to Solidity's `-` operator. + * + * Requirements: + * + * - Subtraction cannot overflow. + */ + function sub(uint256 a, uint256 b) internal pure returns (uint256) { + return a - b; + } + + /** + * @dev Returns the multiplication of two unsigned integers, reverting on + * overflow. + * + * Counterpart to Solidity's `*` operator. + * + * Requirements: + * + * - Multiplication cannot overflow. + */ + function mul(uint256 a, uint256 b) internal pure returns (uint256) { + return a * b; + } + + /** + * @dev Returns the integer division of two unsigned integers, reverting on + * division by zero. The result is rounded towards zero. + * + * Counterpart to Solidity's `/` operator. + * + * Requirements: + * + * - The divisor cannot be zero. + */ + function div(uint256 a, uint256 b) internal pure returns (uint256) { + return a / b; + } + + /** + * @dev Returns the remainder of dividing two unsigned integers. (unsigned integer modulo), + * reverting when dividing by zero. + * + * Counterpart to Solidity's `%` operator. This function uses a `revert` + * opcode (which leaves remaining gas untouched) while Solidity uses an + * invalid opcode to revert (consuming all remaining gas). + * + * Requirements: + * + * - The divisor cannot be zero. + */ + function mod(uint256 a, uint256 b) internal pure returns (uint256) { + return a % b; + } + + /** + * @dev Returns the subtraction of two unsigned integers, reverting with custom message on + * overflow (when the result is negative). + * + * CAUTION: This function is deprecated because it requires allocating memory for the error + * message unnecessarily. For custom revert reasons use {trySub}. + * + * Counterpart to Solidity's `-` operator. + * + * Requirements: + * + * - Subtraction cannot overflow. + */ + function sub( + uint256 a, + uint256 b, + string memory errorMessage + ) internal pure returns (uint256) { + unchecked { + require(b <= a, errorMessage); + return a - b; + } + } + + /** + * @dev Returns the integer division of two unsigned integers, reverting with custom message on + * division by zero. The result is rounded towards zero. + * + * Counterpart to Solidity's `/` operator. Note: this function uses a + * `revert` opcode (which leaves remaining gas untouched) while Solidity + * uses an invalid opcode to revert (consuming all remaining gas). + * + * Requirements: + * + * - The divisor cannot be zero. + */ + function div( + uint256 a, + uint256 b, + string memory errorMessage + ) internal pure returns (uint256) { + unchecked { + require(b > 0, errorMessage); + return a / b; + } + } + + /** + * @dev Returns the remainder of dividing two unsigned integers. (unsigned integer modulo), + * reverting with custom message when dividing by zero. + * + * CAUTION: This function is deprecated because it requires allocating memory for the error + * message unnecessarily. For custom revert reasons use {tryMod}. + * + * Counterpart to Solidity's `%` operator. This function uses a `revert` + * opcode (which leaves remaining gas untouched) while Solidity uses an + * invalid opcode to revert (consuming all remaining gas). + * + * Requirements: + * + * - The divisor cannot be zero. + */ + function mod( + uint256 a, + uint256 b, + string memory errorMessage + ) internal pure returns (uint256) { + unchecked { + require(b > 0, errorMessage); + return a % b; + } + } +} diff --git a/contracts/package.json b/contracts/package.json index dfc1a4138..0c8ea7817 100644 --- a/contracts/package.json +++ b/contracts/package.json @@ -1,7 +1,7 @@ { "name": "@thirdweb-dev/contracts", "description": "Collection of smart contracts deployable via the thirdweb SDK, dashboard and CLI", - "version": "3.2.8", + "version": "3.2.9", "license": "Apache-2.0", "repository": { "type": "git", diff --git a/contracts/staking/EditionStake.sol b/contracts/staking/EditionStake.sol index c807c38b1..f42389fbf 100644 --- a/contracts/staking/EditionStake.sol +++ b/contracts/staking/EditionStake.sol @@ -3,6 +3,7 @@ pragma solidity ^0.8.11; // Token import "@openzeppelin/contracts/token/ERC1155/IERC1155.sol"; +import "@openzeppelin/contracts-upgradeable/utils/introspection/ERC165Upgradeable.sol"; import "@openzeppelin/contracts-upgradeable/token/ERC1155/IERC1155ReceiverUpgradeable.sol"; // Meta transactions @@ -24,12 +25,16 @@ contract EditionStake is PermissionsEnumerable, ERC2771ContextUpgradeable, MulticallUpgradeable, - IERC1155ReceiverUpgradeable, - Staking1155Upgradeable + Staking1155Upgradeable, + ERC165Upgradeable, + IERC1155ReceiverUpgradeable { bytes32 private constant MODULE_TYPE = bytes32("EditionStake"); uint256 private constant VERSION = 1; + /// @dev Emitted when contract admin withdraws reward tokens. + event RewardTokensWithdrawnByAdmin(uint256 _amount); + /// @dev ERC20 Reward Token address. See {_mintRewards} below. address public rewardToken; @@ -45,13 +50,11 @@ contract EditionStake is uint256 _defaultTimeUnit, uint256 _defaultRewardsPerUnitTime ) external initializer { - __ReentrancyGuard_init(); __ERC2771Context_init_unchained(_trustedForwarders); rewardToken = _rewardToken; __Staking1155_init(_edition); - _setDefaultTimeUnit(_defaultTimeUnit); - _setDefaultRewardsPerUnitTime(_defaultRewardsPerUnitTime); + _setDefaultStakingCondition(_defaultTimeUnit, _defaultRewardsPerUnitTime); _setupContractURI(_contractURI); _setupRole(DEFAULT_ADMIN_ROLE, _defaultAdmin); @@ -72,6 +75,13 @@ contract EditionStake is require(hasRole(DEFAULT_ADMIN_ROLE, _msgSender()), "Not authorized"); CurrencyTransferLib.transferCurrency(rewardToken, address(this), _msgSender(), _amount); + + emit RewardTokensWithdrawnByAdmin(_amount); + } + + /// @notice View total rewards available in the staking contract. + function getRewardTokenBalance() external view override returns (uint256 _rewardsAvailableInContract) { + return IERC20(rewardToken).balanceOf(address(this)); } /*/////////////////////////////////////////////////////////////// @@ -84,7 +94,8 @@ contract EditionStake is uint256, uint256, bytes calldata - ) external pure returns (bytes4) { + ) external returns (bytes4) { + require(isStaking == 2, "Direct transfer"); return this.onERC1155Received.selector; } @@ -96,8 +107,13 @@ contract EditionStake is bytes calldata data ) external returns (bytes4) {} - function supportsInterface(bytes4 interfaceId) public view virtual returns (bool) { - return interfaceId == type(IERC1155ReceiverUpgradeable).interfaceId; + function supportsInterface(bytes4 interfaceId) + public + view + override(ERC165Upgradeable, IERC165Upgradeable) + returns (bool) + { + return interfaceId == type(IERC1155ReceiverUpgradeable).interfaceId || super.supportsInterface(interfaceId); } /*/////////////////////////////////////////////////////////////// @@ -127,6 +143,10 @@ contract EditionStake is Miscellaneous //////////////////////////////////////////////////////////////*/ + function _stakeMsgSender() internal view virtual override returns (address) { + return _msgSender(); + } + function _msgSender() internal view virtual override returns (address sender) { return ERC2771ContextUpgradeable._msgSender(); } diff --git a/contracts/staking/NFTStake.sol b/contracts/staking/NFTStake.sol index 5c18e3fc2..4e11e3c8e 100644 --- a/contracts/staking/NFTStake.sol +++ b/contracts/staking/NFTStake.sol @@ -3,6 +3,7 @@ pragma solidity ^0.8.11; // Token import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; +import "@openzeppelin/contracts-upgradeable/utils/introspection/ERC165Upgradeable.sol"; import "@openzeppelin/contracts-upgradeable/token/ERC721/IERC721ReceiverUpgradeable.sol"; // Meta transactions @@ -24,12 +25,16 @@ contract NFTStake is PermissionsEnumerable, ERC2771ContextUpgradeable, MulticallUpgradeable, - IERC721ReceiverUpgradeable, - Staking721Upgradeable + Staking721Upgradeable, + ERC165Upgradeable, + IERC721ReceiverUpgradeable { bytes32 private constant MODULE_TYPE = bytes32("NFTStake"); uint256 private constant VERSION = 1; + /// @dev Emitted when contract admin withdraws reward tokens. + event RewardTokensWithdrawnByAdmin(uint256 _amount); + /// @dev ERC20 Reward Token address. See {_mintRewards} below. address public rewardToken; @@ -45,13 +50,11 @@ contract NFTStake is uint256 _timeUnit, uint256 _rewardsPerUnitTime ) external initializer { - __ReentrancyGuard_init(); __ERC2771Context_init_unchained(_trustedForwarders); rewardToken = _rewardToken; __Staking721_init(_nftCollection); - _setTimeUnit(_timeUnit); - _setRewardsPerUnitTime(_rewardsPerUnitTime); + _setStakingCondition(_timeUnit, _rewardsPerUnitTime); _setupContractURI(_contractURI); _setupRole(DEFAULT_ADMIN_ROLE, _defaultAdmin); @@ -72,6 +75,13 @@ contract NFTStake is require(hasRole(DEFAULT_ADMIN_ROLE, _msgSender()), "Not authorized"); CurrencyTransferLib.transferCurrency(rewardToken, address(this), _msgSender(), _amount); + + emit RewardTokensWithdrawnByAdmin(_amount); + } + + /// @notice View total rewards available in the staking contract. + function getRewardTokenBalance() external view override returns (uint256 _rewardsAvailableInContract) { + return IERC20(rewardToken).balanceOf(address(this)); } /*/////////////////////////////////////////////////////////////// @@ -83,12 +93,13 @@ contract NFTStake is address, uint256, bytes calldata - ) external pure override returns (bytes4) { + ) external view override returns (bytes4) { + require(isStaking == 2, "Direct transfer"); return this.onERC721Received.selector; } - function supportsInterface(bytes4 interfaceId) public view virtual returns (bool) { - return interfaceId == type(IERC721ReceiverUpgradeable).interfaceId; + function supportsInterface(bytes4 interfaceId) public view override returns (bool) { + return interfaceId == type(IERC721ReceiverUpgradeable).interfaceId || super.supportsInterface(interfaceId); } /*/////////////////////////////////////////////////////////////// @@ -118,6 +129,10 @@ contract NFTStake is Miscellaneous //////////////////////////////////////////////////////////////*/ + function _stakeMsgSender() internal view virtual override returns (address) { + return _msgSender(); + } + function _msgSender() internal view virtual override returns (address sender) { return ERC2771ContextUpgradeable._msgSender(); } diff --git a/contracts/staking/TokenStake.sol b/contracts/staking/TokenStake.sol index 49fd07a11..e23aa4c98 100644 --- a/contracts/staking/TokenStake.sol +++ b/contracts/staking/TokenStake.sol @@ -10,6 +10,7 @@ import "../openzeppelin-presets/metatx/ERC2771ContextUpgradeable.sol"; // Utils import "@openzeppelin/contracts-upgradeable/utils/MulticallUpgradeable.sol"; import { CurrencyTransferLib } from "../lib/CurrencyTransferLib.sol"; +import "../eip/interface/IERC20Metadata.sol"; // ========== Features ========== @@ -28,6 +29,9 @@ contract TokenStake is bytes32 private constant MODULE_TYPE = bytes32("TokenStake"); uint256 private constant VERSION = 1; + /// @dev Emitted when contract admin withdraws reward tokens. + event RewardTokensWithdrawnByAdmin(uint256 _amount); + /// @dev ERC20 Reward Token address. See {_mintRewards} below. address public rewardToken; @@ -44,14 +48,16 @@ contract TokenStake is uint256 _rewardRatioNumerator, uint256 _rewardRatioDenominator ) external initializer { - __ReentrancyGuard_init(); __ERC2771Context_init_unchained(_trustedForwarders); require(_rewardToken != _stakingToken, "Reward Token and Staking Token can't be same."); rewardToken = _rewardToken; - __Staking20_init(_stakingToken); - _setTimeUnit(_timeUnit); - _setRewardRatio(_rewardRatioNumerator, _rewardRatioDenominator); + __Staking20_init( + _stakingToken, + IERC20Metadata(_stakingToken).decimals(), + IERC20Metadata(_rewardToken).decimals() + ); + _setStakingCondition(_timeUnit, _rewardRatioNumerator, _rewardRatioDenominator); _setupContractURI(_contractURI); _setupRole(DEFAULT_ADMIN_ROLE, _defaultAdmin); @@ -68,10 +74,20 @@ contract TokenStake is } /// @dev Admin can withdraw excess reward tokens. - function withdrawRewardTokens(uint256 _amount) external { + function withdrawRewardTokens(uint256 _amount) external nonReentrant { require(hasRole(DEFAULT_ADMIN_ROLE, _msgSender()), "Not authorized"); CurrencyTransferLib.transferCurrency(rewardToken, address(this), _msgSender(), _amount); + + // The withdrawal shouldn't reduce staking token balance. `>=` accounts for any accidental transfers. + require(IERC20(token).balanceOf(address(this)) >= stakingTokenBalance, "Staking token balance reduced."); + + emit RewardTokensWithdrawnByAdmin(_amount); + } + + /// @notice View total rewards available in the staking contract. + function getRewardTokenBalance() external view override returns (uint256 _rewardsAvailableInContract) { + return IERC20(rewardToken).balanceOf(address(this)); } /*/////////////////////////////////////////////////////////////// @@ -101,6 +117,10 @@ contract TokenStake is Miscellaneous //////////////////////////////////////////////////////////////*/ + function _stakeMsgSender() internal view virtual override returns (address) { + return _msgSender(); + } + function _msgSender() internal view virtual override returns (address sender) { return ERC2771ContextUpgradeable._msgSender(); } diff --git a/docs/EditionStake.md b/docs/EditionStake.md index 5c28091e8..b4a5da99b 100644 --- a/docs/EditionStake.md +++ b/docs/EditionStake.md @@ -94,10 +94,10 @@ function contractVersion() external pure returns (uint8) |---|---|---| | _0 | uint8 | undefined | -### defaultRewardsPerUnitTime +### edition ```solidity -function defaultRewardsPerUnitTime() external view returns (uint256) +function edition() external view returns (address) ``` @@ -109,12 +109,12 @@ function defaultRewardsPerUnitTime() external view returns (uint256) | Name | Type | Description | |---|---|---| -| _0 | uint256 | undefined | +| _0 | address | undefined | -### defaultTimeUnit +### getDefaultRewardsPerUnitTime ```solidity -function defaultTimeUnit() external view returns (uint256) +function getDefaultRewardsPerUnitTime() external view returns (uint256 _rewardsPerUnitTime) ``` @@ -126,12 +126,12 @@ function defaultTimeUnit() external view returns (uint256) | Name | Type | Description | |---|---|---| -| _0 | uint256 | undefined | +| _rewardsPerUnitTime | uint256 | undefined | -### edition +### getDefaultTimeUnit ```solidity -function edition() external view returns (address) +function getDefaultTimeUnit() external view returns (uint256 _timeUnit) ``` @@ -143,7 +143,46 @@ function edition() external view returns (address) | Name | Type | Description | |---|---|---| -| _0 | address | undefined | +| _timeUnit | uint256 | undefined | + +### getRewardTokenBalance + +```solidity +function getRewardTokenBalance() external view returns (uint256 _rewardsAvailableInContract) +``` + +View total rewards available in the staking contract. + + + + +#### Returns + +| Name | Type | Description | +|---|---|---| +| _rewardsAvailableInContract | uint256 | undefined | + +### getRewardsPerUnitTime + +```solidity +function getRewardsPerUnitTime(uint256 _tokenId) external view returns (uint256 _rewardsPerUnitTime) +``` + + + + + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| _tokenId | uint256 | undefined | + +#### Returns + +| Name | Type | Description | +|---|---|---| +| _rewardsPerUnitTime | uint256 | undefined | ### getRoleAdmin @@ -260,6 +299,28 @@ View amount staked and rewards for a user, for a given token-id. | _tokensStaked | uint256 | Amount of tokens staked for given token-id. | | _rewards | uint256 | Available reward amount. | +### getTimeUnit + +```solidity +function getTimeUnit(uint256 _tokenId) external view returns (uint256 _timeUnit) +``` + + + + + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| _tokenId | uint256 | undefined | + +#### Returns + +| Name | Type | Description | +|---|---|---| +| _timeUnit | uint256 | undefined | + ### grantRole ```solidity @@ -462,7 +523,7 @@ function onERC1155BatchReceived(address operator, address from, uint256[] ids, u ### onERC1155Received ```solidity -function onERC1155Received(address, address, uint256, uint256, bytes) external pure returns (bytes4) +function onERC1155Received(address, address, uint256, uint256, bytes) external nonpayable returns (bytes4) ``` @@ -536,28 +597,6 @@ function rewardToken() external view returns (address) |---|---|---| | _0 | address | undefined | -### rewardsPerUnitTime - -```solidity -function rewardsPerUnitTime(uint256) external view returns (uint256) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined | - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined | - ### setContractURI ```solidity @@ -660,7 +699,7 @@ Stake ERC721 Tokens. ### stakers ```solidity -function stakers(uint256, address) external view returns (uint256 amountStaked, uint256 timeOfLastUpdate, uint256 unclaimedRewards) +function stakers(uint256, address) external view returns (uint256 amountStaked, uint256 timeOfLastUpdate, uint256 unclaimedRewards, uint256 conditionIdOflastUpdate) ``` @@ -681,6 +720,7 @@ function stakers(uint256, address) external view returns (uint256 amountStaked, | amountStaked | uint256 | undefined | | timeOfLastUpdate | uint256 | undefined | | unclaimedRewards | uint256 | undefined | +| conditionIdOflastUpdate | uint256 | undefined | ### stakersArray @@ -713,7 +753,7 @@ function supportsInterface(bytes4 interfaceId) external view returns (bool) -*Returns true if this contract implements the interface defined by `interfaceId`. See the corresponding https://eips.ethereum.org/EIPS/eip-165#how-interfaces-are-identified[EIP section] to learn more about how these ids are created. This function call must use less than 30 000 gas.* + #### Parameters @@ -727,28 +767,6 @@ function supportsInterface(bytes4 interfaceId) external view returns (bool) |---|---|---| | _0 | bool | undefined | -### timeUnit - -```solidity -function timeUnit(uint256) external view returns (uint256) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined | - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined | - ### withdraw ```solidity @@ -819,6 +837,22 @@ event Initialized(uint8 version) |---|---|---| | version | uint8 | undefined | +### RewardTokensWithdrawnByAdmin + +```solidity +event RewardTokensWithdrawnByAdmin(uint256 _amount) +``` + + + +*Emitted when contract admin withdraws reward tokens.* + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| _amount | uint256 | undefined | + ### RewardsClaimed ```solidity diff --git a/docs/NFTStake.md b/docs/NFTStake.md index 8af027892..03e224db8 100644 --- a/docs/NFTStake.md +++ b/docs/NFTStake.md @@ -89,6 +89,40 @@ function contractVersion() external pure returns (uint8) |---|---|---| | _0 | uint8 | undefined | +### getRewardTokenBalance + +```solidity +function getRewardTokenBalance() external view returns (uint256 _rewardsAvailableInContract) +``` + +View total rewards available in the staking contract. + + + + +#### Returns + +| Name | Type | Description | +|---|---|---| +| _rewardsAvailableInContract | uint256 | undefined | + +### getRewardsPerUnitTime + +```solidity +function getRewardsPerUnitTime() external view returns (uint256 _rewardsPerUnitTime) +``` + + + + + + +#### Returns + +| Name | Type | Description | +|---|---|---| +| _rewardsPerUnitTime | uint256 | undefined | + ### getRoleAdmin ```solidity @@ -179,6 +213,23 @@ View amount staked and total rewards for a user. | _tokensStaked | uint256[] | List of token-ids staked by staker. | | _rewards | uint256 | Available reward amount. | +### getTimeUnit + +```solidity +function getTimeUnit() external view returns (uint256 _timeUnit) +``` + + + + + + +#### Returns + +| Name | Type | Description | +|---|---|---| +| _timeUnit | uint256 | undefined | + ### grantRole ```solidity @@ -372,7 +423,7 @@ function nftCollection() external view returns (address) ### onERC721Received ```solidity -function onERC721Received(address, address, uint256, bytes) external pure returns (bytes4) +function onERC721Received(address, address, uint256, bytes) external view returns (bytes4) ``` @@ -445,23 +496,6 @@ function rewardToken() external view returns (address) |---|---|---| | _0 | address | undefined | -### rewardsPerUnitTime - -```solidity -function rewardsPerUnitTime() external view returns (uint256) -``` - - - - - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined | - ### setContractURI ```solidity @@ -551,7 +585,7 @@ function stakerAddress(uint256) external view returns (address) ### stakers ```solidity -function stakers(address) external view returns (uint256 amountStaked, uint256 timeOfLastUpdate, uint256 unclaimedRewards) +function stakers(address) external view returns (uint256 amountStaked, uint256 timeOfLastUpdate, uint256 unclaimedRewards, uint256 conditionIdOflastUpdate) ``` @@ -571,6 +605,7 @@ function stakers(address) external view returns (uint256 amountStaked, uint256 t | amountStaked | uint256 | undefined | | timeOfLastUpdate | uint256 | undefined | | unclaimedRewards | uint256 | undefined | +| conditionIdOflastUpdate | uint256 | undefined | ### stakersArray @@ -602,7 +637,7 @@ function supportsInterface(bytes4 interfaceId) external view returns (bool) - +*See {IERC165-supportsInterface}.* #### Parameters @@ -616,23 +651,6 @@ function supportsInterface(bytes4 interfaceId) external view returns (bool) |---|---|---| | _0 | bool | undefined | -### timeUnit - -```solidity -function timeUnit() external view returns (uint256) -``` - - - - - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined | - ### withdraw ```solidity @@ -702,6 +720,22 @@ event Initialized(uint8 version) |---|---|---| | version | uint8 | undefined | +### RewardTokensWithdrawnByAdmin + +```solidity +event RewardTokensWithdrawnByAdmin(uint256 _amount) +``` + + + +*Emitted when contract admin withdraws reward tokens.* + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| _amount | uint256 | undefined | + ### RewardsClaimed ```solidity diff --git a/docs/SafeMath.md b/docs/SafeMath.md new file mode 100644 index 000000000..7bf9fd7a5 --- /dev/null +++ b/docs/SafeMath.md @@ -0,0 +1,12 @@ +# SafeMath + + + + + + + +*Wrappers over Solidity's arithmetic operations. NOTE: `SafeMath` is generally not needed starting with Solidity 0.8, since the compiler now has built in overflow checking.* + + + diff --git a/docs/Staking1155.md b/docs/Staking1155.md index 0f08572bc..8a1ce0574 100644 --- a/docs/Staking1155.md +++ b/docs/Staking1155.md @@ -26,56 +26,95 @@ Claim accumulated rewards. |---|---|---| | _tokenId | uint256 | Staked token Id. | -### defaultRewardsPerUnitTime +### edition ```solidity -function defaultRewardsPerUnitTime() external view returns (uint256) +function edition() external view returns (address) ``` -*Default rewards accumulated per unit of time.* +*Address of ERC1155 contract -- staked tokens belong to this contract.* #### Returns | Name | Type | Description | |---|---|---| -| _0 | uint256 | undefined | +| _0 | address | undefined | -### defaultTimeUnit +### getDefaultRewardsPerUnitTime ```solidity -function defaultTimeUnit() external view returns (uint256) +function getDefaultRewardsPerUnitTime() external view returns (uint256 _rewardsPerUnitTime) ``` -*Default unit of time specified in number of seconds. Can be set as 1 seconds, 1 days, 1 hours, etc.* + #### Returns | Name | Type | Description | |---|---|---| -| _0 | uint256 | undefined | +| _rewardsPerUnitTime | uint256 | undefined | -### edition +### getDefaultTimeUnit ```solidity -function edition() external view returns (address) +function getDefaultTimeUnit() external view returns (uint256 _timeUnit) ``` -*Address of ERC1155 contract -- staked tokens belong to this contract.* + #### Returns | Name | Type | Description | |---|---|---| -| _0 | address | undefined | +| _timeUnit | uint256 | undefined | + +### getRewardTokenBalance + +```solidity +function getRewardTokenBalance() external view returns (uint256 _rewardsAvailableInContract) +``` + +View total rewards available in the staking contract. + + + + +#### Returns + +| Name | Type | Description | +|---|---|---| +| _rewardsAvailableInContract | uint256 | undefined | + +### getRewardsPerUnitTime + +```solidity +function getRewardsPerUnitTime(uint256 _tokenId) external view returns (uint256 _rewardsPerUnitTime) +``` + + + + + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| _tokenId | uint256 | undefined | + +#### Returns + +| Name | Type | Description | +|---|---|---| +| _rewardsPerUnitTime | uint256 | undefined | ### getStakeInfo @@ -125,37 +164,37 @@ View amount staked and rewards for a user, for a given token-id. | _tokensStaked | uint256 | Amount of tokens staked for given token-id. | | _rewards | uint256 | Available reward amount. | -### indexedTokens +### getTimeUnit ```solidity -function indexedTokens(uint256) external view returns (uint256) +function getTimeUnit(uint256 _tokenId) external view returns (uint256 _timeUnit) ``` -*List of token-ids ever staked.* + #### Parameters | Name | Type | Description | |---|---|---| -| _0 | uint256 | undefined | +| _tokenId | uint256 | undefined | #### Returns | Name | Type | Description | |---|---|---| -| _0 | uint256 | undefined | +| _timeUnit | uint256 | undefined | -### isIndexed +### indexedTokens ```solidity -function isIndexed(uint256) external view returns (bool) +function indexedTokens(uint256) external view returns (uint256) ``` -*Mapping from token-id to whether it is indexed or not.* +*List of token-ids ever staked.* #### Parameters @@ -167,17 +206,17 @@ function isIndexed(uint256) external view returns (bool) | Name | Type | Description | |---|---|---| -| _0 | bool | undefined | +| _0 | uint256 | undefined | -### rewardsPerUnitTime +### isIndexed ```solidity -function rewardsPerUnitTime(uint256) external view returns (uint256) +function isIndexed(uint256) external view returns (bool) ``` -*Mapping from token-id to rewards accumulated per unit of time.* +*Mapping from token-id to whether it is indexed or not.* #### Parameters @@ -189,7 +228,7 @@ function rewardsPerUnitTime(uint256) external view returns (uint256) | Name | Type | Description | |---|---|---| -| _0 | uint256 | undefined | +| _0 | bool | undefined | ### setDefaultRewardsPerUnitTime @@ -277,7 +316,7 @@ Stake ERC721 Tokens. ### stakers ```solidity -function stakers(uint256, address) external view returns (uint256 amountStaked, uint256 timeOfLastUpdate, uint256 unclaimedRewards) +function stakers(uint256, address) external view returns (uint256 amountStaked, uint256 timeOfLastUpdate, uint256 unclaimedRewards, uint256 conditionIdOflastUpdate) ``` @@ -298,6 +337,7 @@ function stakers(uint256, address) external view returns (uint256 amountStaked, | amountStaked | uint256 | undefined | | timeOfLastUpdate | uint256 | undefined | | unclaimedRewards | uint256 | undefined | +| conditionIdOflastUpdate | uint256 | undefined | ### stakersArray @@ -322,28 +362,6 @@ function stakersArray(uint256, uint256) external view returns (address) |---|---|---| | _0 | address | undefined | -### timeUnit - -```solidity -function timeUnit(uint256) external view returns (uint256) -``` - - - -*Mapping from token-id to unit of time specified in number of seconds. Can be set as 1 seconds, 1 days, 1 hours, etc.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined | - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined | - ### withdraw ```solidity diff --git a/docs/Staking1155Base.md b/docs/Staking1155Base.md index a3a5a242a..ff6f77dea 100644 --- a/docs/Staking1155Base.md +++ b/docs/Staking1155Base.md @@ -43,10 +43,10 @@ Returns the contract metadata URI. |---|---|---| | _0 | string | undefined | -### defaultRewardsPerUnitTime +### edition ```solidity -function defaultRewardsPerUnitTime() external view returns (uint256) +function edition() external view returns (address) ``` @@ -58,12 +58,12 @@ function defaultRewardsPerUnitTime() external view returns (uint256) | Name | Type | Description | |---|---|---| -| _0 | uint256 | undefined | +| _0 | address | undefined | -### defaultTimeUnit +### getDefaultRewardsPerUnitTime ```solidity -function defaultTimeUnit() external view returns (uint256) +function getDefaultRewardsPerUnitTime() external view returns (uint256 _rewardsPerUnitTime) ``` @@ -75,12 +75,12 @@ function defaultTimeUnit() external view returns (uint256) | Name | Type | Description | |---|---|---| -| _0 | uint256 | undefined | +| _rewardsPerUnitTime | uint256 | undefined | -### edition +### getDefaultTimeUnit ```solidity -function edition() external view returns (address) +function getDefaultTimeUnit() external view returns (uint256 _timeUnit) ``` @@ -92,7 +92,46 @@ function edition() external view returns (address) | Name | Type | Description | |---|---|---| -| _0 | address | undefined | +| _timeUnit | uint256 | undefined | + +### getRewardTokenBalance + +```solidity +function getRewardTokenBalance() external view returns (uint256 _rewardsAvailableInContract) +``` + +View total rewards available in the staking contract. + + + + +#### Returns + +| Name | Type | Description | +|---|---|---| +| _rewardsAvailableInContract | uint256 | undefined | + +### getRewardsPerUnitTime + +```solidity +function getRewardsPerUnitTime(uint256 _tokenId) external view returns (uint256 _rewardsPerUnitTime) +``` + + + + + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| _tokenId | uint256 | undefined | + +#### Returns + +| Name | Type | Description | +|---|---|---| +| _rewardsPerUnitTime | uint256 | undefined | ### getStakeInfo @@ -142,6 +181,28 @@ View amount staked and rewards for a user, for a given token-id. | _tokensStaked | uint256 | Amount of tokens staked for given token-id. | | _rewards | uint256 | Available reward amount. | +### getTimeUnit + +```solidity +function getTimeUnit(uint256 _tokenId) external view returns (uint256 _timeUnit) +``` + + + + + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| _tokenId | uint256 | undefined | + +#### Returns + +| Name | Type | Description | +|---|---|---| +| _timeUnit | uint256 | undefined | + ### indexedTokens ```solidity @@ -242,28 +303,6 @@ function rewardToken() external view returns (address) |---|---|---| | _0 | address | undefined | -### rewardsPerUnitTime - -```solidity -function rewardsPerUnitTime(uint256) external view returns (uint256) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined | - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined | - ### setContractURI ```solidity @@ -382,7 +421,7 @@ Stake ERC721 Tokens. ### stakers ```solidity -function stakers(uint256, address) external view returns (uint256 amountStaked, uint256 timeOfLastUpdate, uint256 unclaimedRewards) +function stakers(uint256, address) external view returns (uint256 amountStaked, uint256 timeOfLastUpdate, uint256 unclaimedRewards, uint256 conditionIdOflastUpdate) ``` @@ -403,6 +442,7 @@ function stakers(uint256, address) external view returns (uint256 amountStaked, | amountStaked | uint256 | undefined | | timeOfLastUpdate | uint256 | undefined | | unclaimedRewards | uint256 | undefined | +| conditionIdOflastUpdate | uint256 | undefined | ### stakersArray @@ -427,28 +467,6 @@ function stakersArray(uint256, uint256) external view returns (address) |---|---|---| | _0 | address | undefined | -### timeUnit - -```solidity -function timeUnit(uint256) external view returns (uint256) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined | - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined | - ### withdraw ```solidity diff --git a/docs/Staking1155Upgradeable.md b/docs/Staking1155Upgradeable.md index 80e7a23a0..1d5d0542c 100644 --- a/docs/Staking1155Upgradeable.md +++ b/docs/Staking1155Upgradeable.md @@ -26,56 +26,95 @@ Claim accumulated rewards. |---|---|---| | _tokenId | uint256 | Staked token Id. | -### defaultRewardsPerUnitTime +### edition ```solidity -function defaultRewardsPerUnitTime() external view returns (uint256) +function edition() external view returns (address) ``` -*Default rewards accumulated per unit of time.* +*Address of ERC1155 contract -- staked tokens belong to this contract.* #### Returns | Name | Type | Description | |---|---|---| -| _0 | uint256 | undefined | +| _0 | address | undefined | -### defaultTimeUnit +### getDefaultRewardsPerUnitTime ```solidity -function defaultTimeUnit() external view returns (uint256) +function getDefaultRewardsPerUnitTime() external view returns (uint256 _rewardsPerUnitTime) ``` -*Default unit of time specified in number of seconds. Can be set as 1 seconds, 1 days, 1 hours, etc.* + #### Returns | Name | Type | Description | |---|---|---| -| _0 | uint256 | undefined | +| _rewardsPerUnitTime | uint256 | undefined | -### edition +### getDefaultTimeUnit ```solidity -function edition() external view returns (address) +function getDefaultTimeUnit() external view returns (uint256 _timeUnit) ``` -*Address of ERC1155 contract -- staked tokens belong to this contract.* + #### Returns | Name | Type | Description | |---|---|---| -| _0 | address | undefined | +| _timeUnit | uint256 | undefined | + +### getRewardTokenBalance + +```solidity +function getRewardTokenBalance() external view returns (uint256 _rewardsAvailableInContract) +``` + +View total rewards available in the staking contract. + + + + +#### Returns + +| Name | Type | Description | +|---|---|---| +| _rewardsAvailableInContract | uint256 | undefined | + +### getRewardsPerUnitTime + +```solidity +function getRewardsPerUnitTime(uint256 _tokenId) external view returns (uint256 _rewardsPerUnitTime) +``` + + + + + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| _tokenId | uint256 | undefined | + +#### Returns + +| Name | Type | Description | +|---|---|---| +| _rewardsPerUnitTime | uint256 | undefined | ### getStakeInfo @@ -125,37 +164,37 @@ View amount staked and rewards for a user, for a given token-id. | _tokensStaked | uint256 | Amount of tokens staked for given token-id. | | _rewards | uint256 | Available reward amount. | -### indexedTokens +### getTimeUnit ```solidity -function indexedTokens(uint256) external view returns (uint256) +function getTimeUnit(uint256 _tokenId) external view returns (uint256 _timeUnit) ``` -*List of token-ids ever staked.* + #### Parameters | Name | Type | Description | |---|---|---| -| _0 | uint256 | undefined | +| _tokenId | uint256 | undefined | #### Returns | Name | Type | Description | |---|---|---| -| _0 | uint256 | undefined | +| _timeUnit | uint256 | undefined | -### isIndexed +### indexedTokens ```solidity -function isIndexed(uint256) external view returns (bool) +function indexedTokens(uint256) external view returns (uint256) ``` -*Mapping from token-id to whether it is indexed or not.* +*List of token-ids ever staked.* #### Parameters @@ -167,17 +206,17 @@ function isIndexed(uint256) external view returns (bool) | Name | Type | Description | |---|---|---| -| _0 | bool | undefined | +| _0 | uint256 | undefined | -### rewardsPerUnitTime +### isIndexed ```solidity -function rewardsPerUnitTime(uint256) external view returns (uint256) +function isIndexed(uint256) external view returns (bool) ``` -*Mapping from token-id to rewards accumulated per unit of time.* +*Mapping from token-id to whether it is indexed or not.* #### Parameters @@ -189,7 +228,7 @@ function rewardsPerUnitTime(uint256) external view returns (uint256) | Name | Type | Description | |---|---|---| -| _0 | uint256 | undefined | +| _0 | bool | undefined | ### setDefaultRewardsPerUnitTime @@ -277,7 +316,7 @@ Stake ERC721 Tokens. ### stakers ```solidity -function stakers(uint256, address) external view returns (uint256 amountStaked, uint256 timeOfLastUpdate, uint256 unclaimedRewards) +function stakers(uint256, address) external view returns (uint256 amountStaked, uint256 timeOfLastUpdate, uint256 unclaimedRewards, uint256 conditionIdOflastUpdate) ``` @@ -298,6 +337,7 @@ function stakers(uint256, address) external view returns (uint256 amountStaked, | amountStaked | uint256 | undefined | | timeOfLastUpdate | uint256 | undefined | | unclaimedRewards | uint256 | undefined | +| conditionIdOflastUpdate | uint256 | undefined | ### stakersArray @@ -322,28 +362,6 @@ function stakersArray(uint256, uint256) external view returns (address) |---|---|---| | _0 | address | undefined | -### timeUnit - -```solidity -function timeUnit(uint256) external view returns (uint256) -``` - - - -*Mapping from token-id to unit of time specified in number of seconds. Can be set as 1 seconds, 1 days, 1 hours, etc.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined | - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined | - ### withdraw ```solidity diff --git a/docs/Staking20.md b/docs/Staking20.md index 6a332a98d..69f0436f1 100644 --- a/docs/Staking20.md +++ b/docs/Staking20.md @@ -21,6 +21,41 @@ Claim accumulated rewards. *See {_claimRewards}. Override that to implement custom logic. See {_calculateRewards} for reward-calculation logic.* +### getRewardRatio + +```solidity +function getRewardRatio() external view returns (uint256 _numerator, uint256 _denominator) +``` + + + + + + +#### Returns + +| Name | Type | Description | +|---|---|---| +| _numerator | uint256 | undefined | +| _denominator | uint256 | undefined | + +### getRewardTokenBalance + +```solidity +function getRewardTokenBalance() external view returns (uint256 _rewardsAvailableInContract) +``` + +View total rewards available in the staking contract. + + + + +#### Returns + +| Name | Type | Description | +|---|---|---| +| _rewardsAvailableInContract | uint256 | undefined | + ### getStakeInfo ```solidity @@ -44,32 +79,32 @@ View amount staked and rewards for a user. | _tokensStaked | uint256 | Amount of tokens staked. | | _rewards | uint256 | Available reward amount. | -### rewardRatioDenominator +### getTimeUnit ```solidity -function rewardRatioDenominator() external view returns (uint256) +function getTimeUnit() external view returns (uint256 _timeUnit) ``` -*Rewards ratio is the number of reward tokens for a number of staked tokens, per unit of time.* + #### Returns | Name | Type | Description | |---|---|---| -| _0 | uint256 | undefined | +| _timeUnit | uint256 | undefined | -### rewardRatioNumerator +### rewardTokenDecimals ```solidity -function rewardRatioNumerator() external view returns (uint256) +function rewardTokenDecimals() external view returns (uint256) ``` -*Rewards ratio is the number of reward tokens for a number of staked tokens, per unit of time.* +*Decimals of reward token.* #### Returns @@ -130,7 +165,7 @@ Stake ERC20 Tokens. ### stakers ```solidity -function stakers(address) external view returns (uint256 amountStaked, uint256 timeOfLastUpdate, uint256 unclaimedRewards) +function stakers(address) external view returns (uint256 amountStaked, uint256 timeOfLastUpdate, uint256 unclaimedRewards, uint256 conditionIdOflastUpdate) ``` @@ -150,6 +185,7 @@ function stakers(address) external view returns (uint256 amountStaked, uint256 t | amountStaked | uint256 | undefined | | timeOfLastUpdate | uint256 | undefined | | unclaimedRewards | uint256 | undefined | +| conditionIdOflastUpdate | uint256 | undefined | ### stakersArray @@ -173,15 +209,32 @@ function stakersArray(uint256) external view returns (address) |---|---|---| | _0 | address | undefined | -### timeUnit +### stakingTokenBalance + +```solidity +function stakingTokenBalance() external view returns (uint256) +``` + + + +*Total amount of tokens staked in the contract.* + + +#### Returns + +| Name | Type | Description | +|---|---|---| +| _0 | uint256 | undefined | + +### stakingTokenDecimals ```solidity -function timeUnit() external view returns (uint256) +function stakingTokenDecimals() external view returns (uint256) ``` -*Unit of time specified in number of seconds. Can be set as 1 seconds, 1 days, 1 hours, etc.* +*Decimals of staking token.* #### Returns diff --git a/docs/Staking20Base.md b/docs/Staking20Base.md index 8f5d291f9..105b35c5c 100644 --- a/docs/Staking20Base.md +++ b/docs/Staking20Base.md @@ -38,6 +38,41 @@ Returns the contract metadata URI. |---|---|---| | _0 | string | undefined | +### getRewardRatio + +```solidity +function getRewardRatio() external view returns (uint256 _numerator, uint256 _denominator) +``` + + + + + + +#### Returns + +| Name | Type | Description | +|---|---|---| +| _numerator | uint256 | undefined | +| _denominator | uint256 | undefined | + +### getRewardTokenBalance + +```solidity +function getRewardTokenBalance() external view returns (uint256 _rewardsAvailableInContract) +``` + +View total rewards available in the staking contract. + + + + +#### Returns + +| Name | Type | Description | +|---|---|---| +| _rewardsAvailableInContract | uint256 | undefined | + ### getStakeInfo ```solidity @@ -61,6 +96,23 @@ View amount staked and rewards for a user. | _tokensStaked | uint256 | Amount of tokens staked. | | _rewards | uint256 | Available reward amount. | +### getTimeUnit + +```solidity +function getTimeUnit() external view returns (uint256 _timeUnit) +``` + + + + + + +#### Returns + +| Name | Type | Description | +|---|---|---| +| _timeUnit | uint256 | undefined | + ### multicall ```solidity @@ -100,27 +152,27 @@ Returns the owner of the contract. |---|---|---| | _0 | address | undefined | -### rewardRatioDenominator +### rewardToken ```solidity -function rewardRatioDenominator() external view returns (uint256) +function rewardToken() external view returns (address) ``` - +*ERC20 Reward Token address. See {_mintRewards} below.* #### Returns | Name | Type | Description | |---|---|---| -| _0 | uint256 | undefined | +| _0 | address | undefined | -### rewardRatioNumerator +### rewardTokenDecimals ```solidity -function rewardRatioNumerator() external view returns (uint256) +function rewardTokenDecimals() external view returns (uint256) ``` @@ -134,23 +186,6 @@ function rewardRatioNumerator() external view returns (uint256) |---|---|---| | _0 | uint256 | undefined | -### rewardToken - -```solidity -function rewardToken() external view returns (address) -``` - - - -*ERC20 Reward Token address. See {_mintRewards} below.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined | - ### setContractURI ```solidity @@ -235,7 +270,7 @@ Stake ERC20 Tokens. ### stakers ```solidity -function stakers(address) external view returns (uint256 amountStaked, uint256 timeOfLastUpdate, uint256 unclaimedRewards) +function stakers(address) external view returns (uint256 amountStaked, uint256 timeOfLastUpdate, uint256 unclaimedRewards, uint256 conditionIdOflastUpdate) ``` @@ -255,6 +290,7 @@ function stakers(address) external view returns (uint256 amountStaked, uint256 t | amountStaked | uint256 | undefined | | timeOfLastUpdate | uint256 | undefined | | unclaimedRewards | uint256 | undefined | +| conditionIdOflastUpdate | uint256 | undefined | ### stakersArray @@ -278,10 +314,27 @@ function stakersArray(uint256) external view returns (address) |---|---|---| | _0 | address | undefined | -### timeUnit +### stakingTokenBalance + +```solidity +function stakingTokenBalance() external view returns (uint256) +``` + + + + + + +#### Returns + +| Name | Type | Description | +|---|---|---| +| _0 | uint256 | undefined | + +### stakingTokenDecimals ```solidity -function timeUnit() external view returns (uint256) +function stakingTokenDecimals() external view returns (uint256) ``` diff --git a/docs/Staking20Upgradeable.md b/docs/Staking20Upgradeable.md index 4a25d499a..ba006b0b3 100644 --- a/docs/Staking20Upgradeable.md +++ b/docs/Staking20Upgradeable.md @@ -21,6 +21,41 @@ Claim accumulated rewards. *See {_claimRewards}. Override that to implement custom logic. See {_calculateRewards} for reward-calculation logic.* +### getRewardRatio + +```solidity +function getRewardRatio() external view returns (uint256 _numerator, uint256 _denominator) +``` + + + + + + +#### Returns + +| Name | Type | Description | +|---|---|---| +| _numerator | uint256 | undefined | +| _denominator | uint256 | undefined | + +### getRewardTokenBalance + +```solidity +function getRewardTokenBalance() external view returns (uint256 _rewardsAvailableInContract) +``` + +View total rewards available in the staking contract. + + + + +#### Returns + +| Name | Type | Description | +|---|---|---| +| _rewardsAvailableInContract | uint256 | undefined | + ### getStakeInfo ```solidity @@ -44,32 +79,32 @@ View amount staked and rewards for a user. | _tokensStaked | uint256 | Amount of tokens staked. | | _rewards | uint256 | Available reward amount. | -### rewardRatioDenominator +### getTimeUnit ```solidity -function rewardRatioDenominator() external view returns (uint256) +function getTimeUnit() external view returns (uint256 _timeUnit) ``` -*Rewards ratio is the number of reward tokens for a number of staked tokens, per unit of time.* + #### Returns | Name | Type | Description | |---|---|---| -| _0 | uint256 | undefined | +| _timeUnit | uint256 | undefined | -### rewardRatioNumerator +### rewardTokenDecimals ```solidity -function rewardRatioNumerator() external view returns (uint256) +function rewardTokenDecimals() external view returns (uint256) ``` -*Rewards ratio is the number of reward tokens for a number of staked tokens, per unit of time.* +*Decimals of reward token.* #### Returns @@ -130,7 +165,7 @@ Stake ERC20 Tokens. ### stakers ```solidity -function stakers(address) external view returns (uint256 amountStaked, uint256 timeOfLastUpdate, uint256 unclaimedRewards) +function stakers(address) external view returns (uint256 amountStaked, uint256 timeOfLastUpdate, uint256 unclaimedRewards, uint256 conditionIdOflastUpdate) ``` @@ -150,6 +185,7 @@ function stakers(address) external view returns (uint256 amountStaked, uint256 t | amountStaked | uint256 | undefined | | timeOfLastUpdate | uint256 | undefined | | unclaimedRewards | uint256 | undefined | +| conditionIdOflastUpdate | uint256 | undefined | ### stakersArray @@ -173,15 +209,32 @@ function stakersArray(uint256) external view returns (address) |---|---|---| | _0 | address | undefined | -### timeUnit +### stakingTokenBalance + +```solidity +function stakingTokenBalance() external view returns (uint256) +``` + + + +*Total amount of tokens staked in the contract.* + + +#### Returns + +| Name | Type | Description | +|---|---|---| +| _0 | uint256 | undefined | + +### stakingTokenDecimals ```solidity -function timeUnit() external view returns (uint256) +function stakingTokenDecimals() external view returns (uint256) ``` -*Unit of time specified in number of seconds. Can be set as 1 seconds, 1 days, 1 hours, etc.* +*Decimals of staking token.* #### Returns diff --git a/docs/Staking721.md b/docs/Staking721.md index b5409d74b..e26355b36 100644 --- a/docs/Staking721.md +++ b/docs/Staking721.md @@ -21,6 +21,40 @@ Claim accumulated rewards. *See {_claimRewards}. Override that to implement custom logic. See {_calculateRewards} for reward-calculation logic.* +### getRewardTokenBalance + +```solidity +function getRewardTokenBalance() external view returns (uint256 _rewardsAvailableInContract) +``` + +View total rewards available in the staking contract. + + + + +#### Returns + +| Name | Type | Description | +|---|---|---| +| _rewardsAvailableInContract | uint256 | undefined | + +### getRewardsPerUnitTime + +```solidity +function getRewardsPerUnitTime() external view returns (uint256 _rewardsPerUnitTime) +``` + + + + + + +#### Returns + +| Name | Type | Description | +|---|---|---| +| _rewardsPerUnitTime | uint256 | undefined | + ### getStakeInfo ```solidity @@ -44,6 +78,23 @@ View amount staked and total rewards for a user. | _tokensStaked | uint256[] | List of token-ids staked by staker. | | _rewards | uint256 | Available reward amount. | +### getTimeUnit + +```solidity +function getTimeUnit() external view returns (uint256 _timeUnit) +``` + + + + + + +#### Returns + +| Name | Type | Description | +|---|---|---| +| _timeUnit | uint256 | undefined | + ### indexedTokens ```solidity @@ -105,23 +156,6 @@ function nftCollection() external view returns (address) |---|---|---| | _0 | address | undefined | -### rewardsPerUnitTime - -```solidity -function rewardsPerUnitTime() external view returns (uint256) -``` - - - -*Rewards accumulated per unit of time.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined | - ### setRewardsPerUnitTime ```solidity @@ -195,7 +229,7 @@ function stakerAddress(uint256) external view returns (address) ### stakers ```solidity -function stakers(address) external view returns (uint256 amountStaked, uint256 timeOfLastUpdate, uint256 unclaimedRewards) +function stakers(address) external view returns (uint256 amountStaked, uint256 timeOfLastUpdate, uint256 unclaimedRewards, uint256 conditionIdOflastUpdate) ``` @@ -215,6 +249,7 @@ function stakers(address) external view returns (uint256 amountStaked, uint256 t | amountStaked | uint256 | undefined | | timeOfLastUpdate | uint256 | undefined | | unclaimedRewards | uint256 | undefined | +| conditionIdOflastUpdate | uint256 | undefined | ### stakersArray @@ -238,23 +273,6 @@ function stakersArray(uint256) external view returns (address) |---|---|---| | _0 | address | undefined | -### timeUnit - -```solidity -function timeUnit() external view returns (uint256) -``` - - - -*Unit of time specified in number of seconds. Can be set as 1 seconds, 1 days, 1 hours, etc.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined | - ### withdraw ```solidity diff --git a/docs/Staking721Base.md b/docs/Staking721Base.md index 93da489e8..456a9a371 100644 --- a/docs/Staking721Base.md +++ b/docs/Staking721Base.md @@ -38,6 +38,40 @@ Returns the contract metadata URI. |---|---|---| | _0 | string | undefined | +### getRewardTokenBalance + +```solidity +function getRewardTokenBalance() external view returns (uint256 _rewardsAvailableInContract) +``` + +View total rewards available in the staking contract. + + + + +#### Returns + +| Name | Type | Description | +|---|---|---| +| _rewardsAvailableInContract | uint256 | undefined | + +### getRewardsPerUnitTime + +```solidity +function getRewardsPerUnitTime() external view returns (uint256 _rewardsPerUnitTime) +``` + + + + + + +#### Returns + +| Name | Type | Description | +|---|---|---| +| _rewardsPerUnitTime | uint256 | undefined | + ### getStakeInfo ```solidity @@ -61,6 +95,23 @@ View amount staked and total rewards for a user. | _tokensStaked | uint256[] | List of token-ids staked by staker. | | _rewards | uint256 | Available reward amount. | +### getTimeUnit + +```solidity +function getTimeUnit() external view returns (uint256 _timeUnit) +``` + + + + + + +#### Returns + +| Name | Type | Description | +|---|---|---| +| _timeUnit | uint256 | undefined | + ### indexedTokens ```solidity @@ -178,23 +229,6 @@ function rewardToken() external view returns (address) |---|---|---| | _0 | address | undefined | -### rewardsPerUnitTime - -```solidity -function rewardsPerUnitTime() external view returns (uint256) -``` - - - - - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined | - ### setContractURI ```solidity @@ -300,7 +334,7 @@ function stakerAddress(uint256) external view returns (address) ### stakers ```solidity -function stakers(address) external view returns (uint256 amountStaked, uint256 timeOfLastUpdate, uint256 unclaimedRewards) +function stakers(address) external view returns (uint256 amountStaked, uint256 timeOfLastUpdate, uint256 unclaimedRewards, uint256 conditionIdOflastUpdate) ``` @@ -320,6 +354,7 @@ function stakers(address) external view returns (uint256 amountStaked, uint256 t | amountStaked | uint256 | undefined | | timeOfLastUpdate | uint256 | undefined | | unclaimedRewards | uint256 | undefined | +| conditionIdOflastUpdate | uint256 | undefined | ### stakersArray @@ -343,23 +378,6 @@ function stakersArray(uint256) external view returns (address) |---|---|---| | _0 | address | undefined | -### timeUnit - -```solidity -function timeUnit() external view returns (uint256) -``` - - - - - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined | - ### withdraw ```solidity diff --git a/docs/Staking721Upgradeable.md b/docs/Staking721Upgradeable.md index fd7c31277..872a6be47 100644 --- a/docs/Staking721Upgradeable.md +++ b/docs/Staking721Upgradeable.md @@ -21,6 +21,40 @@ Claim accumulated rewards. *See {_claimRewards}. Override that to implement custom logic. See {_calculateRewards} for reward-calculation logic.* +### getRewardTokenBalance + +```solidity +function getRewardTokenBalance() external view returns (uint256 _rewardsAvailableInContract) +``` + +View total rewards available in the staking contract. + + + + +#### Returns + +| Name | Type | Description | +|---|---|---| +| _rewardsAvailableInContract | uint256 | undefined | + +### getRewardsPerUnitTime + +```solidity +function getRewardsPerUnitTime() external view returns (uint256 _rewardsPerUnitTime) +``` + + + + + + +#### Returns + +| Name | Type | Description | +|---|---|---| +| _rewardsPerUnitTime | uint256 | undefined | + ### getStakeInfo ```solidity @@ -44,6 +78,23 @@ View amount staked and total rewards for a user. | _tokensStaked | uint256[] | List of token-ids staked by staker. | | _rewards | uint256 | Available reward amount. | +### getTimeUnit + +```solidity +function getTimeUnit() external view returns (uint256 _timeUnit) +``` + + + + + + +#### Returns + +| Name | Type | Description | +|---|---|---| +| _timeUnit | uint256 | undefined | + ### indexedTokens ```solidity @@ -105,23 +156,6 @@ function nftCollection() external view returns (address) |---|---|---| | _0 | address | undefined | -### rewardsPerUnitTime - -```solidity -function rewardsPerUnitTime() external view returns (uint256) -``` - - - -*Rewards accumulated per unit of time.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined | - ### setRewardsPerUnitTime ```solidity @@ -195,7 +229,7 @@ function stakerAddress(uint256) external view returns (address) ### stakers ```solidity -function stakers(address) external view returns (uint256 amountStaked, uint256 timeOfLastUpdate, uint256 unclaimedRewards) +function stakers(address) external view returns (uint256 amountStaked, uint256 timeOfLastUpdate, uint256 unclaimedRewards, uint256 conditionIdOflastUpdate) ``` @@ -215,6 +249,7 @@ function stakers(address) external view returns (uint256 amountStaked, uint256 t | amountStaked | uint256 | undefined | | timeOfLastUpdate | uint256 | undefined | | unclaimedRewards | uint256 | undefined | +| conditionIdOflastUpdate | uint256 | undefined | ### stakersArray @@ -238,23 +273,6 @@ function stakersArray(uint256) external view returns (address) |---|---|---| | _0 | address | undefined | -### timeUnit - -```solidity -function timeUnit() external view returns (uint256) -``` - - - -*Unit of time specified in number of seconds. Can be set as 1 seconds, 1 days, 1 hours, etc.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined | - ### withdraw ```solidity diff --git a/docs/TokenStake.md b/docs/TokenStake.md index e3d9ad995..e5249e3b5 100644 --- a/docs/TokenStake.md +++ b/docs/TokenStake.md @@ -89,6 +89,41 @@ function contractVersion() external pure returns (uint8) |---|---|---| | _0 | uint8 | undefined | +### getRewardRatio + +```solidity +function getRewardRatio() external view returns (uint256 _numerator, uint256 _denominator) +``` + + + + + + +#### Returns + +| Name | Type | Description | +|---|---|---| +| _numerator | uint256 | undefined | +| _denominator | uint256 | undefined | + +### getRewardTokenBalance + +```solidity +function getRewardTokenBalance() external view returns (uint256 _rewardsAvailableInContract) +``` + +View total rewards available in the staking contract. + + + + +#### Returns + +| Name | Type | Description | +|---|---|---| +| _rewardsAvailableInContract | uint256 | undefined | + ### getRoleAdmin ```solidity @@ -179,6 +214,23 @@ View amount staked and rewards for a user. | _tokensStaked | uint256 | Amount of tokens staked. | | _rewards | uint256 | Available reward amount. | +### getTimeUnit + +```solidity +function getTimeUnit() external view returns (uint256 _timeUnit) +``` + + + + + + +#### Returns + +| Name | Type | Description | +|---|---|---| +| _timeUnit | uint256 | undefined | + ### grantRole ```solidity @@ -343,27 +395,27 @@ Revokes role from an account. | role | bytes32 | keccak256 hash of the role. e.g. keccak256("TRANSFER_ROLE") | | account | address | Address of the account from which the role is being revoked. | -### rewardRatioDenominator +### rewardToken ```solidity -function rewardRatioDenominator() external view returns (uint256) +function rewardToken() external view returns (address) ``` - +*ERC20 Reward Token address. See {_mintRewards} below.* #### Returns | Name | Type | Description | |---|---|---| -| _0 | uint256 | undefined | +| _0 | address | undefined | -### rewardRatioNumerator +### rewardTokenDecimals ```solidity -function rewardRatioNumerator() external view returns (uint256) +function rewardTokenDecimals() external view returns (uint256) ``` @@ -377,23 +429,6 @@ function rewardRatioNumerator() external view returns (uint256) |---|---|---| | _0 | uint256 | undefined | -### rewardToken - -```solidity -function rewardToken() external view returns (address) -``` - - - -*ERC20 Reward Token address. See {_mintRewards} below.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined | - ### setContractURI ```solidity @@ -462,7 +497,7 @@ Stake ERC20 Tokens. ### stakers ```solidity -function stakers(address) external view returns (uint256 amountStaked, uint256 timeOfLastUpdate, uint256 unclaimedRewards) +function stakers(address) external view returns (uint256 amountStaked, uint256 timeOfLastUpdate, uint256 unclaimedRewards, uint256 conditionIdOflastUpdate) ``` @@ -482,6 +517,7 @@ function stakers(address) external view returns (uint256 amountStaked, uint256 t | amountStaked | uint256 | undefined | | timeOfLastUpdate | uint256 | undefined | | unclaimedRewards | uint256 | undefined | +| conditionIdOflastUpdate | uint256 | undefined | ### stakersArray @@ -505,10 +541,27 @@ function stakersArray(uint256) external view returns (address) |---|---|---| | _0 | address | undefined | -### timeUnit +### stakingTokenBalance + +```solidity +function stakingTokenBalance() external view returns (uint256) +``` + + + + + + +#### Returns + +| Name | Type | Description | +|---|---|---| +| _0 | uint256 | undefined | + +### stakingTokenDecimals ```solidity -function timeUnit() external view returns (uint256) +function stakingTokenDecimals() external view returns (uint256) ``` @@ -608,6 +661,22 @@ event Initialized(uint8 version) |---|---|---| | version | uint8 | undefined | +### RewardTokensWithdrawnByAdmin + +```solidity +event RewardTokensWithdrawnByAdmin(uint256 _amount) +``` + + + +*Emitted when contract admin withdraws reward tokens.* + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| _amount | uint256 | undefined | + ### RewardsClaimed ```solidity diff --git a/lib/forge-std b/lib/forge-std index cd7d533f9..bd533b5ed 160000 --- a/lib/forge-std +++ b/lib/forge-std @@ -1 +1 @@ -Subproject commit cd7d533f9a0ee0ec02ad81e0a8f262bc4203c653 +Subproject commit bd533b5ed42c6e45d81ab5a460981256cb5a76e4 diff --git a/src/test/mocks/MockERC20.sol b/src/test/mocks/MockERC20.sol index e73e6910a..d895d08e0 100644 --- a/src/test/mocks/MockERC20.sol +++ b/src/test/mocks/MockERC20.sol @@ -6,12 +6,31 @@ import "@openzeppelin/contracts/token/ERC20/presets/ERC20PresetMinterPauser.sol" import "@openzeppelin/contracts/token/ERC20/extensions/draft-ERC20Permit.sol"; contract MockERC20 is ERC20PresetMinterPauser, ERC20Permit { + bool internal taxActive; + constructor() ERC20PresetMinterPauser("Mock Coin", "MOCK") ERC20Permit("Mock Coin") {} function mint(address to, uint256 amount) public override(ERC20PresetMinterPauser) { _mint(to, amount); } + function toggleTax() external { + taxActive = !taxActive; + } + + function _transfer( + address from, + address to, + uint256 amount + ) internal override { + if (taxActive) { + uint256 tax = (amount * 10) / 100; + amount -= tax; + super._transfer(from, address(this), tax); + } + super._transfer(from, to, amount); + } + function _beforeTokenTransfer( address from, address to, diff --git a/src/test/sdk/extension/StakingExtension.t.sol b/src/test/sdk/extension/StakingExtension.t.sol index 2fa732d07..54494ae49 100644 --- a/src/test/sdk/extension/StakingExtension.t.sol +++ b/src/test/sdk/extension/StakingExtension.t.sol @@ -7,10 +7,11 @@ import "@ds-test/test.sol"; import { Staking721 } from "contracts/extension/Staking721.sol"; import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; +import "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol"; import "../../mocks/MockERC721.sol"; -contract MyStakingContract is ERC20, Staking721 { +contract MyStakingContract is ERC20, Staking721, IERC721Receiver { bool condition; constructor( @@ -21,8 +22,28 @@ contract MyStakingContract is ERC20, Staking721 { uint256 _rewardsPerUnitTime ) ERC20(_name, _symbol) Staking721(_nftCollection) { condition = true; - _setTimeUnit(_timeUnit); - _setRewardsPerUnitTime(_rewardsPerUnitTime); + _setStakingCondition(_timeUnit, _rewardsPerUnitTime); + } + + /// @notice View total rewards available in the staking contract. + function getRewardTokenBalance() external view override returns (uint256) {} + + /*/////////////////////////////////////////////////////////////// + ERC 165 / 721 logic + //////////////////////////////////////////////////////////////*/ + + function onERC721Received( + address, + address, + uint256, + bytes calldata + ) external view override returns (bytes4) { + require(isStaking == 2, "Direct transfer"); + return this.onERC721Received.selector; + } + + function supportsInterface(bytes4 interfaceId) public view virtual returns (bool) { + return interfaceId == type(IERC721Receiver).interfaceId; } function setCondition(bool _condition) external { @@ -258,12 +279,12 @@ contract StakingExtensionTest is DSTest, Test { function test_state_setRewardsPerUnitTime() public { // check current value - assertEq(rewardsPerUnitTime, ext.rewardsPerUnitTime()); + assertEq(rewardsPerUnitTime, ext.getRewardsPerUnitTime()); // set new value and check uint256 newRewardsPerUnitTime = 50; ext.setRewardsPerUnitTime(newRewardsPerUnitTime); - assertEq(newRewardsPerUnitTime, ext.rewardsPerUnitTime()); + assertEq(newRewardsPerUnitTime, ext.getRewardsPerUnitTime()); //================ stake tokens vm.warp(1); @@ -282,7 +303,7 @@ contract StakingExtensionTest is DSTest, Test { vm.warp(1000); ext.setRewardsPerUnitTime(200); - assertEq(200, ext.rewardsPerUnitTime()); + assertEq(200, ext.getRewardsPerUnitTime()); uint256 newTimeOfLastUpdate = block.timestamp; // check available rewards -- should use previous value for rewardsPerUnitTime for calculation @@ -314,12 +335,12 @@ contract StakingExtensionTest is DSTest, Test { function test_state_setTimeUnit() public { // check current value - assertEq(timeUnit, ext.timeUnit()); + assertEq(timeUnit, ext.getTimeUnit()); // set new value and check uint256 newTimeUnit = 1 minutes; ext.setTimeUnit(newTimeUnit); - assertEq(newTimeUnit, ext.timeUnit()); + assertEq(newTimeUnit, ext.getTimeUnit()); //================ stake tokens vm.warp(1); @@ -338,7 +359,7 @@ contract StakingExtensionTest is DSTest, Test { vm.warp(1000); ext.setTimeUnit(1 seconds); - assertEq(1 seconds, ext.timeUnit()); + assertEq(1 seconds, ext.getTimeUnit()); uint256 newTimeOfLastUpdate = block.timestamp; // check available rewards -- should use previous value for rewardsPerUnitTime for calculation diff --git a/src/test/staking/EditionStake.t.sol b/src/test/staking/EditionStake.t.sol index 18cf75536..3c8a36512 100644 --- a/src/test/staking/EditionStake.t.sol +++ b/src/test/staking/EditionStake.t.sol @@ -397,7 +397,7 @@ contract EditionStakeTest is BaseTest { uint256 rewardsPerUnitTime = 50; vm.prank(deployer); stakeContract.setRewardsPerUnitTime(0, rewardsPerUnitTime); - assertEq(rewardsPerUnitTime, stakeContract.rewardsPerUnitTime(0)); + assertEq(rewardsPerUnitTime, stakeContract.getRewardsPerUnitTime(0)); //================ stake tokens vm.warp(1); @@ -412,7 +412,7 @@ contract EditionStakeTest is BaseTest { vm.prank(deployer); stakeContract.setRewardsPerUnitTime(0, 200); - assertEq(200, stakeContract.rewardsPerUnitTime(0)); + assertEq(200, stakeContract.getRewardsPerUnitTime(0)); uint256 newTimeOfLastUpdate = block.timestamp; // check available rewards -- should use previous value for rewardsPerUnitTime for calculation @@ -447,7 +447,7 @@ contract EditionStakeTest is BaseTest { vm.prank(deployer); stakeContract.setRewardsPerUnitTime(0, 300); - assertEq(300, stakeContract.rewardsPerUnitTime(0)); + assertEq(300, stakeContract.getRewardsPerUnitTime(0)); newTimeOfLastUpdate = block.timestamp; // check available rewards for token-1 -- should use defaultRewardsPerUnitTime for calculation @@ -481,7 +481,7 @@ contract EditionStakeTest is BaseTest { uint256 rewardsPerUnitTime = 50; vm.prank(deployer); stakeContract.setRewardsPerUnitTime(0, rewardsPerUnitTime); - assertEq(rewardsPerUnitTime, stakeContract.rewardsPerUnitTime(0)); + assertEq(rewardsPerUnitTime, stakeContract.getRewardsPerUnitTime(0)); //================ stake tokens vm.warp(1); @@ -496,7 +496,7 @@ contract EditionStakeTest is BaseTest { vm.prank(deployer); stakeContract.setRewardsPerUnitTime(0, 200); - assertEq(200, stakeContract.rewardsPerUnitTime(0)); + assertEq(200, stakeContract.getRewardsPerUnitTime(0)); uint256 newTimeOfLastUpdate = block.timestamp; // check available rewards -- should use previous value for rewardsPerUnitTime for calculation @@ -531,7 +531,7 @@ contract EditionStakeTest is BaseTest { vm.prank(deployer); stakeContract.setRewardsPerUnitTime(1, 300); - assertEq(300, stakeContract.rewardsPerUnitTime(1)); + assertEq(300, stakeContract.getRewardsPerUnitTime(1)); newTimeOfLastUpdate = block.timestamp; // check available rewards for token-1 -- should use defaultRewardsPerUnitTime for calculation @@ -566,7 +566,7 @@ contract EditionStakeTest is BaseTest { uint256 timeUnit = 100; vm.prank(deployer); stakeContract.setTimeUnit(0, timeUnit); - assertEq(timeUnit, stakeContract.timeUnit(0)); + assertEq(timeUnit, stakeContract.getTimeUnit(0)); //================ stake tokens vm.warp(1); @@ -581,7 +581,7 @@ contract EditionStakeTest is BaseTest { vm.prank(deployer); stakeContract.setTimeUnit(0, 200); - assertEq(200, stakeContract.timeUnit(0)); + assertEq(200, stakeContract.getTimeUnit(0)); uint256 newTimeOfLastUpdate = block.timestamp; // check available rewards -- should use previous value for timeUnit for calculation @@ -616,7 +616,7 @@ contract EditionStakeTest is BaseTest { vm.prank(deployer); stakeContract.setTimeUnit(0, 10); - assertEq(10, stakeContract.timeUnit(0)); + assertEq(10, stakeContract.getTimeUnit(0)); newTimeOfLastUpdate = block.timestamp; // check available rewards for token-1 -- should use defaultTimeUnit for calculation @@ -650,7 +650,7 @@ contract EditionStakeTest is BaseTest { uint256 timeUnit = 100; vm.prank(deployer); stakeContract.setTimeUnit(0, timeUnit); - assertEq(timeUnit, stakeContract.timeUnit(0)); + assertEq(timeUnit, stakeContract.getTimeUnit(0)); //================ stake tokens vm.warp(1); @@ -665,7 +665,7 @@ contract EditionStakeTest is BaseTest { vm.prank(deployer); stakeContract.setTimeUnit(0, 200); - assertEq(200, stakeContract.timeUnit(0)); + assertEq(200, stakeContract.getTimeUnit(0)); uint256 newTimeOfLastUpdate = block.timestamp; // check available rewards -- should use previous value for timeUnit for calculation @@ -700,7 +700,7 @@ contract EditionStakeTest is BaseTest { vm.prank(deployer); stakeContract.setTimeUnit(1, 300); - assertEq(300, stakeContract.timeUnit(1)); + assertEq(300, stakeContract.getTimeUnit(1)); newTimeOfLastUpdate = block.timestamp; // check available rewards for token-1 -- should use defaultTimeUnit for calculation @@ -972,4 +972,150 @@ contract EditionStakeTest is BaseTest { vm.expectRevert("Withdrawing more than staked"); stakeContract.withdraw(1, 20); } + + /*/////////////////////////////////////////////////////////////// + Miscellaneous + //////////////////////////////////////////////////////////////*/ + + function test_revert_zeroTimeUnit_adminLockTokens() public { + //================ stake tokens + vm.warp(1); + + // User stakes tokens + vm.prank(stakerOne); + stakeContract.stake(0, 50); + + // set default timeUnit to zero + uint256 newTimeUnit = 0; + vm.prank(deployer); + vm.expectRevert("time-unit can't be 0"); + stakeContract.setDefaultTimeUnit(newTimeUnit); + + // set timeUnit to zero + vm.prank(deployer); + vm.expectRevert("time-unit can't be 0"); + stakeContract.setTimeUnit(0, newTimeUnit); + + // stakerOne and stakerTwo can withdraw their tokens + // vm.expectRevert(stdError.divisionError); + vm.prank(stakerOne); + stakeContract.withdraw(0, 50); + } + + function test_Macro_EditionDirectSafeTransferLocksToken() public { + uint256 tokenId = 0; + + // stakerOne mistakenly safe-transfers direct to the staking contract + vm.prank(stakerOne); + vm.expectRevert("Direct transfer"); + erc1155.safeTransferFrom(stakerOne, address(stakeContract), tokenId, 100, ""); + + // show that the transferred tokens were not properly staked + // (uint256 tokensStaked, uint256 rewards) = stakeContract.getStakeInfoForToken(tokenId, stakerOne); + // assertEq(0, tokensStaked); + + // // show that stakerOne cannot recover the tokens + // vm.expectRevert(); + // vm.prank(stakerOne); + // stakeContract.withdraw(tokenId, 100); + } +} + +contract Macro_EditionStakeTest is BaseTest { + EditionStake internal stakeContract; + + uint256 internal defaultTimeUnit; + uint256 internal defaultRewardsPerUnitTime; + uint256 internal tokenAmount = 100; + address internal stakerOne = address(0x345); + address internal stakerTwo = address(0x567); + + function setUp() public override { + super.setUp(); + + defaultTimeUnit = 60; + defaultRewardsPerUnitTime = 1; + + // mint erc1155 tokens to stakers + erc1155.mint(stakerOne, 1, tokenAmount); + erc1155.mint(stakerTwo, 2, tokenAmount); + + // mint reward tokens to contract admin + erc20.mint(deployer, 1000 ether); + + stakeContract = EditionStake(getContract("EditionStake")); + + // set approval + vm.prank(stakerOne); + erc1155.setApprovalForAll(address(stakeContract), true); + vm.prank(stakerTwo); + erc1155.setApprovalForAll(address(stakeContract), true); + } + + // Demostrate setting unitTime to 0 locks the tokens irreversibly + function testEdition_adminLockTokens() public { + //================ stake tokens + vm.warp(1); + + // Two users stake 1 tokens each + vm.prank(stakerOne); + stakeContract.stake(1, tokenAmount); + vm.prank(stakerTwo); + stakeContract.stake(2, tokenAmount); + + // set timeUnit to zero + uint256 newTimeUnit = 0; + vm.prank(deployer); + vm.expectRevert("time-unit can't be 0"); + stakeContract.setDefaultTimeUnit(newTimeUnit); + + // stakerOne and stakerTwo can't withdraw their tokens + // vm.expectRevert(stdError.divisionError); + vm.prank(stakerOne); + stakeContract.withdraw(1, tokenAmount); + + // vm.expectRevert(stdError.divisionError); + vm.prank(stakerTwo); + stakeContract.withdraw(2, tokenAmount); + + // timeUnit can't be changed back to a nonzero value + newTimeUnit = 40; + // vm.expectRevert(stdError.divisionError); + vm.prank(deployer); + stakeContract.setDefaultTimeUnit(newTimeUnit); + } + + // Demostrate setting rewardsPerTimeUnit to a high value locks the tokens irreversibly + function testEdition_demostrate_adminRewardsLock() public { + //================ stake tokens + vm.warp(1); + + // Two users stake 1 tokens each + vm.prank(stakerOne); + stakeContract.stake(1, tokenAmount); + vm.prank(stakerTwo); + stakeContract.stake(2, tokenAmount); + + // set rewardsPerTimeUnit to max value + uint256 rewardsPerTimeUnit = type(uint256).max; + vm.prank(deployer); + stakeContract.setDefaultRewardsPerUnitTime(rewardsPerTimeUnit); + + vm.warp(1 days); + + // stakerOne and stakerTwo can't withdraw their tokens + // vm.expectRevert(stdError.arithmeticError); + vm.prank(stakerOne); + stakeContract.withdraw(1, tokenAmount); + + // vm.expectRevert(stdError.arithmeticError); + vm.prank(stakerTwo); + stakeContract.withdraw(2, tokenAmount); + + // timeUnit can't be changed back + rewardsPerTimeUnit = 60; + // vm.expectRevert(stdError.arithmeticError); + vm.prank(deployer); + stakeContract.setDefaultRewardsPerUnitTime(rewardsPerTimeUnit); + } } diff --git a/src/test/staking/NFTStake.t.sol b/src/test/staking/NFTStake.t.sol index 54edb24ae..15c808e01 100644 --- a/src/test/staking/NFTStake.t.sol +++ b/src/test/staking/NFTStake.t.sol @@ -228,13 +228,13 @@ contract NFTStakeTest is BaseTest { function test_state_setRewardsPerUnitTime() public { // check current value - assertEq(rewardsPerUnitTime, stakeContract.rewardsPerUnitTime()); + assertEq(rewardsPerUnitTime, stakeContract.getRewardsPerUnitTime()); // set new value and check uint256 newRewardsPerUnitTime = 50; vm.prank(deployer); stakeContract.setRewardsPerUnitTime(newRewardsPerUnitTime); - assertEq(newRewardsPerUnitTime, stakeContract.rewardsPerUnitTime()); + assertEq(newRewardsPerUnitTime, stakeContract.getRewardsPerUnitTime()); //================ stake tokens vm.warp(1); @@ -254,7 +254,7 @@ contract NFTStakeTest is BaseTest { vm.prank(deployer); stakeContract.setRewardsPerUnitTime(200); - assertEq(200, stakeContract.rewardsPerUnitTime()); + assertEq(200, stakeContract.getRewardsPerUnitTime()); uint256 newTimeOfLastUpdate = block.timestamp; // check available rewards -- should use previous value for rewardsPerUnitTime for calculation @@ -284,13 +284,13 @@ contract NFTStakeTest is BaseTest { function test_state_setTimeUnit() public { // check current value - assertEq(timeUnit, stakeContract.timeUnit()); + assertEq(timeUnit, stakeContract.getTimeUnit()); // set new value and check - uint256 newTimeUnit = 1 minutes; + uint256 newTimeUnit = 2 minutes; vm.prank(deployer); stakeContract.setTimeUnit(newTimeUnit); - assertEq(newTimeUnit, stakeContract.timeUnit()); + assertEq(newTimeUnit, stakeContract.getTimeUnit()); //================ stake tokens vm.warp(1); @@ -310,7 +310,7 @@ contract NFTStakeTest is BaseTest { vm.prank(deployer); stakeContract.setTimeUnit(1 seconds); - assertEq(1 seconds, stakeContract.timeUnit()); + assertEq(1 seconds, stakeContract.getTimeUnit()); uint256 newTimeOfLastUpdate = block.timestamp; // check available rewards -- should use previous value for rewardsPerUnitTime for calculation @@ -474,4 +474,99 @@ contract NFTStakeTest is BaseTest { vm.expectRevert("Withdrawing more than staked"); stakeContract.withdraw(_tokensToWithdraw); } + + /*/////////////////////////////////////////////////////////////// + Miscellaneous + //////////////////////////////////////////////////////////////*/ + + function test_revert_zeroTimeUnit_adminLockTokens() public { + //================ stake tokens + vm.warp(1); + uint256[] memory _tokenIdsOne = new uint256[](1); + uint256[] memory _tokenIdsTwo = new uint256[](1); + _tokenIdsOne[0] = 0; + _tokenIdsTwo[0] = 5; + + // Two different users stake 1 tokens each + vm.prank(stakerOne); + stakeContract.stake(_tokenIdsOne); + vm.prank(stakerTwo); + stakeContract.stake(_tokenIdsTwo); + + // set timeUnit to zero + uint256 newTimeUnit = 0; + vm.prank(deployer); + vm.expectRevert("time-unit can't be 0"); + stakeContract.setTimeUnit(newTimeUnit); + + // stakerOne and stakerTwo can withdraw their tokens + // vm.expectRevert(stdError.divisionError); + vm.prank(stakerOne); + stakeContract.withdraw(_tokenIdsOne); + + // vm.expectRevert(stdError.divisionError); + vm.prank(stakerTwo); + stakeContract.withdraw(_tokenIdsTwo); + } + + function test_revert_largeRewardsPerUnitTime_adminRewardsLock() public { + //================ stake tokens + vm.warp(1); + uint256[] memory _tokenIdsOne = new uint256[](1); + uint256[] memory _tokenIdsTwo = new uint256[](1); + + uint256 stakerOneToken = erc721.nextTokenIdToMint(); + erc721.mint(stakerOne, 5); // mint token id 0 to 4 + uint256 stakerTwoToken = erc721.nextTokenIdToMint(); + erc721.mint(stakerTwo, 5); // mint token id 5 to 9 + _tokenIdsOne[0] = stakerOneToken; + _tokenIdsTwo[0] = stakerTwoToken; + + // Two users stake 1 tokens each + vm.prank(stakerOne); + stakeContract.stake(_tokenIdsOne); + vm.prank(stakerTwo); + stakeContract.stake(_tokenIdsTwo); + + // set rewardsPerTimeUnit to max value + uint256 rewardsPerTimeUnit = type(uint256).max; + vm.prank(deployer); + stakeContract.setRewardsPerUnitTime(rewardsPerTimeUnit); + + vm.warp(1 days); + + // stakerOne and stakerTwo can't withdraw their tokens + // vm.expectRevert(stdError.arithmeticError); + vm.prank(stakerOne); + stakeContract.withdraw(_tokenIdsOne); + + // vm.expectRevert(stdError.arithmeticError); + vm.prank(stakerTwo); + stakeContract.withdraw(_tokenIdsTwo); + + // rewardsPerTimeUnit can't be changed + rewardsPerTimeUnit = 60; + // vm.expectRevert(stdError.arithmeticError); + vm.prank(deployer); + stakeContract.setRewardsPerUnitTime(rewardsPerTimeUnit); + } + + function test_Macro_NFTDirectSafeTransferLocksToken() public { + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = 0; + + // stakerOne mistakenly safe-transfers direct to the staking contract + vm.prank(stakerOne); + vm.expectRevert("Direct transfer"); + erc721.safeTransferFrom(stakerOne, address(stakeContract), tokenIds[0]); + + // show that the transferred token was not properly staked + // (uint256[] memory tokensStaked, uint256 rewards) = stakeContract.getStakeInfo(stakerOne); + // assertEq(0, tokensStaked.length); + + // // show that stakerOne cannot recover the token + // vm.expectRevert(); + // vm.prank(stakerOne); + // stakeContract.withdraw(tokenIds); + } } diff --git a/src/test/staking/TokenStake.t.sol b/src/test/staking/TokenStake.t.sol index ce6fcfaa0..75da406a3 100644 --- a/src/test/staking/TokenStake.t.sol +++ b/src/test/staking/TokenStake.t.sol @@ -237,8 +237,9 @@ contract TokenStakeTest is BaseTest { // set value and check vm.prank(deployer); stakeContract.setRewardRatio(3, 70); - assertEq(3, stakeContract.rewardRatioNumerator()); - assertEq(70, stakeContract.rewardRatioDenominator()); + (uint256 numerator, uint256 denominator) = stakeContract.getRewardRatio(); + assertEq(3, numerator); + assertEq(70, denominator); //================ stake tokens vm.warp(1); @@ -253,8 +254,9 @@ contract TokenStakeTest is BaseTest { vm.prank(deployer); stakeContract.setRewardRatio(3, 80); - assertEq(3, stakeContract.rewardRatioNumerator()); - assertEq(80, stakeContract.rewardRatioDenominator()); + (numerator, denominator) = stakeContract.getRewardRatio(); + assertEq(3, numerator); + assertEq(80, denominator); uint256 newTimeOfLastUpdate = block.timestamp; // check available rewards -- should use previous value for rewardsPerUnitTime for calculation @@ -279,7 +281,7 @@ contract TokenStakeTest is BaseTest { uint256 timeUnitToSet = 100; vm.prank(deployer); stakeContract.setTimeUnit(timeUnitToSet); - assertEq(timeUnitToSet, stakeContract.timeUnit()); + assertEq(timeUnitToSet, stakeContract.getTimeUnit()); //================ stake tokens vm.warp(1); @@ -294,7 +296,7 @@ contract TokenStakeTest is BaseTest { vm.prank(deployer); stakeContract.setTimeUnit(200); - assertEq(200, stakeContract.timeUnit()); + assertEq(200, stakeContract.getTimeUnit()); uint256 newTimeOfLastUpdate = block.timestamp; // check available rewards -- should use previous value for timeUnit for calculation @@ -468,4 +470,375 @@ contract TokenStakeTest is BaseTest { vm.expectRevert("Withdrawing more than staked"); stakeContract.withdraw(500); } + + /*/////////////////////////////////////////////////////////////// + Miscellaneous + //////////////////////////////////////////////////////////////*/ + + function test_revert_zeroTimeUnit_adminLockTokens() public { + //================ stake tokens + vm.warp(1); + + // User stakes tokens + vm.prank(stakerOne); + stakeContract.stake(400); + + // set timeUnit to zero + uint256 newTimeUnit = 0; + vm.prank(deployer); + vm.expectRevert("time-unit can't be 0"); + stakeContract.setTimeUnit(newTimeUnit); + + // stakerOne and stakerTwo can withdraw their tokens + // vm.expectRevert(stdError.divisionError); + vm.prank(stakerOne); + stakeContract.withdraw(400); + } +} + +contract MockERC20Decimals is MockERC20 { + uint8 private immutable DECIMALS; + + constructor(uint8 _decimals) MockERC20() { + DECIMALS = _decimals; + } + + function decimals() public view virtual override returns (uint8) { + return DECIMALS; + } +} + +// Test scenario where reward token has 6 decimals and staking token has 18 +contract Macro_TokenStake_Rewards6_Staking18_Test is BaseTest { + MockERC20Decimals public erc20_reward6; + MockERC20Decimals public erc20_staking18; + + TokenStake internal stakeContract_reward6_staking18; + + address internal stakerOne; + + uint256 internal timeUnit; + uint256 internal rewardRatioNumerator; + uint256 internal rewardRatioDenominator; + + function setUp() public override { + super.setUp(); + + erc20_reward6 = new MockERC20Decimals(6); + erc20_staking18 = new MockERC20Decimals(18); + + // every 60s earns 1 reward token per 2 tokens staked + timeUnit = 60; + rewardRatioNumerator = 1; + rewardRatioDenominator = 2; + + deployContractProxy( + "TokenStake", + abi.encodeCall( + TokenStake.initialize, + ( + deployer, + CONTRACT_URI, + forwarders(), + address(erc20_reward6), + address(erc20_staking18), + timeUnit, + rewardRatioNumerator, + rewardRatioDenominator + ) + ) + ); + + stakeContract_reward6_staking18 = TokenStake(getContract("TokenStake")); + + stakerOne = address(0x345); + + // mint 1000 tokens to stakerOne + erc20_staking18.mint(stakerOne, 1000e18); + + // mint 1000 reward tokens to contract admin + erc20_reward6.mint(deployer, 1000e6); + + // set approvals + vm.prank(stakerOne); + erc20_staking18.approve(address(stakeContract_reward6_staking18), type(uint256).max); + + // transfer 100 reward tokens + vm.prank(deployer); + erc20_reward6.transfer(address(stakeContract_reward6_staking18), 100e6); + } + + //===== Reward Token 6 Decimals, Staking Token 18 Decimals =====// + function test_Macro_reward6_staking18() public { + vm.warp(1); + + // stake 400 tokens + vm.prank(stakerOne); + stakeContract_reward6_staking18.stake(400e18); + uint256 timeOfLastUpdate = block.timestamp; + + // check balances/ownership of staked tokens + assertEq(erc20_staking18.balanceOf(address(stakeContract_reward6_staking18)), 400e18); + assertEq(erc20_staking18.balanceOf(address(stakerOne)), 600e18); + + // check available rewards right after staking + (uint256 _amountStaked, uint256 _availableRewards) = stakeContract_reward6_staking18.getStakeInfo(stakerOne); + + assertEq(_amountStaked, 400e18); + assertEq(_availableRewards, 0); + + //=================== warp ahead exactly 1 timeUnit: 60s + vm.roll(4); + vm.warp(61); + assertEq(timeUnit, block.timestamp - timeOfLastUpdate); + + // With 400 tokens staked, we expect 200 reward tokens earned + (, _availableRewards) = stakeContract_reward6_staking18.getStakeInfo(stakerOne); + console2.log("Expect 200 reward tokens. Amount earned: ", _availableRewards / 1e6); + assertEq(_availableRewards, 200e6); + } +} + +// Test scenario where reward token has 18 decimals and staking token has 6 +contract Macro_TokenStake_Rewards18_Staking6_Test is BaseTest { + MockERC20Decimals public erc20_reward18; + MockERC20Decimals public erc20_staking6; + + TokenStake internal stakeContract_reward18_staking6; + + address internal stakerOne; + + uint256 internal timeUnit; + uint256 internal rewardRatioNumerator; + uint256 internal rewardRatioDenominator; + + function setUp() public override { + super.setUp(); + + erc20_reward18 = new MockERC20Decimals(18); + erc20_staking6 = new MockERC20Decimals(6); + + // every 60s earns 1 reward token per 2 tokens staked + timeUnit = 60; + rewardRatioNumerator = 1; + rewardRatioDenominator = 2; + + deployContractProxy( + "TokenStake", + abi.encodeCall( + TokenStake.initialize, + ( + deployer, + CONTRACT_URI, + forwarders(), + address(erc20_reward18), + address(erc20_staking6), + timeUnit, + rewardRatioNumerator, + rewardRatioDenominator + ) + ) + ); + + stakeContract_reward18_staking6 = TokenStake(getContract("TokenStake")); + + stakerOne = address(0x345); + + // mint 1000 tokens to stakerOne + erc20_staking6.mint(stakerOne, 1000e6); + + // mint 1000 reward tokens to contract admin + erc20_reward18.mint(deployer, 1000e18); + + // set approvals + vm.prank(stakerOne); + erc20_staking6.approve(address(stakeContract_reward18_staking6), type(uint256).max); + + // transfer 100 reward tokens + vm.prank(deployer); + erc20_reward18.transfer(address(stakeContract_reward18_staking6), 100e18); + } + + //===== Reward Token 18 Decimals, Staking Token 6 Decimals =====// + function test_Macro_reward18_staking6() public { + vm.warp(1); + + // stake 400 tokens + vm.prank(stakerOne); + stakeContract_reward18_staking6.stake(400e6); + uint256 timeOfLastUpdate = block.timestamp; + + // check balances/ownership of staked tokens + assertEq(erc20_staking6.balanceOf(address(stakeContract_reward18_staking6)), 400e6); + assertEq(erc20_staking6.balanceOf(address(stakerOne)), 600e6); + + // check available rewards right after staking + (uint256 _amountStaked, uint256 _availableRewards) = stakeContract_reward18_staking6.getStakeInfo(stakerOne); + + assertEq(_amountStaked, 400e6); + assertEq(_availableRewards, 0); + + //=================== warp ahead exactly 1 timeUnit: 60s + vm.roll(4); + vm.warp(61); + assertEq(timeUnit, block.timestamp - timeOfLastUpdate); + + // With 400 tokens staked, we expect 200 reward tokens earned + (, _availableRewards) = stakeContract_reward18_staking6.getStakeInfo(stakerOne); + console2.log("Expect 200 reward tokens. Amount earned: ", _availableRewards / 1e18); + assertEq(_availableRewards, 200e18); + } +} + +contract Macro_TokenStakeTest is BaseTest { + TokenStake internal stakeContract; + + uint256 internal timeUnit; + uint256 internal rewardsPerUnitTime; + uint256 internal rewardRatioNumerator; + uint256 internal rewardRatioDenominator; + uint256 internal tokenAmount = 100; + address internal stakerOne = address(0x345); + address internal stakerTwo = address(0x567); + + function setUp() public override { + super.setUp(); + + timeUnit = 60; + rewardRatioNumerator = 3; + rewardRatioDenominator = 50; + // mint 1000 tokens to stakerOne + erc20Aux.mint(stakerOne, tokenAmount); + // mint 1000 tokens to stakerOne + erc20Aux.mint(stakerTwo, tokenAmount); + // mint reward tokens to contract admin + erc20.mint(deployer, 1000 ether); + + stakeContract = TokenStake(getContract("TokenStake")); + + // set approvals + vm.prank(stakerOne); + erc20Aux.approve(address(stakeContract), type(uint256).max); + vm.prank(stakerTwo); + erc20Aux.approve(address(stakeContract), type(uint256).max); + + vm.prank(deployer); + erc20.transfer(address(stakeContract), 100 ether); + } + + // Demostrate setting unitTime to 0 locks the tokens irreversibly + function testToken_adminLockTokens() public { + //================ stake tokens + vm.warp(1); + + // Two users stake 1 tokens each + vm.prank(stakerOne); + stakeContract.stake(tokenAmount); + vm.prank(stakerTwo); + stakeContract.stake(tokenAmount); + + // set timeUnit to zero + uint256 newTimeUnit = 0; + vm.prank(deployer); + vm.expectRevert("time-unit can't be 0"); + stakeContract.setTimeUnit(newTimeUnit); + } + + function testToken_demostrate_adminRewardsLock() public { + //================ stake tokens + vm.warp(1); + // Two users stake 1 tokens each + vm.prank(stakerOne); + stakeContract.stake(tokenAmount); + vm.prank(stakerTwo); + stakeContract.stake(tokenAmount); + + // set timeUnit to a fraction of uint256 maximum value + uint256 newRewardsPerTimeUnit = type(uint256).max / 100; + vm.prank(deployer); + stakeContract.setRewardRatio(newRewardsPerTimeUnit, 1); + + vm.warp(1 days); + + // stakerOne and stakerTwo can't withdraw their tokens + // vm.expectRevert(stdError.arithmeticError); + vm.prank(stakerOne); + stakeContract.withdraw(tokenAmount); + + // vm.expectRevert(stdError.arithmeticError); + vm.prank(stakerTwo); + stakeContract.withdraw(tokenAmount); + + // rewardRatio can't be changed back + newRewardsPerTimeUnit = 60; + // vm.expectRevert(stdError.arithmeticError); + vm.prank(deployer); + stakeContract.setRewardRatio(newRewardsPerTimeUnit, 1); + } +} + +contract Macro_TokenStake_Tax is BaseTest { + TokenStake internal stakeContract; + uint256 internal tokenAmount = 100 ether; + address internal stakerOne = address(0x345); + address internal stakerTwo = address(0x567); + + function setUp() public override { + super.setUp(); + + stakeContract = TokenStake(getContract("TokenStake")); + + // mint reward tokens to contract admin + erc20.mint(deployer, tokenAmount); + // mint 100 tokens to stakers + erc20Aux.mint(stakerOne, tokenAmount); + erc20Aux.mint(stakerTwo, tokenAmount); + + // Activate Mock tax + erc20Aux.toggleTax(); + + vm.prank(stakerOne); + erc20Aux.approve(address(stakeContract), type(uint256).max); + + vm.prank(deployer); + erc20.transfer(address(stakeContract), 100 ether); + } + + // Demonstrate griefer can drain staked tokens for other users + function testToken_demonstrate_inaccurate_amount() public { + // First user stakes 100 tokens + vm.prank(stakerOne); + stakeContract.stake(tokenAmount); + + // Since there is 10% tax only 90 should be in the contract + uint256 stakingTokenBalance = erc20Aux.balanceOf(address(stakeContract)); + assertEq(stakingTokenBalance, 90 ether); + // Assert the amount was correctly assigned + (uint256 stakingTokenAmount, ) = stakeContract.getStakeInfo(stakerOne); + assertEq(stakingTokenAmount, 90 ether); + + // Users stake and withdraw tokens, draining other users staked balances + // for (uint256 i = 1; i <= 9; i++) { + // address staker = vm.addr(i); + // erc20Aux.mint(staker, tokenAmount); + // vm.startPrank(staker); + // erc20Aux.approve(address(stakeContract), type(uint256).max); + // stakeContract.stake(tokenAmount); + // stakeContract.withdraw(tokenAmount); + // vm.stopPrank(); + // } + + // // Staked amount still reamins unchanged for stakerOne + // (stakingTokenAmount, ) = stakeContract.getStakeInfo(stakerOne); + // assertEq(stakingTokenAmount, 100 ether); + + // // However there are no tokens left in the contract + // stakingTokenBalance = erc20Aux.balanceOf(address(stakeContract)); + // assertEq(stakingTokenBalance, 0 ether); + + // // StakerOne can't withdraw since there is no balance left + // vm.expectRevert("ERC20: transfer amount exceeds balance"); + // vm.prank(stakerOne); + // stakeContract.withdraw(stakingTokenAmount); + } }