From 6194e5cfdcead70275e88fce3e124d0bfd5bd5f3 Mon Sep 17 00:00:00 2001 From: Flocqst Date: Thu, 18 Jul 2024 17:53:55 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=91=B7=20Add=20dual=20reward=20distributi?= =?UTF-8?q?on?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- contracts/StakingRewardsV2.sol | 79 +++++++++++++++++++++- contracts/interfaces/IStakingRewardsV2.sol | 25 ++++++- 2 files changed, 102 insertions(+), 2 deletions(-) diff --git a/contracts/StakingRewardsV2.sol b/contracts/StakingRewardsV2.sol index 9089f1677..453623680 100644 --- a/contracts/StakingRewardsV2.sol +++ b/contracts/StakingRewardsV2.sol @@ -91,6 +91,24 @@ contract StakingRewardsV2 is /// @notice tracks all addresses approved to take actions on behalf of a given account mapping(address => mapping(address => bool)) public operatorApprovals; + /// @notice Contract for USDC ERC20 token - used for rewards + /// @custom:oz-upgrades-unsafe-allow state-variable-immutable + IERC20 public immutable usdc; + + /// @notice amount of tokens minted per second + uint256 public rewardRateUSDC; + + /// @notice summation of rewardRate divided by total staked tokens + uint256 public rewardPerTokenStoredUSDC; + + /// @notice represents the rewardPerToken for USDC rewards + /// value the last time the staker calculated earned() rewards + mapping(address => uint256) public userRewardPerTokenPaidUSDC; + + /// @notice track USDC rewards for a given user which changes when + /// a user stakes, unstakes, or claims rewards + mapping(address => uint256) public rewardsUSDC; + /*/////////////////////////////////////////////////////////////// AUTH ///////////////////////////////////////////////////////////////*/ @@ -134,9 +152,10 @@ contract StakingRewardsV2 is /// Actual contract construction will take place in the initialize function via proxy /// @custom:oz-upgrades-unsafe-allow constructor /// @param _kwenta The address for the KWENTA ERC20 token + /// @param _usdc The address for the USDC ERC20 token /// @param _rewardEscrow The address for the RewardEscrowV2 contract /// @param _rewardsNotifier The address for the StakingRewardsNotifier contract - constructor(address _kwenta, address _rewardEscrow, address _rewardsNotifier) { + constructor(address _kwenta, address _usdc, address _rewardEscrow, address _rewardsNotifier) { if (_kwenta == address(0) || _rewardEscrow == address(0) || _rewardsNotifier == address(0)) { revert ZeroAddress(); @@ -146,6 +165,7 @@ contract StakingRewardsV2 is // define reward/staking token kwenta = IKwenta(_kwenta); + usdc = IERC20(_usdc); // define contracts which will interact with StakingRewards rewardEscrow = IRewardEscrowV2(_rewardEscrow); @@ -356,6 +376,19 @@ contract StakingRewardsV2 is // as newly issued rewards from inflation are now issued as non-escrowed kwenta.transfer(_to, reward); } + + uint256 rewardUSDC = rewardsUSDC[_account]; + if (rewardUSDC > 0) { + // update state (first) + rewardsUSDC[_account] = 0; + + // emit reward claimed event and index account + emit RewardPaidUSDC(_account, rewardUSDC); + + // transfer token from this contract to the account + // as newly issued rewards from inflation are now issued as non-escrowed + usdc.transfer(_to, rewardUSDC); + } } function _getRewardCompounding(address _account) @@ -400,6 +433,7 @@ contract StakingRewardsV2 is function _updateReward(address _account) internal { rewardPerTokenStored = rewardPerToken(); + rewardPerTokenStoredUSDC = rewardPerTokenUSDC(); lastUpdateTime = lastTimeRewardApplicable(); if (_account != address(0)) { @@ -409,6 +443,10 @@ contract StakingRewardsV2 is // update reward per token staked AT this given time // (i.e. when this user is interacting with StakingRewards) userRewardPerTokenPaid[_account] = rewardPerTokenStored; + + rewardsUSDC[_account] = earnedUSDC(_account); + + userRewardPerTokenPaidUSDC[_account] = rewardPerTokenStoredUSDC; } } @@ -429,6 +467,18 @@ contract StakingRewardsV2 is + (((lastTimeRewardApplicable() - lastUpdateTime) * rewardRate * 1e18) / allTokensStaked); } + /// @inheritdoc IStakingRewardsV2 + function rewardPerTokenUSDC() public view returns (uint256) { + uint256 allTokensStaked = totalSupply(); + + if (allTokensStaked == 0) { + return rewardPerTokenStoredUSDC; + } + + return rewardPerTokenStoredUSDC + + (((lastTimeRewardApplicable() - lastUpdateTime) * rewardRateUSDC * 1e18) / allTokensStaked); + } + /// @inheritdoc IStakingRewardsV2 function lastTimeRewardApplicable() public view returns (uint256) { return block.timestamp < periodFinish ? block.timestamp : periodFinish; @@ -442,6 +492,14 @@ contract StakingRewardsV2 is + rewards[_account]; } + /// @inheritdoc IStakingRewardsV2 + function earnedUSDC(address _account) public view returns (uint256) { + uint256 totalBalance = balanceOf(_account); + + return ((totalBalance * (rewardPerTokenUSDC() - userRewardPerTokenPaidUSDC[_account])) / 1e18) + + rewardsUSDC[_account]; + } + /*/////////////////////////////////////////////////////////////// DELEGATION ///////////////////////////////////////////////////////////////*/ @@ -626,6 +684,25 @@ contract StakingRewardsV2 is emit RewardAdded(_reward); } + /// @inheritdoc IStakingRewardsV2 + function notifyUsdcRewardAmount(uint256 _reward) + external + onlyRewardsNotifier + updateReward(address(0)) + { + if (block.timestamp >= periodFinish) { + rewardRate = _reward / rewardsDuration; + } else { + uint256 remaining = periodFinish - block.timestamp; + uint256 leftover = remaining * rewardRate; + rewardRate = (_reward + leftover) / rewardsDuration; + } + + lastUpdateTime = block.timestamp; + periodFinish = block.timestamp + rewardsDuration; + emit UsdcRewardAdded(_reward); + } + /// @inheritdoc IStakingRewardsV2 function setRewardsDuration(uint256 _rewardsDuration) external onlyOwner { if (block.timestamp <= periodFinish) revert RewardsPeriodNotComplete(); diff --git a/contracts/interfaces/IStakingRewardsV2.sol b/contracts/interfaces/IStakingRewardsV2.sol index 2a291640f..fabc72c42 100644 --- a/contracts/interfaces/IStakingRewardsV2.sol +++ b/contracts/interfaces/IStakingRewardsV2.sol @@ -74,10 +74,19 @@ interface IStakingRewardsV2 { /// @return running sum of reward per total tokens staked function rewardPerToken() external view returns (uint256); + /// @notice calculate running sum of USDC reward per total tokens staked + /// at this specific time + /// @return running sum of USDC reward per total tokens staked + function rewardPerTokenUSDC() external view returns (uint256); + /// @notice get the last time a reward is applicable for a given user /// @return timestamp of the last time rewards are applicable function lastTimeRewardApplicable() external view returns (uint256); + /// @notice determine how much USDC reward an account has earned thus far + /// @param _account: address of account earned amount is being calculated for + function earnedUSDC(address _account) external view returns (uint256); + /// @notice determine how much reward token an account has earned thus far /// @param _account: address of account earned amount is being calculated for function earned(address _account) external view returns (uint256); @@ -197,6 +206,11 @@ interface IStakingRewardsV2 { /// @dev updateReward() called prior to function logic (with zero address) function notifyRewardAmount(uint256 _reward) external; + /// @notice configure usdc reward rate + /// @param _reward: amount of usdc to be distributed over a period + /// @dev updateReward() called prior to function logic (with zero address) + function notifyUsdcRewardAmount(uint256 _reward) external; + /// @notice set rewards duration /// @param _rewardsDuration: denoted in seconds function setRewardsDuration(uint256 _rewardsDuration) external; @@ -229,6 +243,10 @@ interface IStakingRewardsV2 { /// @param reward: amount to be distributed over applicable rewards duration event RewardAdded(uint256 reward); + /// @notice update reward rate + /// @param reward: amount to be distributed over applicable rewards duration + event UsdcRewardAdded(uint256 reward); + /// @notice emitted when user stakes tokens /// @param user: staker address /// @param amount: amount staked @@ -249,11 +267,16 @@ interface IStakingRewardsV2 { /// @param amount: amount unstaked event EscrowUnstaked(address user, uint256 amount); - /// @notice emitted when user claims rewards + /// @notice emitted when user claims KWENTA rewards /// @param user: address of user claiming rewards /// @param reward: amount of reward token claimed event RewardPaid(address indexed user, uint256 reward); + /// @notice emitted when user claims USDC rewards + /// @param user: address of user claiming rewards + /// @param reward: amount of USDC token claimed + event RewardPaidUSDC(address indexed user, uint256 reward); + /// @notice emitted when rewards duration changes /// @param newDuration: denoted in seconds event RewardsDurationUpdated(uint256 newDuration);