Core Contracts

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

Gauge weights are not smoothed out; the gauge reward distribution can be manipulated by a last-minute vote

Summary

The weight allocated for a gauge is not the Time-weighted average. This allows an user to front run the gauge's reward distribution tx and divert an bigger amount of rewards to desired gauge.

Vulnerability Details

Users can vote gauges and allocate allocate votingPower to gauges. The gauges mapping is updated.

function _updateGaugeWeight(
address gauge,
uint256 oldWeight,
uint256 newWeight,
uint256 votingPower
) internal {
@> Gauge storage g = gauges[gauge];
uint256 oldGaugeWeight = g.weight;
uint256 newGaugeWeight = oldGaugeWeight - (oldWeight * votingPower / WEIGHT_PRECISION)
+ (newWeight * votingPower / WEIGHT_PRECISION);
@> g.weight = newGaugeWeight;
g.lastUpdateTime = block.timestamp;
}

When the time period for a gauge is updated to the next period, the gaugePeriods[gauge].weight is set to gauges[gauge].weight value. But this is not even used.

On distributeRewards the _calculateReward is used to calculates rewards based on gauge weight.

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

The latest value stored in gauges[gauge].weight is used to calculate the %rewards for gauge.

A malicious user can exploit this this and front run the distributeRewards.

  • gauges A and B have both 1000 votesweight

  • admin send rewards to gaugeA and calls distributeRewards. He needs to send more rewards than the minimum require to ensure the notification tx doesn't revert due to InsufficientRewardBalance check.

  • Alice frontrun admin's transaction and vote gaugeA with an additional 1000 votes.

  • now gaugeA must have have at least (1000 + 1000) / (2000 + 1000) = 2/3 of the total rewards allocated for that gaugeType.

  • Alice remove her vote from gaugeA and frontruns the next gauge's distributeRewards.
    Alice can repeat these steps to force admin to allocate more rewards

Impact

The gauge reward distribution may be manipulated.

Tools Used

Recommendations

When updatePeriod is called, the weight of a gauge for the next period must be calculated based on the weighted average of its weights over the last N periods.
Use this time-averaged weight to calculate the % of rewards each gauge shall receive.

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.