Core Contracts

Regnum Aurum Acquisition Corp
HardhatReal World AssetsNFT
77,280 USDC
View results
Submission Details
Severity: medium
Invalid

User Rewards Can Exceed Total Distributed Amount in FeeCollector

Summary

The FeeCollector contract has a flaw in reward distribution calculations where a user's rewards can exceed the total distributed amount. This breaks fundamental accounting principles and could lead to reward inflation. In the reward calculation logic where voting power changes aren't properly bounded against total distributions: FeeCollector.sol#_calculatePendingRewards

function _calculatePendingRewards(address user) internal view returns (uint256) {
// Voting power snapshot can be manipulated before reward calculation
uint256 userVotingPower = veRAACToken.getVotingPower(user);
if (userVotingPower == 0) return 0;
uint256 totalVotingPower = veRAACToken.getTotalVotingPower();
if (totalVotingPower == 0) return 0;
// Critical: No upper bound check on share calculation
// Share can exceed totalDistributed when userVotingPower/totalVotingPower ratio is manipulated
uint256 share = (totalDistributed * userVotingPower) / totalVotingPower;
// Missing validation that share <= totalDistributed
// This allows claiming more rewards than what's been distributed
return share > userRewards[user] ? share - userRewards[user] : 0;
}

The core vulnerability lies in the unbounded share calculation and lack of validation against totalDistributed. This matches exactly with the Certora verification rule that failed: verifyUserRewardBounds.

Vulnerability Details

The FeeCollector contract manages reward distribution in a way similar to a bank's dividend system. Just as a bank tracks total dividends paid and individual account balances, the contract tracks totalDistributed and individual userRewards. However, there's flaw in this accounting system.

Imagine a bank allowing withdrawals to exceed its total deposits, this is exactly what happens in the FeeCollector. When users interact with the contract, their voting power through veRAACToken determines their reward share. The contract calculates this share using: FeeCollector.sol#L486

uint256 share = (totalDistributed * userVotingPower) / totalVotingPower;

A malicious user could manipulate their voting power right before reward distribution. For example, if totalDistributed is 1000 RAAC tokens and a user temporarily increases their voting power to 90% of the total, they could claim 900 RAAC tokens even if the intended distribution was much lower.

The impact ripples through the entire protocol. When excessive rewards are claimed, it depletes the reward pool faster than intended, directly affecting other users' ability to claim their fair share. And this isn't just about individual losses it undermines the entire incentive structure designed to encourage long-term protocol participation.

Looking at the code:

function _calculatePendingRewards(address user) internal view returns (uint256) {
// Voting power snapshot can be manipulated before reward calculation
uint256 userVotingPower = veRAACToken.getVotingPower(user);
if (userVotingPower == 0) return 0;
uint256 totalVotingPower = veRAACToken.getTotalVotingPower();
if (totalVotingPower == 0) return 0;
// Critical: No upper bound check on share calculation
// Share can exceed totalDistributed when userVotingPower/totalVotingPower ratio is manipulated
uint256 share = (totalDistributed * userVotingPower) / totalVotingPower;
// Missing validation that share <= totalDistributed
// This allows claiming more rewards than what's been distributed
return share > userRewards[user] ? share - userRewards[user] : 0;
}

Impact

The contract assumes voting power ratios will always result in valid reward amounts, but fails to enforce this invariant.

Recommendations

function _calculatePendingRewards(address user) internal view returns (uint256) {
// Get user's current voting power
uint256 userVotingPower = veRAACToken.getVotingPower(user);
if (userVotingPower == 0) return 0; // Early return for zero voting power
// Get total voting power across all users
uint256 totalVotingPower = veRAACToken.getTotalVotingPower();
if (totalVotingPower == 0) return 0; // Early return for zero total power
// Calculate user's share of rewards based on voting power ratio
uint256 share = (totalDistributed * userVotingPower) / totalVotingPower;
// CRITICAL FIX - Add bounds check before calculating pending amount
if (share > totalDistributed) {
share = totalDistributed;
}
// Calculate pending amount as difference between share and already claimed rewards
return share > userRewards[user] ? share - userRewards[user] : 0;
}
Updates

Lead Judging Commences

inallhonesty Lead Judge about 1 month ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.