Summary
Gauge distributes rewards wrongly with wrong decimals.
Vulnerability Details
BaseGauge.sol#earned() function is as follows.
function earned(address account) public view returns (uint256) {
return (getUserWeight(account) *
(getRewardPerToken() - userStates[account].rewardPerTokenPaid) / 1e18
) + userStates[account].rewards;
}
As we can see above, getUserWeight(account) means amount of token which is compared to veRAACToken.totalSupply().
And getUserWeight() function is as follows.
function getUserWeight(address account) public view virtual returns (uint256) {
@> uint256 baseWeight = _getBaseWeight(account);
@> return _applyBoost(account, baseWeight);
}
Here, baseWeight has 18 decimals same as voting power.
And _applyBoost() function is as follows.
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;
}
In upper code, boost seems to have 1e18 of precision.
But BoostCalculator.calculateBoost() function is as follows.
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;
}
As we can see from doc, boost has 10000 basis points.
Impact
This vulnerability leads to low decimals of getUserWeight() and causes less user's rewards.
This is user's loss.
Tools Used
Manual review
Recommendations
Modify _applyBoost() function as follows.
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;
++ return (baseWeight * boost) / WEIGHT_PRECISION;
}