Core Contracts

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

Reward Distribution Calculation Vulnerability Analysis

Summary

After reviewing FeeCollector.sol, I can confirm this vulnerability exists and poses a serious risk to reward distribution fairness. The current implementation uses present-time voting power snapshots for historical reward calculations.

Technical Details

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;
// VULNERABILITY: Uses current voting power for historical rewards
uint256 share = (totalDistributed * userVotingPower) / totalVotingPower;
return share > userRewards[user] ? share - userRewards[user] : 0;
}

Impact

  1. Reward Manipulation

    • Users can back-date claim larger shares by increasing voting power after fee distribution

    • Early participants get diluted by late entries

    • Total reward distribution may exceed intended amounts

  2. Economic Impact

    // Economic Exploitation Example
    Time 1: Distribution of 1000 RAAC
    - Alice: 100 VP (50% of 200 total) → Should get 500 RAAC
    - Bob: 100 VP (50% of 200 total) → Should get 500 RAAC
    Time 2: Before claims
    - Charlie adds 800 VP
    - New total VP = 1000
    Time 3: Claims
    Alice claims: (100/1000) * 1000 = 100 RAAC (Lost 400 RAAC)
    Bob claims: (100/1000) * 1000 = 100 RAAC (Lost 400 RAAC)
    Charlie claims: (800/1000) * 1000 = 800 RAAC (Gained 800 RAAC unfairly)

Proof of Concept

// Test scenario
function testRewardManipulation() public {
// Setup
vm.startPrank(admin);
feeCollector.collectFee(1000e18, 0); // Collect 1000 RAAC
// Initial state
address alice = address(1);
address bob = address(2);
veRAACToken.mint(alice, 100e18);
veRAACToken.mint(bob, 100e18);
// Distribute rewards
feeCollector.distributeCollectedFees();
// Charlie enters with large position
address charlie = address(3);
veRAACToken.mint(charlie, 800e18);
// Claims
uint256 aliceReward = feeCollector.claimRewards(alice);
uint256 bobReward = feeCollector.claimRewards(bob);
uint256 charlieReward = feeCollector.claimRewards(charlie);
// Assertions
assert(aliceReward == 100e18); // Should be 500e18
assert(bobReward == 100e18); // Should be 500e18
assert(charlieReward == 800e18); // Should be 0
}

Recommended Mitigation

contract FeeCollector {
struct RewardSnapshot {
uint256 timestamp;
uint256 totalVotingPower;
uint256 rewardAmount;
}
RewardSnapshot[] public rewardSnapshots;
mapping(address => uint256) public lastClaimedIndex;
function _processDistributions(uint256 totalFees, uint256[4] memory shares) internal {
// ... existing code ...
if (shares[0] > 0) {
rewardSnapshots.push(RewardSnapshot({
timestamp: block.timestamp,
totalVotingPower: veRAACToken.getTotalVotingPower(),
rewardAmount: shares[0]
}));
}
}
function _calculatePendingRewards(address user) internal view returns (uint256) {
uint256 pending = 0;
uint256 startIndex = lastClaimedIndex[user];
for (uint256 i = startIndex; i < rewardSnapshots.length; i++) {
RewardSnapshot memory snapshot = rewardSnapshots[i];
uint256 userVotingPowerAt = veRAACToken.getPastVotingPower(user, snapshot.timestamp);
pending += (snapshot.rewardAmount * userVotingPowerAt) / snapshot.totalVotingPower;
}
return pending;
}
}
Updates

Lead Judging Commences

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

Time-Weighted Average Logic is Not Applied to Reward Distribution in `FeeCollector`

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

Time-Weighted Average Logic is Not Applied to Reward Distribution in `FeeCollector`

Support

FAQs

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

Give us feedback!