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

New Stakers Can Exploit Reward Mechanism Frontrunning Epoch Rollovers

Summary

As per the natspec in function FjordStaking::addRewards:

/// must be only call if it's can trigger update next epoch so the total staked won't increase anymore
/// must be the action to trigger update epoch and the last action of the epoch

However, since new stakers have a financial incentive to claim rewards, they can exploit this by frontrunning the addReward function call to trigger an epoch rollover. As a result, the newly added rewards are credited in the next epoch, allowing the new staker to unjustly earn a portion of the rewards.

Vulnerability Details

Rewards added during an epoch are intended to be distributed among users who have staked their tokens for the entire duration of that epoch. However, a flaw in the system allows new stakers to manipulate the process. By frontrunning the addRewards transaction, new stakers can trigger an epoch rollover, ensuring that the rewards intended for long-term stakers are also distributed to them. This creates a perverse incentive that undermines the core principle of staking, which is to reward long-term commitment.

Impact

Consider the following scenario:

  1. Just one second before epoch 6 ends, Alice stakes 10 ether.

  2. Immediately after the epoch ends, the reward admin submits a transaction to add rewards to the staking contract.

  3. Alice notices this transaction in the mempool and decides to front-run it by staking an additional 1 wei and paying a higher gas fee to prioritize her transaction. This triggers an epoch rollover, advancing the contract to epoch 7.

  4. The reward admin's transaction is then processed, and the rewards are added for epoch 7.

As a result, Alice is now entitled to claim a portion of the rewards. This situation is unfair to the other stakers, who kept their tokens locked for the entire epoch, while Alice only staked for a few seconds. This behavior undermines the fairness and integrity of the staking process.

See coded PoC

Place in addReward.t.sol.

function test_StakerFrontruns() public {
// Alice stakes at week 6
vm.warp(vm.getBlockTimestamp() + 6 weeks - 1);
vm.prank(alice);
fjordStaking.stake(10 ether);
vm.warp(vm.getBlockTimestamp() + 1);
// Alice frontruns rewarder deposit
vm.prank(alice);
fjordStaking.stake(1 wei);
// Rollover has been triggered and rewards are added in the new epoch
vm.prank(minter);
fjordStaking.addReward(10 ether);
vm.warp(vm.getBlockTimestamp() + 1 weeks);
(
uint256 totalStaked,
uint256 unclaimedRewards,
uint16 unredeemedEpoch,
uint16 lastClaimedEpoch
) = fjordStaking.userData(alice);
// Alice has received the rewards by frontrunning the minter's tx
uint256 balanceAliceBefore = token.balanceOf(alice);
vm.prank(alice);
fjordStaking.claimReward(true);
// Alice owns half of the staked tokens and receives a 50% penalty due to the early claim
assertEq(token.balanceOf(alice) - balanceAliceBefore, 2.5 ether);
}

Tools Used

Manual review.

Recommendations

Check if the rewarder admin action triggered an epoch rollover, if it didn't, revert:

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

Support

FAQs

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