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;