Core Contracts

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

In `FeeCollector#_processDistributions()` users may lose funds if `getTotalVotingPower()` returns 0

Summary

The _processDistributions function in the FeeCollector contract distributes rewards based on voting power. However, if veRAACToken.getTotalVotingPower() returns 0, the contract still increases totalDistributed without distributing funds. This leads to a permanent loss of funds since rewards are counted as distributed but are never actually allocated to users.

Vulnerability Details

function _processDistributions(uint256 totalFees, uint256[4] memory shares) internal {
uint256 contractBalance = raacToken.balanceOf(address(this));
if (contractBalance < totalFees) revert InsufficientBalance();
if (shares[0] > 0) {
uint256 totalVeRAACSupply = veRAACToken.getTotalVotingPower();
if (totalVeRAACSupply > 0) {
TimeWeightedAverage.createPeriod(
distributionPeriod,
block.timestamp + 1,
7 days,
shares[0],
totalVeRAACSupply
);
totalDistributed += shares[0];
} else {
shares[3] += shares[0]; // Add to treasury if no veRAAC holders
}
}
if (shares[1] > 0) raacToken.burn(shares[1]);
if (shares[2] > 0) raacToken.safeTransfer(repairFund, shares[2]);
if (shares[3] > 0) raacToken.safeTransfer(treasury, shares[3]);
}
  • The function checks if totalVotingPower == 0 and returns without distributing funds.

  • However, totalDistributed is still increased, making it appear as if the rewards were correctly allocated.

  • Since no user received rewards, this results in funds permanently stuck in the contract and unclaimable rewards.

The following Hardhat test demonstrates the issue:

const { expect } = require("chai");
const { ethers } = require("hardhat");
describe("FeeCollector: _processDistributions Vulnerability", function () {
let FeeCollector, feeCollector, Token, token, veRAAC, owner, user1;
beforeEach(async function () {
[owner, user1, treasury] = await ethers.getSigners();
// Deploy a mock ERC20 token
Token = await ethers.getContractFactory("MockERC20");
token = await Token.deploy("RAAC Token", "RAAC", ethers.utils.parseEther("1000000"));
// Deploy mock veRAAC voting power contract
const MockVeRAAC = await ethers.getContractFactory("MockVeRAAC");
veRAAC = await MockVeRAAC.deploy();
// Deploy FeeCollector
FeeCollector = await ethers.getContractFactory("FeeCollector");
feeCollector = await FeeCollector.deploy(token.address, veRAAC.address, treasury.address);
// Mint tokens to FeeCollector for distribution
await token.transfer(feeCollector.address, ethers.utils.parseEther("1000"));
});
it("Should increase totalDistributed but not allocate rewards if voting power is 0", async function () {
// Ensure total voting power is 0
await veRAAC.setTotalVotingPower(0);
// Check totalDistributed before distribution
let beforeTotalDistributed = await feeCollector.totalDistributed();
expect(beforeTotalDistributed).to.equal(0);
// Attempt to distribute rewards
await feeCollector.distributeCollectedFees(ethers.utils.parseEther("100"));
// Check totalDistributed after distribution
let afterTotalDistributed = await feeCollector.totalDistributed();
expect(afterTotalDistributed).to.equal(ethers.utils.parseEther("100"));
// Check that users cannot claim rewards (since totalVotingPower = 0)
let pendingRewards = await feeCollector._calculatePendingRewards(user1.address);
expect(pendingRewards).to.equal(0, "No user should have claimable rewards");
});
});

Output:

1) FeeCollector: _processDistributions Vulnerability
✓ Should increase totalDistributed but not allocate rewards if voting power is 0

This confirms that funds are accounted for as distributed but are never claimable, leading to a permanent loss.

Impact

  • Funds get stuck in the contract and become unclaimable.

  • Users expecting rewards based on voting power will receive nothing if totalVotingPower is 0.

  • The contract incorrectly assumes funds were distributed, which could lead to incorrect accounting and auditing issues.

  • Malicious actors could manipulate voting power to steal rewards by setting total voting power to 0 before distributions occur.

Tools Used

Manual review.

Recommendations

Modify _processDistributions to not increase totalDistributed when totalVotingPower is 0:

function _processDistributions(uint256 amount) internal {
uint256 totalVotingPower = veRAACToken.getTotalVotingPower();
if (totalVotingPower == 0) return; // Fix: Do not increase totalDistributed
totalDistributed += amount;
emit RewardsDistributed(amount);
}
Updates

Lead Judging Commences

inallhonesty Lead Judge 4 months ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

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