Last active
July 15, 2023 04:47
-
-
Save atisheksingh/6e858c5511e2f489f062513bbe097a1e to your computer and use it in GitHub Desktop.
staking contract with all the function
This file contains hidden or 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
// SPDX-License-Identifier: UNLICENSED | |
pragma solidity ^0.8.0; | |
import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/ERC20.sol"; | |
import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/utils/SafeERC20.sol"; | |
import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/access/Ownable.sol"; | |
import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/security/ReentrancyGuard.sol"; | |
import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/utils/SafeERC20.sol"; | |
contract Asva is ERC20 { | |
constructor() ERC20("asva", "asva") { | |
_mint(msg.sender, 10000 * 10 ** decimals()); | |
} | |
} | |
contract AsvaStaking is Ownable, ReentrancyGuard { | |
using SafeERC20 for IERC20; | |
IERC20 public immutable asvaToken; | |
/// @notice timestamp when the rewards period will finish, can be updated if wants to end earlier | |
uint256 public periodFinish; | |
uint256 public rewardRate; | |
mapping(StakingPeriod => uint256) rewardRateX; | |
uint256 public rewardsDuration = 365 days; | |
/// @notice time when the rewards mapping or the rewardsPerTokenStored(APY) was last updated | |
uint256 public lastUpdateTime; | |
/// @notice reward amount for every staking token (similar to APY) | |
uint256 public rewardPerTokenStored; | |
uint256 public lastPauseTime; | |
bool public paused; | |
/// @notice rewards already added to the rewards mapping during the last update | |
mapping(address => uint256) public userRewardPerTokenPaid; | |
/// @notice rewards amount per account | |
mapping(address => uint256) public rewards; | |
/// @notice total supply of the staking token in this contract | |
uint256 private _totalSupply; | |
/// @notice balance of the staking token per account | |
mapping(address => uint256) private _balances; | |
/// @notice staking period for a particular account's funds | |
mapping(address => uint256) public stakingPeriod; | |
mapping(address => StakingPeriod) public userToStakingPeriod; | |
uint256 public constant WITHDRAWAL_FEE = 20; // rate_X_100 // 0.2% fee | |
enum StakingPeriod { | |
THIRTY, | |
SIXTY, | |
NINETY | |
} | |
event RewardAdded(uint256 reward); | |
event Staked(address indexed user, uint256 amount); | |
event Withdrawn(address indexed user, uint256 amount); | |
event RewardClaimed(address indexed user, uint256 reward); | |
event RewardReDeposited(address indexed user, uint256 reward); | |
event RewardsDurationUpdated(uint256 newDuration); | |
event Recovered(address token, uint256 amount); | |
event PauseChanged(bool paused); | |
/// @notice Updates the rewards mapping for the user | |
/// @param _account the account whose rewards is getting updated | |
modifier updateReward(address _account) { | |
// using with local variable | |
StakingPeriod daysStaking = userToStakingPeriod[_account]; | |
rewardPerTokenStored = rewardPerToken(daysStaking); | |
lastUpdateTime = lastTimeRewardApplicable(); | |
if (_account != address(0)) { | |
rewards[_account] = earned(_account); | |
userRewardPerTokenPaid[_account] = rewardPerTokenStored; | |
} | |
_; | |
} | |
/// @notice checks if the contract is paused because of bugs | |
modifier notPaused() { | |
require(!paused, "AsvaStaking: Paused"); | |
_; | |
} | |
constructor(address _asvaToken, address _owner) { | |
asvaToken = IERC20(_asvaToken); | |
rewardRateX[StakingPeriod.THIRTY] = 2000; // 20% | |
rewardRateX[StakingPeriod.SIXTY] = 2500; // 25% | |
rewardRateX[StakingPeriod.NINETY] = 3000; // 30% | |
transferOwnership(_owner); | |
} | |
/// @notice returns the total supply of the staking token | |
function totalSupply() external view returns (uint256) { | |
return _totalSupply; | |
} | |
/// @notice returns the balance of the staking token for a particular account | |
function balanceOf(address _account) external view returns (uint256) { | |
return _balances[_account]; | |
} | |
/// @notice returns the total reward that will be formed at the end of the rewards distribution | |
/// @dev according to the current reward rate | |
function getRewardForDuration() external view returns (uint256) { | |
return rewardRate * rewardsDuration; | |
} | |
/// @notice help to stake the $ASVA tokens | |
/// @param _amount the amount to stake | |
/// @param _stakingPeriod Unix timestamp for the end of the stake | |
function stake(uint256 _amount, StakingPeriod _stakingPeriod) | |
external | |
notPaused | |
nonReentrant | |
updateReward(msg.sender) | |
{ | |
require(_amount > 0, "AsvaStaking stake: Cannot stake 0"); | |
// require( | |
// _stakingPeriod >= block.timestamp && _stakingPeriod <= periodFinish, | |
// "AsvaStaking stake: staking for more than the rewards distribution period" | |
// ); | |
reInvestRewards(); | |
_totalSupply += _amount; | |
_balances[msg.sender] += _amount; | |
// we can give a slider for the same in days/weeks, calculate the timestamp in the client side | |
// stakingPeriod[msg.sender] = _stakingPeriod; | |
// if the person stakes more again with a new staking period, this staking period should be more than the initial one | |
// increaseStakingPeriod(_stakingPeriod); // ## additons // for first time stake, this would set a staking period | |
userToStakingPeriod[msg.sender] = _stakingPeriod; | |
if (_stakingPeriod == StakingPeriod.THIRTY) { | |
stakingPeriod[msg.sender] = block.timestamp + 30 days; | |
} else if (_stakingPeriod == StakingPeriod.SIXTY) { | |
stakingPeriod[msg.sender] = block.timestamp + 60 days; | |
} else { | |
stakingPeriod[msg.sender] = block.timestamp + 90 days; | |
} | |
asvaToken.safeTransferFrom(msg.sender, address(this), _amount); | |
emit Staked(msg.sender, _amount); | |
} | |
/// @notice re-deposit the rewards formed to the staked balance of the user | |
function reInvestRewards() internal { | |
uint256 reward = rewards[msg.sender]; | |
if (reward > 0) { | |
rewards[msg.sender] = 0; | |
// reward re-deposited | |
_totalSupply += reward; | |
_balances[msg.sender] += reward; | |
emit RewardReDeposited(msg.sender, reward); | |
} | |
} | |
/// @notice help to unstake the $ASVA tokens | |
/// @param _amount the amount to unstake | |
function withdraw(uint256 _amount) | |
public | |
nonReentrant | |
updateReward(msg.sender) | |
{ | |
require(_amount > 0, "AsvaStaking withdraw: Cannot withdraw 0"); | |
require( | |
_amount <= _balances[msg.sender], | |
"AsvaStaking withdraw: Cannot withdraw more than staked" | |
); | |
if (stakingPeriod[msg.sender] > block.timestamp) { | |
uint256 fee = (_amount * WITHDRAWAL_FEE) / 1e4; | |
_amount -= fee; | |
// transfer the fees to somewhere to burn or add to rewards/ distribute to Asva stakers | |
} | |
_totalSupply -= _amount; | |
_balances[msg.sender] -= _amount; // for overflow/ underflow support in 0.8 version, it will automatically revert for amount greater than the balance | |
getReward(); | |
asvaToken.safeTransfer(msg.sender, _amount); | |
emit Withdrawn(msg.sender, _amount); | |
} | |
/// @notice help to claim the $ASVA rewards formed yet | |
function getReward() | |
public | |
updateReward(msg.sender) | |
returns (uint256) | |
{ | |
// require( | |
// stakingPeriod[msg.sender] <= block.timestamp, | |
// "AsvaStaking getReward: You can claim your $ASVA token rewards after the staking period" | |
// ); | |
uint256 reward = rewards[msg.sender]; | |
if (reward > 0) { | |
rewards[msg.sender] = 0; | |
asvaToken.safeTransfer(msg.sender, reward); | |
emit RewardClaimed(msg.sender, reward); | |
} | |
return reward; | |
} | |
/// @notice help to exit your position with unstake and claiming the rewards in one txn | |
function exit() external { | |
withdraw(_balances[msg.sender]); | |
getReward(); | |
} | |
// ############ onlyOwner Functions ############## | |
/// @notice to set the reward amount | |
/// @dev rewardAmount should not exceed the rewardToken balance of this contract | |
/// @dev rewardsDuration should be fixed before hand, bcz the periodFinish will be calculated here | |
/// @param _reward the amount to be fixed for the rewards | |
function notifyRewardAmount(uint256 _reward) | |
external | |
onlyOwner | |
updateReward(address(0)) | |
{ | |
if (block.timestamp >= periodFinish) { | |
rewardRate = _reward / rewardsDuration; | |
periodFinish = block.timestamp + rewardsDuration; | |
} else { | |
uint256 remaining = periodFinish - block.timestamp; | |
uint256 leftover = remaining * rewardRate; | |
rewardRate = (_reward + leftover) / rewardsDuration; | |
} | |
// Ensure the provided reward amount is not more than the balance in the contract. | |
// This keeps the reward rate in the right range, preventing overflows due to | |
// very high values of rewardRate in the earned and rewardsPerToken functions; | |
// Reward + leftover must be less than 2^256 / 10^18 to avoid overflow. | |
uint256 rewardTokenBalance = asvaToken.balanceOf(address(this)) - | |
_totalSupply; | |
// Here, rewardsToken type is same as stakingToken type, this will give added balance for rewards as well as staked totalSupply, see for it | |
// so we need to sub the total supply of the staked token from the asva token balance | |
require( | |
rewardRate <= rewardTokenBalance / rewardsDuration, | |
"AsvaStaking notifyRewardAmount: Provided reward too high" | |
); | |
lastUpdateTime = block.timestamp; | |
emit RewardAdded(_reward); | |
} | |
// End rewards emission earlier | |
function updatePeriodFinish(uint256 timestamp) | |
external | |
onlyOwner | |
updateReward(address(0)) | |
{ | |
require( | |
timestamp >= block.timestamp && timestamp < periodFinish, | |
"AsvaStaking updatePeriodFinish: Invalid timestamp" | |
); | |
periodFinish = timestamp; | |
} | |
/// @notice set the rewards duration for a reward period | |
/// @dev can only be updated before fixing the periodFinish, basically can't be updated in the middle | |
/// @param _rewardsDuration duration for the rewards distribution in days, weeks, etc. | |
function setRewardsDuration(uint256 _rewardsDuration) external onlyOwner { | |
require( | |
block.timestamp > periodFinish, | |
"AsvaStaking setRewardsDuration: Previous rewards period must be complete before changing the duration for the new period" | |
); | |
rewardsDuration = _rewardsDuration; | |
emit RewardsDurationUpdated(rewardsDuration); | |
} | |
/// @notice help to recover any ERC20 token accidentally sent to the contract | |
/// @dev other than the staking token | |
function recoverERC20(address _tokenAddress, uint256 _tokenAmount) | |
external | |
onlyOwner | |
{ | |
require( | |
_tokenAddress != address(asvaToken), | |
"AsvaStaking recoverERC20: Cannot withdraw the staking token" | |
); | |
IERC20(_tokenAddress).safeTransfer(owner(), _tokenAmount); | |
emit Recovered(_tokenAddress, _tokenAmount); | |
} | |
function setPaused(bool _paused) external onlyOwner { | |
require( | |
_paused != paused, | |
"AsvaStaking setPaused: Paused state already set" | |
); | |
paused = _paused; | |
if (_paused) lastPauseTime = block.timestamp; | |
emit PauseChanged(paused); | |
} | |
// ########################################## | |
/// @notice gives the current time, or the finishing period if the rewards distribution is over | |
/// @dev Explain to a developer any extra details | |
/// @return the time for this update | |
function lastTimeRewardApplicable() public view returns (uint256) { | |
return block.timestamp < periodFinish ? block.timestamp : periodFinish; | |
} | |
/// @notice returns what should be the reward for each staking token | |
/// @dev if all the staking tokens are withdrawn, it would return the last update | |
/// @return reward per staked token (APY) | |
// function rewardPerToken() public view returns (uint256) { | |
// return | |
// _totalSupply == 0 | |
// ? rewardPerTokenStored | |
// : rewardPerTokenStored + | |
// (((lastTimeRewardApplicable() - lastUpdateTime) * | |
// rewardRate * | |
// 1e18) / _totalSupply); | |
// } | |
function rewardPerToken(StakingPeriod _stakingPeriod) | |
public | |
view | |
returns (uint256) | |
{ | |
return | |
_totalSupply == 0 | |
? rewardPerTokenStored | |
: rewardPerTokenStored + | |
(((lastTimeRewardApplicable() - lastUpdateTime) * | |
rewardRateX[_stakingPeriod] * | |
1e14 * 1e12) / | |
(rewardsDuration * _totalSupply)); // 1e18 / 1e4 = 1e14 | |
} | |
/// @notice returns the claimable rewards amount | |
function earned(address _account) public view returns (uint256) { | |
StakingPeriod daysStaking = userToStakingPeriod[_account]; | |
return | |
rewards[_account] + | |
((_balances[_account] * | |
(rewardPerToken(daysStaking) - | |
userRewardPerTokenPaid[_account])) / (1e18 * 1e12)); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment