Core Contracts

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

Boost Calculation Underflow Revert Due to Incorrect Minimum Boost Value In BaseGauge Constructor

Summary

In the BaseGauge contract's constructor, the minBoost parameter is incorrectly set to 1e18 instead of 10000 (basis points). This causes an arithmetic underflow in the boost calculation, leading to a complete failure of the staking mechanism and potential denial of service for the gauge system.

The BaseGauge is an abstract contract that serves as the base implementation for RWA and RAAC gauges. Because of the above bug, both contracts become useless because the boost calculation mechanism is broken as described below.

Vulnerability Details

  • The boost system is designed to work in basis points (10000 = 1x) [https://github.com/Cyfrin/2025-02-raac/blob/89ccb062e2b175374d40d824263a4c0b601bcb7f/contracts/libraries/governance/BoostCalculator.sol#L37]

  • The BaseGauge uses the BoostCalculator library for it's boostState variable [https://github.com/Cyfrin/2025-02-raac/blob/89ccb062e2b175374d40d824263a4c0b601bcb7f/contracts/core/governance/gauges/BaseGauge.sol#L70]

  • The maxBoost is correctly set to 25000 (2.5x)

  • The minBoost is incorrectly set to 1e18 instead of 10000

constructor(
address _rewardToken,
address _stakingToken,
address _controller,
uint256 _maxEmission,
uint256 _periodDuration
) {
//...
// Initialize boost parameters
boostState.maxBoost = 25000; // 2.5x
@> boostState.minBoost = 1e18; // should be in basis points = 10_000 = 1x
}
  • If a user wants to stake the transaction reverts as described in the flow below:

// has the updateReward modifier
function stake(uint256 amount) external nonReentrant updateReward(msg.sender){}
// updateReward calls _updateReward
function _updateReward(address account) internal {
@> state.rewards = earned(account);
}
// calls earned()
function earned(address account) public view returns (uint256) {
return
@> ((getUserWeight(account) * (getRewardPerToken() - userStates[account].rewardPerTokenPaid)) / 1e18) +
userStates[account].rewards;
}
// calls getUserWeight()
function getUserWeight(address account) public view virtual returns (uint256) {
uint256 baseWeight = _getBaseWeight(account);
return _applyBoost(account, baseWeight);
}
function _applyBoost(address account, uint256 baseWeight) internal view virtual returns (uint256) {
//calls calculateBoost()
uint256 boost = BoostCalculator.calculateBoost(veBalance, totalVeSupply, params);
}
  • This causes an arithmetic underflow in the boost calculation:

// In BoostCalculator.sol:
function calculateBoost(uint256 veBalance, uint256 totalVeSupply, BoostParameters memory params)
internal
pure
returns (uint256)
{
// 25000 - 1e18 = underflow
uint256 boostRange = params.maxBoost - params.minBoost;
}

PoC

In order to run the test you need to:

  1. Run foundryup to get the latest version of Foundry

  2. Install hardhat-foundry: npm install --save-dev @nomicfoundation/hardhat-foundry

  3. Import it in your Hardhat config: require("@nomicfoundation/hardhat-foundry");

  4. Make sure you've set the BASE_RPC_URL in the .env file or comment out the forking option in the hardhat config.

  5. Run npx hardhat init-foundry

  6. There is one file in the test folder that will throw an error during compilation so rename the file in test/unit/libraries/ReserveLibraryMock.sol to => ReserveLibraryMock.sol_broken so it doesn't get compiled anymore (we don't need it anyways).

  7. Create a new folder test/foundry

  8. Paste the below code into a new test file i.e.: FoundryTest.t.sol

  9. Run the test: forge test --mc FoundryTest -vvvv

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.19;
import {Test} from "forge-std/Test.sol";
import {console2} from "forge-std/console2.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {ERC20Mock} from "../../contracts/mocks/core/tokens/ERC20Mock.sol";
import {GaugeController} from "../../contracts/core/governance/gauges/GaugeController.sol";
import {RAACGauge} from "../../contracts/core/governance/gauges/RAACGauge.sol";
import {RWAGauge} from "../../contracts/core/governance/gauges/RWAGauge.sol";
import {veRAACToken} from "../../contracts/core/tokens/veRAACToken.sol";
import {IGaugeController} from "../../contracts/interfaces/core/governance/gauges/IGaugeController.sol";
contract FoundryTest is Test {
// Contracts
ERC20Mock public raacToken;
ERC20Mock public stakingToken;
ERC20Mock public rewardToken;
veRAACToken public veToken;
GaugeController public controller;
RAACGauge public raacGauge;
RWAGauge public rwaGauge;
// Test addresses
address public admin = address(this);
address public alice = address(0x1);
address public bob = address(0x2);
address public carol = address(0x3);
// Constants
uint256 public constant INITIAL_SUPPLY = 1_000_000e18;
uint256 public constant LOCK_AMOUNT = 100_000e18;
uint256 public constant YEAR = 365 days;
function setUp() public {
// Deploy mock tokens
raacToken = new ERC20Mock("RAAC Token", "RAAC");
stakingToken = new ERC20Mock("Staking Token", "STK");
rewardToken = new ERC20Mock("Reward Token", "RWD");
// Deploy veToken
veToken = new veRAACToken(address(raacToken));
// Deploy controller
controller = new GaugeController(address(veToken));
// Deploy gauges
raacGauge = new RAACGauge(address(rewardToken), address(stakingToken), address(controller));
rwaGauge = new RWAGauge(address(rewardToken), address(stakingToken), address(controller));
// Setup initial token balances
raacToken.mint(alice, INITIAL_SUPPLY);
raacToken.mint(bob, INITIAL_SUPPLY);
stakingToken.mint(alice, INITIAL_SUPPLY);
stakingToken.mint(bob, INITIAL_SUPPLY);
rewardToken.mint(address(controller), INITIAL_SUPPLY * 10);
// Add gauges to controller
vm.startPrank(admin);
controller.addGauge(address(raacGauge), IGaugeController.GaugeType.RAAC, 5000);
controller.addGauge(address(rwaGauge), IGaugeController.GaugeType.RWA, 5000);
vm.stopPrank();
// Setup approvals
vm.startPrank(alice);
raacToken.approve(address(veToken), type(uint256).max);
stakingToken.approve(address(raacGauge), type(uint256).max);
stakingToken.approve(address(rwaGauge), type(uint256).max);
vm.stopPrank();
vm.startPrank(bob);
raacToken.approve(address(veToken), type(uint256).max);
stakingToken.approve(address(raacGauge), type(uint256).max);
stakingToken.approve(address(rwaGauge), type(uint256).max);
vm.stopPrank();
}
function test_IncorrectMinBoostSettingInConstructor() public {
vm.startPrank(alice);
veToken.lock(LOCK_AMOUNT, YEAR);
uint256 stakeAmount = 10_000e18;
vm.expectRevert();
raacGauge.stake(stakeAmount);
vm.expectRevert();
rwaGauge.stake(stakeAmount);
vm.stopPrank();
}
}

Impact

  • Complete failure of the staking mechanism

  • Denial of service for the entire gauge system

  • Inability to stake tokens or earn rewards

  • Requirement to redeploy the gauge contracts

The Likelihood is High => it will always happen

The Impact is Low/Med => no direct loss of funds but it breaks the whole gauge reward system

Tools Used

  • Foundry

  • Manual Review

Recommendations

  • Add constants for clarity and update constructor:

uint256 public constant BASIS_POINTS = 10000; // 1x
uint256 public constant MIN_BOOST_VALUE = BASIS_POINTS; // 1x
uint256 public constant MAX_BOOST_VALUE = 25000; // 2.5x
constructor() {
// ... existing initialization ...
// Initialize boost parameters
boostState.maxBoost = MAX_BOOST_VALUE;
boostState.minBoost = MIN_BOOST_VALUE;
// ... rest of initialization ...
}
Updates

Lead Judging Commences

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