Core Contracts

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

Incorrect Gauge Weight Initialization in addGauge Causes Permanent DoS in updatePeriod

Summary

The GaugeController::addGauge function is responsible for adding new gauges to the system, initializing gauge-specific parameters, and creating an associated time period using the TimeWeightedAverage library. In this process, a non-zero minimum weight is enforced for period tracking by using the expression:

uint256 periodWeight = initialWeight == 0 ? 1 : initialWeight;

However, while periodWeight is correctly computed and used for period creation, the gauge's own stored weight is mistakenly set to initialWeight instead of periodWeight. As a result, if initialWeight is zero, the gauge’s stored weight becomes zero. Later, when the updatePeriod function is called, it passes g.weight (which is zero) to the TimeWeightedAverage.createPeriod function. Since that function checks that weight is not zero (reverting with ZeroWeight if it is), every attempt to update the gauge period results in a revert. Consequently, the gauge’s period state cannot be updated, effectively causing a permanent denial of service (DoS) for that gauge.

Vulnerability Details

Incorrect Weight Storage in addGauge

In the addGauge function, the minimum period weight is determined to ensure that period creation does not fail due to a zero weight:

uint256 periodWeight = initialWeight == 0 ? 1 : initialWeight;

This ensures that if the admin passes initialWeight == 0, the period used for time-weighted calculations will have a weight of 1.

However, when the gauge is stored in the contract’s mapping, the code incorrectly assigns the gauge’s weight using initialWeight rather than periodWeight:

gauges[gauge] = Gauge({
weight: initialWeight, // Incorrect: Should use periodWeight
typeWeight: 0,
lastUpdateTime: block.timestamp,
gaugeType: gaugeType,
isActive: true,
lastRewardTime: block.timestamp
});

Impact on updatePeriod Function

Later, the updatePeriod function attempts to update the gauge’s time period state. It retrieves the gauge’s stored weight (which may be zero) and passes it to the TimeWeightedAverage period creation call:

TimeWeightedAverage.createPeriod(
period,
// Add 1 second to avoid timestamp collision
block.timestamp + 1,
duration,
// initialValue for new period (e.g., 0 or calculated average)
0, // or average in the rollover case
g.weight // g.weight is taken from the Gauge struct
);

The TimeWeightedAverage library’s createPeriod function includes a check that prevents period creation if weight is zero:

if (weight == 0) revert ZeroWeight();

Thus, if initialWeight was zero at the time of gauge creation, g.weight remains zero, and every subsequent call to updatePeriod will revert with a ZeroWeight error. This failure to update the gauge’s period state causes a permanent denial of service (DoS) for that gauge, disrupting time-weighted average calculations, reward distributions, and governance mechanisms that depend on up-to-date gauge data.

Visual Code Highlights

  • In addGauge:

    // Calculate minimum period weight for tracking (non-zero)
    uint256 periodWeight = initialWeight == 0 ? 1 : initialWeight;
    ...
    gauges[gauge] = Gauge({
    weight: initialWeight, // <-- BUG: Should be periodWeight!
    typeWeight: 0,
    lastUpdateTime: block.timestamp,
    gaugeType: gaugeType,
    isActive: true,
    lastRewardTime: block.timestamp
    });
  • In updatePeriod:

    // In updatePeriod, gauge weight is used for period creation:
    TimeWeightedAverage.createPeriod(
    period,
    block.timestamp + 1,
    duration,
    average, // or 0 if first period
    g.weight // <-- If g.weight is 0, this call reverts with ZeroWeight()
    );
  • In TimeWeightedAverage::createPeriod:

    if (weight == 0) revert ZeroWeight();

Proof of Concept

