Summary
The GaugeController has two critical issues that can lead to DoS of reward distribution:
Missing functions to call essential BaseGauge operations, specifically the updatePeriod function that sets the next period to receive rewards, and
Lack of period eligibility checks before reward distribution, causing rewards to exceed period caps when periods should have been updated.
Vulnerability Details
Issue 1: Missing controller functions leading to permanent DoS of the gauge distribution rewards mechanism
The BaseGauge implements several controller-only functions that have no corresponding calls from the GaugeController:
In GaugeController, distributeRewards calculates and notifies rewards without transferring tokens:
function updatePeriod() external override onlyController {
uint256 currentTime = block.timestamp;
uint256 periodEnd = periodState.periodStartTime + getPeriodDuration();
...
}
function setEmission(uint256 emission) external onlyController {
if (emission > periodState.emission) revert RewardCapExceeded();
periodState.emission = emission;
}
function setInitialWeight(uint256 weight) external onlyController {
uint256 periodDuration = getPeriodDuration();
uint256 currentTime = block.timestamp;
uint256 nextPeriodStart = ((currentTime / periodDuration) + 2) * periodDuration;
}
function setBoostParameters(
uint256 _maxBoost,
uint256 _minBoost,
uint256 _boostWindow
) external onlyController {
boostState.maxBoost = _maxBoost;
boostState.minBoost = _minBoost;
}
Notice that for the onlyController is representing the GaugeController. A prove of this is that the notifyRewardAmount which is protected by onlyControlleris called from the GaugeController:
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);
}
function notifyRewardAmount(uint256 amount) external override onlyController updateReward(address(0)) {
...
}
Problem is: there is no way to call updatePeriodfrom BaseGauge, meaning BaseGauge can't proceed to the next period.
This will result in the current period reaching its emission cap:
function notifyReward(
PeriodState storage state,
uint256 amount,
uint256 maxEmission,
uint256 periodDuration
) internal view returns (uint256) {
if (amount > maxEmission) revert RewardCapExceeded();
@> if (amount + state.distributed > state.emission) {
revert RewardCapExceeded();
}
...
}
It prevents the BaseGauge contract to distribute any additional rewards. The main root cause is that the GaugeController cannot call updatePeriodbut at the same time updatePeriod can only be called by the GaugeController.
Issue 2: DoS in Reward Distribution
The GaugeController distributes rewards without checking if periods need updating:
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);
}
CopyInsert
function _distributeToGauges(
GaugeType gaugeType,
uint256 amount,
address[] memory _gaugeList
) internal {
IGauge(gauge).notifyRewardAmount(gaugeShare);
}
This leads to a DoS when:
function notifyReward(
PeriodState storage state,
uint256 amount,
uint256 maxEmission,
uint256 periodDuration
) internal view returns (uint256) {
if (amount + state.distributed > state.emission) {
revert RewardCapExceeded();
}
}
Impact
Issue 1: Missing controller functions leading to permanent DoS of the gauge distribution rewards mechanism.
Reward distribution becomes permanently blocked once the emission cap is reached, with no way to update the period.
Critical gauge parameters (emission rates, weights, boost parameters) cannot be modified due to missing controller functions
The entire gauge reward system becomes non-functional, affecting protocol incentives and tokenomics
Issue 2: DoS in Reward Distribution
Tools Used
Manual Review
Recommendations
Implement missing functions in GaugeController and try to update the base gauge period before notifying reward amount.
contract GaugeController {
+ function updateGaugePeriod(address gauge) external {
+ // it doesnt't need to be a restricted function
+ require(isGauge[gauge], "Invalid gauge");
+ _updateGaugePeriodIfNeeded(gauge);
+ }
+ function _updateGaugePeriodIfNeeded(address gauge) internal {
+ try IGauge(gauge).updatePeriod() {
+ // Period was successfully updated
+ } catch {
+ // Other errors - continue
+ }
+ }
+ function setGaugeEmission(address gauge, uint256 emission) external onlyGaugeAdmin {
+ require(isGauge[gauge], "Invalid gauge");
+ IGauge(gauge).setEmission(emission);
+ }
+ function setGaugeInitialWeight(address gauge, uint256 weight) external onlyGaugeAdmin {
+ require(isGauge[gauge], "Invalid gauge");
+ IGauge(gauge).setInitialWeight(weight);
+ }
+ function setGaugeBoostParameters(
+ address gauge,
+ uint256 maxBoost,
+ uint256 minBoost,
+ uint256 boostWindow
+ ) external onlyGaugeAdmin {
+ require(isGauge[gauge], "Invalid gauge");
+ IGauge(gauge).setBoostParameters(maxBoost, minBoost, boostWindow);
+ }
function distributeRewards(address gauge) external override nonReentrant whenNotPaused {
if (!isGauge(gauge)) revert GaugeNotFound();
if (!gauges[gauge].isActive) revert GaugeNotActive();
+ _updateGaugePeriodIfNeeded(gauge);
...
}
function _distributeToGauges(
GaugeType gaugeType,
uint256 amount,
address[] memory _gaugeList
) internal {
for (uint256 i = 0; i < _gaugeList.length; i++) {
address gauge = _gaugeList[i];
+ _updateGaugePeriodIfNeeded(gauge);
...
}
}
}