Core Contracts

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

Incorrect Fee Type Initialization and Faulty UpdateFeeType Validation: Miscalculated Fee Distribution and Protocol Instability

Summary

The FeeCollector contract manages the collection and distribution of protocol fees across various categories, including Protocol Fees, Lending Fees, Performance Fees, Insurance Fees, Mint/Redeem Fees, Vault Fees, Swap Tax, and NFT Royalties. While fee types 0–5 are correctly configured, fee types 6 and 7—intended to represent Swap Tax and NFT Royalties (each expected to total 2% of fees)—are misinitialized, resulting in values that imply a 20% share instead of 2%. This error leads to grossly inflated fee calculations for these categories, causing misallocation of rewards and undermining the protocol’s economic integrity.

Additionally, although the contract provides an updateFeeType function to modify fee type parameters post-initialization, it includes a flawed validation check. The function enforces that the sum of fee shares equals exactly BASIS_POINTS (i.e., 100%), using an equality comparison (==) negation. However, this strict equality negation check prevents fee managers from setting valid fee shares for Swap Tax and NFT Royalties when slight adjustments are necessary, effectively blocking correct configuration. This combination of misinitialization and faulty validation elevates the issue to a high severity due to its potential to disrupt fee distribution and erode stakeholder trust.

Vulnerability Details

Fee Types Misinitialization

The _initializeFeeTypes function sets default fee configurations:

function _initializeFeeTypes() internal {
// Fee types 0–5 (e.g., Protocol, Lending, Performance, Insurance, Mint/Redeem, Vault Fees) are set correctly.
// Buy/Sell Swap Tax (Fee Type 6, intended total: 2%)
// @info: wrong initialization; currently interpreted as 20%
feeTypes[6] = FeeType({
veRAACShare: 500, // Intended 0.5% but 500 basis points = 5%
burnShare: 500, // Intended 0.5%
repairShare: 1000, // Intended 1.0%
treasuryShare: 0
});
// NFT Royalty Fees (Fee Type 7, intended total: 2%)
// @info: wrong initialization; currently interpreted as 20%
feeTypes[7] = FeeType({
veRAACShare: 500, // Intended 0.5%
burnShare: 0,
repairShare: 1000, // Intended 1.0%
treasuryShare: 500 // Intended 0.5%
});
}

Due to a unit or scaling error, fee types 6 and 7 are set with values that are 10 times higher than intended. This misconfiguration leads to an overestimation of fees in these categories during distribution, resulting in misallocated rewards and economic imbalance.

Faulty Validation in updateFeeType Function

The contract also provides a function to update fee type parameters:

function updateFeeType(uint8 feeType, FeeType calldata newFee) external override {
if (!hasRole(FEE_MANAGER_ROLE, msg.sender)) revert UnauthorizedCaller();
if (feeType > 7) revert InvalidFeeType();
// Validate fee shares total to 100%
// @info: the equality operator (==) is used here, but a > comparison should be used.
// @danger: this strict check prevents fee managers from setting valid parameters for fee types 6 and 7.
if (newFee.veRAACShare + newFee.burnShare + newFee.repairShare + newFee.treasuryShare != BASIS_POINTS) {
revert InvalidDistributionParams();
}
feeTypes[feeType] = newFee;
emit FeeTypeUpdated(feeType, newFee);
}

Issue:
The function enforces that the total of fee shares must equal exactly BASIS_POINTS (typically 10,000), using an equality check. In practice, when adjusting fee types 6 and 7 (Swap Tax and NFT Royalties), the correct fee configuration might require slight deviations or a different interpretation of basis points. For instance, if the intended total is 200 basis points (2%) rather than 2000 (20%), the current check will always fail, preventing fee managers from applying the correct configuration. The validation logic should allow for proper configuration (e.g., using a greater-than operator or an acceptable range) rather than demanding an exact match.

Combined Impact

  • Miscalculated Fee Distribution:
    The misinitialization of fee types 6 and 7 leads to fees being recorded at roughly 10 times the intended percentage. When these fees are used in the distribution process, the shares calculated for veRAAC holders, burn, repair fund, and treasury become grossly inflated or misdirected.

  • Inability to Correct Fee Configuration:
    The faulty updateFeeType function prevents fee managers from updating the misconfigured fee types because the strict equality check for fee shares does not allow valid adjustments, especially for fee types 6 and 7. This locks the system into a state with incorrect fee distributions.

  • Economic and Governance Instability:
    Over time, these issues can lead to significant economic imbalances, misaligned incentives, and erosion of trust among protocol participants, undermining both reward distribution and governance processes.

Proof of Concept

