Core Contracts

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

RAAC Tokens Locked in Treasury Due to Direct Transfer and Missing Deposit Call

Summary

The FeeCollector contract directly transfers RAAC tokens to the Treasury contract without invoking the deposit function. This bypasses the necessary state updates in the Treasury contract, specifically _balances[token] and _totalValue. As a result, when the withdraw function is called, it attempts to decrement these states, leading to underflow errors because the states were never incremented in the first place. This design flaw renders the RAAC tokens effectively stuck in the treasury.

Vulnerability Details

  1. FeeCollector Contract:

    • The function _processDistributions transfers RAAC tokens to the treasury using the following line:

      if (shares[3] > 0) raacToken.safeTransfer(treasury, shares[3]);
    • This sends a portion of the collected fees (specifically, the treasury's share) to the treasury contract.

  2. Treasury Contract:

    • The withdraw function relies on the _balances[token] and _totalValue states being correctly updated. However, these states are only updated in the deposit function:

      function deposit(address token, uint256 amount) external override nonReentrant {
      if (token == address(0)) revert InvalidAddress();
      if (amount == 0) revert InvalidAmount();
      IERC20(token).transferFrom(msg.sender, address(this), amount);
      _balances[token] += amount;
      _totalValue += amount;
      emit Deposited(token, amount);
      }
    • Since the deposit function is not called when RAAC tokens are sent directly, the _balances[token] and _totalValue states remain unchanged. This causes the withdraw function to revert due to underflow when attempting to decrement these states:

      function withdraw(
      address token,
      uint256 amount,
      address recipient
      ) external override nonReentrant onlyRole(MANAGER_ROLE) {
      if (token == address(0)) revert InvalidAddress();
      if (recipient == address(0)) revert InvalidRecipient();
      if (_balances[token] < amount) revert InsufficientBalance();
      _balances[token] -= amount; // Underflow if _balances[token] was never incremented
      _totalValue -= amount; // Underflow if _totalValue was never incremented
      IERC20(token).transfer(recipient, amount);
      emit Withdrawn(token, amount, recipient);
      }

Root Cause

The issue arises because the FeeCollector contract directly transfers RAAC tokens to the treasury without invoking the deposit function. This bypasses the state updates in the Treasury contract, leading to inconsistent state management. When the withdraw function is called, it attempts to decrement _balances[token] and _totalValue, which were never incremented, resulting in underflow errors.

Impact

RAAC tokens sent to the treasury are permanently locked, as the withdraw function will always revert due to underflow errors.

Tools Used

Manual Review

Recommendations*

Instead of directly transferring RAAC tokens to the treasury, call the deposit function of the Treasury contract to ensure proper state updates.

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]);
+ if (shares[3] > 0) treasury.deposit(raacToken,shares[3]);
}
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.