Core Contracts

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

Users Without veTokens Can Update Boost State in BoostController

Summary

The updateUserBoost function in BoostController allows users without any veToken balance to update boost states and affect pool metrics, violating the core principle that boost should only be available to veToken holders.

Vulnerability Details

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

function updateUserBoost(address user, address pool) external override nonReentrant whenNotPaused {
// No check for veToken balance before state changes
uint256 oldBoost = userBoost.amount;
uint256 newBoost = _calculateBoost(user, pool, 10000); // Returns base amount (10000) for zero balance
// State changes occur even with no tokens
userBoost.amount = newBoost;
userBoost.lastUpdateTime = block.timestamp;
// Pool metrics affected
if (newBoost >= oldBoost) {
poolBoost.totalBoost = poolBoost.totalBoost + (newBoost - oldBoost);
} else {
poolBoost.totalBoost = poolBoost.totalBoost - (oldBoost - newBoost);
}
poolBoost.workingSupply = newBoost;
}

In _calculateBoost:

uint256 userBalance = IERC20(address(veToken)).balanceOf(user);
uint256 totalSupply = IERC20(address(veToken)).totalSupply();
if (userBalance == 0 || totalSupply == 0) {
return amount; // @audit Returns base amount without preventing state updates
}

Poc:

// set attacker at the top level
let attacker ;
// add attacker to part of the account
[owner, user1, user2, manager, attacker] = await ethers.getSigners();
it.only("should handle boost for none holders ", async () => {
console.log(
"vetoken balance of attacker: ",
await veToken.balanceOf(attacker.address)
);
expect(
await boostController
.connect(attacker)
.updateUserBoost(attacker.address, mockPool.getAddress())
)
.to.emit(boostController, "BoostUpdated")
.withArgs(attacker.address, mockPool.getAddress(), 0);
});
});

Output:

vetoken balance of attacker: 0n
✔ should handle boost for none holders

we can see that the test pass, anyone can just come and update their boost using the updateUserBoost

Impact

  • It breaks the veToken-based access model by allowing participation of non-veRAACToken holder to updateBoost without stake

  • Unnecessary state updates for non-token holders

Tools Used

Manual review

Recommendations

Add veToken balance check:

function updateUserBoost(address user, address pool) external override nonReentrant whenNotPaused {
if (paused()) revert EmergencyPaused();
if (user == address(0)) revert InvalidPool();
if (!supportedPools[pool]) revert PoolNotSupported();
uint256 userBalance = IERC20(address(veToken)).balanceOf(user);
if (userBalance == 0) revert NoVeTokenBalance();
// Rest of the function...
}

or revert for zero 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) {
revert ZeroBalance();
}
... rest of the code
}
Updates

Lead Judging Commences

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