Skip to content

Commit

Permalink
🔀 Resolved conflicts after merging main into staking-cooldown
Browse files Browse the repository at this point in the history
  • Loading branch information
Flocqst committed Aug 27, 2024
2 parents 7ff1a0a + 0a73b0d commit 9cb9509
Show file tree
Hide file tree
Showing 30 changed files with 2,115 additions and 156 deletions.
Binary file not shown.
326 changes: 326 additions & 0 deletions audits/external/kwenta-staking-v2-second-migration-final-report.md

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions audits/external/omniscia.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Omniscia Staking V2 w Migrators Security Audit

This document contains a link to the Omniscia audit report.

- [Omniscia Staking V2 w Migrators Security Audit Report](https://omniscia.io/reports/kwenta-staking-v2-w-migrators-64e48aba18c4480014cf4897/)
26 changes: 18 additions & 8 deletions contracts/StakingRewardsNotifier.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@ import {IRewardEscrowV2} from "./interfaces/IRewardEscrowV2.sol";
import {IStakingRewardsV2} from "./interfaces/IStakingRewardsV2.sol";
import {ISupplySchedule} from "./interfaces/ISupplySchedule.sol";
import {Ownable2Step} from "@openzeppelin/contracts/access/Ownable2Step.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";

/// @title StakingRewardsNotifier
/// @notice This contract is responsible for sending and notifying the staking rewards contract about the reward amounts.
/// @dev The rewards notification can only be triggered by the supply schedule contract, which is called weekly.
contract StakingRewardsNotifier is Ownable2Step, IStakingRewardsNotifier {
/*//////////////////////////////////////////////////////////////
IMMUTABLES
Expand All @@ -16,6 +20,9 @@ contract StakingRewardsNotifier is Ownable2Step, IStakingRewardsNotifier {
/// @notice kwenta interface
IKwenta public immutable kwenta;

/// @notice usdc interface
IERC20 public immutable usdc;

/// @notice supply schedule contract
ISupplySchedule public immutable supplySchedule;

Expand All @@ -33,13 +40,17 @@ contract StakingRewardsNotifier is Ownable2Step, IStakingRewardsNotifier {
/// @notice Constructor function for StakingRewardsNotifier contract
/// @param _contractOwner: address of the contract owner
/// @param _kwenta: address of the Kwenta contract
/// @param _usdc: address of the USDC contract
/// @param _supplySchedule: address of the SupplySchedule contract
constructor(address _contractOwner, address _kwenta, address _supplySchedule) {
if (_contractOwner == address(0) || _kwenta == address(0) || _supplySchedule == address(0))
{
constructor(address _contractOwner, address _kwenta, address _usdc, address _supplySchedule) {
if (
_contractOwner == address(0) || _kwenta == address(0) || _usdc == address(0)
|| _supplySchedule == address(0)
) {
revert ZeroAddress();
}
kwenta = IKwenta(_kwenta);
usdc = IERC20(_usdc);
supplySchedule = ISupplySchedule(_supplySchedule);

// transfer ownership
Expand Down Expand Up @@ -77,12 +88,11 @@ contract StakingRewardsNotifier is Ownable2Step, IStakingRewardsNotifier {

/// @inheritdoc IStakingRewardsNotifier
function notifyRewardAmount(uint256 mintedAmount) external onlySupplySchedule {
/// @dev delete mintedAmount because it is not used but cannot be removed from the function signature
/// as it is called by SupplySchedule which is immutable and expects to pass this value
/// instead currentBalance is used
delete mintedAmount;
uint256 currentBalance = kwenta.balanceOf(address(this));
kwenta.transfer(address(stakingRewardsV2), currentBalance);
stakingRewardsV2.notifyRewardAmount(currentBalance);
uint256 currentBalanceUsdc = usdc.balanceOf(address(this));
usdc.transfer(address(stakingRewardsV2), currentBalanceUsdc);

stakingRewardsV2.notifyRewardAmount(currentBalance, currentBalanceUsdc);
}
}
88 changes: 83 additions & 5 deletions contracts/StakingRewardsV2.sol
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,13 @@ contract StakingRewardsV2 is
/// @custom:oz-upgrades-unsafe-allow state-variable-immutable
IStakingRewardsNotifier public immutable rewardsNotifier;

/// @notice Contract for USDC ERC20 token - used for rewards
/// @custom:oz-upgrades-unsafe-allow state-variable-immutable
IERC20 public immutable usdc;

/// @notice Used to scale USDC precision to 18 decimals
uint256 private constant PRECISION = 1e12;

/*///////////////////////////////////////////////////////////////
STATE
///////////////////////////////////////////////////////////////*/
Expand Down Expand Up @@ -82,6 +89,20 @@ 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 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
///////////////////////////////////////////////////////////////*/
Expand Down Expand Up @@ -114,17 +135,22 @@ 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) {
if (_kwenta == address(0) || _rewardEscrow == address(0) || _rewardsNotifier == address(0)) {
constructor(address _kwenta, address _usdc, address _rewardEscrow, address _rewardsNotifier) {
if (
_kwenta == address(0) || _usdc == address(0) || _rewardEscrow == address(0)
|| _rewardsNotifier == address(0)
) {
revert ZeroAddress();
}

_disableInitializers();

// define reward/staking token
kwenta = IKwenta(_kwenta);
usdc = IERC20(_usdc);

// define contracts which will interact with StakingRewards
rewardEscrow = IRewardEscrowV2(_rewardEscrow);
Expand Down Expand Up @@ -326,6 +352,19 @@ contract StakingRewardsV2 is
kwenta.transfer(address(rewardEscrow), reward);
rewardEscrow.appendVestingEntry(_to, reward);
}

uint256 rewardUSDC = rewardsUSDC[_account] / PRECISION;
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);
}
}

