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

Delayed Total Stake Update Leading to Inaccurate Reward Distribution

Summary

In the staking contract, there is an issue where the total staked tokens are not updated immediately when a user stakes. Instead, the update occurs only at the beginning of the next epoch. This delayed update causes an incorrect calculation of rewards, particularly when new rewards are added by the admin. The current implementation fails to include the stakes made just before the reward distribution, leading to an unfair distribution of rewards. This scenario can result in a situation where the reward pool is insufficient to cover all eligible stakers.

Vulnerability Details

The vulnerability arises from the way the `_checkEpochRollover` and `addReward` functions handle the update of `totalStaked`. Specifically, the contract delays updating the total staked tokens until the next epoch, which can lead to incorrect reward calculations when rewards are added before this update.

Relevant code excerpts:

ERROR

uint256 pendingRewardsPerToken = (pendingRewards * PRECISION_18) / totalStaked;

NOTE : TOTALSTAKED SHOULD ADD NEW STAKED

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));
// Calculate pending rewards for 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;
}
}
function addReward(uint256 \_amount) external onlyRewardAdmin {
//CHECK
if (\_amount == 0) revert InvalidAmount();
//EFFECT
uint16 previousEpoch = currentEpoch;
//INTERACT
fjordToken.safeTransferFrom(msg.sender, address(this), \_amount);
\_checkEpochRollover();
emit RewardAdded(previousEpoch, msg.sender, \_amount);
}

**Issue Description:**

When the admin adds new rewards through the `addReward` function, `_checkEpochRollover` is called to handle the epoch transition and reward distribution. However, since `totalStaked` is updated only after the pending rewards are calculated, any stakes made just before the reward update are not factored into the reward calculation.

For example:

- **Epoch 1**: User A stakes 1000 tokens.

- **Epoch 2**: Admin adds 1000 tokens as rewards, and `_checkEpochRollover` is triggered. However, `totalStaked` still reflects the value from the previous epoch, and the newly staked tokens by User A are not included in the reward calculation.

- **Epoch 3**: User B stakes another 1000 tokens, but by this time, the rewards have already been distributed based on the outdated total staked amount.

This sequence causes User A to receive fewer rewards than deserved, while User B may receive more than deserved in the next epoch. Over time, this can result in a scenario where the rewards pool is insufficient to cover all staked tokens, leading to financial imbalances.

Reference to other implementations - https://github.com/Synthetixio/synthetix/blob/cd66f93fa41fc9b7eb97e21eaf8b5f6b9eea5ecf/contracts/StakingRewards.sol#L83-L85

function stake(uint256 amount) external nonReentrant notPaused updateReward(msg.sender) {
require(amount > 0, "Cannot stake 0");
_totalSupply = _totalSupply.add(amount);
_balances[msg.sender] = _balances[msg.sender].add(amount);
stakingToken.safeTransferFrom(msg.sender, address(this), amount);
emit Staked(msg.sender, amount);
}

Impact

This vulnerability results in an unfair distribution of rewards, where users who stake just before a reward update may not receive their fair share of rewards. Additionally, this mismanagement could cause the rewards pool to become insufficient.

Tools Used

Manual Code Review

Recommendations

1. **Immediate Update of `totalStaked`**: Modify the `_checkEpochRollover` function to update `totalStaked` before calculating `pendingRewardsPerToken`. This ensures that all stakes made before the reward distribution are accurately included in the reward calculations.

function \_checkEpochRollover() internal {
uint16 latestEpoch = getEpoch(block.timestamp);
if (latestEpoch > currentEpoch) {
// Time to rollover
currentEpoch = latestEpoch;
// Update total staked immediately
++ totalStaked += newStaked;
++ totalVestedStaked += newVestedStaked;
++ newStaked = 0;
++ newVestedStaked = 0;
if (totalStaked > 0) {
uint256 currentBalance = fjordToken.balanceOf(address(this));
// Calculate pending rewards for the current epoch
uint256 pendingRewards = (currentBalance + totalVestedStaked)
- totalStaked - 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;
}
}
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.