DeFiFoundry
20,000 USDC
View results
Submission Details
Severity: medium
Invalid

Staked amount in first epoch is not accounted properly and users wont be able to claim rewards in second epoch

Summary

When users stake in epoch X, they typically expect to receive rewards in epoch X+1. However, this expectation is not met for users who stake during the first epoch. In the current protocol implementation, any amounts staked in the first epoch are not included in the reward calculations for the second epoch. As a result, these users must wait until the third epoch to claim their rewards, causing an unintended delay in receiving their rewards.

Vulnerability Details

In the _checkEpochRollover() function, the rewardPerToken for the current epoch is only updated when totalStaked > 0. If totalStaked is 0, rewardPerToken is simply set to the rewardPerToken value from the last rewarded epoch. Since totalStaked is updated at the end of the function, it will always be 0 during epoch 1, meaning no rewards are distributed for that epoch.

function _checkEpochRollover() internal {
uint16 latestEpoch = getEpoch(block.timestamp);
if (latestEpoch > currentEpoch) {
//Time to rollover
currentEpoch = latestEpoch;
if (totalStaked > 0) {
uint256 currentBalance = fjordToken.balanceOf(address(this));
// no distribute the rewards to the users coming in the current epoch
uint256 pendingRewards = (currentBalance + totalVestedStaked + newVestedStaked)
- totalStaked - newStaked - totalRewards;
uint256 pendingRewardsPerToken = (pendingRewards * PRECISION_18) / totalStaked;
totalRewards += pendingRewards;
for (uint16 i = lastEpochRewarded + 1; i < currentEpoch; i++) {
rewardPerToken[i] = rewardPerToken[lastEpochRewarded] + pendingRewardsPerToken;
emit RewardPerTokenChanged(i, rewardPerToken[i]);
}
} else {
for (uint16 i = lastEpochRewarded + 1; i < currentEpoch; i++) {
rewardPerToken[i] = rewardPerToken[lastEpochRewarded];
emit RewardPerTokenChanged(i, rewardPerToken[i]);
}
}
totalStaked += newStaked;
totalVestedStaked += newVestedStaked;
newStaked = 0;
newVestedStaked = 0;
lastEpochRewarded = currentEpoch - 1;
}
}

_redeem() function, which is used to calculate the rewards for user, will fail to calculate the unclaimed rewards as the rewardPerToken for epoch 1 will be equal to 0.

You can add the following test in stake.t.sol and run forge test -vv --mt test_Stake_IsRewardFromFirstEpochAccounted

function test_Stake_IsRewardFromFirstEpochAccounted() public {
console.log("User staking in %s epoch", fjordStaking.currentEpoch());
fjordStaking.stake(1 ether);
_addRewardAndEpochRollover(1 ether, 1);
console.log("Started epoch: %s ", fjordStaking.currentEpoch());
(uint256 totalStaked, uint256 unclaimedRewards,,) = fjordStaking.userData(address(this));
console.log("Total staked: %s", totalStaked);
console.log("Unclaimed rewards: %s", unclaimedRewards);
// claimReward() will revert due to incorrect reward accounting in the firt epoch
vm.expectRevert();
fjordStaking.claimReward(false);
}
Logs:
User staking in 1 epoch
Started epoch: 2
Total staked: 0
Unclaimed rewards: 0
Reward per token in first epoch: 0

Impact

The impact of this issue is considered low because it affects the core functionality of the protocol only temporarily—until the third epoch, at which point rewards will be calculated correctly. However, the likelihood of this issue occurring is high, as it will inevitably happen to all users who stake during the first epoch. Given that the issue disrupts user expectations and the protocol's intended reward distribution for a short period, it can be classified as Medium severity.

Tools Used

Manual review, Foundry.

Recommendations

To mitigate this issue, one approach could be explicitly handling the transition from epoch 1 to epoch 2 by calculating rewardPerToken using newStaked and newVestedStaked values. Another option is to adjust the calculateRewards() function to account for this specific edge case, ensuring accurate reward distribution from the first epoch.

Updates

Lead Judging Commences

inallhonesty Lead Judge
10 months ago
inallhonesty Lead Judge 10 months ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.