Description
The FeeCollector contract clearly intends to implement a time-weighted reward distribution system as is evident in natspec:
File: contracts/core/collectors/FeeCollector.sol
15:
16: * @title Fee Collector Contract
17: * @author RAAC Protocol Team
18:@---> * @notice Manages protocol fee collection and distribution with time-weighted rewards
19: * @dev Core contract for handling all protocol fee operations
20: * Key features:
21: * - Fee collection from different protocol activities
22:@---> * - Time-weighted reward distribution to veRAAC holders
23: * - Configurable fee splits between stakeholders
24: * - Emergency controls and access role management
25: */
26: contract FeeCollector is IFeeCollector, AccessControl, ReentrancyGuard, Pausable {
To implement this the function _processDistributions()
even calls TimeWeightedAverage.createPeriod
here to create a 7 day period:
File: contracts/core/collectors/FeeCollector.sol
401: function _processDistributions(uint256 totalFees, uint256[4] memory shares) internal {
402: uint256 contractBalance = raacToken.balanceOf(address(this));
403: if (contractBalance < totalFees) revert InsufficientBalance();
404:
405: if (shares[0] > 0) {
406: uint256 totalVeRAACSupply = veRAACToken.getTotalVotingPower();
407: if (totalVeRAACSupply > 0) {
408:@---> TimeWeightedAverage.createPeriod(
409: distributionPeriod,
410: block.timestamp + 1,
411:@---> 7 days,
412: shares[0],
413: totalVeRAACSupply
414: );
415: totalDistributed += shares[0];
416: } else {
417: shares[3] += shares[0];
418: }
419: }
420:
421: if (shares[1] > 0) raacToken.burn(shares[1]);
422: if (shares[2] > 0) raacToken.safeTransfer(repairFund, shares[2]);
423: if (shares[3] > 0) raacToken.safeTransfer(treasury, shares[3]);
424: }
The intention most likely is:
Track user voting power over time windows (7 day periods)
Calculate rewards based on average voting power over those periods
This would make rewards fairer by using averaged voting power rather than instantaneous voting power at claim time
It would help prevent gaming by users who might try to time their claims based on voting power fluctuations
However, this period
is never actually linked to _calculatePendingRewards()
which simply uses current voting power ratios against total distributions instead of time-weighted averages:
File: contracts/core/collectors/FeeCollector.sol
474:
475:@---> * @dev Calculates pending rewards for a user using time-weighted average
476: * @param user Address of the user
477: * @return pendingAmount Amount of pending rewards
478: */
479: function _calculatePendingRewards(address user) internal view returns (uint256) {
480:@---> uint256 userVotingPower = veRAACToken.getVotingPower(user);
481: if (userVotingPower == 0) return 0;
482:
483: uint256 totalVotingPower = veRAACToken.getTotalVotingPower();
484: if (totalVotingPower == 0) return 0;
485:
486:@---> uint256 share = (totalDistributed * userVotingPower) / totalVotingPower;
487: return share > userRewards[user] ? share - userRewards[user] : 0;
488: }
Note that L475 says "time-weighted" but the userVotingPower
on L480 is really just the decayed veRAACToken balance at the current point of time.
Impact
Time-weighted voting power & reward never calculated in reality. Users can lock tokens to increase rewards instantaneously.
Mitigation
Make use of the time-weighted period
inside _calculatePendingRewards()
.