Core Contracts

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

Untracked Direct Fee Transfers from RAACToken to FeeCollector Break Fee Distribution System

Summary

The RAACToken contract sends tax fees directly to the FeeCollector through transfer(), bypassing the FeeCollector's accounting system. This causes a mismatch between actual token balances and tracked fees, breaking the fee distribution mechanism as the FeeCollector's internal accounting shows zero fees collected despite having tokens.

Vulnerability Details

In RAACToken.sol, the _update and burn functions sends fees directly to the FeeCollector:

function _update(address from, address to, uint256 amount) internal virtual override {
.
.
.
@> super._update(from, feeCollector, totalTax - burnAmount);
super._update(from, address(0), burnAmount);
super._update(from, to, amount - totalTax);
}
function burn(uint256 amount) external {
uint256 taxAmount = amount.percentMul(burnTaxRate);
_burn(msg.sender, amount - taxAmount);
if (taxAmount > 0 && feeCollector != address(0)) {
@> _transfer(msg.sender, feeCollector, taxAmount);
}
}

However, FeeCollector only tracks fees through its collectFee function:

function collectFee(uint256 amount, uint8 feeType) external override nonReentrant whenNotPaused returns (bool) {
.
.
.
raacToken.safeTransferFrom(msg.sender, address(this), amount);
_updateCollectedFees(amount, feeType);
// ... other code ...
}

When RAACToken sends fees directly:

  • The tokens arrive at FeeCollector

  • No corresponding update to CollectedFees occurs

  • The distributeCollectedFees function sees zero tracked fees despite having tokens

  • No distribution occurs as _calculateTotalFees() returns 0

This is demonstrated in the POC where burning tokens sends fees to FeeCollector but shows zero tracked fees across all fee types.

POC

To use foundry in the codebase, follow the hardhat guide here: Foundry-Hardhat hybrid integration by Nomic foundation

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import {FeeCollector} from "../../../../contracts/core/collectors/FeeCollector.sol";
import {RAACToken} from "../../../../contracts/core/tokens/RAACToken.sol";
import {veRAACToken} from "../../../../contracts/core/tokens/veRAACToken.sol";
import {Test, console} from "forge-std/Test.sol";
contract TestSuite is Test {
FeeCollector feeCollector;
RAACToken raacToken;
veRAACToken veRAACTok;
address treasury;
address repairFund;
address admin;
uint256 initialSwapTaxRate = 100; //1%
uint256 initialBurnTaxRate = 50; //0.5%
function setUp() public {
treasury = makeAddr("treasury");
repairFund = makeAddr("repairFund");
admin = makeAddr("admin");
raacToken = new RAACToken(admin, initialSwapTaxRate, initialBurnTaxRate);
veRAACTok = new veRAACToken(address(raacToken));
feeCollector = new FeeCollector(address(raacToken), address(veRAACTok), treasury, repairFund, admin);
vm.startPrank(admin);
raacToken.setFeeCollector(address(feeCollector));
raacToken.setMinter(admin);
vm.stopPrank();
}
function testFundsSentDirectlyToFeeCollectorIsntTracked() public {
address user = makeAddr("user");
uint256 amount = 1e18;
deal(address(raacToken), user, amount);
//user burns tokens
vm.startPrank(user);
raacToken.approve(address(feeCollector), amount);
raacToken.burn(amount - 2e17);
vm.stopPrank();
//fees are not tracked
uint256 feeCollectorBalanceAfterBurn = raacToken.balanceOf(address(feeCollector));
console.log("Fee collector balance after burn: ", feeCollectorBalanceAfterBurn);
FeeCollector.CollectedFees memory fees = feeCollector.getCollectedFees();
assertEq(fees.protocolFees, 0);
assertEq(fees.lendingFees, 0);
assertEq(fees.performanceFees, 0);
assertEq(fees.insuranceFees, 0);
assertEq(fees.mintRedeemFees, 0);
assertEq(fees.vaultFees, 0);
assertEq(fees.swapTaxes, 0);
assertEq(fees.nftRoyalties, 0);
}
}

Impact

All fees from RAACToken operations (burns, transfers) become stuck in FeeCollector. No distribution to veRAACToken holders, treasury, or repair fund occurs

Tools Used

Manual review, foundry test suite

Recommendations

Modify RAACToken to use FeeCollector's interface for fee handling

Updates

Lead Judging Commences

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