Core Contracts

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

Repeatable Payout Attack in `distributeRewards`

Summary

The distributeRewards function fails to update state after distributing rewards. This omission permits an attacker to trigger duplicate reward distributions for the same gauge, draining reward funds.

Vulnerability Details

The distributeRewards function computes a reward via the _calculateReward call and then immediately calls IGauge(gauge).notifyRewardAmount(reward) without updating any state to mark that rewards have been distributed for the current period.

This design flaw breaks the guarantee a gauge receives a single reward distribution per period. As a malicious input, an attacker can repeatedly call distributeRewards for a specific gauge, and each call will trigger reward distribution without any check to prevent duplicate emissions.

Impact

This issue allows a single user to drain the rewards allocated to gauges. It produces high financial impact because an attacker repeatedly collects the same payout and depletes the system’s funds. The scenario is highly probable if the contract is publicly accessible and contains substantial reward balances, and the bug is directly exploitable without additional prerequisites.

Tools Used

Manual Review

  • Deploy or fork the affected contract with a funded reward pool.

  • Call distributeRewards(gauge) successfully to receive the calculated reward.

  • Immediately call distributeRewards(gauge) again to obtain the same reward repeatedly.

  • No state variable prevents endless identical distributions.

Recommendations

Prevent duplicate reward distributions by updating state after each reward emission. Implement a timestamp or distribution counter that tracks when rewards are distributed for each gauge.

// Add state to track the last reward distribution per gauge.
mapping(address => uint256) public lastRewardTimestamp;
uint256 public constant REWARD_PERIOD = 1 days;
function distributeRewards(address gauge) external override nonReentrant whenNotPaused {
if (!isGauge(gauge)) revert GaugeNotFound();
if (!gauges[gauge].isActive) revert GaugeNotActive();
// Prevent duplicate distributions within the same reward period.
if (block.timestamp < lastRewardTimestamp[gauge] + REWARD_PERIOD) {
revert("Duplicate reward distribution");
}
uint256 reward = _calculateReward(gauge);
if (reward == 0) return;
lastRewardTimestamp[gauge] = block.timestamp; // Update state to prevent duplicate emissions.
IGauge(gauge).notifyRewardAmount(reward);
emit RewardDistributed(gauge, msg.sender, reward);
}

Updates

Lead Judging Commences

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

GaugeController's distributeRewards lacks time-tracking, allowing attackers to repeatedly distribute full period rewards until hitting emission caps

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

GaugeController's distributeRewards lacks time-tracking, allowing attackers to repeatedly distribute full period rewards until hitting emission caps

Support

FAQs

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