Core Contracts

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

Inaccuracies in User Boost Update and Pool Totals

Location

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

Summary

The updateUserBoost function calculates a user’s boost using a fixed base amount (10000) instead of a dynamically derived value from the user’s current veToken balance or voting power. It then directly assigns this boost value to the pool’s working supply rather than aggregating boosts across users. This approach may lead to inaccurate pool metrics and reward distribution errors.

Vulnerability Details

Fixed Base Amount Issue:
The function calls:

uint256 newBoost = _calculateBoost(user, pool, 10000);

This fixed base value does not adapt to real-time changes in the user’s veToken holdings.

Pool Update Logic:
The pool’s workingSupply is directly overwritten:

poolBoost.workingSupply = newBoost;

This design does not aggregate boosts from all users and can lead to miscalculations if multiple user boosts are updated separately.

Impact

Reward Distribution Errors:
Incorrect boost calculations can lead to improper reward allocations.

Manipulation Potential:
Users might time balance changes to benefit from the fixed calculation mechanism.

Inconsistent Pool Metrics:
The working supply may not accurately represent the collective boost state, leading to systemic errors in rewards or fee calculations.

Proof of Code

function updateUserBoost(address user, address pool) external override nonReentrant whenNotPaused {
// ...
uint256 newBoost = _calculateBoost(user, pool, 10000); // Fixed base amount used
userBoost.amount = newBoost;
// Direct assignment instead of aggregation:
poolBoost.workingSupply = newBoost;
// ...
}

The following test demonstrates that when the user’s veToken balance increases, the updated boost value increases—but the fixed base amount in the calculation does not allow for a truly dynamic recalculation.

We deploy a DummyGauge contract (DummyGauge.sol) as the pool, ensuring that a valid, non‑null gauge address is passed. Also, we no longer call setVotingPower since our MockVeToken does not implement it.

// 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 - Inaccurate User Boost Update", function () {
let boostController, veToken, owner, user, dummyGauge;

beforeEach(async function () {
[owner, user] = await ethers.getSigners();

// Deploy the MockVeToken (artifact name: "MockVeToken")
const VeTokenMock = await ethers.getContractFactory("MockVeToken");
veToken = await VeTokenMock.deploy();
// No need for .deployed() in ethers v6
await veToken.setBalance(user.address, ethers.parseUnits("100", 18));
// Note: we do not call setVotingPower because our MockVeToken doesn't implement it.
// Deploy the BoostController with 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 valid gauge
const DummyGauge = await ethers.getContractFactory("DummyGauge");
dummyGauge = await DummyGauge.deploy();
await dummyGauge.deployed();
// Mark dummyGauge as a supported pool in the BoostController
await boostController.connect(owner).modifySupportedPool(dummyGauge.address, true);

});

it("should update boost inaccurately using a fixed base amount", async function () {
// First update with the initial veToken balance of 100 units
await boostController.connect(owner).updateUserBoost(user.address, dummyGauge.address);
const userBoostData = await boostController.getUserBoost(user.address, dummyGauge.address);

// Increase the user's veToken balance to 200 units
await veToken.setBalance(user.address, ethers.parseUnits("200", 18));
// Update boost again after balance change
await boostController.connect(owner).updateUserBoost(user.address, dummyGauge.address);
const updatedUserBoostData = await boostController.getUserBoost(user.address, dummyGauge.address);
// Expect the updated boost to be greater than the initial boost
expect(updatedUserBoostData.amount).to.be.gt(userBoostData.amount);

});
});

## Recommendations
* Replace the fixed base amount (10000) with a value derived from the current veToken balance or a time‑weighted voting power measure. For example, define a new constant:
```solidity
uint256 public constant TARGET_BOOST = 10000;

Then update the boost multiplier (and/or the boost calculation logic) to use linear interpolation:

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;
// Linearly interpolate between MIN_BOOST and MAX_BOOST based on userBoost.amount relative to TARGET_BOOST.
uint256 multiplier = MIN_BOOST + ((userBoost.amount * (MAX_BOOST - MIN_BOOST)) / TARGET_BOOST);
if (multiplier > MAX_BOOST) {
multiplier = MAX_BOOST;
}
return multiplier;
}
  • Modify the pool update logic so that instead of directly assigning newBoost to poolBoost.workingSupply, the contract aggregates boost contributions from all users. This change may require maintaining a running total and updating it when any user’s boost changes.

Updates

Lead Judging Commences

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

BoostController::updateUserBoost overwrites workingSupply with single user's boost value instead of accumulating, breaking reward multipliers and allowing last updater to capture all benefits

Support

FAQs

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