Skip to content

Instantly share code, notes, and snippets.

@redmont
Created November 18, 2024 20:52
Show Gist options
  • Select an option

  • Save redmont/1d53cfeb7ad0912852203231f1d45b5d to your computer and use it in GitHub Desktop.

Select an option

Save redmont/1d53cfeb7ad0912852203231f1d45b5d 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=
// 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;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment