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.