Skip to content

Instantly share code, notes, and snippets.

@redmont
Created November 18, 2024 17:12
Show Gist options
  • Select an option

  • Save redmont/0bdd88e51246c0860f783726cb162101 to your computer and use it in GitHub Desktop.

Select an option

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=
// 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