Skip to content

Instantly share code, notes, and snippets.

@atisheksingh
Last active July 15, 2023 04:47
Show Gist options
  • Save atisheksingh/6e858c5511e2f489f062513bbe097a1e to your computer and use it in GitHub Desktop.
Save atisheksingh/6e858c5511e2f489f062513bbe097a1e to your computer and use it in GitHub Desktop.
staking contract with all the function
// 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