The BaseGauge contract implements staking and reward distribution functionality. Users can stake tokens and earn rewards over time. The reward distribution period is meant to be fixed (7 days by default) when rewards are notified via notifyRewardAmount. The reward rate is calculated by dividing the reward amount by the period duration.
The BaseGauge#periodFinish function incorrectly calculates the end time of the reward period by adding the period duration to lastUpdateTime. Since lastUpdateTime is updated in the updateReward modifier (which is used in many functions like stake, withdraw, etc.), this causes the period end time to extend each time these functions are called.
The root cause is in BaseGauge#periodFinish:
This affects reward calculations in getRewardPerToken which uses lastTimeRewardApplicable() that depends on periodFinish():
Medium. The reward rate remains fixed as set in notifyRewardAmount, but the incorrect period finish calculation allows rewards to continue being distributed after the intended period end, as long as there are remaining rewards in the contract. This affects the intended reward distribution schedule but does not lead to loss of funds or manipulation of reward rates.
High. This occurs whenever there are remaining rewards in the contract after the intended period end, and users continue to interact with the contract through functions with the updateReward modifier.
At t=100, admin calls notifyRewardAmount with 700 tokens for a 7-day period
Reward rate is set to 100 tokens/day
Intended period finish: t=107
At t=107 (period end), 100 tokens remain unclaimed in the contract
Due to various factors, such as users not claiming,...
At t=108, User A calls stake
updateReward modifier updates lastUpdateTime to 108
periodFinish now returns 115 (108 + 7)
The remaining rewards continue to be distributed using the same reward rate, despite the intended period being over
The key point is that the reward rate remains constant as set in notifyRewardAmount, but the distribution period incorrectly extends, allowing distribution of any remaining rewards beyond the intended period end.
Store the period finish timestamp explicitly and handle the case where lastTimeRewardApplicable() is less than lastUpdateTime:
This change prevents potential underflow in the reward calculation and ensures rewards are correctly calculated when the period has finished.
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.