feat(gauge): add staking gauge with reward distribution
This commit is contained in:
1 parent
86d4536629
commit
30dc10dc01
1 file changed
+131
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user