Core Contracts

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

Funds Permanently Locked in Treasury Due to Direct Transfers

Summary

The FeeCollector contract directly transfers tokens to the Treasury contract using safeTransfer, bypassing the deposit function of Treasury. This prevents the funds from being withdrawn later, leading to permanent locking of assets.

Vulnerability Details

Problematic Code

FeeCollector:: emergencyWithdraw

/**
* @notice Emergency withdrawal of tokens
* @param token Address of token to withdraw
*/
function emergencyWithdraw(address token) external override whenPaused {
if (!hasRole(EMERGENCY_ROLE, msg.sender)) revert UnauthorizedCaller();
if (token == address(0)) revert InvalidAddress();
uint256 balance;
if (token == address(raacToken)) {
balance = raacToken.balanceOf(address(this));
@> raacToken.safeTransfer(treasury, balance);
} else {
balance = IERC20(token).balanceOf(address(this));
@> SafeERC20.safeTransfer(IERC20(token), treasury, balance);
}
emit EmergencyWithdrawal(token, balance);
}

FeeCollector:: _processDistributions

/**
* @dev Processes the distribution of collected fees
* @param totalFees Total fees to distribute
* @param shares Distribution shares for different stakeholders
*/
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]);
}

Treasury Contract Design

The Treasury contract enforces strict fund management rules:

  1. All deposits must go throughdeposit() to update internal tracking variables (_balances and _totalValue).https://github.com/Cyfrin/2025-02-raac/blob/89ccb062e2b175374d40d824263a4c0b601bcb7f/contracts/core/collectors/Treasury.sol#L46

  2. Withdrawals rely on _balances to validate fund availability.https://github.com/Cyfrin/2025-02-raac/blob/89ccb062e2b175374d40d824263a4c0b601bcb7f/contracts/core/collectors/Treasury.sol#L64

Issue Analysis

The FeeCollector contract directly transfers tokens to the Treasury address without invoking deposit(). This results in:

  • _balances and _totalValue not being updated.

  • Tokens existing in the Treasury contract but being unretrievable through withdraw().

Impact

Funds transferred via FeeCollector::distributeCollectedFeesand FeeCollector::emergencyWithdraw are permanently locked in Treasury, making them inaccessible.

Tools Used

Manual

Recommendations

Modify FeeCollector to use Treasury.deposit() instead of safeTransfer.

Updates

Lead Judging Commences

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

FeeCollector::_processDistributions and emergencyWithdraw directly transfer funds to Treasury where they get permanently stuck

Support

FAQs

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