Summary
An underflow bug exists in BoostCalculator.calculateBoost(), which is caused by the incorrect initialization of boostState.minBoost = 1e18 in BaseGauge.sol. When executing:
BoostCalculator.sol#L89
uint256 boostRange = params.maxBoost - params.minBoost;
this results in an underflow since params.minBoost (1e18) is significantly larger than params.maxBoost (25000). This will cause the contract to revert, breaking all reward distribution calculations and making the contract completely unusable until the issue is fixed by the admin via setBoostParameters().
Vulnerability Details
Here's the root cause due to the incorrect minBoost initialization:
BaseGauge.sol#L140-L142
boostState.maxBoost = 25000;
boostState.minBoost = 1e18;
It should have been initialized as 1000 but instead a way inflated 1e18 so much bigger than that of maxBoost.
Apparently, all functions, i.e. stake(), withdraw(), getreward(), and voteDirection() with the modifier updateReward(msg.sender) visibility are going to revert readily.
This is because when earned() is triggered to calculate earned rewards for the user's account:
BaseGauge.sol#L583-L587
function earned(address account) public view returns (uint256) {
return (getUserWeight(account) *
(getRewardPerToken() - userStates[account].rewardPerTokenPaid) / 1e18
) + userStates[account].rewards;
}
getUserWeight() gets invoked:
BaseGauge.sol#L594-L597
function getUserWeight(address account) public view virtual returns (uint256) {
uint256 baseWeight = _getBaseWeight(account);
return _applyBoost(account, baseWeight);
}
After getting the baseweight via _getBaseWeight, _applyBoost() is called next:
BaseGauge.sol#L229-L253
function _applyBoost(address account, uint256 baseWeight) internal view virtual returns (uint256) {
if (baseWeight == 0) return 0;
IERC20 veToken = IERC20(IGaugeController(controller).veRAACToken());
uint256 veBalance = veToken.balanceOf(account);
uint256 totalVeSupply = veToken.totalSupply();
BoostCalculator.BoostParameters memory params = BoostCalculator.BoostParameters({
maxBoost: boostState.maxBoost,
minBoost: boostState.minBoost,
boostWindow: boostState.boostWindow,
totalWeight: boostState.totalWeight,
totalVotingPower: boostState.totalVotingPower,
votingPower: boostState.votingPower
});
uint256 boost = BoostCalculator.calculateBoost(
veBalance,
totalVeSupply,
params
);
return (baseWeight * boost) / 1e18;
}
As can be seen from the logic above, minBoost will be assigned 1e18 serving as part of the params inputted as the third parameter in BoostCalculator.calculateBoost():
BoostCalculator.sol#L76-L101
function calculateBoost(
uint256 veBalance,
uint256 totalVeSupply,
BoostParameters memory params
) internal pure returns (uint256) {
if (totalVeSupply == 0) {
return params.minBoost;
}
uint256 votingPowerRatio = (veBalance * 1e18) / totalVeSupply;
uint256 boostRange = params.maxBoost - params.minBoost;
uint256 boost = params.minBoost + ((votingPowerRatio * boostRange) / 1e18);
if (boost < params.minBoost) {
return params.minBoost;
}
if (boost > params.maxBoost) {
return params.maxBoost;
}
return boost;
}
Clearly, boostRange = params.maxBoost - params.minBoost is going to revert because 25000 - 1e18 is going to incur an underflow.
Impact
Boost calculations will readily fail causing DoS to all functions with updateReward visibility.
Tools Used
Recommendations
Consider making the following fix:
BaseGauge.sol#L142
- boostState.minBoost = 1e18;
+ boostState.minBoost = 10000;