Core Contracts

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

Reward Calculation Ignores Time

Summary

The contract calculates rewards based on a user's current veRAAC balance relative to the current total supply, but applies this ratio to all historical distributions. This allows users to claim rewards from periods when they did not hold any tokens.

  • totalDistributed accumulates rewards across all historical distributions.

  • New users can claim rewards from before they staked by using current voting power

Vulnerability Details

// Tracks cumulative rewards across all distributions
uint256 public totalDistributed;
// Reward calculation ignores timing of distributions
function _calculatePendingRewards(address user) internal view returns (uint256) {
uint256 userVotingPower = veRAACToken.getVotingPower(user); // Current balance
uint256 totalVotingPower = veRAACToken.getTotalVotingPower(); // Current total
// Uses cumulative totalDistributed for all historical periods
uint256 share = (totalDistributed * userVotingPower) / totalVotingPower;
return share > userRewards[user] ? share - userRewards[user] : 0;
}

The contract calculates rewards based on a user's current veRAAC balance relative to the current total supply, but applies this ratio to all historical distributions. This allows users to claim rewards from periods when they did not hold any tokens.

  1. Initial State:

    • Total veRAAC supply: 100

    • Alice holds 100 veRAAC (100% of supply)

    • totalDistributed = 100 after first distribution

Alice's share = (100 * 100) / 100 = 100 tokens
Total veRAAC supply increases to 200
totalDistributed becomes 200 (100 + 100)
Bob buys/gets 200 veRAAC (now 100% of 200 total supply)
Bob's share = (200 * 200) / 200 = 200 tokens
Includes 100 tokens from Distribution 1 when Bob had 0 veRAAC!
uint256 public totalDistributed; // Cumulative sum of all distributions
function _processDistributions() internal {
totalDistributed += shares[0]; // Adds to global total
}
function _calculatePendingRewards() {
// Uses CURRENT balances for ALL historical distributions
uint256 share = (totalDistributed * userVotingPower) / totalVotingPower;
}
// No tracking of:
// - When distributions occurred
// - User balances at distribution time
// - Total supply at distribution time

Impact

  1. Protocol distributes $1M to early stakers (total supply: 1000 veRAAC).

  2. Attacker buys 1000 veRAAC later (total supply: 2000).

  3. Attacker claims: (1,000,000 * 1000) / 2000 = $500,000 they never earned.

Tools Used

Foundry

Recommendations

Track rewards per distribution period with historical snapshots

// Add to contract
struct DistributionEpoch {
uint256 amount;
uint256 totalVotingPower;
mapping(address => uint256) userVotingPower;
}
mapping(uint256 => DistributionEpoch) public epochs;
uint256 public currentEpoch;
// Modified distribution
function _processDistributions() internal {
epochs[currentEpoch].amount = shares[0];
epochs[currentEpoch].totalVotingPower = veRAACToken.getTotalVotingPower();
// Snapshot user balances
for (each user) {
epochs[currentEpoch].userVotingPower[user] = veRAACToken.getVotingPower(user);
}
currentEpoch++;
}
// Modified reward calculation
function _calculatePendingRewards(address user) internal view {
uint256 total;
for (uint256 i = 0; i < currentEpoch; i++) {
DistributionEpoch storage epoch = epochs[i];
if (epoch.userVotingPower[user] > 0) {
total += (epoch.amount * epoch.userVotingPower[user]) / epoch.totalVotingPower;
}
}
return total - claimed[user];
}
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!