Core Contracts

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

Reentrancy Vulnerability in `FeeCollector::distributeCollectedFees` Due to External Calls After State Changes

Summary

The FeeCollector::distributeCollectedFees function is vulnerable to reentrancy attacks because it performs external calls after modifying contract state variables and lacks proper reentrancy protection around critical token transfers.

The vulnerability exists in the following code pattern:

function distributeCollectedFees() external override nonReentrant whenNotPaused {
uint256 totalFees = _calculateTotalFees();
if (totalFees == 0) revert InsufficientBalance();
uint256[4] memory shares = _calculateDistribution(totalFees);
_processDistributions(totalFees, shares);
delete collectedFees; // State change before external call
emit FeeDistributed(shares[0], shares[1], shares[2], shares[3]);
}
function _processDistributions(uint256 totalFees, uint256[4] memory shares) internal {
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]);
}

The contract clears the collectedFees state before making external token transfers in _processDistributions. If any of these external calls are to a malicious contract, it could reenter distributeCollectedFees when fees are already deleted but before all transfers are complete.

PoC

describe("FeeCollector Reentrancy", function() {
it("should be vulnerable to reentrancy in distributeCollectedFees", async function() {
const AttackerContract = await ethers.getContractFactory("MockAttacker");
const attacker = await AttackerContract.deploy(feeCollector.address);
await feeCollector.setTreasury(attacker.address);
await raacToken.transfer(feeCollector.address, ethers.parseEther("100"));
await feeCollector.collectFee(ethers.parseEther("100"), 0);
await attacker.attack();
const treasuryBalance = await raacToken.balanceOf(attacker.address);
expect(treasuryBalance).to.be.gt(ethers.parseEther("100"));
});
});

Impact

  • Potential double spending of collected fees

  • Loss of protocol revenue

  • Inconsistent state between fee collection and distribution

Tools Used

  • Manual code review

  • Hardhat testing environment

Recommendations

  1. Move the state changes (delete collectedFees) after all external calls

  2. Add checks to prevent reentrancy between fee collection and distribution

  3. Use a reentrancy guard specifically around the distribution logic

  4. Add checks to validate distribution state during execution

function distributeCollectedFees() external nonReentrant whenNotPaused {
uint256 totalFees = _calculateTotalFees();
if (totalFees == 0) revert InsufficientBalance();
uint256[4] memory shares = _calculateDistribution(totalFees);
_processDistributions(totalFees, shares);
emit FeeDistributed(shares[0], shares[1], shares[2], shares[3]);
delete collectedFees; // Move state change after external calls
}
Updates

Lead Judging Commences

inallhonesty Lead Judge 3 months ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

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