Core Contracts

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

`notifyRewardAmount` could overwrite `rewardRate`

Summary

In BaseGauge, when a new reward period starts, the notifyRewardAmount function is called by the gauge controller. However, if the previous period is still ongoing, the reward rate is overridden, causing all unclaimed rewards from the previous period to be lost.

BaseGauge:

function notifyRewardAmount(uint256 amount)
external override onlyController updateReward(address(0))
{
if (amount > periodState.emission) revert RewardCapExceeded();
rewardRate = notifyReward(periodState, amount, periodState.emission, getPeriodDuration());
periodState.distributed += amount;
uint256 balance = rewardToken.balanceOf(address(this));
if (rewardRate * getPeriodDuration() > balance) {
revert InsufficientRewardBalance();
}
lastUpdateTime = block.timestamp;
emit RewardNotified(amount);
}

GaugeController:

function distributeRevenue(
GaugeType gaugeType,
uint256 amount
) external onlyRole(EMERGENCY_ADMIN) whenNotPaused {
if (amount == 0) revert InvalidAmount();
uint256 veRAACShare = amount * 80 / 100; // 80% to veRAAC holders
uint256 performanceShare = amount * 20 / 100; // 20% performance fee
revenueShares[gaugeType] += veRAACShare;
_distributeToGauges(gaugeType, veRAACShare);
emit RevenueDistributed(gaugeType, amount, veRAACShare, performanceShare);
}
/**
* @notice Distributes rewards to gauges of a specific type
* @dev Internal function to handle gauge reward distribution
* @param gaugeType Type of gauges to distribute to
* @param amount Total amount to distribute
*/
function _distributeToGauges(
GaugeType gaugeType,
uint256 amount
) internal {
uint256 totalTypeWeight = 0;
uint256[] memory gaugeWeights = new uint256[](_gaugeList.length);
uint256 activeGaugeCount = 0;
// First pass: calculate total weight and store gauge weights
for (uint256 i = 0; i < _gaugeList.length; i++) {
address gauge = _gaugeList[i];
if (gauges[gauge].isActive && gauges[gauge].gaugeType == gaugeType) {
gaugeWeights[i] = gauges[gauge].weight;
totalTypeWeight += gaugeWeights[i];
activeGaugeCount++;
}
}
if (totalTypeWeight == 0 || activeGaugeCount == 0) return;
// Second pass: distribute rewards
for (uint256 i = 0; i < _gaugeList.length; i++) {
address gauge = _gaugeList[i];
if (gauges[gauge].isActive && gauges[gauge].gaugeType == gaugeType) {
uint256 gaugeShare = (amount * gaugeWeights[i]) / totalTypeWeight;
if (gaugeShare > 0) {
IGauge(gauge).notifyRewardAmount(gaugeShare);
}
}
}
}

Vulnerability Details

Scenario:

  1. Two gauges exist under the same GaugeController—one with an ongoing reward period and another without.

  2. distributeRevenue is called, triggering _distributeToGauges.

  3. The gauge with an ongoing period has its reward rate overridden, causing unclaimed rewards from the previous period to be lost.

Impact

If a new reward period starts while the previous one is still active, the reward rate gets overridden, leading to the loss of all remaining rewards from the previous period. This negatively affects users who are expecting their pending rewards.

Tools Used

Manual review

Recommendations

  • Implement a mechanism to ensure that a new period cannot override an active one before it completes.

Updates

Lead Judging Commences

inallhonesty Lead Judge about 1 month ago
Submission Judgement Published
Validated
Assigned finding tags:

BaseGauge's notifyRewardAmount overwrites reward rates without accounting for undistributed rewards, allowing attackers to reset admin-distributed rewards

Support

FAQs

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