Core Contracts

Regnum Aurum Acquisition Corp
HardhatReal World AssetsNFT
77,280 USDC
View results
Submission Details
Severity: high
Valid

Missing `GaugeController` functions lead to reward distribution DoS

Summary

The GaugeController has two critical issues that can lead to DoS of reward distribution:

  1. Missing functions to call essential BaseGauge operations, specifically the updatePeriod function that sets the next period to receive rewards, and

  2. 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 {
// @audit-issue missing call from the GaugeController.
uint256 currentTime = block.timestamp;
uint256 periodEnd = periodState.periodStartTime + getPeriodDuration();
...
}
function setEmission(uint256 emission) external onlyController {
// @audit-issue missing call from the GaugeController.
if (emission > periodState.emission) revert RewardCapExceeded();
periodState.emission = emission;
}
function setInitialWeight(uint256 weight) external onlyController {
// @audit-issue missing call from the GaugeController.
uint256 periodDuration = getPeriodDuration();
uint256 currentTime = block.timestamp;
uint256 nextPeriodStart = ((currentTime / periodDuration) + 2) * periodDuration;
}
function setBoostParameters(
uint256 _maxBoost,
uint256 _minBoost,
uint256 _boostWindow
) external onlyController {
// @audit-issue missing call from the GaugeController
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:

// GaugeController.sol
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);
}
// BaseGauge.sol
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;
// @audit-issue Can revert if period should be updated
IGauge(gauge).notifyRewardAmount(reward);
}
CopyInsert
function _distributeToGauges(
GaugeType gaugeType,
uint256 amount,
address[] memory _gaugeList
) internal {
// ... distribution logic
// @audit-issue Can revert if period should be updated
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) {
// @audit-issue Reverts as current period reached its cap and should be updated.
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

  • Reward distribution fails when periods need updating but aren't checked.

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);
...
}
}
}
Updates

Lead Judging Commences

inallhonesty Lead Judge 7 months ago
Submission Judgement Published
Validated
Assigned finding tags:

`setWeeklyEmission`, `setBoostParameters`, `setEmission` and `setInitialWeight` cannot be called due to controller access control - not implemented in controller

inallhonesty Lead Judge 7 months ago
Submission Judgement Published
Validated
Assigned finding tags:

`setWeeklyEmission`, `setBoostParameters`, `setEmission` and `setInitialWeight` cannot be called due to controller access control - not implemented in controller

Appeal created

inallhonesty Lead Judge 6 months ago
Submission Judgement Published
Validated
Assigned finding tags:

`setWeeklyEmission`, `setBoostParameters`, `setEmission` and `setInitialWeight` cannot be called due to controller access control - not implemented in controller

GaugeController::updatePeriod doesn't call the gauge's updatePeriod function, preventing periodState.distributed from resetting and eventually causing distributeRewards to permanently fail

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.

Give us feedback!