Summary
The contract calculates rewards based on a user's current veRAAC balance relative to the current total supply, but applies this ratio to all historical distributions. This allows users to claim rewards from periods when they did not hold any tokens.
Vulnerability Details
uint256 public totalDistributed;
function _calculatePendingRewards(address user) internal view returns (uint256) {
uint256 userVotingPower = veRAACToken.getVotingPower(user);
uint256 totalVotingPower = veRAACToken.getTotalVotingPower();
uint256 share = (totalDistributed * userVotingPower) / totalVotingPower;
return share > userRewards[user] ? share - userRewards[user] : 0;
}
The contract calculates rewards based on a user's current veRAAC balance relative to the current total supply, but applies this ratio to all historical distributions. This allows users to claim rewards from periods when they did not hold any tokens.
-
Initial State:
Alice's share = (100 * 100) / 100 = 100 tokens
Total veRAAC supply increases to 200
totalDistributed becomes 200 (100 + 100)
Bob buys/gets 200 veRAAC (now 100% of 200 total supply)
Bob's share = (200 * 200) / 200 = 200 tokens
Includes 100 tokens from Distribution 1 when Bob had 0 veRAAC!
uint256 public totalDistributed;
function _processDistributions() internal {
totalDistributed += shares[0];
}
function _calculatePendingRewards() {
uint256 share = (totalDistributed * userVotingPower) / totalVotingPower;
}
Impact
Protocol distributes $1M to early stakers (total supply: 1000 veRAAC).
Attacker buys 1000 veRAAC later (total supply: 2000).
Attacker claims: (1,000,000 * 1000) / 2000 = $500,000 they never earned.
Tools Used
Foundry
Recommendations
Track rewards per distribution period with historical snapshots
struct DistributionEpoch {
uint256 amount;
uint256 totalVotingPower;
mapping(address => uint256) userVotingPower;
}
mapping(uint256 => DistributionEpoch) public epochs;
uint256 public currentEpoch;
function _processDistributions() internal {
epochs[currentEpoch].amount = shares[0];
epochs[currentEpoch].totalVotingPower = veRAACToken.getTotalVotingPower();
for (each user) {
epochs[currentEpoch].userVotingPower[user] = veRAACToken.getVotingPower(user);
}
currentEpoch++;
}
function _calculatePendingRewards(address user) internal view {
uint256 total;
for (uint256 i = 0; i < currentEpoch; i++) {
DistributionEpoch storage epoch = epochs[i];
if (epoch.userVotingPower[user] > 0) {
total += (epoch.amount * epoch.userVotingPower[user]) / epoch.totalVotingPower;
}
}
return total - claimed[user];
}