Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dual rewards audit fixes #257

Merged
merged 6 commits into from
Aug 20, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 20 additions & 10 deletions contracts/StakingRewardsV2.sol
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ contract StakingRewardsV2 is
/// @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 @@ -156,7 +159,10 @@ contract StakingRewardsV2 is
/// @param _rewardEscrow The address for the RewardEscrowV2 contract
/// @param _rewardsNotifier The address for the StakingRewardsNotifier contract
constructor(address _kwenta, address _usdc, address _rewardEscrow, address _rewardsNotifier) {
if (_kwenta == address(0) || _usdc == address(0) || _rewardEscrow == address(0) || _rewardsNotifier == address(0)) {
if (
_kwenta == address(0) || _usdc == address(0) || _rewardEscrow == address(0)
|| _rewardsNotifier == address(0)
) {
revert ZeroAddress();
}

Expand Down Expand Up @@ -370,16 +376,16 @@ contract StakingRewardsV2 is
}

uint256 rewardUSDC = rewardsUSDC[_account];
if (rewardUSDC > 0) {
if (rewardUSDC / PRECISION > 0) {
Flocqst marked this conversation as resolved.
Show resolved Hide resolved
// update state (first)
rewardsUSDC[_account] = 0;

// emit reward claimed event and index account
emit RewardPaidUSDC(_account, rewardUSDC);
emit RewardPaidUSDC(_account, rewardUSDC / PRECISION);

// transfer token from this contract to the account
// as newly issued rewards from inflation are now issued as non-escrowed
usdc.transfer(_to, rewardUSDC);
usdc.transfer(_to, rewardUSDC / PRECISION);
}
}

Expand Down Expand Up @@ -452,7 +458,10 @@ contract StakingRewardsV2 is
}

return rewardPerTokenStoredUSDC
+ (((lastTimeRewardApplicable() - lastUpdateTime) * rewardRateUSDC * 1e18) / allTokensStaked);
+ (
((lastTimeRewardApplicable() - lastUpdateTime) * rewardRateUSDC * 1e18)
/ allTokensStaked
);
}

/// @inheritdoc IStakingRewardsV2
Expand All @@ -472,8 +481,9 @@ contract StakingRewardsV2 is
function earnedUSDC(address _account) public view returns (uint256) {
uint256 totalBalance = balanceOf(_account);

return ((totalBalance * (rewardPerTokenUSDC() - userRewardPerTokenPaidUSDC[_account])) / 1e18)
+ rewardsUSDC[_account];
return (
(totalBalance * (rewardPerTokenUSDC() - userRewardPerTokenPaidUSDC[_account])) / 1e18
) + rewardsUSDC[_account];
}

