Core Contracts

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

Incorrect user reward tracking in `FeeCollector::claimRewards` causes DoS of reward claims

Summary

In the FeeCollector contract, the claimRewards function incorrectly sets userRewards[user] to totalDistributed instead of the user's proportional share, causing all subsequent reward claims to fail as the calculated pending rewards will always be zero.

Vulnerability Details

The issue lies in how reward tracking and claiming is implemented. Let's break down the flow:

The contract tracks total distributed rewards in totalDistributed, which accumulates all rewards allocated to veRAAC holders over time.

When calculating pending rewards, it uses the formula share = (totalDistributed * userVotingPower) / totalVotingPower which is the right one for calculating the rewards share for the user:

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;
// @audit this is the correct formular for share
uint256 share = (totalDistributed * userVotingPower) / totalVotingPower;
return share > userRewards[user] ? share - userRewards[user] : 0;
}

However, in claimRewards, during the first successful claim:

function claimRewards(address user) ... {
// ...
// @audit userRewards[user] is inccorrectly set.
userRewards[user] = totalDistributed;

During the upcoming claims, it always reverts;

function claimRewards(address user) ... {
// ...
// @audit will always return zero
uint256 pendingReward = _calculatePendingRewards(user);
if (pendingReward == 0) revert InsufficientBalance();

pendingReward after the first claim, will be zero always because in _calculatePendingRewards, return share > userRewards[user] ? share - userRewards[user] : 0; will evaluate to zero since share will always be smaller than userRewards[user] = totalDistributed.

Therefore setting userRewards[user] as totalDistributed is incorrect because totalDistributed represents ALL rewards ever distributed to ALL users, not just this user's share. This effectively makes it impossible for users to claim rewards after their first claim, as their recorded rewards will always be higher than their actual share.

PoC

  1. Alice has 10% of total veRAAC voting power

  2. Contract distributes 1000 RAAC tokens (totalDistributed = 1000)

  3. Alice claims rewards for first time:

    • Calculated share = 1000 * 10% = 100 RAAC

    • Alice receives 100 RAAC

    • userRewards[Alice] is set to 1000

  4. Contract distributes another 1000 RAAC (totalDistributed = 2000)

  5. Alice tries to claim again:

    • Calculated share = 2000 * 10% = 200

    • 200 is not > 1000 (userRewards[Alice])

    • Returns 0

    • Transaction reverts with InsufficientBalance

Impact

Complete denial of service for the reward claiming functionality after first claim, permanently locking user rewards in the contract.

Tools Used

Manual review

Recommendations

Fix the tracking of claimed rewards by storing the user's proportional share instead of total distributed:

function claimRewards(address user) external override nonReentrant whenNotPaused returns (uint256) {
if (user == address(0)) revert InvalidAddress();
uint256 pendingReward = _calculatePendingRewards(user);
if (pendingReward == 0) revert InsufficientBalance();
// Store user's proportional share instead of total
uint256 userVotingPower = veRAACToken.getVotingPower(user);
uint256 totalVotingPower = veRAACToken.getTotalVotingPower();
userRewards[user] = (totalDistributed * userVotingPower) / totalVotingPower; // @audit fixed
// ...
}
Updates

Lead Judging Commences

inallhonesty Lead Judge 6 months ago
Submission Judgement Published
Validated
Assigned finding tags:

FeeCollector::claimRewards sets `userRewards[user]` to `totalDistributed` seriously grieving users from rewards

Support

FAQs

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