Core Contracts

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

Late Participation Exploit Allows Backdated Reward Claims in Fee Distribution System

Summary

The FeeCollector's reward distribution mechanism contains a flaw where malicious users can claim rewards for entire collection periods despite minimal participation. By strategically timing token locks to coincide with reward distribution snapshots, attackers gain eligibility for rewards proportional to their last-minute stake size rather than actual protocol contribution duration. This is possible because the contract calculates rewards based on the user's current voting power at the time of the reward snapshot, rather than verifying whether the user was actively contributing throughout the fee collection period.

https://github.com/Cyfrin/2025-02-raac/blob/main/contracts/core/collectors/FeeCollector.sol

Vulnerability Details

problematic implementation in the _calculatePendingRewards

uint256 share = (totalDistributed * userVotingPower) / totalVotingPower;

Attack scenario

  1. Alice locks 100 RAAC for 310 days at Day 0.

  2. Bob locks 100 RAAC for 300 days at Day 10.

  3. At Day 10, a fee distribution occurs. Both Alice and Bob have identical voting power (300 days remaining).

Result: Alice and Bob split rewards equally, despite Alice contributing to the protocol for 10 additional days.

Impact

Unfair Dilution: Legitimate users experience reduced yields as malicious users capture disproportionate shares despite minimal participation.

Tools Used

manual review

Recommendations

  1. initialize lastClaimTime at lock time .

function lock(uint256 amount, uint256 duration) external {
// Existing logic
if (lastClaimTime[msg.sender] == 0) {
lastClaimTime[msg.sender] = block.timestamp;
}
// Rest of the lock logic
}
  1. Implement Time-Weighted Voting Power and check if lastclaimtime is after distribution start period to prevent malicious users from claiming rewards after start of distribution .

function _calculatePendingRewards(address user) internal view returns (uint256) {
uint256 lastClaim = lastClaimTime[user];
uint256 distributionStart = distributionPeriod.startTime;
if (lastClaim == 0) return 0;
if (lastClaim > distributionStart) return 0;
uint256 currentTime = block.timestamp;
// Get voting power at start and end of the period
uint256 powerStart = veRAACToken.getVotingPowerAtTimestamp(user, lastClaim);
uint256 powerEnd = veRAACToken.getVotingPowerAtTimestamp(user, currentTime);
uint256 avgUserVotingPower = (powerStart + powerEnd) / 2;
uint256 share = (totalDistributed * avgUserVotingPower) / totalVotingPower;
return share > userRewards[user] ? share - userRewards[user] : 0;
}
  1. Call _updateLastClaimTime() in the claimRewards function to update the new lastclaimTime after every claim .

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);
_updateLastClaimTime(user);
emit RewardClaimed(user, pendingReward);
return pendingReward;
}
Updates

Lead Judging Commences

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

_updateLastClaimTime not properly used to track rewards claim time

Support

FAQs

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

Give us feedback!