Scenario Walkthrough

  1. Before Fee Collection:
    The FeeCollector’s fee types are initialized using _initializeFeeTypes. Fee types 6 and 7 are misconfigured, recording excessively high values.

  2. Fee Collection:
    When fees are collected via collectFee, the FeeCollector records the full (inflated) amounts for fee types 6 and 7.

  3. Fee Distribution:
    During distribution, the function _calculateDistribution computes distribution shares based on the inflated fee type values, leading to an overall allocation that deviates significantly from the protocol's intended design.

  4. Attempted Correction via updateFeeType:
    A fee manager tries to update fee types 6 and 7 using the updateFeeType function with the correct (scaled-down) values (e.g., veRAACShare: 50, burnShare: 50, repairShare: 100, treasuryShare: 0 for Swap Tax). However, the validation check fails because the sum of these values (50+50+100+0 = 200) does not exactly equal BASIS_POINTS (10,000), thereby preventing the update.

Test Suite

The following test suite demonstrates the impact:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import {FeeCollector} from "../src/core/collectors/FeeCollector.sol";
import {Treasury} from "../src/core/collectors/Treasury.sol";
import {RAACToken} from "../src/core/tokens/RAACToken.sol";
import {veRAACToken} from "../src/core/tokens/veRAACToken.sol";
import {IFeeCollector} from "../src/interfaces/core/collectors/IFeeCollector.sol";
import {Test, console} from "forge-std/Test.sol";
contract FeeCollectorTest is Test {
FeeCollector feeCollector;
Treasury treasury;
RAACToken raacToken;
veRAACToken veToken;
address RAAC_OWNER = makeAddr("RAAC_OWNER");
address RAAC_MINTER = makeAddr("RAAC_MINTER");
uint256 initialRaacSwapTaxRateInBps = 200; // 2%
uint256 initialRaacBurnTaxRateInBps = 150; // 1.5%
address TREASURY_ADMIN = makeAddr("TREASURY_ADMIN");
address FEE_COLLECTOR_ADMIN = makeAddr("FEE_COLLECTOR_ADMIN");
address REPAIR_FUND_ADDRESS = makeAddr("REPAIR_FUND_ADDRESS");
address VE_RAAC_OWNER = makeAddr("VE_RAAC_OWNER");
address ALICE = makeAddr("ALICE");
address BOB = makeAddr("BOB");
address CHARLIE = makeAddr("CHARLIE");
address DEVIL = makeAddr("DEVIL");
address FEES_PAYER = makeAddr("FEES_PAYER");
function setUp() public {
raacToken = new RAACToken(RAAC_OWNER, initialRaacSwapTaxRateInBps, initialRaacBurnTaxRateInBps);
vm.startPrank(VE_RAAC_OWNER);
veToken = new veRAACToken(address(raacToken));
vm.stopPrank();
vm.startPrank(TREASURY_ADMIN);
treasury = new Treasury(TREASURY_ADMIN);
vm.stopPrank();
vm.startPrank(FEE_COLLECTOR_ADMIN);
feeCollector = new FeeCollector(
address(raacToken), address(veToken), address(treasury), REPAIR_FUND_ADDRESS, FEE_COLLECTOR_ADMIN
);
vm.stopPrank();
getveRaacTokenForProposer();
}
function getveRaacTokenForProposer() private {
uint256 LOCK_AMOUNT = 1_000_000e18;
uint256 LOCK_DURATION = 365 days;
vm.startPrank(RAAC_OWNER);
raacToken.setMinter(RAAC_MINTER);
vm.stopPrank();
vm.startPrank(RAAC_MINTER);
raacToken.mint(ALICE, LOCK_AMOUNT);
raacToken.mint(BOB, LOCK_AMOUNT);
raacToken.mint(CHARLIE, LOCK_AMOUNT);
raacToken.mint(DEVIL, LOCK_AMOUNT);
raacToken.mint(FEES_PAYER, 100_000_000e18);
vm.stopPrank();
vm.startPrank(FEES_PAYER);
raacToken.approve(address(feeCollector), 100_000_000e18);
vm.stopPrank();
vm.startPrank(ALICE);
raacToken.approve(address(veToken), LOCK_AMOUNT);
veToken.lock(LOCK_AMOUNT, LOCK_DURATION);
vm.stopPrank();
vm.startPrank(BOB);
raacToken.approve(address(veToken), LOCK_AMOUNT);
veToken.lock(LOCK_AMOUNT, LOCK_DURATION);
vm.stopPrank();
vm.startPrank(CHARLIE);
raacToken.approve(address(veToken), LOCK_AMOUNT);
veToken.lock(LOCK_AMOUNT, LOCK_DURATION);
vm.stopPrank();
vm.startPrank(DEVIL);
raacToken.approve(address(veToken), LOCK_AMOUNT);
veToken.lock(LOCK_AMOUNT, LOCK_DURATION);
vm.stopPrank();
}
function testInvalidFeeTypesFeesAssignmentAffectsProtocolGoodwillAndTrust() public {
IFeeCollector.CollectedFees memory collectedFee = feeCollector.getCollectedFees();
console.log("before collection...");
console.log("collectedFee.protocolFees : ", collectedFee.protocolFees);
console.log("collectedFee.lendingFees : ", collectedFee.lendingFees);
console.log("collectedFee.performanceFees: ", collectedFee.performanceFees);
console.log("collectedFee.insuranceFees : ", collectedFee.insuranceFees);
console.log("collectedFee.mintRedeemFees : ", collectedFee.mintRedeemFees);
console.log("collectedFee.vaultFees : ", collectedFee.vaultFees);
console.log("collectedFee.swapTaxes : ", collectedFee.swapTaxes);
console.log("collectedFee.nftRoyalties : ", collectedFee.nftRoyalties);
console.log("fee collector raac balance : ", raacToken.balanceOf(address(feeCollector)));
console.log("repair raac balance : ", raacToken.balanceOf(REPAIR_FUND_ADDRESS));
console.log("treasury raac balance : ", raacToken.balanceOf(address(treasury)));
for (uint8 i = 0; i < 8; i++) {
vm.startPrank(FEES_PAYER);
feeCollector.collectFee(1_000_000e18, i);
vm.stopPrank();
}
collectedFee = feeCollector.getCollectedFees();
console.log("\nafter collection...");
console.log("collectedFee.protocolFees : ", collectedFee.protocolFees);
console.log("collectedFee.lendingFees : ", collectedFee.lendingFees);
console.log("collectedFee.performanceFees: ", collectedFee.performanceFees);
console.log("collectedFee.insuranceFees : ", collectedFee.insuranceFees);
console.log("collectedFee.mintRedeemFees : ", collectedFee.mintRedeemFees);
console.log("collectedFee.vaultFees : ", collectedFee.vaultFees);
console.log("collectedFee.swapTaxes : ", collectedFee.swapTaxes);
console.log("collectedFee.nftRoyalties : ", collectedFee.nftRoyalties);
console.log("fee collector raac balance : ", raacToken.balanceOf(address(feeCollector)));
console.log("repair raac balance : ", raacToken.balanceOf(REPAIR_FUND_ADDRESS));
console.log("treasury raac balance : ", raacToken.balanceOf(address(treasury)));
vm.startPrank(FEE_COLLECTOR_ADMIN);
feeCollector.distributeCollectedFees();
vm.stopPrank();
collectedFee = feeCollector.getCollectedFees();
console.log("\nafter distribution...");
console.log("collectedFee.protocolFees : ", collectedFee.protocolFees);
console.log("collectedFee.lendingFees : ", collectedFee.lendingFees);
console.log("collectedFee.performanceFees: ", collectedFee.performanceFees);
console.log("collectedFee.insuranceFees : ", collectedFee.insuranceFees);
console.log("collectedFee.mintRedeemFees : ", collectedFee.mintRedeemFees);
console.log("collectedFee.vaultFees : ", collectedFee.vaultFees);
console.log("collectedFee.swapTaxes : ", collectedFee.swapTaxes);
console.log("collectedFee.nftRoyalties : ", collectedFee.nftRoyalties);
console.log("fee collector raac balance : ", raacToken.balanceOf(address(feeCollector)));
console.log("repair raac balance : ", raacToken.balanceOf(REPAIR_FUND_ADDRESS));
console.log("treasury raac balance : ", raacToken.balanceOf(address(treasury)));
// Attempt to update fee types for Swap Tax and NFT Royalties with corrected values.
// Expected new configuration: total shares sum to BASIS_POINTS (e.g., 10000).
// For instance, new fee for type 6: veRAACShare: 50, burnShare: 50, repairShare: 100, treasuryShare: 0.
// New fee for type 7: veRAACShare: 50, burnShare: 0, repairShare: 100, treasuryShare: 50.
// However, the current validation requires an exact sum of BASIS_POINTS, which is 10000,
// so the update will fail if the new parameters do not meet this strict equality.
// This demonstrates that fee managers cannot correct the misconfiguration.
// Buy/Sell Swap Tax FeeType update
IFeeCollector.FeeType memory swapTaxFeeTypeUpdate =
IFeeCollector.FeeType({veRAACShare: 50, burnShare: 50, repairShare: 100, treasuryShare: 0});
// NFT Royalty Fees FeeType update
IFeeCollector.FeeType memory nftRoyaltyFeeTypeUpdate =
IFeeCollector.FeeType({veRAACShare: 50, burnShare: 0, repairShare: 100, treasuryShare: 50});
// Buy/Sell Swap Tax FeeType update request tx
vm.startPrank(FEE_COLLECTOR_ADMIN);
vm.expectRevert(bytes4(keccak256("InvalidDistributionParams()")));
feeCollector.updateFeeType(0, swapTaxFeeTypeUpdate);
vm.stopPrank();
// NFT Royalty Fees FeeType update request tx
vm.startPrank(FEE_COLLECTOR_ADMIN);
vm.expectRevert(bytes4(keccak256("InvalidDistributionParams()")));
feeCollector.updateFeeType(0, nftRoyaltyFeeTypeUpdate);
vm.stopPrank();
}
}

