Core Contracts

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

Incorrect Boost Multiplier Calculation Always Returns Maximum Boost

Summary

There is a logic error in the getBoostMultiplier function of the BoostController contract. This function inaccurately calculates the boost multiplier, always returning the maximum boost value (denoted as 25000, which represents 2.5x). As a result, it disregards the user's actual veRAAC holdings and lock duration. This flaw effectively disables the intended boost mechanism, allowing all users to receive the maximum boost.

Vulnerability Details

getBoostMultiplier (https://github.com/Cyfrin/2025-02-raac/blob/89ccb062e2b175374d40d824263a4c0b601bcb7f/contracts/core/governance/boost/BoostController.sol#L282)

The vulnerability lies in the following lines of code within the getBoostMultiplier function:

uint256 baseAmount = userBoost.amount * 10000 / MAX_BOOST;
return userBoost.amount * 10000 / baseAmount; // Always = 25000

The calculation userBoost.amount * 10000 / baseAmount is mathematically equivalent to MAX_BOOST due to how baseAmount is calculated. Substituting the definition of baseAmount into the return statement yields:

(userBoost.amount * 10000) / (userBoost.amount * 10000 / MAX_BOOST)
= (userBoost.amount * 10000) * (MAX_BOOST / (userBoost.amount * 10000))
= MAX_BOOST

Impact

  1. Critical Severity: This vulnerability completely undermines the intended boost mechanism.

  2. Unfair Reward Distribution: All users receive the maximum 2.5x boost, regardless of their veRAAC holdings or lock duration. Users who lock a small amount of RAAC for a short period receive the same boost as those who lock a large amount for the maximum duration. This situation is unfair and defeats the purpose of the veRAAC locking system.

  3. Loss of Incentive: The incentive to lock RAAC for longer periods is eliminated, as the boost is consistently maximized.

Tools Used

Manual Review

Recommendations

  1. Modify updateUserBoost: Store the base amount (before applying the boost) in userBoost.amount. The boosted amount should not be stored in persistent storage, as it can be calculated on the fly.

    function updateUserBoost(address user, address pool) external override nonReentrant whenNotPaused {
    // ... (Existing checks)
    UserBoost storage userBoost = userBoosts[user][pool];
    PoolBoost storage poolBoost = poolBoosts[pool];
    uint256 oldBoost = userBoost.amount; // This is now the *base* amount
    // Get current weights without modifying state
    (uint256 totalWeight, uint256 totalVotingPower, uint256 votingPower) = updateTotalWeight();
    // Calculate the boost multiplier and boosted amount.
    BoostCalculator.BoostParameters memory params = BoostCalculator.BoostParameters({
    maxBoost: boostState.maxBoost,
    minBoost: boostState.minBoost,
    boostWindow: boostState.boostWindow,
    totalWeight: totalWeight,
    totalVotingPower: totalVotingPower,
    votingPower: votingPower
    });
    uint256 userBalance = IERC20(address(veToken)).balanceOf(user);
    uint256 totalSupply = IERC20(address(veToken)).totalSupply();
    (, uint256 boostedAmount) = BoostCalculator.calculateTimeWeightedBoost(
    params,
    userBalance,
    totalSupply,
    10000 // Example base amount, or use a stored base amount
    );
    // Store the *base* amount, NOT the boosted amount.
    // Assuming a base amount of 10000 for the calculation.
    // You might need to adjust this based on how you want to handle the base amount.
    userBoost.amount = 10000; // Or a stored base amount.
    userBoost.lastUpdateTime = block.timestamp;
    // Update pool totals safely (using the *difference* in boosted amounts)
    uint256 newBoostedAmount = _calculateBoost(user, pool, userBoost.amount); // Calculate the *new* boosted amount
    if (newBoostedAmount >= oldBoost) { // Compare *boosted* amounts
    poolBoost.totalBoost = poolBoost.totalBoost + (newBoostedAmount - oldBoost);
    } else {
    poolBoost.totalBoost = poolBoost.totalBoost - (oldBoost - newBoostedAmount);
    }
    poolBoost.workingSupply = newBoostedAmount; // Update working supply
    poolBoost.lastUpdateTime = block.timestamp;
    emit BoostUpdated(user, pool, newBoostedAmount); // Emit the *boosted* amount
    emit PoolBoostUpdated(pool, poolBoost.totalBoost, poolBoost.workingSupply);
    }
  2. Modify getBoostMultiplier: Calculate and return the boost multiplier (in basis points).

    function getBoostMultiplier(
    address user,
    address pool
    ) external view override returns (uint256) {
    if (!supportedPools[pool]) revert UnsupportedPool();
    UserBoost storage userBoost = userBoosts[user][pool];
    // get user voting power
    uint256 userVotingPower = veToken.getVotingPower(user, block.timestamp);
    // get total Voiting Power
    uint256 totalVotingPower = veToken.getTotalVotingPower();
    // Calculate boost
    uint256 boostBasisPoints;
    (boostBasisPoints,) = BoostCalculator.calculateTimeWeightedBoost(
    _boostState,
    userVotingPower,
    totalVotingPower,
    userBoost.amount
    );
    return boostBasisPoints;
    }
Updates

Lead Judging Commences

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

BoostController::getBoostMultiplier always returns MAX_BOOST for any non-zero boost due to mathematical calculation error, defeating the incentive mechanism

Support

FAQs

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

Give us feedback!