-
Notifications
You must be signed in to change notification settings - Fork 529
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
ERC20 staking prebuilt and extension #286
Merged
+1,343
−15
Merged
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
4ed683e
ERC20 staking extension and prebuilt
kumaryash90 8da82f0
tests for erc20 staking prebuilt
kumaryash90 af61ab4
base contract
kumaryash90 5f74d04
reward calculation in BPS
kumaryash90 f91fe45
reward ratio
kumaryash90 4c33956
withdraw function to prevent locking of reward tokens
kumaryash90 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,97 @@ | ||
// SPDX-License-Identifier: Apache-2.0 | ||
pragma solidity ^0.8.0; | ||
|
||
import "../extension/ContractMetadata.sol"; | ||
import "../extension/Multicall.sol"; | ||
import "../extension/Ownable.sol"; | ||
import "../extension/Staking20.sol"; | ||
|
||
import "../eip/interface/IERC20.sol"; | ||
|
||
/** | ||
* note: This is a Beta release. | ||
* | ||
* EXTENSION: Staking20 | ||
* | ||
* The `Staking20Base` smart contract implements Token staking mechanism. | ||
* Allows users to stake their ERC-20 Tokens and earn rewards in form of another ERC-20 tokens. | ||
* | ||
* Following features and implementation setup must be noted: | ||
* | ||
* - ERC-20 Tokens from only one contract can be staked. | ||
* | ||
* - Contract admin can choose to give out rewards by either transferring or minting the rewardToken, | ||
* which is ideally a different ERC20 token. See {_mintRewards}. | ||
* | ||
* - To implement custom logic for staking, reward calculation, etc. corresponding functions can be | ||
* overridden from the extension `Staking20`. | ||
* | ||
* - Ownership of the contract, with the ability to restrict certain functions to | ||
* only be called by the contract's owner. | ||
* | ||
* - Multicall capability to perform multiple actions atomically. | ||
* | ||
*/ | ||
contract Staking20Base is ContractMetadata, Multicall, Ownable, Staking20 { | ||
/// @dev ERC20 Reward Token address. See {_mintRewards} below. | ||
address public rewardToken; | ||
|
||
constructor( | ||
uint256 _timeUnit, | ||
uint256 _rewardRatioNumerator, | ||
uint256 _rewardRatioDenominator, | ||
address _stakingToken, | ||
address _rewardToken | ||
) Staking20(_stakingToken) { | ||
_setupOwner(msg.sender); | ||
_setTimeUnit(_timeUnit); | ||
_setRewardRatio(_rewardRatioNumerator, _rewardRatioDenominator); | ||
|
||
require(_rewardToken != _stakingToken, "Reward Token and Staking Token can't be same."); | ||
rewardToken = _rewardToken; | ||
} | ||
|
||
/*////////////////////////////////////////////////////////////// | ||
Minting logic | ||
//////////////////////////////////////////////////////////////*/ | ||
|
||
/** | ||
* @dev Mint ERC20 rewards to the staker. Must override. | ||
* | ||
* @param _staker Address for which to calculated rewards. | ||
* @param _rewards Amount of tokens to be given out as reward. | ||
* | ||
*/ | ||
function _mintRewards(address _staker, uint256 _rewards) internal override { | ||
// Mint or transfer reward-tokens here. | ||
// e.g. | ||
// | ||
// IERC20(rewardToken).transfer(_staker, _rewards); | ||
// | ||
// OR | ||
// | ||
// Use a mintable ERC20, such as thirdweb's `TokenERC20.sol` | ||
// | ||
// TokenERC20(rewardToken).mintTo(_staker, _rewards); | ||
// note: The staking contract should have minter role to mint tokens. | ||
} | ||
|
||
/*////////////////////////////////////////////////////////////// | ||
Other Internal functions | ||
//////////////////////////////////////////////////////////////*/ | ||
|
||
/// @dev Returns whether staking restrictions can be set in given execution context. | ||
function _canSetStakeConditions() internal view override returns (bool) { | ||
return msg.sender == owner(); | ||
} | ||
|
||
/// @dev Returns whether contract metadata can be set in the given execution context. | ||
function _canSetContractURI() internal view virtual override returns (bool) { | ||
return msg.sender == owner(); | ||
} | ||
|
||
/// @dev Returns whether owner can be set in the given execution context. | ||
function _canSetOwner() internal view virtual override returns (bool) { | ||
return msg.sender == owner(); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,281 @@ | ||
// SPDX-License-Identifier: Apache-2.0 | ||
pragma solidity ^0.8.11; | ||
|
||
import "../openzeppelin-presets/security/ReentrancyGuard.sol"; | ||
import "../eip/interface/IERC20.sol"; | ||
import "../lib/CurrencyTransferLib.sol"; | ||
|
||
import "./interface/IStaking20.sol"; | ||
|
||
/** | ||
* note: This is a Beta release. | ||
*/ | ||
|
||
abstract contract Staking20 is ReentrancyGuard, IStaking20 { | ||
/*/////////////////////////////////////////////////////////////// | ||
State variables / Mappings | ||
//////////////////////////////////////////////////////////////*/ | ||
|
||
///@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 Rewards ratio is the number of reward tokens for a number of staked tokens, per unit of time. | ||
uint256 public rewardRatioNumerator; | ||
|
||
///@dev Rewards ratio is the number of reward tokens for a number of staked tokens, per unit of time. | ||
uint256 public rewardRatioDenominator; | ||
|
||
///@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; | ||
|
||
constructor(address _token) ReentrancyGuard() { | ||
require(address(_token) != address(0), "address 0"); | ||
token = _token; | ||
} | ||
|
||
/*/////////////////////////////////////////////////////////////// | ||
External/Public Functions | ||
//////////////////////////////////////////////////////////////*/ | ||
|
||
/** | ||
* @notice Stake ERC20 Tokens. | ||
* | ||
* @dev See {_stake}. Override that to implement custom logic. | ||
* | ||
* @param _amount Amount to stake. | ||
*/ | ||
function stake(uint256 _amount) external nonReentrant { | ||
_stake(_amount); | ||
} | ||
|
||
/** | ||
* @notice Withdraw staked ERC20 tokens. | ||
* | ||
* @dev See {_withdraw}. Override that to implement custom logic. | ||
* | ||
* @param _amount Amount to withdraw. | ||
*/ | ||
function withdraw(uint256 _amount) external nonReentrant { | ||
_withdraw(_amount); | ||
} | ||
|
||
/** | ||
* @notice Claim accumulated rewards. | ||
* | ||
* @dev See {_claimRewards}. Override that to implement custom logic. | ||
* See {_calculateRewards} for reward-calculation logic. | ||
*/ | ||
function claimRewards() external nonReentrant { | ||
_claimRewards(); | ||
} | ||
|
||
/** | ||
* @notice Set time unit. Set as a number of seconds. | ||
* Could be specified as -- x * 1 hours, x * 1 days, etc. | ||
* | ||
* @dev Only admin/authorized-account can call it. | ||
* | ||
* @param _timeUnit New time unit. | ||
*/ | ||
function setTimeUnit(uint256 _timeUnit) external virtual { | ||
if (!_canSetStakeConditions()) { | ||
revert("Not authorized"); | ||
} | ||
|
||
_updateUnclaimedRewardsForAll(); | ||
|
||
uint256 currentTimeUnit = timeUnit; | ||
_setTimeUnit(_timeUnit); | ||
|
||
emit UpdatedTimeUnit(currentTimeUnit, _timeUnit); | ||
} | ||
|
||
/** | ||
* @notice Set rewards per unit of time. | ||
* Interpreted as (numerator/denominator) rewards per second/per day/etc based on time-unit. | ||
* | ||
* For e.g., ratio of 1/20 would mean 1 reward token for every 20 tokens staked. | ||
* | ||
* @dev Only admin/authorized-account can call it. | ||
* | ||
* @param _numerator Reward ratio numerator. | ||
* @param _denominator Reward ratio denominator. | ||
*/ | ||
function setRewardRatio(uint256 _numerator, uint256 _denominator) external virtual { | ||
if (!_canSetStakeConditions()) { | ||
revert("Not authorized"); | ||
} | ||
|
||
_updateUnclaimedRewardsForAll(); | ||
|
||
uint256 currentNumerator = rewardRatioNumerator; | ||
uint256 currentDenominator = rewardRatioDenominator; | ||
_setRewardRatio(_numerator, _denominator); | ||
|
||
emit UpdatedRewardRatio(currentNumerator, _numerator, currentDenominator, _denominator); | ||
} | ||
|
||
/** | ||
* @notice View amount staked and rewards for a user. | ||
* | ||
* @param _staker Address for which to calculated rewards. | ||
* @return _tokensStaked Amount of tokens staked. | ||
* @return _rewards Available reward amount. | ||
*/ | ||
function getStakeInfo(address _staker) public view virtual returns (uint256 _tokensStaked, uint256 _rewards) { | ||
_tokensStaked = stakers[_staker].amountStaked; | ||
_rewards = _availableRewards(_staker); | ||
} | ||
|
||
/*/////////////////////////////////////////////////////////////// | ||
Internal Functions | ||
//////////////////////////////////////////////////////////////*/ | ||
|
||
/// @dev Staking logic. Override to add custom logic. | ||
function _stake(uint256 _amount) internal virtual { | ||
require(_amount != 0, "Staking 0 tokens"); | ||
address _token = token; | ||
|
||
if (stakers[msg.sender].amountStaked > 0) { | ||
_updateUnclaimedRewardsForStaker(msg.sender); | ||
} else { | ||
stakersArray.push(msg.sender); | ||
stakers[msg.sender].timeOfLastUpdate = block.timestamp; | ||
} | ||
|
||
CurrencyTransferLib.transferCurrency(_token, msg.sender, address(this), _amount); | ||
|
||
stakers[msg.sender].amountStaked += _amount; | ||
|
||
emit TokensStaked(msg.sender, _amount); | ||
} | ||
Comment on lines
+141
to
+157
Check notice Code scanning / Slither Reentrancy vulnerabilities
Reentrancy in Staking20._stake(uint256) (contracts/extension/Staking20.sol#134-150):
External calls:
- CurrencyTransferLib.transferCurrency(_token,msg.sender,address(this),_amount) (contracts/extension/Staking20.sol#145)
Event emitted after the call(s):
- TokensStaked(msg.sender,_amount) (contracts/extension/Staking20.sol#149)
|
||
|
||
/// @dev Withdraw logic. Override to add custom logic. | ||
function _withdraw(uint256 _amount) internal virtual { | ||
uint256 _amountStaked = stakers[msg.sender].amountStaked; | ||
require(_amount != 0, "Withdrawing 0 tokens"); | ||
require(_amountStaked >= _amount, "Withdrawing more than staked"); | ||
|
||
_updateUnclaimedRewardsForStaker(msg.sender); | ||
|
||
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]; | ||
stakersArray.pop(); | ||
break; | ||
} | ||
} | ||
} | ||
stakers[msg.sender].amountStaked -= _amount; | ||
|
||
CurrencyTransferLib.transferCurrency(token, address(this), msg.sender, _amount); | ||
|
||
emit TokensWithdrawn(msg.sender, _amount); | ||
} | ||
Comment on lines
+160
to
+182
Check notice Code scanning / Slither Reentrancy vulnerabilities
Reentrancy in Staking20._withdraw(uint256) (contracts/extension/Staking20.sol#153-175):
External calls:
- CurrencyTransferLib.transferCurrency(token,address(this),msg.sender,_amount) (contracts/extension/Staking20.sol#172)
Event emitted after the call(s):
- TokensWithdrawn(msg.sender,_amount) (contracts/extension/Staking20.sol#174)
|
||
|
||
/// @dev Logic for claiming rewards. Override to add custom logic. | ||
function _claimRewards() internal virtual { | ||
uint256 rewards = stakers[msg.sender].unclaimedRewards + _calculateRewards(msg.sender); | ||
|
||
require(rewards != 0, "No rewards"); | ||
|
||
stakers[msg.sender].timeOfLastUpdate = block.timestamp; | ||
stakers[msg.sender].unclaimedRewards = 0; | ||
|
||
_mintRewards(msg.sender, rewards); | ||
|
||
emit RewardsClaimed(msg.sender, rewards); | ||
} | ||
|
||
/// @dev View available rewards for a user. | ||
function _availableRewards(address _staker) internal view virtual returns (uint256 _rewards) { | ||
if (stakers[_staker].amountStaked == 0) { | ||
_rewards = stakers[_staker].unclaimedRewards; | ||
} else { | ||
_rewards = stakers[_staker].unclaimedRewards + _calculateRewards(_staker); | ||
} | ||
} | ||
|
||
/// @dev Update unclaimed rewards for 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; | ||
} | ||
|
||
/// @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 { | ||
require(_denominator != 0, "divide by 0"); | ||
rewardRatioNumerator = _numerator; | ||
rewardRatioDenominator = _denominator; | ||
} | ||
|
||
/// @dev Reward calculation logic. Override to implement custom logic. | ||
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); | ||
} | ||
|
||
/** | ||
* @dev Mint/Transfer ERC20 rewards to the staker. Must override. | ||
* | ||
* @param _staker Address for which to calculated rewards. | ||
* @param _rewards Amount of tokens to be given out as reward. | ||
* | ||
* For example, override as below to mint ERC20 rewards: | ||
* | ||
* ``` | ||
* function _mintRewards(address _staker, uint256 _rewards) internal override { | ||
* | ||
* TokenERC20(rewardTokenAddress).mintTo(_staker, _rewards); | ||
* | ||
* } | ||
* ``` | ||
*/ | ||
function _mintRewards(address _staker, uint256 _rewards) internal virtual; | ||
|
||
/** | ||
* @dev Returns whether staking restrictions can be set in given execution context. | ||
* Must override. | ||
* | ||
* | ||
* For example, override as below to restrict access to admin: | ||
* | ||
* ``` | ||
* function _canSetStakeConditions() internal override { | ||
* | ||
* return msg.sender == adminAddress; | ||
* | ||
* } | ||
* ``` | ||
*/ | ||
function _canSetStakeConditions() internal view virtual returns (bool); | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Check warning
Code scanning / Slither
Reentrancy vulnerabilities