Observations

  • Before Collection:
    All fee types are initialized to 0.

  • After Collection:
    Fee types 6 and 7 record values that are significantly inflated (e.g., ~965000000000000000000000 each), leading to a total fee figure that exceeds the actual token balance of the FeeCollector.

  • Update Attempt:
    The test attempts to update fee types 6 and 7 with corrected values (scaled down by a factor of 10). However, due to the strict equality check in updateFeeType (which requires the sum of fee shares to equal exactly BASIS_POINTS), these update attempts revert, demonstrating that fee managers cannot fix the misconfiguration.

  • After Distribution:
    The final distribution logs show misallocated funds among the fee collector, repair fund, and treasury, confirming that the misconfiguration negatively impacts fee distribution.

Impact

  • Severe Economic Imbalance:
    The misconfiguration of fee types 6 and 7 leads to an overestimation of fees (approximately 20% instead of 2%), resulting in disproportionate fee allocation that distorts reward distribution.

  • Governance and Stakeholder Trust Erosion:
    Incorrect fee distributions undermine the protocol's fairness, potentially leading to disputes among stakeholders and a loss of confidence in the system's economic model.

  • Inability to Correct Fee Parameters:
    The flawed validation in updateFeeType prevents fee managers from updating fee type parameters to their intended values, locking the system into a state of misallocation.

  • Long-Term Protocol Instability:
    Persistent misallocation of fees can destabilize the protocol's tokenomics, affecting governance, participation incentives, and overall economic sustainability.

