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

Zero Rewards Calculated for Staker Due to Lack of Additional Reward Transfers Across Epochs

Summary

The reward distribution system currently relies on a centralized address controlled by the contract owner for transferring rewards. This centralization introduces risks, as the amount of rewards transferred can vary between epochs, leading to inconsistencies and anomalies in the rewards assigned to each epoch and user. To address this issue, it is recommended to implement an automated system that transfers a fixed amount of rewards at regular intervals, such as weekly. This approach will standardize reward allocation, reduce the impact of centralization, and ensure consistent and fair rewards for all stakers.

Vulnerability Details

The FjordStaking::_checkEpochRollover function calculates pendingRewards using the formula:

pendingRewards = (currentBalance + totalVestedStaked + newVestedStaked) - (totalStaked + newStaked + totalRewards)

This calculation works correctly when new rewards are transferred by the reward admin at each epoch. However, if no additional rewards are transferred across consecutive epochs, the currentBalance remains unchanged, causing pendingRewards to be zero in FjordStaking::_checkEpochRollover function. As a result, the pendingRewardsPerToken becomes zero, and no rewards are allocated to stakers in those epochs which means FjordStaking::rewardPerToken being set to Zero.

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;
}
}

Centralization Concern:

function setRewardAdmin(address _rewardAdmin) external onlyOwner {
if (_rewardAdmin == address(0)) revert InvalidZeroAddress();
rewardAdmin = _rewardAdmin;
}

The rewards are transferred by a specific address controlled by the contract owner, introducing a centralization risk. If the owner fails to transfer rewards regularly or deliberately withholds them, it can lead to the issue of zero rewards for stakers, as described above. This centralization introduces a single point of failure, which can undermine the fairness and reliability of the reward distribution process.

This issue, coupled with the centralization of reward transfers, can significantly impact the staking experience and reduce the overall trust in the system.

Impact

When no rewards are transferred for multiple consecutive epochs, stakers receive zero rewards for those periods, even if they continue staking. This leads to significantly reduced rewards for users, negatively affecting the incentive to participate in staking.

Poc

In the claimReward.t.sol test, the initial staked amount is set in the ClaimReward_Unit_Test::afterSetUp function. When the reward admin (controlled by the contract owner) transfers rewards and the contract moves to the next epoch, the pendingRewards are correctly calculated and allocated to the staker. However, in subsequent epochs where no rewards are sent by the admin, the pendingRewards becomes zero, leading to a FjordStaking::rewardPerToken of zero for those epochs. If the staker un-stakes or claims rewards after six weeks without additional reward transfers, they receive no additional rewards, despite continuing to stake.

function testIncorrectCalculationOfPendingRewards() public {
uint256 reward = 1 ether;
vm.startPrank(minter);
vm.warp(vm.getBlockTimestamp() + fjordStaking.epochDuration());
fjordStaking.addReward(reward);
vm.stopPrank();
uint256 pendingRewardsStart = (token.balanceOf(address(fjordStaking)) +
fjordStaking.totalVestedStaked() +
fjordStaking.newVestedStaked()) -
fjordStaking.totalStaked() -
fjordStaking.newStaked() -
fjordStaking.totalRewards();
vm.startPrank(minter);
fjordStaking.addReward(reward);
vm.stopPrank();
fjordStaking.stake(10 ether);
vm.startPrank(minter);
vm.warp(vm.getBlockTimestamp() + fjordStaking.epochDuration());
fjordStaking.addReward(reward);
vm.stopPrank();
uint256 pendingRewardsEnd = (token.balanceOf(address(fjordStaking)) +
fjordStaking.totalVestedStaked() +
fjordStaking.newVestedStaked()) -
fjordStaking.totalStaked() -
fjordStaking.newStaked() -
fjordStaking.totalRewards();
assert(pendingRewardsStart > pendingRewardsEnd);
assert(pendingRewardsEnd == 0);
}

Tools Used

Manual

Recommendations

Remove Centralization:

  1. To address the centralization risk, remove the dependency on a single address controlled by the contract owner for transferring rewards. Implement a decentralized or automated mechanism to handle reward transfers, reducing the risk of centralized control affecting reward distribution.
    Automate Reward Transfers:

  2. Develop an automated system to ensure that rewards are transferred consistently every week. This system should be programmed to transfer a fixed amount of rewards at the end of each epoch, ensuring that rewards are always allocated accurately and fairly to stakers.

Updates

Lead Judging Commences

inallhonesty Lead Judge 10 months ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity

Support

FAQs

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