Summary
distributeCollectedFees is used to distribute collected fees for burning, repair fund, etc. For the calculation of how much tokens per fee type to allocate it uses _calculateDistribution.
Vulnerability Details
Explanation
The calculation is made by calculating the weight for each type uint256 weight = (feeAmount * BASIS_POINTS) / totalFees; and calculating how much shares it should allocate based on that 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;
This function assumes that veRAACShare + burnShare + repairShare + treasuryShare will equal BASIS_POINTS.
If we look at the fee types we can see thats not the case with all types:
function _initializeFeeTypes() internal {
// Protocol Fees: 80% to veRAAC holders, 20% to treasury
feeTypes[0] = FeeType({
veRAACShare: 8000, // 80%
burnShare: 0,
repairShare: 0,
treasuryShare: 2000 // 20%
})
feeTypes[1] = FeeType({
veRAACShare: 7000, // 70%
burnShare: 0,
repairShare: 0,
treasuryShare: 3000 // 30%
})
// Performance Fees: 20% from yield products
feeTypes[2] = FeeType({
veRAACShare: 6000, // 60%
burnShare: 0,
repairShare: 0,
treasuryShare: 4000 // 40%
})
feeTypes[3] = FeeType({
veRAACShare: 5000, // 50%
burnShare: 0,
repairShare: 2000, // 20%
treasuryShare: 3000 // 30%
})
feeTypes[4] = FeeType({
veRAACShare: 6000, // 60%
burnShare: 0,
repairShare: 2000, // 20%
treasuryShare: 2000 // 20%
})
feeTypes[5] = FeeType({
veRAACShare: 7000, // 70%
burnShare: 0,
repairShare: 0,
treasuryShare: 3000 // 30%
})
// dont add up to 10,000
feeTypes[6] = FeeType({
veRAACShare: 500, // 0.5%
burnShare: 500, // 0.5%
repairShare: 1000, // 1.0%
treasuryShare: 0
})
// dont add up to 10,000
feeTypes[7] = FeeType({
veRAACShare: 500, // 0.5%
burnShare: 0,
repairShare: 1000, // 1.0%
treasuryShare: 500 // 0.5%
})
}
As you can see, fee type 6 and fee type 7 dont add up to 10,000 (basis_points). Because of that this function will only allocate 20% to the expected locations and the other 80% will be put in the treasury.
Example
Lets assume the following:
totalFees = 1000
collectedFees.nftRoyalties = 200
Lets track the execution of the _calculateDistribution fn for fee 7:
we get the fee amount for that type feeAmount = 200
we calculate the weight (feeAmount * BASIS_POINTS) / totalFees;. (200 * 10,000) / 1000 = 2,000. weight is now 2,000
we calculate for shares[0] (weight * feeType.veRAACShare) / BASIS_POINTS (2,000 * 500) / 10,000 = 100
we calculate for shares[1] (weight * feeType.burnShare) / BASIS_POINTS. burnShare is 0, so its 0
we calculate for shares[2] (weight * feeType.repairShare) / BASIS_POINTS (2,000 * 1,100) / 10,000 = 200
we finally calculate for shares[3] (weight * feeType.treasuryShare) / BASIS_POINTS (2,000 * 500) / 10,000 = 100
Total shares before scaling would be 100 + 0 + 200 + 100 = 400
For the final calculation:
veRAACShare = (1000 * 100) / 10000 = 10
burnShare = 0
repairShare = (1000 * 200) / 10000 = 20
treasuryShare = (1000 * 100) / 10000 = 10
As you can see the total amount that is distributed is 40. Thats not expected since feeAmount = 200. This means that treasury fund receives feeAmount - totalAmountDistributed which equals to 160.
POC:
in FeeCollector.test.js:
-
comment out, since this overrides the default fee type
-
place the following code in fee collection and distribution section:
it('wont distribute the fees correctly for fee type 6 and 7', async () => {
console.log(feeCollector.address);
console.log(owner.address);
console.log(await feeCollector.getAddress());
const feeAmount = ethers.parseEther("200");
await raacToken.mint(owner.address, feeAmount);
await raacToken.connect(owner).approve(await feeCollector.getAddress(), feeAmount);
await feeCollector.connect(owner).collectFee(feeAmount, 7);
const collectedFees = await feeCollector.getCollectedFees();
expect(collectedFees.nftRoyalties).to.equal(feeAmount);
await feeCollector.connect(owner).distributeCollectedFees();
const treasuryBalance = await raacToken.balanceOf(treasury.address);
const repairBalance = await raacToken.balanceOf(repairFund.address);
expect(treasuryBalance).greaterThanOrEqual(ethers.parseEther("170"));
expect(repairBalance).greaterThanOrEqual(ethers.parseEther("19"));
});
Impact
More funds allocated to the treasury when fees are deposited for buy/sell swap and nft royalty fees.
Tools Used
Manual review
Recommendations