Tools Used

  • Manual Review

  • Foundry

Recommendations

To mitigate this high-severity issue, the following corrective actions are recommended:

1. Correct Fee Type Initialization for Fee Types 6 and 7

Adjust the fee type initialization in _initializeFeeTypes to correctly represent a total of 2% (200 basis points) instead of 20% (2000 basis points):

function _initializeFeeTypes() internal {
// ... Fee types 0 to 5 remain unchanged ...
// Buy/Sell Swap Tax (2% total)
- feeTypes[6] = FeeType({
- veRAACShare: 500,
- burnShare: 500,
- repairShare: 1000,
- treasuryShare: 0
- });
+ feeTypes[6] = FeeType({
+ veRAACShare: 50, // 0.5% in basis points
+ burnShare: 50, // 0.5%
+ repairShare: 100, // 1.0%
+ treasuryShare: 0
+ });
// NFT Royalty Fees (2% total)
- feeTypes[7] = FeeType({
- veRAACShare: 500,
- burnShare: 0,
- repairShare: 1000,
- treasuryShare: 500
- });
+ feeTypes[7] = FeeType({
+ veRAACShare: 50, // 0.5%
+ burnShare: 0,
+ repairShare: 100, // 1.0%
+ treasuryShare: 50 // 0.5%
+ });
}

2. Revise updateFeeType Validation

Modify the updateFeeType function to allow fee managers to update fee types without requiring the fee shares to equal exactly BASIS_POINTS (i.e., 10,000 basis points). Instead, use a comparison that ensures the sum does not exceed BASIS_POINTS or is within an acceptable range. For instance:

function updateFeeType(uint8 feeType, FeeType calldata newFee) external override {
if (!hasRole(FEE_MANAGER_ROLE, msg.sender)) revert UnauthorizedCaller();
if (feeType > 7) revert InvalidFeeType();
- // Validate fee shares total to 100%
- if (newFee.veRAACShare + newFee.burnShare + newFee.repairShare + newFee.treasuryShare != BASIS_POINTS) {
- revert InvalidDistributionParams();
- }
+ // Validate fee shares total does not exceed 100%
+ if (newFee.veRAACShare + newFee.burnShare + newFee.repairShare + newFee.treasuryShare > BASIS_POINTS) {
+ revert InvalidDistributionParams();
+ }
feeTypes[feeType] = newFee;
emit FeeTypeUpdated(feeType, newFee);
}
Updates

Lead Judging Commences

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

Fee shares for fee type 6 and 7 inside FeeCollector do not total up to the expected 10000 basis points, this leads to update problems, moreover they are 10x the specifications

Support

FAQs

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

Give us feedback!