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 ...
}