Core Contracts

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

BaseGauge Contract Incorrectly Initializes minBoost: Denial of Service in Reward and Governance Functions

Summary

In the BaseGauge contract, the boost parameters used to calculate reward multipliers are misconfigured. During initialization, the contract sets boostState.minBoost to 1e18 instead of the intended value of 10_000 (i.e., 10,000 basis points representing a 1× boost). This erroneous initialization causes the boost calculation in the reward distribution flow to underflow when computing the boost range (i.e., maxBoost - minBoost), leading to arithmetic errors and transaction reversion. As a result, when a user—such as Alice—attempts to claim rewards via the getReward function, the reward update mechanism (triggered by the updateReward modifier) eventually calls _applyBoost. This function then invokes BoostCalculator.calculateBoost, where the subtraction of minBoost from maxBoost underflows, causing the transaction to revert and effectively denying reward claims. The fallout is a critical denial of service affecting both RAACGauge and RWAGauge, ultimately undermining the protocol's governance and incentive mechanisms.

Vulnerability Details

Flow of the Vulnerability

  1. User Locking and Voting Power Acquisition:
    Users lock their RAAC tokens using veRAACToken::lock to receive time-weighted veRAAC tokens. This process provides users with voting power for governance and rewards.

  2. Gauge Setup and Reward Claiming:
    The GaugeController admin adds a gauge (e.g., a RWA gauge) with a nonzero initial weight. When a user (Alice) later calls getReward, the reward flow begins:

    • The getReward function is called.

    • The updateReward modifier triggers an internal call to _updateReward, which computes the user's earned rewards.

    • Within _updateReward, the earned function is invoked, which in turn calls getUserWeight.

    • The getUserWeight function calls _applyBoost, whose role is to adjust the base weight by a boost factor calculated using the user's veRAAC balance and the total supply.

  3. Boost Calculation Breakdown:
    In _applyBoost, the contract constructs a BoostParameters struct using the boost state:

    BoostCalculator.BoostParameters memory params = BoostCalculator.BoostParameters({
    maxBoost: boostState.maxBoost,
    minBoost: boostState.minBoost, // Incorrectly set to 1e18 instead of 10_000
    boostWindow: boostState.boostWindow,
    totalWeight: boostState.totalWeight,
    totalVotingPower: boostState.totalVotingPower,
    votingPower: boostState.votingPower
    });

    Then, it calls:

    uint256 boost = BoostCalculator.calculateBoost(veBalance, totalVeSupply, params);

    Inside BoostCalculator.calculateBoost, the boost range is computed as:

    uint256 boostRange = params.maxBoost - params.minBoost;

    Since params.minBoost is set to 1e18 (a value far exceeding any reasonable maxBoost that might be set around 10_000), the subtraction underflows. Solidity (version 0.8+) automatically reverts on underflow, so the entire reward calculation fails.

Code Excerpts Highlighting the Issue

  • BaseGauge Constructor (Initialization Bug):

    // Within BaseGauge constructor (or initialization function)
    boostState.maxBoost = <correct_value>; // e.g., set appropriately
    boostState.minBoost = 1e18; // BUG: Should be 10_000 (or 10000 basis points)
    boostState.boostWindow = <appropriate_duration>;
    // Other boostState parameters...
  • _applyBoost Function in BaseGauge:

    function _applyBoost(address account, uint256 baseWeight) internal view virtual returns (uint256) {
    if (baseWeight == 0) return 0;
    IERC20 veToken = IERC20(IGaugeController(controller).veRAACToken());
    uint256 veBalance = veToken.balanceOf(account);
    uint256 totalVeSupply = veToken.totalSupply();
    BoostCalculator.BoostParameters memory params = BoostCalculator.BoostParameters({
    maxBoost: boostState.maxBoost,
    minBoost: boostState.minBoost, // BUG: Incorrectly set to 1e18
    boostWindow: boostState.boostWindow,
    totalWeight: boostState.totalWeight,
    totalVotingPower: boostState.totalVotingPower,
    votingPower: boostState.votingPower
    });
    uint256 boost = BoostCalculator.calculateBoost(veBalance, totalVeSupply, params);
    return (baseWeight * boost) / 1e18;
    }
  • BoostCalculator.calculateBoost Function:

    function calculateBoost(uint256 veBalance, uint256 totalVeSupply, BoostParameters memory params)
    internal
    pure
    returns (uint256)
    {
    if (totalVeSupply == 0) {
    return params.minBoost;
    }
    uint256 votingPowerRatio = (veBalance * 1e18) / totalVeSupply;
    uint256 boostRange = params.maxBoost - params.minBoost; // Underflow here if minBoost > maxBoost
    uint256 boost = params.minBoost + ((votingPowerRatio * boostRange) / 1e18);
    if (boost < params.minBoost) {
    return params.minBoost;
    }
    if (boost > params.maxBoost) {
    return params.maxBoost;
    }
    return boost;
    }

Proof of Concept

