Summary
The contract references a TimeWeightedAverage mechanism and calls TimeWeightedAverage.createPeriod(...)
in _processDistributions()
. However, the actual reward calculation in _calculatePendingRewards()
does not read or incorporate any time-weighted logic. Instead, rewards are distributed based on a simple snapshot fraction of totalDistributed.
Vulnerability Details
Missing Time Weighting: While the contract claims to use time-weighted distribution, it never actually applies the data from distributionPeriod
or the TimeWeightedAverage
library in the final reward calculation.
Simplistic Formula: The FeeCollector::_calculatePendingRewards()
function merely does:
uint256 share = (totalDistributed * userVotingPower) / totalVotingPower;
This formula does not account for the passage of time of the distribution period.
Impact
Users who acquire veRAAC mid-period receive rewards based on their voting power at the snapshot, not over the duration they held it.
POC
Here is a POC of the issue:
it("should calculate pending rewards correctly", async function () {
const pendingRewardsBeforeUser1 = await feeCollector.getPendingRewards(user1.address);
console.log("User1 Pending Rewards Before Distribution:", pendingRewardsBeforeUser1.toString());
await feeCollector.connect(owner).distributeCollectedFees();
await time.increase(WEEK);
const pendingRewardsBeforeUser3 = await feeCollector.getPendingRewards(user3.address);
console.log("User3 Pending Rewards After Distribution and one week:", pendingRewardsBeforeUser3.toString());
await veRAACToken.connect(user3).lock(ethers.parseEther("1000"), ONE_YEAR);
const pendingRewardsAfterLockUser3 = await feeCollector.getPendingRewards(user3.address);
console.log(
"User3 Pending Rewards After Distribution, straight after lock:",
pendingRewardsAfterLockUser3.toString()
);
const pendingRewardsAfterWeekUser1 = await feeCollector.getPendingRewards(user1.address);
console.log(
"User1 Pending Rewards After Distribution, straight after user3 locked:",
pendingRewardsAfterWeekUser1.toString()
);
});
and the output:
User1 Pending Rewards Before Distribution: 0
User3 Pending Rewards After Distribution and one week: 0
User3 Pending Rewards After Distribution, straight after lock: 2000000000000000
User1 Pending Rewards After Distribution, straight after user3 locked: 1923287354134958
Tools Used
Manual Review
Recommendations
Incorporate Time Weighting in _calculatePendingRewards()