Core Contracts

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

`FeeCollector` unable to withdraw collected `RAAC` tokens through `FeeCollector::distributeCollectedFees`

Summary

The FeeCollector contract accumulates RAAC token as burn fees and swap fees, but these fees are not properly accounted for, making it impossible for the fee collector to withdraw the collected RAAC tokens via FeeCollector::distributeCollectedFees. While the contract includes a function (FeeCollector::distributeCollectedFees) intended for distributing fees, the mechanism only updates the fee amount in FeeCollector::collectFee, which does not track RAAC fees collected through token operations.

Vulnerability Details

Problem Description

The RAACToken contract collects fees on token transactions and transfers them to the FeeCollector. However, FeeCollector does not properly track these fees, making them inaccessible when attempting to distribute the collected amounts. The root cause is that FeeCollector::collectFee does not register fees collected through RAAC token operations, causing all tracked fee categories to remain zero.

Since fees collected from RAAC token operations are not recorded under collectedFees, the function always evaluates totalDistributable as zero, preventing the distribution of funds.

function distributeCollectedFees() external override nonReentrant whenNotPaused {
if (!hasRole(DISTRIBUTOR_ROLE, msg.sender)) revert UnauthorizedCaller();
uint256 totalFees = _calculateTotalFees();
if (totalFees == 0) revert InsufficientBalance();
uint256[4] memory shares = _calculateDistribution(totalFees);
_processDistributions(totalFees, shares);
delete collectedFees;
emit FeeDistributed(shares[0], shares[1], shares[2], shares[3]);
}

A simple test demonstrates the issue:

// SPDX-License-Identifier: MIT
pragma solidity 0.8.20;
import {Test, console} from "forge-std/Test.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "src/core/tokens/RAACToken.sol";
import "src/core/tokens/veRAACToken.sol";
import "src/core/collectors/FeeCollector.sol";
import "src/core/collectors/Treasury.sol";
contract FeeCollectorCannotWithdrawCollectedFeeTest is Test {
RAACToken internal raac;
veRAACToken internal veRaac;
FeeCollector internal feeCollector;
Treasury internal treasury;
address internal repairFund;
address internal minter;
address internal user;
function setUp() public {
// Role Creation
repairFund = makeAddr("repairFund");
minter = makeAddr("minter");
user = makeAddr("user");
// RAAC Token Creation
address initialOwner = address(this);
uint256 initialSwapTaxRate = 0; // 1% swap fee
uint256 initialBurnTaxRate = 0; // 0.5% burn fee
raac = new RAACToken(initialOwner, initialSwapTaxRate, initialBurnTaxRate);
// veRAAC Token Creation
veRaac = new veRAACToken(address(raac));
// Treasury Creation
treasury = new Treasury(address(this));
feeCollector = new FeeCollector(address(raac), address(veRaac), address(treasury), repairFund, address(this));
raac.setMinter(minter);
raac.setFeeCollector(address(feeCollector));
vm.prank(minter);
raac.mint(user, 100e18);
}
function testExploit() public {
// feeCollector should collect fee upon user operation
vm.prank(user);
raac.transfer(address(0xdead), 100e18);
// All of the fee type amount in zero
FeeCollector.CollectedFees memory fee = feeCollector.getCollectedFees();
assertEq(fee.protocolFees, 0);
assertEq(fee.performanceFees, 0);
assertEq(fee.insuranceFees, 0);
assertEq(fee.mintRedeemFees, 0);
assertEq(fee.vaultFees, 0);
assertEq(fee.swapTaxes, 0);
assertEq(fee.nftRoyalties, 0);
// All the fee distributed are stuck in the contract
assertGt(raac.balanceOf(address(feeCollector)), 0);
}
}

Although there is a FeeCollector::emergencyWithdrawoperation, but it should only be used under emergency state, not the ordinary withdrawal circumstance.

Steps to Reproduce

  1. Deploy RAACToken, FeeCollector, and related contracts.

  2. Set up a user transaction that generates fees (e.g., a token transfer).

  3. Observe that the FeeCollector contract receives RAAC tokens.

  4. Call FeeCollector::distributeCollectedFees, which fails because collectedFees remains zero.

  5. The RAAC tokens remain stuck in the FeeCollector contract.

Impact

  • Fee Collector Cannot Withdraw Funds: Accumulated fees in the contract are effectively locked and cannot be used as intended.

  • Protocol Revenue Stuck: Since fees cannot be distributed, protocol operations relying on these funds may be hindered.

  • Accounting Discrepancies: The fee collector does not accurately reflect the amount of collected fees, leading to misreporting of available protocol funds.

Tools Used

Manual Review

Recommendations

Instead of only relying on collectedFees, the function should also check the actual RAAC balance in the contract.

Updates

Lead Judging Commences

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

RAACToken::burn sends tax directly to FeeCollector without using collectFee(), causing tokens to bypass accounting and remain undistributed. `collectFee` is not used anywhere.

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

RAACToken::burn sends tax directly to FeeCollector without using collectFee(), causing tokens to bypass accounting and remain undistributed. `collectFee` is not used anywhere.

Support

FAQs

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

Give us feedback!