Core Contracts

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

Unbounded veRAACToken Fee Distribution Bypass

Summary

The FeeCollector contract allows veRAACShare distribution to exceed the specified fee type limits. This breaks core protocol economics by potentially distributing more rewards than intended to veRAACToken holders.

The issue occurs in the fee distribution mechanism where the FeeCollector calculates and distributes rewards to veRAACToken holders. The contract tracks total distributed amounts but fails to properly enforce per-fee-type distribution limits. When fees are collected and distributed, the veRAACShare portion can accumulate beyond the intended percentage cap for each fee type.

The FeeCollector's core promise is maintaining strict fee distribution ratios (80/20 split for protocol fees between veRAACToken holders and treasury). The code assumed the share calculations in _calculateDistribution() would naturally enforce limits. However, the accumulation of totalDistributed across multiple fee types creates an accounting disconnect.

Vulnerability Details

The FeeCollector's core purpose's managing protocol fees across eight distinct revenue streams, from lending fees to NFT royalties. Each stream has carefully designed distribution ratios, like the 80/20 split between veRAACToken holders and treasury for protocol fees. /FeeCollector.sol#_processDistributions() function

function _processDistributions(uint256 totalFees, uint256[4] memory shares) internal {
// 🏦 Check contract has enough tokens
uint256 contractBalance = raacToken.balanceOf(address(this));
if (contractBalance < totalFees) revert InsufficientBalance();
// 🎯 veRAACToken holder distribution
if (shares[0] > 0) {
uint256 totalVeRAACSupply = veRAACToken.getTotalVotingPower();
if (totalVeRAACSupply > 0) {
// ⏰ Create time-weighted distribution period
TimeWeightedAverage.createPeriod(
distributionPeriod,
block.timestamp + 1,
7 days,
shares[0],
totalVeRAACSupply
);
// 🚩 Vulnerability: Accumulates without checking against fee type limits
totalDistributed += shares[0];
} else {
// 📤 Redirect to treasury if no veRAAC holders
shares[3] += shares[0];
}
}
// 🔥 Process burn share
if (shares[1] > 0) raacToken.burn(shares[1]);
// 🛡️ Transfer to repair fund
if (shares[2] > 0) raacToken.safeTransfer(repairFund, shares[2]);
// 💰 Transfer to treasury
if (shares[3] > 0) raacToken.safeTransfer(treasury, shares[3]);
}

The contract tracks total distributions through totalDistributed, where totalDistributed accumulates without validating against the fee type's configured veRAACShare limit but fails to enforce per-fee-type limits during the actual distribution process. This creates a disconnect between intended and actual token flow. like a profit-sharing system where employees somehow receive 120% of the allocated bonus pool.

When the protocol collects 1000 RAAC in protocol fees, veRAACToken holders should receive 800 RAAC (80%). However, the current implementation allows their share to exceed this limit over time. The _processDistributions() function simply accumulates distributions without checking against the configured veRAACShare percentages.

This impacts the protocol's economic design in concrete ways. The treasury, meant to receive 20% of protocol fees for development, receives less than intended. The repair fund, crucial for protocol safety, accumulates reserves below target levels. Meanwhile, veRAACToken holders receive rewards beyond their designated share, distorting incentive mechanisms.

Impact

This allows accumulated distributions to veRAACToken holders to exceed their designated share percentage over time. The implementation shows careful thought in calculating shares.

The implementation shows careful thought in calculating shares: #L450

// 📊 Weighted calculations in _calculateDistribution()
shares[0] = (totalFees * shares[0]) / BASIS_POINTS;

But here's the part while the calculations look right, the accumulation in _processDistributions() lacks bounds:

// 💥 The critical moment
totalDistributed += shares[0]; // No limit checking!

Recommendations

The vulnerability is in the interaction between _processDistributions() and _calculateDistribution().

function _processDistributions(uint256 totalFees, uint256[4] memory shares) internal {
// 🏦 Validate contract balance
uint256 contractBalance = raacToken.balanceOf(address(this));
if (contractBalance < totalFees) revert InsufficientBalance();
// ✅ Add validation before distribution
for (uint8 i = 0; i < 8; i++) {
uint256 feeAmount = _getFeeAmountByType(i);
if (feeAmount > 0) {
uint256 typeLimit = (feeAmount * feeTypes[i].veRAACShare) / BASIS_POINTS;
require(shares[0] <= typeLimit, "Distribution exceeds type limit");
}
}
// 🎯 veRAACToken holder distribution
if (shares[0] > 0) {
uint256 totalVeRAACSupply = veRAACToken.getTotalVotingPower();
if (totalVeRAACSupply > 0) {
// ⏰ Create time-weighted distribution period
TimeWeightedAverage.createPeriod(
distributionPeriod,
block.timestamp + 1,
7 days,
shares[0],
totalVeRAACSupply
);
totalDistributed += shares[0];
} else {
// 📤 Redirect to treasury if no veRAAC holders
shares[3] += shares[0];
}
}
// 🔥 Process burn share
if (shares[1] > 0) raacToken.burn(shares[1]);
// 🛡️ Transfer to repair fund
if (shares[2] > 0) raacToken.safeTransfer(repairFund, shares[2]);
// 💰 Transfer to treasury
if (shares[3] > 0) raacToken.safeTransfer(treasury, shares[3]);
}

This ensures that shares[0] (veRAACToken holder share) never exceeds the configured limit for each fee type before the actual distribution occurs. The validation works in conjunction with the weighted calculations from _calculateDistribution() to maintain the protocol's economic constraints.

Updates

Lead Judging Commences

inallhonesty Lead Judge 4 months ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity
inallhonesty Lead Judge 4 months ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity

Support

FAQs

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