Summary
*Fees collected under specific parameters must be distributed using those same parameters.*The updateFeeType() function allows changing fee distribution percentages after fees are collected but before distribution. This enables malicious actors to:
Collect fees under favorable terms (e.g., 80% to users).
Update feeType to divert funds (e.g., 80% to treasury).
Execute distribution with new parameters
Vulnerability Details
function updateFeeType(uint8 feeType, FeeType calldata newFee) external override {
if (!hasRole(FEE_MANAGER_ROLE, msg.sender)) revert UnauthorizedCaller();
if (feeType > 7) revert InvalidFeeType();
if (newFee.veRAACShare + newFee.burnShare + newFee.repairShare + newFee.treasuryShare != BASIS_POINTS) {
revert InvalidDistributionParams();
}
feeTypes[feeType] = newFee;
emit FeeTypeUpdated(feeType, newFee);
}
function _calculateDistribution(uint256 totalFees) internal view returns (uint256[4] memory shares) {
uint256 totalCollected;
for (uint8 i = 0; i < 8; i++) {
uint256 feeAmount = _getFeeAmountByType(i);
if (feeAmount == 0) continue;
FeeType memory feeType = feeTypes[i];
totalCollected += feeAmount;
uint256 weight = (feeAmount * BASIS_POINTS) / totalFees;
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;
}
if (totalCollected != totalFees) revert InvalidFeeAmount();
shares[0] = (totalFees * shares[0]) / BASIS_POINTS;
shares[1] = (totalFees * shares[1]) / BASIS_POINTS;
shares[2] = (totalFees * shares[2]) / BASIS_POINTS;
shares[3] = (totalFees * shares[3]) / BASIS_POINTS;
uint256 remainder = totalFees - (shares[0] + shares[1] + shares[2] + shares[3]);
if (remainder > 0) shares[3] += remainder;
}
* @dev Calculates total fees collected across all fee types
* @return total Total fees collected
*/
function _calculateTotalFees() internal view returns (uint256) {
return collectedFees.protocolFees +
collectedFees.lendingFees +
collectedFees.performanceFees +
collectedFees.insuranceFees +
collectedFees.mintRedeemFees +
collectedFees.vaultFees +
collectedFees.swapTaxes +
collectedFees.nftRoyalties;
}
function collectFee(uint256 amount, uint8 feeType) external override nonReentrant whenNotPaused returns (bool) {
if (amount == 0 || amount > MAX_FEE_AMOUNT) revert InvalidFeeAmount();
if (feeType > 7) revert InvalidFeeType();
raacToken.safeTransferFrom(msg.sender, address(this), amount);
_updateCollectedFees(amount, feeType);
emit FeeCollected(feeType, amount);
return true;
}
There is the parameter mutability after collection of Fees. Fees collected under one set of distribution rules can be distributed using different rules if parameters are updated before distribution.
**consider the scenario below:**
collectFee(100, 0) (FeeType 0: 80% to users)
Stores 100 in protocolFees
updateFeeType(0, {veRAACShare: 20%})
distributeCollectedFees()
Distributes 20% to users (not 80%)
Another Example:
function _updateCollectedFees(uint256 amount, uint8 feeType) internal {
if (feeType == 0) collectedFees.protocolFees += amount;
}
function _calculateDistribution(...) {
FeeType memory feeType = feeTypes[i];
shares[0] += (weight * feeType.veRAACShare) / BASIS_POINTS;
}
function updateFeeType(...) external {
feeTypes[feeType] = newFee;
}
Protocol collects $10M in fees with 80% user rewards.
Malicious admin changes to 20% user rewards.
$8M diverted to treasury instead of users.
Impact
Treasury/repair fund can steal user rewards retroactively. Users cannot rely on fee distribution rules at collection time.
Tools Used
Foundry
Recommendations
Store fee parameters at collection time using a snapshot mechanism.