Skip to content
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
merged 6 commits into from
Nov 29, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 97 additions & 0 deletions contracts/base/Staking20Base.sol
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();
}
}
281 changes: 281 additions & 0 deletions contracts/extension/Staking20.sol
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 warning

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) State variables written after the call(s): - stakers[msg.sender].amountStaked += _amount (contracts/extension/Staking20.sol#147)
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);
}
Loading