/// @inheritdoc IStakingRewardsV2
Expand Down Expand Up @@ -354,6 +393,7 @@ contract StakingRewardsV2 is

function _updateReward(address _account) internal {
rewardPerTokenStored = rewardPerToken();
rewardPerTokenStoredUSDC = rewardPerTokenUSDC();
lastUpdateTime = lastTimeRewardApplicable();

if (_account != address(0)) {
Expand All @@ -363,6 +403,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;
}
}

Expand All @@ -371,6 +415,10 @@ contract StakingRewardsV2 is
return rewardRate * rewardsDuration;
}

function getRewardForDurationUSDC() external view returns (uint256) {
return rewardRateUSDC * rewardsDuration;
}

/// @inheritdoc IStakingRewardsV2
function rewardPerToken() public view returns (uint256) {
uint256 allTokensStaked = totalSupply();
Expand All @@ -383,6 +431,21 @@ 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;
Expand All @@ -396,6 +459,15 @@ 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
///////////////////////////////////////////////////////////////*/
Expand Down Expand Up @@ -480,7 +552,7 @@ contract StakingRewardsV2 is
/// @param _timestamp: timestamp to check
/// @dev returns 0 if no checkpoints exist, uses iterative binary search
/// @dev if called with a timestamp that equals the current block timestamp, then the function might return inconsistent
/// values as further transactions changing the balances can still occur within the same block.
/// values as further transactions changing the balances can still occur within the same block.
function _checkpointBinarySearch(Checkpoint[] storage _checkpoints, uint256 _timestamp)
internal
view
Expand Down Expand Up @@ -562,22 +634,27 @@ contract StakingRewardsV2 is
///////////////////////////////////////////////////////////////*/

/// @inheritdoc IStakingRewardsV2
function notifyRewardAmount(uint256 _reward)
function notifyRewardAmount(uint256 _reward, uint256 _rewardUsdc)
external
onlyRewardsNotifier
updateReward(address(0))
{
if (block.timestamp >= periodFinish) {
rewardRate = _reward / rewardsDuration;
rewardRateUSDC = (_rewardUsdc * PRECISION) / rewardsDuration;
} else {
uint256 remaining = periodFinish - block.timestamp;

uint256 leftover = remaining * rewardRate;
rewardRate = (_reward + leftover) / rewardsDuration;

uint256 leftoverUsdc = remaining * rewardRateUSDC;
rewardRateUSDC = (_rewardUsdc * PRECISION + leftoverUsdc) / rewardsDuration;
}

lastUpdateTime = block.timestamp;
periodFinish = block.timestamp + rewardsDuration;
emit RewardAdded(_reward);
emit RewardAdded(_reward, _rewardUsdc);
}

/// @inheritdoc IStakingRewardsV2
Expand Down Expand Up @@ -613,6 +690,7 @@ contract StakingRewardsV2 is
/// @inheritdoc IStakingRewardsV2
function recoverERC20(address _tokenAddress, uint256 _tokenAmount) external onlyOwner {
if (_tokenAddress == address(kwenta)) revert CannotRecoverStakingToken();
if (_tokenAddress == address(usdc)) revert CannotRecoverRewardToken();
emit Recovered(_tokenAddress, _tokenAmount);
IERC20(_tokenAddress).transfer(owner(), _tokenAmount);
}
Expand Down
3 changes: 3 additions & 0 deletions contracts/interfaces/IStakingRewardsNotifier.sol
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,8 @@ interface IStakingRewardsNotifier {

/// @notice notify the StakingRewardsV2 contract of the reward amount
/// @param mintedAmount: amount of rewards minted
/// @dev This function will be called on a periodic basis by the SupplySchedule contract
/// @dev mintedAmount is not used but cannot be removed from the function signature
/// as it is called by SupplySchedule which is immutable and expects to pass this value
function notifyRewardAmount(uint256 mintedAmount) external;
}
31 changes: 25 additions & 6 deletions contracts/interfaces/IStakingRewardsV2.sol
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,11 @@ 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);
Expand All @@ -79,6 +84,10 @@ interface IStakingRewardsV2 {
/// @param _account: address of account earned amount is being calculated for
function earned(address _account) 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);

