feat(gauge): add staking gauge with reward distribution

This commit is contained in:
dualfroz committed 2026-08-26 22:40:00 +02:00
1 parent 86d4536629
commit 30dc10dc01
1 file changed
+131
+131
View File
@@ -0,0 +1,131 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.36;
import {IErc20} from "./interfaces/IErc20.sol";
/// @title Staking gauge distributing a reward token pro rata by staked share over time.
contract StakingGauge {
IErc20 public immutable stakingToken;
IErc20 public immutable rewardToken;
address public owner;
uint256 public rewardRate;
uint256 public periodFinish;
uint256 public rewardsDuration = 7 days;
uint256 public lastUpdateTime;
uint256 public rewardPerTokenStored;
mapping(address => uint256) public userRewardPerTokenPaid;
mapping(address => uint256) public rewards;
uint256 public totalSupply;
mapping(address => uint256) public balanceOf;
event Staked(address indexed account, uint256 amount);
event Withdrawn(address indexed account, uint256 amount);
event RewardPaid(address indexed account, uint256 reward);
event RewardAdded(uint256 reward);
event RewardsDurationUpdated(uint256 duration);
constructor(address staking, address reward, address ownerAddress) {
stakingToken = IErc20(staking);
rewardToken = IErc20(reward);
owner = ownerAddress;
}
modifier onlyOwner() {
require(msg.sender == owner, "StakingGauge: FORBIDDEN");
_;
}
modifier updateReward(address account) {
rewardPerTokenStored = rewardPerToken();
lastUpdateTime = lastTimeRewardApplicable();
if (account != address(0)) {
rewards[account] = earned(account);
userRewardPerTokenPaid[account] = rewardPerTokenStored;
}
_;
}
function _safeTransfer(IErc20 token, address to, uint256 value) private {
(bool success, bytes memory returndata) =
address(token).call(abi.encodeWithSelector(IErc20.transfer.selector, to, value));
require(success && (returndata.length == 0 || abi.decode(returndata, (bool))), "StakingGauge: TRANSFER_FAILED");
}
function _safeTransferFrom(IErc20 token, address from, address to, uint256 value) private {
(bool success, bytes memory returndata) =
address(token).call(abi.encodeWithSelector(IErc20.transferFrom.selector, from, to, value));
require(
success && (returndata.length == 0 || abi.decode(returndata, (bool))), "StakingGauge: TRANSFER_FROM_FAILED"
);
}
function lastTimeRewardApplicable() public view returns (uint256) {
return block.timestamp < periodFinish ? block.timestamp : periodFinish;
}
function rewardPerToken() public view returns (uint256) {
if (totalSupply == 0) return rewardPerTokenStored;
return rewardPerTokenStored + ((lastTimeRewardApplicable() - lastUpdateTime) * rewardRate * 1e18) / totalSupply;
}
function earned(address account) public view returns (uint256) {
return (balanceOf[account] * (rewardPerToken() - userRewardPerTokenPaid[account])) / 1e18 + rewards[account];
}
function stake(uint256 amount) external updateReward(msg.sender) {
require(amount > 0, "StakingGauge: ZERO_STAKE");
totalSupply += amount;
balanceOf[msg.sender] += amount;
_safeTransferFrom(stakingToken, msg.sender, address(this), amount);
emit Staked(msg.sender, amount);
}
function withdraw(uint256 amount) public updateReward(msg.sender) {
require(amount > 0, "StakingGauge: ZERO_WITHDRAW");
totalSupply -= amount;
balanceOf[msg.sender] -= amount;
_safeTransfer(stakingToken, msg.sender, amount);
emit Withdrawn(msg.sender, amount);
}
function getReward() public updateReward(msg.sender) {
uint256 reward = rewards[msg.sender];
if (reward > 0) {
rewards[msg.sender] = 0;
_safeTransfer(rewardToken, msg.sender, reward);
emit RewardPaid(msg.sender, reward);
}
}
function exit() external {
withdraw(balanceOf[msg.sender]);
getReward();
}
function notifyRewardAmount(uint256 reward) external onlyOwner updateReward(address(0)) {
if (block.timestamp >= periodFinish) {
rewardRate = reward / rewardsDuration;
} else {
uint256 remaining = periodFinish - block.timestamp;
uint256 leftover = remaining * rewardRate;
rewardRate = (reward + leftover) / rewardsDuration;
}
uint256 balance = rewardToken.balanceOf(address(this));
require(rewardRate <= balance / rewardsDuration, "StakingGauge: REWARD_TOO_HIGH");
lastUpdateTime = block.timestamp;
periodFinish = block.timestamp + rewardsDuration;
emit RewardAdded(reward);
}
function setRewardsDuration(uint256 duration) external onlyOwner {
require(block.timestamp > periodFinish, "StakingGauge: PERIOD_ACTIVE");
require(duration > 0, "StakingGauge: ZERO_DURATION");
rewardsDuration = duration;
emit RewardsDurationUpdated(duration);
}
}