Last active
February 21, 2023 23:25
-
-
Save casweeney/389e5b50519458b431d2a2900260849f to your computer and use it in GitHub Desktop.
Staking Rewards contract with multiple staking positions for a particular user (address)
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: MIT | |
| pragma solidity ^0.8.13; | |
| import "./IERC20.sol"; | |
| contract AtomicStaking { | |
| IERC20 public rewardToken; | |
| IERC20 public stakingToken; | |
| address owner; | |
| uint256 public duration; | |
| uint256 public finishAt; | |
| uint256 public updatedAt; | |
| uint256 public rewardRate; | |
| uint256 public rewardPerTokenStored; | |
| uint256 public totalStake; | |
| struct StakingPosition { | |
| uint256 balance; // amount staked for this position | |
| uint256 reward; // reward earned for this position | |
| uint256 rewardPerToken; | |
| } | |
| mapping(address => StakingPosition[]) public userStakingPositions; | |
| constructor(address _rewardToken, address _stakingToken) { | |
| owner = msg.sender; | |
| rewardToken = IERC20(_rewardToken); | |
| stakingToken = IERC20(_stakingToken); | |
| } | |
| modifier onlyOwner() { | |
| require(msg.sender == owner); | |
| _; | |
| } | |
| modifier updateReward(address _account, uint256 _positionIndex) { | |
| rewardPerTokenStored = rewardPerToken(); | |
| updatedAt = lastTimeRewardApplicable(); | |
| if(_account != address(0)) { | |
| if(userStakingPositions[_account].length != 0){ | |
| uint lastIndex = _positionIndex - 1; | |
| userStakingPositions[_account][lastIndex].reward = rewardsEarned(_account, lastIndex); | |
| userStakingPositions[_account][lastIndex].rewardPerToken = rewardPerTokenStored; | |
| } | |
| } | |
| _; | |
| } | |
| function setRewardDuration(uint256 _duration) external onlyOwner { | |
| require(block.timestamp > finishAt, "duration for reward not ended"); | |
| duration = _duration; | |
| } | |
| function setRewardAmount(uint256 _amount) external onlyOwner updateReward(address(0), 0) { | |
| require(duration > 0, "reward duration = 0"); | |
| if (block.timestamp > finishAt) { | |
| rewardRate = _amount / duration; | |
| } else { | |
| uint256 remainingRewards = rewardRate * (finishAt - block.timestamp); | |
| rewardRate = (remainingRewards + _amount) / duration; | |
| } | |
| require(rewardRate > 0, "reward rate = 0"); | |
| require(rewardRate * duration <= rewardToken.balanceOf(address(this)), "insufficient reward token"); | |
| finishAt = block.timestamp + duration; | |
| updatedAt = block.timestamp; | |
| } | |
| function stake(uint256 _amount) external updateReward(msg.sender, userNextPositionIndex(msg.sender)) { | |
| require(_amount > 0, "can't stake zero amount"); | |
| require(duration > 0, "duration not set"); | |
| require(rewardRate > 0, "reward rate not set"); | |
| stakingToken.transferFrom(msg.sender, address(this), _amount); | |
| StakingPosition memory sp = StakingPosition({balance: _amount, reward: 0, rewardPerToken: 0}); | |
| userStakingPositions[msg.sender].push(sp); | |
| totalStake += _amount; | |
| } | |
| function withdrawToken(uint256 _amount, uint256 _positionIndex) external updateReward(msg.sender, _positionIndex + 1) { | |
| require(_positionIndex <= (userStakingPositions[msg.sender].length - 1), "invalid position index"); | |
| require(_amount > 0, "can't withdraw zero amount"); | |
| userStakingPositions[msg.sender][_positionIndex].balance -= _amount; | |
| totalStake -= _amount; | |
| } | |
| function lastTimeRewardApplicable() public view returns (uint256) { | |
| return _min(block.timestamp, finishAt); | |
| } | |
| function rewardPerToken() public view returns (uint256) { | |
| if (totalStake == 0) { | |
| return rewardPerTokenStored; | |
| } | |
| return rewardPerTokenStored + (rewardRate * (lastTimeRewardApplicable() - updatedAt) * 1e18) / totalStake; | |
| } | |
| function rewardsEarned(address _account, uint256 _positionIndex) public view returns (uint256) { | |
| if(userStakingPositions[_account].length == 0){ | |
| return 0; | |
| } | |
| return ((userStakingPositions[_account][_positionIndex].balance * (rewardPerToken() - userStakingPositions[_account][_positionIndex].rewardPerToken)) / 1e18) + userStakingPositions[_account][_positionIndex].reward; | |
| } | |
| function getReward(uint256 _positionIndex) external updateReward(msg.sender, _positionIndex + 1) { | |
| require(_positionIndex <= (userStakingPositions[msg.sender].length - 1), "invalid position index"); | |
| uint256 reward = userStakingPositions[msg.sender][_positionIndex].reward; | |
| if (reward > 0) { | |
| userStakingPositions[msg.sender][_positionIndex].reward = 0; | |
| rewardToken.transfer(msg.sender, reward); | |
| } | |
| } | |
| function userNextPositionIndex(address _account) public view returns (uint256) { | |
| return userStakingPositions[_account].length; | |
| } | |
| function _min(uint256 x, uint256 y) private pure returns (uint256) { | |
| return x <= y ? x : y; | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment