Created
November 18, 2024 17:12
-
-
Save redmont/0bdd88e51246c0860f783726cb162101 to your computer and use it in GitHub Desktop.
Created using remix-ide: Realtime Ethereum Contract Compiler and Runtime. Load this file by pasting this gists URL or ID at https://remix.ethereum.org/#version=soljson-v0.8.26+commit.8a97fa7a.js&optimize=false&runs=200&gist=
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.26; | |
| import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; | |
| import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; | |
| import "@openzeppelin/contracts/access/Ownable.sol"; | |
| import "hardhat/console.sol"; | |
| contract RWGStakingRewards is ReentrancyGuard, Ownable { | |
| IERC20 public immutable stakingToken; | |
| IERC20 public immutable rewardToken; | |
| uint256 private constant MULTIPLIER = 100; | |
| uint256 public epochDuration = 7; // 7 seconds per epoch (scaled down from 1 week) | |
| uint256 public currentEpoch; | |
| uint256 private defaultEpochRewards = 100; | |
| struct Tier { | |
| uint256 lockPeriod; | |
| uint256 multiplier; | |
| } | |
| Tier[5] private tiers; | |
| struct Stake { | |
| uint256 amount; | |
| uint256 effectiveAmount; | |
| uint256 tierIndex; | |
| uint256 startTime; | |
| uint256 lastClaimEpoch; | |
| } | |
| mapping(address => Stake[]) public userStakes; | |
| mapping(uint256 => uint256) public rewardsPerEpoch; | |
| uint256 public totalEffectiveSupply; | |
| event Staked(address indexed user, uint256 amount, uint256 tierIndex); | |
| event Unstaked(address indexed user, uint256 amount); | |
| event RewardClaimed(address indexed user, uint256 amount); | |
| event RewardSet(uint256 indexed epoch, uint256 amount); | |
| event EpochDurationUpdated(uint256 newDuration); | |
| constructor(address _stakingToken, address _rewardToken) | |
| Ownable(msg.sender) | |
| { | |
| stakingToken = IERC20(_stakingToken); | |
| rewardToken = IERC20(_rewardToken); | |
| tiers[0] = Tier(90, 10); // 90 seconds, 0.1x | |
| tiers[1] = Tier(180, 50); // 180 seconds, 0.5x | |
| tiers[2] = Tier(360, 110); // 360 seconds, 1.1x | |
| tiers[3] = Tier(720, 150); // 720 seconds, 1.5x | |
| tiers[4] = Tier(1440, 210); // 1440 seconds, 2.1x | |
| currentEpoch = block.timestamp / epochDuration; | |
| } | |
| function setRewardForEpoch(uint256 epoch, uint256 reward) | |
| external | |
| onlyOwner | |
| { | |
| require(epoch >= currentEpoch, "Cannot set reward for past epochs"); | |
| rewardsPerEpoch[epoch] = reward; | |
| emit RewardSet(epoch, reward); | |
| } | |
| function setEpochDuration(uint256 newDuration) external onlyOwner { | |
| require(newDuration > 0, "Epoch duration must be greater than 0"); | |
| epochDuration = newDuration; | |
| emit EpochDurationUpdated(newDuration); | |
| } | |
| function stake(uint256 amount, uint256 tierIndex) external nonReentrant { | |
| require(amount > 0, "Cannot stake 0 amount"); | |
| require(tierIndex < 5, "Invalid tier index"); | |
| updateCurrentEpoch(); | |
| uint256 effectiveAmount = (amount * tiers[tierIndex].multiplier) / 100; | |
| totalEffectiveSupply += effectiveAmount; | |
| userStakes[msg.sender].push( | |
| Stake({ | |
| amount: amount, | |
| effectiveAmount: effectiveAmount, | |
| tierIndex: tierIndex, | |
| startTime: block.timestamp, | |
| lastClaimEpoch: currentEpoch | |
| }) | |
| ); | |
| // require( | |
| // stakingToken.transferFrom(msg.sender, address(this), amount), | |
| // "Stake transfer failed" | |
| // ); | |
| emit Staked(msg.sender, amount, tierIndex); | |
| } | |
| function unstake(uint256 stakeIndex) external nonReentrant { | |
| require( | |
| stakeIndex < userStakes[msg.sender].length, | |
| "Invalid stake index" | |
| ); | |
| Stake storage userStake = userStakes[msg.sender][stakeIndex]; | |
| console.log("block.timestamp",block.timestamp); | |
| console.log("user lock end timestamp:",userStake.startTime + tiers[userStake.tierIndex].lockPeriod); | |
| require( | |
| block.timestamp >= | |
| userStake.startTime + tiers[userStake.tierIndex].lockPeriod, | |
| "Lock period not ended" | |
| ); | |
| updateCurrentEpoch(); | |
| claimRewards(stakeIndex); | |
| uint256 amount = userStake.amount; | |
| totalEffectiveSupply -= userStake.effectiveAmount; | |
| // Remove the stake by swapping with the last element and popping | |
| userStakes[msg.sender][stakeIndex] = userStakes[msg.sender][ | |
| userStakes[msg.sender].length - 1 | |
| ]; | |
| userStakes[msg.sender].pop(); | |
| // require( | |
| // stakingToken.transfer(msg.sender, amount), | |
| // "Unstake transfer failed" | |
| // ); | |
| emit Unstaked(msg.sender, amount); | |
| } | |
| function claimRewards(uint256 stakeIndex) public nonReentrant { | |
| require( | |
| stakeIndex < userStakes[msg.sender].length, | |
| "Invalid stake index" | |
| ); | |
| updateCurrentEpoch(); | |
| Stake storage userStake = userStakes[msg.sender][stakeIndex]; | |
| uint256 reward = calculateRewards(stakeIndex); | |
| if (reward > 0) { | |
| userStake.lastClaimEpoch = currentEpoch; | |
| // require( | |
| // rewardToken.transfer(msg.sender, reward), | |
| // "Reward transfer failed" | |
| // ); | |
| emit RewardClaimed(msg.sender, reward); | |
| } | |
| } | |
| function calculateRewards(uint256 stakeIndex) | |
| public | |
| view | |
| returns (uint256) | |
| { | |
| require( | |
| stakeIndex < userStakes[msg.sender].length, | |
| "Invalid stake index" | |
| ); | |
| Stake memory userStake = userStakes[msg.sender][stakeIndex]; | |
| uint256 reward = 0; | |
| uint256 lastEpoch = block.timestamp / epochDuration; | |
| // if ( | |
| // block.timestamp >= | |
| // userStake.startTime + tiers[userStake.tierIndex].lockPeriod | |
| // ) { | |
| // lastEpoch = | |
| // (userStake.startTime + tiers[userStake.tierIndex].lockPeriod) / | |
| // epochDuration; | |
| // } | |
| console.log("user lastClaimEpoch: ", userStake.lastClaimEpoch); | |
| console.log("lastEpoch: ",lastEpoch); | |
| for ( | |
| uint256 epoch = userStake.lastClaimEpoch; | |
| epoch < lastEpoch; | |
| epoch++ | |
| ) { | |
| uint256 epochReward = getRewardsForEpoch(epoch); | |
| reward += | |
| (epochReward * userStake.effectiveAmount) / | |
| totalEffectiveSupply; | |
| } | |
| return reward; | |
| } | |
| function updateCurrentEpoch() private { | |
| currentEpoch = block.timestamp / epochDuration; | |
| } | |
| function getUserStakes(address user) | |
| external | |
| view | |
| returns (Stake[] memory) | |
| { | |
| return userStakes[user]; | |
| } | |
| function getRewardsForEpoch(uint256 epoch) private view returns (uint256) { | |
| uint256 reward = rewardsPerEpoch[epoch]; | |
| if (reward == 0 && epoch > 0) { | |
| reward = rewardsPerEpoch[epoch - 1]; | |
| } | |
| if (reward == 0) { | |
| reward = defaultEpochRewards; | |
| } | |
| return reward; | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment