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;
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
Tools Used
Recommendations
Move the state changes (delete collectedFees
) after all external calls
Add checks to prevent reentrancy between fee collection and distribution
Use a reentrancy guard specifically around the distribution logic
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;
}