Core Contracts

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

Fee distribution miscalculation causes swap and NFT royalties to overallocate to the treasury fund

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%
});
// Lending Fees: Interest income distribution
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%
});
// Insurance Fees: 3% from NFT loans
feeTypes[3] = FeeType({
veRAACShare: 5000, // 50%
burnShare: 0,
repairShare: 2000, // 20%
treasuryShare: 3000 // 30%
});
// Mint/Redeem Fees
feeTypes[4] = FeeType({
veRAACShare: 6000, // 60%
burnShare: 0,
repairShare: 2000, // 20%
treasuryShare: 2000 // 20%
});
// Vault Fees
feeTypes[5] = FeeType({
veRAACShare: 7000, // 70%
burnShare: 0,
repairShare: 0,
treasuryShare: 3000 // 30%
});
// dont add up to 10,000
// Buy/Sell Swap Tax (2% total)
feeTypes[6] = FeeType({
veRAACShare: 500, // 0.5%
burnShare: 500, // 0.5%
repairShare: 1000, // 1.0%
treasuryShare: 0
});
// dont add up to 10,000
// NFT Royalty Fees (2% total)
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 // the result from _calculateTotalFees();
collectedFees.nftRoyalties = 200
//lets assume that there arent any other fees collected for the sake of simplicity

Lets track the execution of the _calculateDistribution fn for fee 7:

  1. we get the fee amount for that type feeAmount = 200

  2. we calculate the weight (feeAmount * BASIS_POINTS) / totalFees;. (200 * 10,000) / 1000 = 2,000. weight is now 2,000

  3. we calculate for shares[0] (weight * feeType.veRAACShare) / BASIS_POINTS (2,000 * 500) / 10,000 = 100

  4. we calculate for shares[1] (weight * feeType.burnShare) / BASIS_POINTS. burnShare is 0, so its 0

  5. we calculate for shares[2] (weight * feeType.repairShare) / BASIS_POINTS (2,000 * 1,100) / 10,000 = 200

  6. 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

    // Setup initial fee types
    // defaultFeeType = {
    // veRAACShare: 5000, // 50%
    // burnShare: 1000, // 10%
    // repairShare: 1000, // 10%
    // treasuryShare: 3000 // 30%
    // };
    // for (let i = 0; i < 8; i++) {
    // await feeCollector.connect(owner).updateFeeType(i, defaultFeeType);
    // }
  • place the following code in fee collection and distribution section:

    it('wont distribute the fees correctly for fee type 6 and 7', async () => {
    // Collect NFT royalty fees (type 7)
    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);
    // Check collected fees
    const collectedFees = await feeCollector.getCollectedFees();
    expect(collectedFees.nftRoyalties).to.equal(feeAmount);
    // Distribute fees
    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

Updates

Lead Judging Commences

inallhonesty Lead Judge 7 months ago
Submission Judgement Published
Validated
Assigned finding tags:

FeeCollector distributes too much to treasury when fee amounts are small relative to total due to precision loss in (feeAmount * BASIS_POINTS) / totalFees

inallhonesty Lead Judge 7 months ago
Submission Judgement Published
Validated
Assigned finding tags:

FeeCollector distributes too much to treasury when fee amounts are small relative to total due to precision loss in (feeAmount * BASIS_POINTS) / totalFees

Support

FAQs

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

Give us feedback!