Core Contracts

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

The claimRewards function in the FeeCollector contract has a logical flaw that will cause reward loss

Summary

The claimRewards function in the FeeCollector contract has a logical flaw that prevents users from claiming their fair share of rewards over multiple distribution periods. Specifically, the function sets userRewards[user] to totalDistributed after a claim, which incorrectly assumes the user has claimed all rewards up to the current totalDistributed. This results in users losing their share of rewards distributed between claims.

Vulnerability Details

In the claimRewards function, it will call _calculatePendingRewards function to calculayte the pending rewards. The _calculatePendingRewards function use the following code to calculate the pending rewards:

uint256 share = (totalDistributed * userVotingPower) / totalVotingPower;
return share > userRewards[user] ? share - userRewards[user] : 0;

This formula ensures that users only receive rewards for the portion of totalDistributed that they haven't already claimed.

After calculating pendingReward, the claimRewards function sets userRewards[user] = totalDistributed:

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();
// Reset user rewards before transfer
userRewards[user] = totalDistributed;
// Transfer rewards
raacToken.safeTransfer(user, pendingReward);
emit RewardClaimed(user, pendingReward);
return pendingReward;
}

This resets the user's reward tracking to the current totalDistributed, which skips this incremental calculation and results in the user receiving no rewards.

Scenario

  1. First Distribution:

    • totalDistributed = 1000

    • userVotingPower = 50

    • totalVotingPower = 100

    • share = (1000 * 50) / 100 = 500

    • Since userRewards[user] = 0, pendingReward = 500 - 0 = 500

    • After claiming, userRewards[user] is set to totalDistributed = 1000.

  2. Second Distribution:

    • totalDistributed = 2000

    • userVotingPower = 50

    • totalVotingPower = 100

    • share = (2000 * 50) / 100 = 1000

    • Since userRewards[user] = 1000, pendingReward = 1000 - 1000 = 0

    • The user receives no rewards in the second claim.

The user should receive their share of the newly distributed rewards (from totalDistributed = 1000 to totalDistributed = 2000):

pendingReward = (2000 * 50 / 100) - (1000 * 50 / 100) = 1000 - 500 = 500

However, the user receives no rewards in the second claim because userRewards[user] is incorrectly set to totalDistributed.

Impact

  • Users lose their fair share of rewards distributed between claims.

  • This undermines the fairness and integrity of the reward distribution mechanism.

The impact is High, the likelihood is High, so the severity is High.

Tools Used

Manual Review

Recommendations

To resolve this issue, userRewards[user] should be updated to reflect the user's claimed share of totalDistributed instead of being set to totalDistributed directly.

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();
// Update userRewards to reflect the user's claimed share
userRewards[user] += pendingReward;
// Transfer rewards
raacToken.safeTransfer(user, pendingReward);
emit RewardClaimed(user, pendingReward);
return pendingReward;
}
Updates

Lead Judging Commences

inallhonesty Lead Judge 4 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.