The FeeCollector contract calculates rewards in the claimRewards function using a user's current voting power (veRAACToken.getVotingPower(user)), rather than their time-weighted average voting power over the distribution period. This flaw enables users to manipulate reward distribution by locking additional RAACToken just before claiming, unfairly increasing their share of rewards at the expense of participants who maintained consistent stakes throughout the period. A test case demonstrated this vulnerability: User B, locking tokens 6 days late into a 7-day distribution period, received higher rewards (0.0014246 RAACToken) than User A (0.0014011 RAACToken), who locked at the start, despite the expectation that earlier participation should yield greater rewards.
The vulnerability originates in the claimRewards function of the FeeCollector contract, which uses the _calculatePendingRewards internal function to compute rewards based on a user's current voting power at the time of claiming. The distributeCollectedFees function establishes a 7-day distribution period via TimeWeightedAverage.createPeriod, suggesting rewards should reflect participation over this time. However, the lack of historical voting power consideration allows users to exploit the system by increasing their veRAACToken balance (voting power) right before claiming.
The test case demonstrates this issue:
Test Result:
Setup:
User A locks 1000 RAACToken at t=0 for one year.
Time advances by 6 days, then User B locks 1000 RAACToken at t=6 days.
Time advances by 1 day to t=7 days, followed by distributeCollectedFees to start the distribution period.
Execution:
At t=7 days, User A’s voting power is 245205479452054883200, and User B’s is 249315068493150697600 (minimal decay, locked 1 day ago).
Both claim rewards, with User A receiving 1401174122997753 and User B receiving 1424657488946872.
Observation:
Despite locking 6 days later, User B receives more rewards than User A due to higher current voting power at claim time.
The test expectation expect(rewardA).to.be.gt(rewardB) fails, confirming that the current implementation does not reward early participation appropriately, instead favoring late lockers who maximize voting power at claim time.
Root Cause: The use of veRAACToken.getVotingPower(user) in _calculatePendingRewards without averaging over the distribution period allows manipulation by timing lock actions.
This vulnerability has significant implications:
Fairness: Users who lock tokens early and maintain stakes throughout the distribution period receive disproportionately lower rewards compared to those who lock late, undermining the protocol's incentive structure.
Economic: Late lockers can exploit the system to claim a larger share of rewards, potentially draining the reward pool and harming honest participants.
Trust: The unfair reward distribution may erode user confidence in the protocol, reducing participation and adoption.
Manual, Hardhat test
Time-Weighted Average Voting Power:
Leverage the PowerCheckpoint library in veRAACToken to compute the time-weighted average voting power over the distribution period (e.g., from t=startTime to t=endTime).
Modify _calculatePendingRewards to use veRAACToken.getPastVotes(user, blockNumber) or a custom average calculation, ensuring rewards reflect historical participation.
Example: Integrate with TimeWeightedAverage.Period to average voting power over the 7-day period.
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.