Scenario Walkthrough

  1. User Setup:

    • Alice locks RAAC tokens via veRAACToken::lock and acquires voting power.

    • A gauge is set up (e.g., via GaugeController) and configured with reward distribution parameters.

  2. Reward Claim Attempt:

    • Alice calls getReward to claim her rewards.

    • The reward flow invokes _updateRewardearnedgetUserWeight_applyBoost.

    • In _applyBoost, the boost calculation fails due to the arithmetic underflow caused by boostState.minBoost being set to 1e18.

    • Consequently, the transaction reverts with an arithmetic error, and Alice cannot claim her rewards.

Test Case (Using Foundry)

Below is a simplified Foundry test illustrating the DoS condition caused by the misconfigured minBoost:

// 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;
BaseGauge baseGauge;
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%, 10000 - 100%
uint256 initialRaacBurnTaxRateInBps = 150; // 1.5%, 10000 - 100%
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(RAAC_GAUGE_OWNER);
raacGauge = new RAACGauge(address(rewardToken), address(stakingToken), address(gaugeController));
vm.stopPrank();
vm.startPrank(RWA_GAUGE_OWNER);
rwaGauge = new RWAGauge(address(rewardToken), address(stakingToken), address(gaugeController));
vm.stopPrank();
}
function raacTokenAllotmentAndAcquireVeRaac() private {
vm.startPrank(RAAC_OWNER);
raacToken.setMinter(RAAC_MINTER);
vm.stopPrank();
vm.startPrank(RAAC_MINTER);
raacToken.mint(ALICE, MAX_LOCK_AMOUNT);
raacToken.mint(BOB, MAX_LOCK_AMOUNT);
raacToken.mint(CHARLIE, MAX_LOCK_AMOUNT);
raacToken.mint(DEVIL, MAX_LOCK_AMOUNT);
raacToken.mint(ALICE, MAX_LOCK_AMOUNT);
vm.stopPrank();
}
function raacTokenLock() private {
vm.startPrank(ALICE);
raacToken.approve(address(veToken), MAX_LOCK_AMOUNT);
veToken.lock(MAX_LOCK_AMOUNT, MAX_LOCK_DURATION);
vm.stopPrank();
vm.startPrank(BOB);
raacToken.approve(address(veToken), MAX_LOCK_AMOUNT);
veToken.lock(MAX_LOCK_AMOUNT, MAX_LOCK_DURATION);
vm.stopPrank();
vm.startPrank(CHARLIE);
raacToken.approve(address(veToken), MAX_LOCK_AMOUNT);
veToken.lock(MAX_LOCK_AMOUNT, MAX_LOCK_DURATION);
vm.stopPrank();
vm.startPrank(DEVIL);
raacToken.approve(address(veToken), MAX_LOCK_AMOUNT);
veToken.lock(MAX_LOCK_AMOUNT, MAX_LOCK_DURATION);
vm.stopPrank();
}
function testGetRewardDoS() public {
raacTokenAllotmentAndAcquireVeRaac();
raacTokenLock();
vm.warp(block.timestamp + 1 days);
uint256 initialWeight = 100;
vm.startPrank(GAUGE_CONTROLLER_OWNER);
gaugeController.addGauge(address(rwaGauge), IGaugeController.GaugeType.RWA, initialWeight);
vm.stopPrank();
vm.startPrank(ALICE);
vm.expectRevert();
rwaGauge.getReward();
vm.stopPrank();
}
}

How to Run the Test

  1. Step 1: Create a Foundry project:

    forge init my-foundry-project
  2. Step 2: Remove unnecessary files.

  3. Step 3: Place your contracts in the src directory.

  4. Step 4: Create a test directory adjacent to src and add the above test file (e.g., BaseGaugeBoostTest.t.sol).

  5. Step 5: Run the test:

    forge test --mt testGetRewardDoS -vv
  6. Expected Output:
    The test should revert as expected, confirming that the reward claim fails due to the misconfigured boost parameter.

Impact

  • Denial of Service in Reward Claims:
    Users like Alice will be unable to claim rewards because the arithmetic underflow in boost calculations causes transactions to revert.

  • Governance and Incentive Distortion:
    Since reward multipliers and boosted voting power are integral to governance, an inability to compute the correct boost results in misaligned incentives and can skew governance outcomes.

  • Protocol Trust and Stability:
    Repeated failures in reward distribution will erode user trust and can cause reduced participation in both staking and governance, destabilizing the protocol’s economic model.

Tools Used

  • Manual Review

  • Foundry

Recommendations

To remediate this vulnerability, the initialization of the boost state in the BaseGauge contract must be corrected. Specifically, boostState.minBoost should be set to the intended value of 10_000 (representing 1× boost or 10000 basis points) rather than 1e18.

Diff Recommendation for BaseGauge Constructor

constructor(/* parameters */) {
// Other initialization code...
boostState.maxBoost = <correct_max_value>; // e.g., 2e18 if using 2x max boost in fixed-point representation
- boostState.minBoost = 1e18; // BUG: Incorrect minimum boost value
+ boostState.minBoost = 10000; // Corrected minimum boost value (10,000 basis points)
boostState.boostWindow = <appropriate_duration>;
// Other boostState initializations...
}
Updates

Lead Judging Commences

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

boostState.minBoost is set to 1e18

Support

FAQs

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

Give us feedback!