Core Contracts

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

Flawed Boost Multiplier Calculation Always Yields Maximum Boost

Location

governance -> boost -> BoostController.sol -> Line 282

Summary

The getBoostMultiplier function is intended to compute a variable multiplier between MIN_BOOST and MAX_BOOST. The boost multiplier calculation is flawed—its math forces it to always return the maximum boost value for any nonzero boost, eliminating any intended variability.

Vulnerability Details

Calculation Issue:
The function computes:

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

Mathematically, for any nonzero userBoost.amount, this simplifies to always yield MAX_BOOST (e.g., 25000 basis points).

Design Flaw:
As a consequence, every user with a nonzero boost is granted the maximum boost multiplier, regardless of their actual contribution or intended weight. This undermines the incentive mechanism.

Impact

Economic Disruption:
Because every user ends up receiving the highest possible boost, the designed reward structure becomes distorted. This uniformity can lead to some users being over-rewarded, upsetting the balance of incentives within the protocol.

Fairness Issues:
With the boost system no longer differentiating between users, strategic efforts to optimize boost levels become pointless. This lack of differentiation can discourage active, meaningful participation.

Exploitation:
If everyone gets the maximum multiplier regardless of their actual contribution, there's no incentive to adjust boost levels. This renders any strategic behavior irrelevant, ultimately reducing the protocol's overall efficiency.

PoC

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;
uint256 baseAmount = userBoost.amount * 10000 / MAX_BOOST;
return userBoost.amount * 10000 / baseAmount;
}

For example, if userBoost.amount = 15000 and MAX_BOOST = 25000:

  • baseAmount = 15000 * 10000 / 25000 = 6000

  • Multiplier = 15000 * 10000 / 6000 ≈ 25000 (i.e., always MAX_BOOST).

Test Suite:

To verfiy the vulnerability, the following test deploys a duumy gauge (since the BoostController expects a valid gauge contract address) - please be mindful of this when checking this PoC - (had a lot of trouble getting it to work)

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
contract DummyGauge {
// Minimal dummy contract to act as a gauge.
}
import pkg from "hardhat";
const { ethers } = pkg;
import { expect } from "chai";
import { time } from "@nomicfoundation/hardhat-network-helpers";
describe("BoostController Vulnerability Test: Flawed Boost Multiplier Calculation", function () {
let boostController, veToken, owner, user, dummyGauge;
beforeEach(async function () {
[owner, user] = await ethers.getSigners();
// Deploy the MockVeToken contract
const VeTokenMock = await ethers.getContractFactory("MockVeToken");
veToken = await VeTokenMock.deploy();
await veToken.deployed();
// Set a balance for the user in the veToken mock
await veToken.setBalance(user.address, ethers.parseUnits("100", 18));
// Deploy the BoostController using the veToken address
const BoostController = await ethers.getContractFactory("BoostController");
boostController = await BoostController.deploy(veToken.address);
await boostController.deployed();
// Deploy a DummyGauge contract to act as a pool
const DummyGauge = await ethers.getContractFactory("DummyGauge");
dummyGauge = await DummyGauge.deploy();
await dummyGauge.deployed();
// Mark dummyGauge as supported in the BoostController
await boostController.connect(owner).modifySupportedPool(dummyGauge.address, true);
// Update the user's boost for the dummy gauge
await boostController.connect(owner).updateUserBoost(user.address, dummyGauge.address);
});
it("should always return MAX_BOOST (25000) for any nonzero boost", async function () {
// Call getBoostMultiplier for the user and dummy gauge
const multiplier = await boostController.getBoostMultiplier(user.address, dummyGauge.address);
// Due to the flawed arithmetic, the multiplier always equals MAX_BOOST
expect(multiplier).to.equal(25000);
});
});

Recommendations

uint256 public constant TARGET_BOOST = 10000;
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;
// Linear interpolation between MIN_BOOST and MAX_BOOST:
uint256 multiplier = MIN_BOOST + ((userBoost.amount * (MAX_BOOST - MIN_BOOST)) / TARGET_BOOST);
if (multiplier > MAX_BOOST) {
multiplier = MAX_BOOST;
}
return multiplier;
}

To restore the intended variability, I recommend using a linear interpolation based on a target boost level.

Updates

Lead Judging Commences

inallhonesty Lead Judge 4 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.