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.

Revisions

  1. redmont created this gist Nov 18, 2024.
    249 changes: 249 additions & 0 deletions contracts...Staking...RealStaking.sol
    Original 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;
    }
    }