Core Contracts

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

Incorrect Reward Checkpointing in `FeeCollector` Allows Users to Claim Past Distributed Rewards

Summary

The userRewards[user] storage variable in FeeCollector is intended to act as a checkpoint for tracking distributed rewards. However, since it is only updated when a user claims rewards, it allows users to retroactively claim past distributed rewards they were not eligible for at the time, leading to reward over-allocations and potential exploitation.

Vulnerability Details

When claimRewards(...) is called, users with voting power are entitled to a share of the totalDistributed RAAC tokens. This total is updated when FeeCollector processes collected fees.

Key Issue

The userRewards[user] checkpoint is only updated during the claim process:

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; // @audit-issue Updated only when claiming, allowing retroactive claims
// Transfer rewards
raacToken.safeTransfer(user, pendingReward);
emit RewardClaimed(user, pendingReward);
return pendingReward;
}

The _calculatePendingRewards(...) function determines the user's reward share:

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; // Accounted immediately
return share > userRewards[user] ? share - userRewards[user] : 0;
}

Exploit Scenario

  1. Assume FeeCollector distributes 1000 RAAC at time X and another 2000 RAAC at time Y.

  2. Users claims their rewards before time Z, correctly receiving their share of the first two distributions.
    3 A new user B gains voting power before time Z. At time Z, FeeCollector distributes 3000 RAAC,

  3. Since the checkpoint is not updated for past distributions, the new user wrongfully claims 6000 RAAC, receiving rewards they were never eligible for.

Additional Issue

  • Voting power changes over time, but the current implementation does not update reward eligibility when tokens are locked or unlocked. As a result, users may miss rewards they are entitled to.

Impact

  • Users can claim past distributed rewards, leading to significant overpayment.

  • Reward calculations become inaccurate, creating a system-wide accounting issue.

  • Legitimate users may lose rewards, as new claimants can unfairly take a larger share.

Tools Used

N/A

Recommendations

The voting contract should update the user's checkpoint upon every locking and unlocking, also storage variable for eligible/unclaimed rewards should be involved. In general, a Synthetix staking contract should be used as a reference for the rewarding model. (ref)

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.