Summary
The GaugeController contract implements time-weighted reward distribution to distribute rewards to the Gauges. However these are never used in actuall rewards calculations.
Vulnerability Details
We the Gauge is added new period is created with expected distribution parameters.
function addGauge(
address gauge,
GaugeType gaugeType,
uint256 initialWeight
) external onlyGaugeAdmin {
if (gauges[gauge].lastUpdateTime != 0) revert GaugeAlreadyExists();
if (gaugeType != GaugeType.RWA && gaugeType != GaugeType.RAAC) {
revert InvalidGaugeType();
}
uint256 periodWeight = initialWeight == 0 ? 1 : initialWeight;
uint256 duration = gaugeType == GaugeType.RWA ? 30 days : 7 days;
gauges[gauge] = Gauge({
weight: initialWeight,
typeWeight: 0,
lastUpdateTime: block.timestamp,
gaugeType: gaugeType,
isActive: true,
lastRewardTime: block.timestamp
});
TimeWeightedAverage.Period storage period = gaugePeriods[gauge];
TimeWeightedAverage.createPeriod(
period,
block.timestamp,
duration,
periodWeight,
periodWeight
);
_gaugeList.push(gauge);
emit GaugeAdded(gauge, gaugeType);
}
To distribute the rewards to gauges the distributeRewards function is used.
function distributeRewards(
address gauge
) external override nonReentrant whenNotPaused {
if (!isGauge(gauge)) revert GaugeNotFound();
if (!gauges[gauge].isActive) revert GaugeNotActive();
uint256 reward = _calculateReward(gauge);
if (reward == 0) return;
IGauge(gauge).notifyRewardAmount(reward);
emit RewardDistributed(gauge, msg.sender, reward);
}
This function calls internal _calculateReward. As we can see it does not use created distribution periods but distributes rewards based on gauge share and period emission.
function _calculateReward(address gauge) internal view returns (uint256) {
Gauge storage g = gauges[gauge];
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
Rewards should be distributed based on created distribution periods however they are distributed based on gauge share and period emission.
Tools Used
Manual Review, Hardhat
Recommendations
Change the rewards calculation function so that rewards are based on implemented distribution periods.