Core Contracts

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

Invariant broken in `getBoostMultiplier.sol` in the BoostController contract.

Summary

precision loss occurs in the getBoostMultiplier function of the BoostController contract. Specifically, the function's calculation of the boost multiplier would lead to a situation where the resulting value exceeds MAX_BOOST, violating key system invariants. This occurs due to an incorrect denominator in the division operation, which can cause an unintended amplification of the boost multiplier, particularly for specific values of userBoost.amount.

Vulnerability Details

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

function getBoostMultiplier(
address user,
address pool
) external view override returns (uint256) {
if (!supportedPools[pool]) revert PoolNotSupported();
UserBoost storage userBoost = userBoosts[user][pool];
if (userBoost.amount == 0) return MIN_BOOST; //10000
// Calculation flaw - Incorrect baseAmount computation
uint256 baseAmount = userBoost.amount * 10000 / MAX_BOOST; //25000
return userBoost.amount * 10000 / baseAmount;
}
  • This function is designed to compute a boost multiplier based on userBoost.amount and return it in basis points (1e4).

  • The issue arises due to the incorrect calculation of baseAmount:

    uint256 baseAmount = userBoost.amount * 10000 / MAX_BOOST;

    This step performs a scaling operation, but when userBoost.amount takes specific values (such as 333, 499, 999), it introduces precision loss due to integer division and users with this amount gets more rewards.

  • The final calculation:

    return userBoost.amount * 10000 / baseAmount;

    would cause the return value to exceed MAX_BOOST, violating expected constraints.

Proof of Concept (PoC);

I added the senario for value 333, and other user amounts that would cause this issue. Invariant is completely broken:

https://github.com/Cyfrin/2025-02-raac/blob/89ccb062e2b175374d40d824263a4c0b601bcb7f/contracts/core/governance/boost/BoostController.sol#L39C4-L40C47

