Summary
The current implementation of the voting mechanism in the GaugeController contract allows users to cast votes for gauges that are inactive. This inconsistency can lead to skewed reward distribution.
Vulnerability Details
The vote() function does not verify if the gauge is active before allowing votes to be cast.
function vote(address gauge, uint256 weight) external override whenNotPaused {
if (!isGauge(gauge)) revert GaugeNotFound();
>>
if (weight > WEIGHT_PRECISION) revert InvalidWeight();
uint256 votingPower = veRAACToken.balanceOf(msg.sender);
if (votingPower == 0) revert NoVotingPower();
uint256 oldWeight = userGaugeVotes[msg.sender][gauge];
userGaugeVotes[msg.sender][gauge] = weight;
_updateGaugeWeight(gauge, oldWeight, weight, votingPower);
emit WeightUpdated(gauge, oldWeight, weight);
}
This oversight means that users can allocate weights to gauges that are not operational.
When a user votes, their vote updates the userGaugeVotes mapping, which tracks how much weight each user has assigned to each gauge.
Now notice that the _updateGaugeWeight() called proceeds to update the weight of the entire gauge based on this vote:
uint256 oldGaugeWeight = g.weight;
>> uint256 newGaugeWeight = oldGaugeWeight - (oldWeight * votingPower / WEIGHT_PRECISION)
+ (newWeight * votingPower / WEIGHT_PRECISION);
>> g.weight = newGaugeWeight;
Now, if a gauge has accumulated a significant amount of votes while inactive, it will receive a disproportionate share of rewards when it is finally activated.
>> uint256 reward = _calculateReward(gauge);
if (reward == 0) return;
>> IGauge(gauge).notifyRewardAmount(reward);
Here is _calculateReward():
>> uint256 totalWeight = getTotalWeight();
if (totalWeight == 0) return 0;
>> uint256 gaugeShare = (g.weight * WEIGHT_PRECISION) / totalWeight;
uint256 typeShare = (typeWeights[g.gaugeType] * WEIGHT_PRECISION) / MAX_TYPE_WEIGHT;
uint256 periodEmission = g.gaugeType == GaugeType.RWA ? _calculateRWAEmission() : _calculateRAACEmission();
>> return (periodEmission * gaugeShare * typeShare) / (WEIGHT_PRECISION * WEIGHT_PRECISION);
Impact
When the inactive gauge is reactivated, the weights assigned during its inactive state will still be in effect. This could lead to disproportionate share of rewards distributed to it.
Tools Used
Manual Review
Recommendations
Modify the vote() function to include a check for the gauge's active status before allowing votes.
function vote(address gauge, uint256 weight) external override whenNotPaused {
if (!isGauge(gauge)) revert GaugeNotFound();
+ Gauge storage g = gauges[gauge];
+ if (!g.isActive) revert GaugeNotActive(); // Check if gauge is active
// ... existing code ...
}