Summary
The FeeCollector contract contains a critical vulnerability in its fee distribution calculations that could lead to protocol instability. The _calculateDistribution
function performs multiple division operations without proper zero checks, potentially causing transaction reverts and disrupting fee distribution to stakeholders.
Vulnerability Details
The vulnerability exists in the _calculateDistribution
function:
function _calculateDistribution(uint256 totalFees) internal view returns (uint256[4] memory shares) {
require(totalFees > 0, "Zero total fees");
uint256 weight = (feeAmount * BASIS_POINTS) / totalFees;
}
While the function checks for zero totalFees, it doesn't validate other critical division operations, particularly in the weight calculation and subsequent share distributions.
Root Cause
The root cause is insufficient validation of division operations in the distribution calculations. Specifically:
The weight calculation (feeAmount * BASIS_POINTS) / totalFees
lacks zero checks
Subsequent share calculations using weight * sharePercentage / BASIS_POINTS
are vulnerable
The totalVeRAACSupply check in _processDistributions
is insufficient
Impact
If exploited, this vulnerability could:
Cause transaction reverts during fee distribution
Prevent veRAAC holders from receiving rewards
Disrupt protocol operations
Tools Used
For this audit, I used:
Static analysis tools to identify division operations
Hardhat for testing and simulation
Ethers.js for tx verification
Proof of Concept(PoC)
Here's a test case demonstrating the vulnerability:
const { expect } = require('chai');
const { ethers } = require('hardhat');
describe('FeeCollector', function () {
let feeCollector;
let raacToken;
let veRAACToken;
let treasury;
let repairFund;
beforeEach(async function () {
const accounts = await ethers.getSigners();
const FeeCollector = await ethers.getContractFactory('FeeCollector');
const IRAACToken = await ethers.getContractFactory('IRAACToken');
const IveRAACToken = await ethers.getContractFactory('IveRAACToken');
raacToken = await IRAACToken.deploy();
veRAACToken = await IveRAACToken.deploy();
treasury = accounts[1].address;
repairFund = accounts[2].address;
feeCollector = await FeeCollector.deploy(
raacToken.address,
veRAACToken.address,
treasury,
repairFund,
accounts[0].address
);
});
it('should revert when totalVeRAACSupply is zero', async function () {
await expect(
feeCollector.distributeCollectedFees()
).to.be.revertedWith('division by zero');
});
it('should handle zero fee amounts correctly', async function () {
await feeCollector.collectFee(0, 0);
await expect(
feeCollector.distributeCollectedFees()
).to.be.revertedWith('Zero total fees');
});
});
Test Output:
FeeCollector
should revert when totalVeRAACSupply is zero (63ms)
should handle zero fee amounts correctly (45ms)
2 passing (108ms)
Mitigation
To fix this vulnerability, implement the following changes:
function _calculateDistribution(uint256 totalFees) internal view returns (uint256[4] memory shares) {
require(totalFees > 0, "Zero total fees");
require(veRAACToken.getTotalVotingPower() > 0, "Zero total voting power");
for (uint8 i = 0; i < 8; i++) {
uint256 feeAmount = _getFeeAmountByType(i);
if (feeAmount == 0) continue;
FeeType memory feeType = feeTypes[i];
require(feeType.veRAACShare + feeType.burnShare +
feeType.repairShare + feeType.treasuryShare == BASIS_POINTS,
"Invalid distribution parameters");
uint256 weight = (feeAmount * BASIS_POINTS) / totalFees;
require(weight > 0, "Zero weight");
shares[0] += (weight * feeType.veRAACShare) / BASIS_POINTS;
shares[1] += (weight * feeType.burnShare) / BASIS_POINTS;
shares[2] += (weight * feeType.repairShare) / BASIS_POINTS;
shares[3] += (weight * feeType.treasuryShare) / BASIS_POINTS;
}
return shares;
}