Created
November 18, 2024 20:52
-
-
Save redmont/1d53cfeb7ad0912852203231f1d45b5d to your computer and use it in GitHub Desktop.
Revisions
-
redmont created this gist
Nov 18, 2024 .There are no files selected for viewing
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 charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,249 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; //import "hardhat/console.sol"; contract RWGStakingRewards is ReentrancyGuard, Ownable { IERC20 public immutable STAKING_TOKEN; IERC20 public immutable REWARD_TOKEN; uint256 private constant MULTIPLIER = 100; uint256 public epochDuration = 7; // 7 seconds per epoch (scaled down from 1 week) uint256 public currentEpoch; uint256 public votingDelay = 1; // 1 second delay (scaled down from 1 day) uint256 private defaultEpochRewards = 100; struct Tier { uint256 lockPeriod; uint256 multiplier; } Tier[] public 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; mapping(address => mapping(uint256 => bool)) public hasVoted; 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); event TierAdded(uint256 lockPeriod, uint256 multiplier); event TierUpdated(uint256 index, uint256 lockPeriod, uint256 multiplier); event VotingDelayUpdated(uint256 newDelay); error MultiplierMustBeGreaterThanZero(); error CannotSetRewardForPastEpochs(); error EpochDurationMustBeGreaterThanZero(); error CannotStakeZeroAmount(); error InvalidTierIndex(); error InvalidStakeIndex(); error LockPeriodNotEnded(); error StakeTransferFailed(); error UnstakeTransferFailed(); error RewardTransferFailed(); constructor(address _stakingToken, address _rewardToken) Ownable(msg.sender) { STAKING_TOKEN = IERC20(_stakingToken); REWARD_TOKEN = IERC20(_rewardToken); // Initialize tiers tiers.push(Tier(90, 10)); // 90 seconds, 0.1x tiers.push(Tier(180, 50)); // 180 seconds, 0.5x tiers.push(Tier(360, 110)); // 360 seconds, 1.1x tiers.push(Tier(720, 150)); // 720 seconds, 1.5x tiers.push(Tier(1440, 210)); // 1440 seconds, 2.1x currentEpoch = block.timestamp / epochDuration; } function setTier( uint256 index, uint256 lockPeriod, uint256 multiplier ) external onlyOwner { if (multiplier == 0) revert MultiplierMustBeGreaterThanZero(); if (index < tiers.length) { tiers[index] = Tier(lockPeriod, multiplier); emit TierUpdated(index, lockPeriod, multiplier); } else { tiers.push(Tier(lockPeriod, multiplier)); emit TierAdded(lockPeriod, multiplier); } } function isVoted(address user, uint256 epoch) public view returns (bool) { // return hasVoted[user][epoch]; return true; } function setVotingDelay(uint256 newDelay) external onlyOwner { votingDelay = newDelay; emit VotingDelayUpdated(newDelay); } function setRewardForEpoch(uint256 epoch, uint256 reward) external onlyOwner { if (epoch < currentEpoch) revert CannotSetRewardForPastEpochs(); rewardsPerEpoch[epoch] = reward; emit RewardSet(epoch, reward); } function setEpochDuration(uint256 newDuration) external onlyOwner { if (newDuration == 0) revert EpochDurationMustBeGreaterThanZero(); epochDuration = newDuration; emit EpochDurationUpdated(newDuration); } function stake(uint256 amount, uint256 tierIndex) external nonReentrant { if (amount == 0) revert CannotStakeZeroAmount(); if (tierIndex >= tiers.length) revert InvalidTierIndex(); updateCurrentEpoch(); uint256 effectiveAmount = (amount * tiers[tierIndex].multiplier) / MULTIPLIER; totalEffectiveSupply += effectiveAmount; userStakes[msg.sender].push( Stake({ amount: amount, effectiveAmount: effectiveAmount, tierIndex: tierIndex, startTime: block.timestamp, lastClaimEpoch: currentEpoch }) ); // if (!STAKING_TOKEN.transferFrom(msg.sender, address(this), amount)) revert StakeTransferFailed(); emit Staked(msg.sender, amount, tierIndex); } function unstake(uint256 stakeIndex) external nonReentrant { if (stakeIndex >= userStakes[msg.sender].length) revert InvalidStakeIndex(); Stake storage userStake = userStakes[msg.sender][stakeIndex]; if ( block.timestamp < userStake.startTime + tiers[userStake.tierIndex].lockPeriod ) revert LockPeriodNotEnded(); 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(); // if (!STAKING_TOKEN.transfer(msg.sender, amount)) revert UnstakeTransferFailed(); emit Unstaked(msg.sender, amount); } function _claimRewards(uint256 stakeIndex) internal { if (stakeIndex >= userStakes[msg.sender].length) revert InvalidStakeIndex(); updateCurrentEpoch(); Stake storage userStake = userStakes[msg.sender][stakeIndex]; uint256 reward = calculateRewards(stakeIndex); if (reward > 0) { userStake.lastClaimEpoch = currentEpoch; // if (!REWARD_TOKEN.transfer(msg.sender, reward)) revert RewardTransferFailed(); emit RewardClaimed(msg.sender, reward); } } function claimRewards(uint256 stakeIndex) external nonReentrant { updateCurrentEpoch(); _claimRewards(stakeIndex); } function calculateRewards(uint256 stakeIndex) public view returns (uint256) { if (stakeIndex >= userStakes[msg.sender].length) revert InvalidStakeIndex(); Stake memory userStake = userStakes[msg.sender][stakeIndex]; uint256 reward = 0; uint256 lastEpoch = (block.timestamp - votingDelay) / epochDuration; //uint256 stakeLockEndEpoch = (userStake.startTime + tiers[userStake.tierIndex].lockPeriod) / epochDuration; //lastEpoch = lastEpoch < stakeLockEndEpoch ? lastEpoch : stakeLockEndEpoch; for ( uint256 epoch = userStake.lastClaimEpoch; epoch < lastEpoch; epoch++ ) { if (isVoted(msg.sender, 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) public view returns (uint256) { uint256 reward = rewardsPerEpoch[epoch]; if (reward == 0 && epoch > 0) { reward = rewardsPerEpoch[epoch - 1]; } if (reward == 0) { reward = defaultEpochRewards; } return reward; } // For testing purposes only function setVoted( address user, uint256 epoch, bool voted ) external onlyOwner { hasVoted[user][epoch] = voted; } }