/*///////////////////////////////////////////////////////////////
Expand Down Expand Up @@ -560,7 +570,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 @@ -649,15 +659,15 @@ contract StakingRewardsV2 is
{
if (block.timestamp >= periodFinish) {
rewardRate = _reward / rewardsDuration;
rewardRateUSDC = _rewardUsdc / 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 + leftoverUsdc) / rewardsDuration;
rewardRateUSDC = (_rewardUsdc * PRECISION + leftoverUsdc) / rewardsDuration;
}

lastUpdateTime = block.timestamp;
Expand Down
16 changes: 6 additions & 10 deletions test/foundry/unit/StakingRewardsV2/StakingRewardsV2.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -388,13 +388,7 @@ contract StakingRewardsV2Test is DefaultStakingV2Setup {

function test_Can_Recover_Non_Staking_Token() public {
// create mockToken
IERC20 mockToken = new Kwenta(
"Mock",
"MOCK",
INITIAL_SUPPLY,
address(this),
treasury
);
IERC20 mockToken = new Kwenta("Mock", "MOCK", INITIAL_SUPPLY, address(this), treasury);

// transfer in non staking tokens
vm.prank(treasury);
Expand Down Expand Up @@ -474,7 +468,7 @@ contract StakingRewardsV2Test is DefaultStakingV2Setup {
vm.warp(block.timestamp + 1 weeks);

// check reward per token updated
assertEq(stakingRewardsV2.rewardPerTokenUSDC(), 1 ether);
assertEq(stakingRewardsV2.rewardPerTokenUSDC(), 1 ether * PRECISION);
}

/*//////////////////////////////////////////////////////////////
Expand Down Expand Up @@ -722,7 +716,7 @@ contract StakingRewardsV2Test is DefaultStakingV2Setup {

// configure reward rate
vm.prank(address(rewardsNotifier));
stakingRewardsV2.notifyRewardAmount(0 , TEST_VALUE);
stakingRewardsV2.notifyRewardAmount(0, TEST_VALUE);

// fast forward 2 weeks
vm.warp(block.timestamp + 2 weeks);
Expand Down Expand Up @@ -761,7 +755,9 @@ contract StakingRewardsV2Test is DefaultStakingV2Setup {
assertEq(finalRewardRate / 2, initialRewardRate);
}

function test_rewardRateUSDC_Should_Increase_If_New_Rewards_Come_Before_Duration_Ends() public {
function test_rewardRateUSDC_Should_Increase_If_New_Rewards_Come_Before_Duration_Ends()
public
{
fundAndApproveAccountV2(address(this), 1 weeks);

uint256 totalToDistribute = 5 ether;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ contract StakingV2RewardCalculationTests is DefaultStakingV2Setup {
rewards = rewardEscrowV2.escrowedBalanceOf(user1);
rewardsUsdc = usdc.balanceOf(user1);
assertEq(rewards, expectedRewards);
assertEq(rewardsUsdc, expectedRewards);
assertGe(rewardsUsdc, expectedRewards);
Flocqst marked this conversation as resolved.
Show resolved Hide resolved

// move forward to the end of the rewards period
jumpToEndOfRewardsPeriod(waitTime);
Expand All @@ -128,7 +128,7 @@ contract StakingV2RewardCalculationTests is DefaultStakingV2Setup {
rewards = rewardEscrowV2.escrowedBalanceOf(user1);
rewardsUsdc = usdc.balanceOf(user1);
assertEq(rewards, expectedRewards);
assertEq(rewardsUsdc, expectedRewards);
assertGe(rewardsUsdc, expectedRewards);
}

function test_Staking_Rewards_Multiple_Stakers_In_Single_Reward_Period_Fuzz(
Expand Down Expand Up @@ -177,7 +177,7 @@ contract StakingV2RewardCalculationTests is DefaultStakingV2Setup {
rewards = rewardEscrowV2.escrowedBalanceOf(user1);
rewardsUsdc = usdc.balanceOf(user1);
assertEq(rewards, expectedRewards);
assertEq(rewardsUsdc, expectedRewards);
assertGe(rewardsUsdc, expectedRewards);

// move forward to the end of the rewards period
jumpToEndOfRewardsPeriod(waitTime);
Expand All @@ -193,7 +193,7 @@ contract StakingV2RewardCalculationTests is DefaultStakingV2Setup {
rewards = rewardEscrowV2.escrowedBalanceOf(user1);
rewardsUsdc = usdc.balanceOf(user1);
assertEq(rewards, expectedRewards);
assertEq(rewardsUsdc, expectedRewards);
assertGe(rewardsUsdc, expectedRewards);
}

function test_Staking_Rewards_One_Staker_Two_Reward_Periods_Fuzz(
Expand Down Expand Up @@ -233,7 +233,7 @@ contract StakingV2RewardCalculationTests is DefaultStakingV2Setup {
rewards = rewardEscrowV2.escrowedBalanceOf(user1);
rewardsUsdc = usdc.balanceOf(user1);
assertEq(rewards, expectedRewards);
assertEq(rewardsUsdc, expectedRewards);
assertGe(rewardsUsdc, expectedRewards);

// move forward to the end of the rewards period
jumpToEndOfRewardsPeriod(waitTime);
Expand All @@ -255,7 +255,7 @@ contract StakingV2RewardCalculationTests is DefaultStakingV2Setup {
rewards = rewardEscrowV2.escrowedBalanceOf(user1);
rewardsUsdc = usdc.balanceOf(user1);
assertEq(rewards, expectedRewards);
assertEq(rewardsUsdc, expectedRewards);
assertGe(rewardsUsdc, expectedRewards);
}

function test_Staking_Rewards_Three_Rounds_Fuzz(
Expand Down Expand Up @@ -295,7 +295,7 @@ contract StakingV2RewardCalculationTests is DefaultStakingV2Setup {
rewards = rewardEscrowV2.escrowedBalanceOf(user1);
rewardsUsdc = usdc.balanceOf(user1);
assertEq(rewards, expectedRewards);
assertEq(rewardsUsdc, expectedRewards);
assertGe(rewardsUsdc, expectedRewards);

// move forward to the end of the rewards period
jumpToEndOfRewardsPeriod(waitTime);
Expand All @@ -317,7 +317,7 @@ contract StakingV2RewardCalculationTests is DefaultStakingV2Setup {
rewards = rewardEscrowV2.escrowedBalanceOf(user1);
rewardsUsdc = usdc.balanceOf(user1);
assertEq(rewards, expectedRewards);
assertEq(rewardsUsdc, expectedRewards);
assertGe(rewardsUsdc, expectedRewards);

// move forward to the end of the rewards period
jumpToEndOfRewardsPeriod(waitTime);
Expand All @@ -339,7 +339,7 @@ contract StakingV2RewardCalculationTests is DefaultStakingV2Setup {
rewards = rewardEscrowV2.escrowedBalanceOf(user1);
rewardsUsdc = usdc.balanceOf(user1);
assertEq(rewards, expectedRewards);
assertEq(rewardsUsdc, expectedRewards);
assertGe(rewardsUsdc, expectedRewards);
}

function test_Staking_Rewards_Multiple_Rounds_Fuzz(
Expand Down Expand Up @@ -449,7 +449,7 @@ contract StakingV2RewardCalculationTests is DefaultStakingV2Setup {
rewards = rewardEscrowV2.escrowedBalanceOf(user1);
rewardsUsdc = usdc.balanceOf(user1);
assertEq(rewards, expectedRewards);
assertEq(rewardsUsdc, expectedRewards);
assertGe(rewardsUsdc, expectedRewards);

for (uint256 i = 0; i < numberOfRounds; i++) {
// add another staker
Expand All @@ -476,7 +476,7 @@ contract StakingV2RewardCalculationTests is DefaultStakingV2Setup {
rewards = rewardEscrowV2.escrowedBalanceOf(user1);
rewardsUsdc = usdc.balanceOf(user1);
assertEq(rewards, expectedRewards);
assertEq(rewardsUsdc, expectedRewards);
assertGe(rewardsUsdc, expectedRewards);
}
}

Expand Down
5 changes: 3 additions & 2 deletions test/foundry/utils/Constants.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ pragma solidity 0.8.19;

uint256 constant INITIAL_SUPPLY = 313_373 ether;
uint256 constant TEST_VALUE = 1 ether;
uint256 constant PRECISION = 1e12;

/*//////////////////////////////////////////////////////////////
FORK CONSTANTS
Expand Down Expand Up @@ -33,8 +34,8 @@ address constant OPTIMISM_STAKING_REWARDS_V2 = 0x61294940CE7cD1BDA10e349adC5B538
address constant OPTIMISM_REWARD_ESCROW_V2 = 0xb2a20fCdc506a685122847b21E34536359E94C56;
address constant OPTIMISM_ESCROW_MIGRATOR = 0xC9aF789Ae606F69cF8Ed073A04eC92f2354b027d;
address constant OPTIMISM_STAKING_REWARDS_NOTIFIER = 0x03f6dC6e616AB3a367a1F2C26B8Bc146f632b451;
uint256 constant OPTIMISM_STAKING_V2_FIRST_REWADRS_EMISSION_BLOCK = 109865536;
uint256 constant OPTIMISM_STAKING_V2_FIRST_REWADRS_EMISSION_TIMESTAMP = 1695329849;
uint256 constant OPTIMISM_STAKING_V2_FIRST_REWADRS_EMISSION_BLOCK = 109_865_536;
uint256 constant OPTIMISM_STAKING_V2_FIRST_REWADRS_EMISSION_TIMESTAMP = 1_695_329_849;

/*//////////////////////////////////////////////////////////////
GOERLI ADDRESSES
Expand Down
Loading