The following Foundry test suite demonstrates the vulnerability. The test creates a gauge with an initialWeight of zero, causing the gauge’s stored weight to be zero. Later, when attempting to update the period, the call reverts with ZeroWeight, confirming the DoS condition.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import {Test, console} from "forge-std/Test.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {BaseGauge} from "../src/core/governance/gauges/BaseGauge.sol";
import {GaugeController} from "../src/core/governance/gauges/GaugeController.sol";
import {RAACGauge} from "../src/core/governance/gauges/RAACGauge.sol";
import {RWAGauge} from "../src/core/governance/gauges/RWAGauge.sol";
import {veRAACToken} from "../src/core/tokens/veRAACToken.sol";
import {RAACToken} from "../src/core/tokens/RAACToken.sol";
import {RewardTokenMock} from "./mocks/RewardTokenMock.m.sol";
import {StakingTokenMock} from "./mocks/StakingTokenMock.m.sol";
import {IGaugeController} from "../src/interfaces/core/governance/gauges/IGaugeController.sol";
import {IGauge} from "../src/interfaces/core/governance/gauges/IGauge.sol";
contract GaugeTest is Test {
GaugeController gaugeController;
RAACGauge raacGauge;
RWAGauge rwaGauge;
veRAACToken veToken;
RAACToken raacToken;
RewardTokenMock rewardToken;
StakingTokenMock stakingToken;
address GAUGE_CONTROLLER_OWNER = makeAddr("GAUGE_CONTROLLER_OWNER");
address RAAC_GAUGE_OWNER = makeAddr("RAAC_GAUGE_OWNER");
address RWA_GAUGE_OWNER = makeAddr("RWA_GAUGE_OWNER");
address RAAC_OWNER = makeAddr("RAAC_OWNER");
address RAAC_MINTER = makeAddr("RAAC_MINTER");
uint256 initialRaacSwapTaxRateInBps = 200; // 2%
uint256 initialRaacBurnTaxRateInBps = 150; // 1.5%
uint256 MAX_LOCK_AMOUNT = 10_000_000e18;
uint256 MAX_LOCK_DURATION = 1460 days;
address VE_RAAC_OWNER = makeAddr("VE_RAAC_OWNER");
address ALICE = makeAddr("ALICE");
address BOB = makeAddr("BOB");
address CHARLIE = makeAddr("CHARLIE");
address DEVIL = makeAddr("DEVIL");
function setUp() public {
vm.startPrank(RAAC_OWNER);
raacToken = new RAACToken(RAAC_OWNER, initialRaacSwapTaxRateInBps, initialRaacBurnTaxRateInBps);
vm.stopPrank();
vm.startPrank(VE_RAAC_OWNER);
veToken = new veRAACToken(address(raacToken));
vm.stopPrank();
rewardToken = new RewardTokenMock();
stakingToken = new StakingTokenMock();
vm.startPrank(GAUGE_CONTROLLER_OWNER);
gaugeController = new GaugeController(address(veToken));
vm.stopPrank();
vm.startPrank(RWA_GAUGE_OWNER);
rwaGauge = new RWAGauge(address(rewardToken), address(stakingToken), address(gaugeController));
vm.stopPrank();
}
function testGaugeControllerUpdatePeriodFailsDueToZeroGaugeWeight() public {
// Add a gauge with an initial weight of zero
uint256 initialWeight = 0; // This will trigger periodWeight = 1 in addGauge calculation...
vm.startPrank(GAUGE_CONTROLLER_OWNER);
gaugeController.addGauge(address(rwaGauge), IGaugeController.GaugeType.RWA, initialWeight);
vm.stopPrank();
vm.warp(block.timestamp + 30 days);
// The gauge is added, but its stored weight is 0.
// Attempt to update the gauge period should revert due to ZeroWeight.
vm.startPrank(ALICE);
vm.expectRevert(bytes4(keccak256("ZeroWeight()")));
gaugeController.updatePeriod(address(rwaGauge));
vm.stopPrank();
}
}

How to Run the Test

  1. Create a Foundry Project:

    forge init my-foundry-project
  2. Remove Unnecessary Files:
    Delete extraneous files to keep the project clean.

  3. Convert or Import Contracts:
    Place your contract files in the src directory.

  4. Create Test and Mocks Folders:
    Create a test directory adjacent to src and include any necessary mock contracts.

  5. Add the Test Suite:
    Create a file named GaugeTest.t.sol in the test directory and paste the above test suite.

  6. Run the Test:

    forge test --mt testGaugeControllerUpdatePeriodFailsDueToZeroGaugeWeight -vv
  7. Expected Output:
    The test should revert with ZeroWeight(), confirming that the gauge period update fails due to the stored gauge weight being zero.

Impact

  • Immediate Denial of Service (DoS):
    Gauges added with an initial weight of zero will never have their period state updated, effectively locking the gauge in a stale state.

  • Disrupted Reward Distribution:
    Since gauge periods are used to calculate time-weighted averages for reward distribution, failing to update these periods leads to incorrect reward allocations.

  • Governance and System Instability:
    Inaccurate gauge information affects governance decisions and user incentives, undermining the trust and stability of the entire protocol.

  • Potential Exploitation:
    Malicious admins or attackers might deliberately set gauges with zero weight to disrupt the system or manipulate reward and governance outcomes.

Tools Used

  • Manual Review

  • Foundry

Recommendations

To resolve this vulnerability, the gauge’s stored weight must be initialized with the non-zero periodWeight value rather than the raw initialWeight. This ensures that subsequent calls to updatePeriod use a valid weight and do not revert.

Diff Recommendations for GaugeController::addGauge

function addGauge(address gauge, GaugeType gaugeType, uint256 initialWeight) external onlyGaugeAdmin {
if (gauges[gauge].lastUpdateTime != 0) revert GaugeAlreadyExists();
if (gaugeType != GaugeType.RWA && gaugeType != GaugeType.RAAC) {
revert InvalidGaugeType();
}
// Use minimum weight (1) for period tracking if initialWeight is 0
uint256 periodWeight = initialWeight == 0 ? 1 : initialWeight;
uint256 duration = gaugeType == GaugeType.RWA ? 30 days : 7 days;
- gauges[gauge] = Gauge({
- weight: initialWeight,
- typeWeight: 0,
- lastUpdateTime: block.timestamp,
- gaugeType: gaugeType,
- isActive: true,
- lastRewardTime: block.timestamp
- });
+ gauges[gauge] = Gauge({
+ weight: periodWeight, // Use non-zero minimum weight for gauge storage
+ typeWeight: 0,
+ lastUpdateTime: block.timestamp,
+ gaugeType: gaugeType,
+ isActive: true,
+ lastRewardTime: block.timestamp
+ });
// Initialize period with current timestamp
TimeWeightedAverage.Period storage period = gaugePeriods[gauge];
TimeWeightedAverage.createPeriod(
period,
block.timestamp, // Start from current timestamp
duration,
periodWeight,
periodWeight
);
_gaugeList.push(gauge);
emit GaugeAdded(gauge, gaugeType);
}
Updates

Lead Judging Commences

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

GaugeController::addGauge incorrectly stores initialWeight instead of periodWeight in gauge struct, causing permanent DoS in updatePeriod when initialWeight is zero

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

GaugeController::addGauge incorrectly stores initialWeight instead of periodWeight in gauge struct, causing permanent DoS in updatePeriod when initialWeight is zero

Support

FAQs

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