The FeeCollector contract fails to maintain a history of distribution periods by overwriting a single distributionPeriod variable with each new distribution, leading to inaccurate reward calculations. This prevents the contract from properly implementing a time-weighted reward distribution, causing users to lose rewards from earlier periods if their voting power changes.
The root cause of this vulnerability lies in the contract’s use of a single distributionPeriod variable of type TimeWeightedAverage.Period, which is updated in the _processDistributions function each time a new fee distribution occurs.
_processDistributions overwrites the existing distributionPeriod with data for the new 7-day period, discarding all information about previous periods.
In the _processDistributions function, when fees are distributed, shares[0] (the portion for veRAAC holders) is assigned to a new period, and totalDistributed is incremented to reflect the cumulative rewards. However, the reward calculation in _calculatePendingRewards, using the formula (totalDistributed * userVotingPower) / totalVotingPower relies solely on the user’s current voting power at the time of claiming, ignoring their historical voting power across past periods. Because distributionPeriod only stores the latest period’s data (e.g., startTime, endTime, value, and weight), the contract cannot reference earlier periods’ details, such as the total voting power or reward amounts specific to those times. This means the TimeWeightedAverage library, despite its capability to track time-weighted values via fields like weightedSum, is underutilized, its data is overwritten and never used for reward computation.
This design conflicts with the intended time-weighted distribution mechanism described in the protocol documentation, which states that it accounts for total voting power changes. A true time-weighted system requires tracking a history of all distribution periods to calculate rewards based on a user’s voting power over time, not just the most recent period.
Users who participated in earlier distribution periods with high voting power but later reduced or withdrew their stakes receive fewer rewards or none at all, if their current voting power is low when they claim. This not only undermines fairness but also discourages long-term participation, as historical contributions are effectively erased.
Users could exploit this by increasing their voting power just before claiming, gaining disproportionate rewards for past periods they didn’t fully participate in.
Manual Review
To address this issue, modify the contract to maintain a history of distribution periods rather than relying on a single distributionPeriod variable. Replace distributionPeriod with a mapping or array (e.g., mapping(uint256 => Period) public distributionPeriods with an incrementing periodId) to store each period’s data persistently. Update _processDistributions to create and store a new period entry without overwriting previous ones, and adjust _calculatePendingRewards to iterate over all past periods, calculating rewards based on the user’s voting power during each period using the TimeWeightedAverage library’s time-weighted features (e.g., leveraging weightedSum).
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.