The _checkEpochRollover()
function in the FjordStaking
contract incorrectly calculates and distributes rewards during epoch transitions, allowing malicious users to manipulate the reward distribution and gain an unfair advantage.
The FjordStaking
contract manages staking and reward distribution across multiple epochs. The _checkEpochRollover()
function is responsible for handling epoch transitions and distributing rewards. It calculates pendingRewards
based on various factors including the current contract balance, total staked amounts, and existing rewards. It then calculates pendingRewardsPerToken
using the current totalStaked
value and distributes these rewards across all epochs from lastEpochRewarded + 1
to currentEpoch
.
The root cause of the vulnerability lies in the calculation of pendingRewardsPerToken
:
This calculation uses the current totalStaked
value, which may not accurately reflect the total staked amount for each specific epoch. This leads to several issues:
Incorrect reward distribution: Users who staked in earlier epochs may receive disproportionate rewards.
Vulnerability to manipulation: A malicious user can exploit this by staking a large amount just before an epoch rollover and unstaking immediately after.
Inconsistent reward rates: The reward rate per token can vary depending on when the rollover occurs, rather than being consistent within an epoch.
This can lead to a significant imbalance in reward distribution among stakers. Malicious users who are aware of this issue can exploit it to gain a disproportionate share of rewards, effectively stealing value from other stakers.
While the total amount of rewards distributed is still bounded by the protocol's limits, the ability to manipulate the per-epoch distribution severely compromises the intended incentive structure.
Alice, an attacker, observes the current epoch and total staked amount.
Just before an epoch rollover, Alice calls stake()
with a large amount of tokens:
This updates totalStaked
to include Alice's large stake.
The epoch rolls over, triggering _checkEpochRollover()
:
pendingRewardsPerToken
is calculated using the inflated totalStaked
value.
Rewards for previous epochs are distributed based on this inflated value.
Immediately after the rollover, Alice calls unstake()
:
Alice removes her large stake, but the rewards for previous epochs have already been distributed using the inflated totalStaked
value.
Result: Alice receives a disproportionate amount of rewards for the previous epochs, while other stakers receive less than their fair share.
Manual review
To address this vulnerability, the contract should be modified to:
Track the total staked amount for each epoch separately.
Calculate rewards on a per-epoch basis, using the correct total staked amount for that specific epoch.
Accumulate rewards properly across multiple epoch rollovers.
The contest is live. Earn rewards by submitting a finding.
This is your time to appeal against judgements on your submissions.
Appeals are being carefully reviewed by our judges.