/// @notice Maximum boost multiplier (2.5x) in basis points
uint256 public constant MAX_BOOST = 25000;
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import {Test} from "forge-std/Test.sol";
import {console2} from "forge-std/console2.sol";
import {BoostController} from "../../../../../contracts/core/governance/boost/BoostController.sol";
import {veRAACToken} from "../../../../../contracts/core/tokens/veRAACToken.sol";
import {RAACToken} from "../../../../../contracts/core/tokens/RAACToken.sol";
contract BoostControllerTest is Test {
BoostController public boostController;
veRAACToken public veToken;
RAACToken public raacToken;
address public alice = address(0x1);
address public bob = address(0x2);
address public pool = address(0x3);
address public owner = address(this);
function setUp() public {
// Deploy real RAAC token with initial parameters
raacToken = new RAACToken(
owner, // initial owner
100, // 1% swap tax
50 // 0.5% burn tax
);
// Deploy veToken
veToken = new veRAACToken(address(raacToken));
// Deploy BoostController
boostController = new BoostController(address(veToken));
// Setup roles and permissions
boostController.modifySupportedPool(pool, true);
raacToken.setMinter(owner);
// Mint tokens to users
raacToken.mint(alice, 1000e18);
raacToken.mint(bob, 5000e18);
// Whitelist users to avoid tax on transfers
raacToken.manageWhitelist(alice, true);
raacToken.manageWhitelist(bob, true);
raacToken.manageWhitelist(address(veToken), true);
// Setup users
vm.startPrank(alice);
raacToken.approve(address(veToken), type(uint256).max);
veToken.lock(1000e18, 365 days); // Lock for 1 year
vm.stopPrank();
vm.startPrank(bob);
raacToken.approve(address(veToken), type(uint256).max);
veToken.lock(5000e18, 365 days); // Lock for 1 year
vm.stopPrank();
}
function testMax_boost_breach() public {
uint256 breachAmount = 333;
// Setup user with breach amount
raacToken.mint(alice, breachAmount * 1e18);
vm.startPrank(alice);
raacToken.approve(address(veToken), type(uint256).max);
veToken.lock(breachAmount * 1e18, 365 days);
boostController.updateUserBoost(alice, pool);
vm.stopPrank();
uint256 multiplier = boostController.getBoostMultiplier(alice, pool);
// Verify the broken invariant
assertGt(
multiplier,
boostController.MAX_BOOST(),
"Multiplier should not exceed MAX_BOOST"
);
// Verify impact on rewards
uint256 dailyRewards = 1000e18;
uint256 expectedMaxRewards = (dailyRewards * boostController.MAX_BOOST()) / 10000;
uint256 actualRewards = (dailyRewards * multiplier) / 10000;
assertGt(
actualRewards,
expectedMaxRewards,
"Rewards exceed maximum allowed"
);
}
function testMax_boost_breach_multiple_amounts() public {
uint256[] memory testAmounts = new uint256[](3);
testAmounts[0] = 333;
testAmounts[1] = 499;
testAmounts[2] = 999;
for(uint256 i = 0; i < testAmounts.length; i++) {
uint256 breachAmount = testAmounts[i];
// Setup user with breach amount
raacToken.mint(alice, breachAmount * 1e18);
vm.startPrank(alice);
raacToken.approve(address(veToken), type(uint256).max);
veToken.lock(breachAmount * 1e18, 365 days);
boostController.updateUserBoost(alice, pool);
vm.stopPrank();
uint256 multiplier = boostController.getBoostMultiplier(alice, pool);
// Verify the broken invariant
assertGt(
multiplier,
boostController.MAX_BOOST(),
string.concat("Multiplier should not exceed MAX_BOOST for amount: ", vm.toString(breachAmount))
);
// Verify impact on rewards
uint256 dailyRewards = 1000e18;
uint256 expectedMaxRewards = (dailyRewards * boostController.MAX_BOOST()) / 10000;
uint256 actualRewards = (dailyRewards * multiplier) / 10000;
assertGt(
actualRewards,
expectedMaxRewards,
string.concat("Rewards exceed maximum allowed for amount: ", vm.toString(breachAmount))
);
}
}
}

Why do this happens?

  1. Integer division causes truncation, leading to unintended behavior in the calculation of baseAmount.

  2. When userBoost.amount is 333,499,999 baseAmount gets truncated to a much smaller value.

  3. As a result, dividing userBoost.amount * 10000 by baseAmount leads to an inflated return value.

  4. This allows an attacker to gain an unintended higher boost multiplier, breaking system invariants.

Impact

  1. Boost Multiplier Inflation:

    • The function would return as seen a value higher than MAX_BOOST, causing unintended reward calculations.

    • Attackers with specific deposit amounts would manipulate their boost multipliers beyond the intended limit.

  2. Reward Exploitation:

    • Since rewards are computed using the boost multiplier, an attacker would earn a disproportionately high reward share.

    • This results in unfair distribution and economic imbalances in the system.

  3. System Integrity Violation:

    • The contract assumes MAX_BOOST is an upper bound, which is violated by this flaw.

Tools Used

Recommendations

Fix the Calculation Logic

The correct calculation should prevent integer division from introducing precision loss:

function getBoostMultiplier(
address user,
address pool
) external view override returns (uint256) {
if (!supportedPools[pool]) revert PoolNotSupported();
UserBoost storage userBoost = userBoosts[user][pool];
if (userBoost.amount == 0) return MIN_BOOST;
// Corrected Calculation
+ uint256 baseAmount = (userBoost.amount * 10000 + MAX_BOOST - 1) / MAX_BOOST; // Ensure rounding up
+ return (userBoost.amount * 10000) / baseAmount;
}
Updates

Lead Judging Commences

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

BoostController::getBoostMultiplier returns values exceeding MAX_BOOST due to precision loss in integer division, breaking system invariants for specific boost amounts

Support

FAQs

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

Give us feedback!