Core Contracts

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

Because time weighted average is not used when calculating rewards for gauges, the reward distribution can be manipulated.

Bug description

In gauge controller, gauges can receive revenue through the distributeRevenue() function, which internally calls _distributeToGauges(). The calculations used to distribute revenue depend on the weight of each gauge.

GaugeController.sol#L543-L563

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

The same can be said about distributeRewards() function that is used to distribute rewards to a specific gauge. The amount to distribute is calculated inside _calculateReward() function and also depends on the weight of a given gauge.

GaugeController.sol#L362-L371

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;
// Calculate period emissions based on gauge type
uint256 periodEmission = g.gaugeType == GaugeType.RWA
? _calculateRWAEmission()
: _calculateRAACEmission();
return
(periodEmission * gaugeShare * typeShare) /
(WEIGHT_PRECISION * WEIGHT_PRECISION);

In both cases, the problem is the fact that time weighted average is not used to calculate weights. When a gauge is first added, the period with initial weight is created for that gauge.

GaugeController.sol#L261-L267

TimeWeightedAverage.Period storage period = gaugePeriods[gauge];
TimeWeightedAverage.createPeriod(
period,
block.timestamp, // Start from current timestamp
duration,
periodWeight,
periodWeight
);

However, later it's disregarded, as when the user votes for a gauge, the period is not updated and when the revenue/rewards calculations are being performed, the weight used is raw weight instead of time-weighted one. This allows anyone to manipulate revenue/reward distribution by front-running calls to distributeRevenue() and distributeRewards() with a call to vote to increase the gauge's weight.

Impact

Anyone can manipulate revenue/rewards distribution with frontrunning to increase the gauge's weight before calculations of revenue/rewards.

Recommended Mitigation

Inside GaugeController, use time-weighted average when calculating rewards and revenue.

Updates

Lead Judging Commences

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

GaugeController uses current gauge weights instead of time-weighted averages for reward calculations

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

GaugeController uses current gauge weights instead of time-weighted averages for reward calculations

Support

FAQs

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