// checkpointing

/// @notice get the number of balances checkpoints for an account
Expand All @@ -100,15 +109,15 @@ interface IStakingRewardsV2 {
/// @param _timestamp: timestamp to check
/// @return balance at given timestamp
/// @dev if called with a timestamp that equals the current block timestamp, then the function might return inconsistent
/// values as further transactions changing the balances can still occur within the same block.
/// values as further transactions changing the balances can still occur within the same block.
function balanceAtTime(address _account, uint256 _timestamp) external view returns (uint256);

/// @notice get a users escrowed balance at a given timestamp
/// @param _account: address of account to check
/// @param _timestamp: timestamp to check
/// @return escrowed balance at given timestamp
/// @dev if called with a timestamp that equals the current block timestamp, then the function might return inconsistent
/// values as further transactions changing the balances can still occur within the same block.
/// values as further transactions changing the balances can still occur within the same block.
function escrowedBalanceAtTime(address _account, uint256 _timestamp)
external
view
Expand All @@ -118,7 +127,7 @@ interface IStakingRewardsV2 {
/// @param _timestamp: timestamp to check
/// @return total supply at given timestamp
/// @dev if called with a timestamp that equals the current block timestamp, then the function might return inconsistent
/// values as further transactions changing the balances can still occur within the same block.
/// values as further transactions changing the balances can still occur within the same block.
function totalSupplyAtTime(uint256 _timestamp) external view returns (uint256);

/*//////////////////////////////////////////////////////////////
Expand Down Expand Up @@ -191,8 +200,9 @@ interface IStakingRewardsV2 {

/// @notice configure reward rate
/// @param _reward: amount of token to be distributed over a period
/// @param _reward: amount of usdc to be distributed over a period
/// @dev updateReward() called prior to function logic (with zero address)
function notifyRewardAmount(uint256 _reward) external;
function notifyRewardAmount(uint256 _reward, uint256 _rewardUsdc) external;

/// @notice set rewards duration
/// @param _rewardsDuration: denoted in seconds
Expand All @@ -219,8 +229,9 @@ interface IStakingRewardsV2 {
///////////////////////////////////////////////////////////////*/

/// @notice update reward rate
/// @param reward: amount to be distributed over applicable rewards duration
event RewardAdded(uint256 reward);
/// @param reward: kwenta amount to be distributed over applicable rewards duration
/// @param rewardUsdc: usdc amount to be distributed over applicable rewards duration
event RewardAdded(uint256 reward, uint256 rewardUsdc);

/// @notice emitted when user stakes tokens
/// @param user: staker address
Expand All @@ -247,6 +258,11 @@ interface IStakingRewardsV2 {
/// @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);
Expand Down Expand Up @@ -292,6 +308,9 @@ interface IStakingRewardsV2 {
/// @notice recovering the staking token is not allowed
error CannotRecoverStakingToken();

/// @notice recovering the usdc reward token is not allowed
error CannotRecoverRewardToken();

/// @notice error when trying to set a rewards duration that is too short
error RewardsDurationCannotBeZero();

Expand Down
Loading

0 comments on commit 9cb9509

Please sign in to comment.