Core Contracts

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

Incorrect Scaling in BaseGauge::_applyBoost() Leads to Wrong Reward Distribution

Summary

In the BaseGauge contract, the _applyBoost function uses an incorrect scaling factor when applying the boost multiplier to the base weight. The function divides by 1e18 instead of the basis points precision (10_000), resulting in severely reduced rewards and potential complete nullification for smaller amounts due to precision loss.

Vulnerability Details

The _applyBoost function in BaseGauge.sol calculates a user's boosted weight by multiplying their base weight with a boost multiplier. However, the function uses incorrect scaling:

function _applyBoost(address account, uint256 baseWeight) internal view virtual returns (uint256) {
// ... boost calculation ...
uint256 boost = BoostCalculator.calculateBoost(veBalance, totalVeSupply, params);
return (baseWeight * boost) / 1e18; // incorrect scaling => should be 10_000 (basis points)
}

The boost value is meant to be in basis points (where 10_000 = 1x, 25_000 = 2.5x), but the function divides by 1e18. This mismatch in scaling factors causes the final weight to be reduced by a factor of 1e14 (1e18/10_000).

The calculateBoost() function returns a value in basis points and thus the value should be divided by 10_000. [https://github.com/Cyfrin/2025-02-raac/blob/89ccb062e2b175374d40d824263a4c0b601bcb7f/contracts/libraries/governance/BoostCalculator.sol#L74]

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

⚠️ Fix the BaseGauge constructor first, otherwise you'll receive an arithmetic underflow revert because it sets 1e18 to the minBoost value which should be in basis points as you can easily verify by checking the BoostCalculator library

constructor(...) {
- boostState.minBoost = 1e18;
+ boostState.minBoost = 10_000;
}

// 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";
import {IGauge} from "../../contracts/interfaces/core/governance/gauges/IGauge.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");
rewardToken = new ERC20Mock("Reward Token", "RWD");
stakingToken = new ERC20Mock("Staking Token", "STK");
// 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);
stakingToken.mint(alice, INITIAL_SUPPLY);
raacToken.mint(bob, INITIAL_SUPPLY);
stakingToken.mint(bob, INITIAL_SUPPLY);
rewardToken.mint(address(raacGauge), INITIAL_SUPPLY * 10);
rewardToken.mint(address(rwaGauge), INITIAL_SUPPLY * 10);
// Add gauges to controller
vm.startPrank(admin);
controller.addGauge(address(raacGauge), IGaugeController.GaugeType.RAAC, 0);
controller.addGauge(address(rwaGauge), IGaugeController.GaugeType.RWA, 0);
vm.stopPrank();
// Setup approvals
vm.startPrank(alice);
raacToken.approve(address(veToken), type(uint256).max);
veToken.approve(address(raacGauge), type(uint256).max);
stakingToken.approve(address(raacGauge), type(uint256).max);
vm.stopPrank();
vm.startPrank(bob);
raacToken.approve(address(veToken), type(uint256).max);
veToken.approve(address(raacGauge), type(uint256).max);
stakingToken.approve(address(raacGauge), type(uint256).max);
vm.stopPrank();
}
function test_WrongBoostCalculation() public {
uint256 weeklyEmission = 250_000e18;
uint256 stakeAmount = 10_000e18;
vm.prank(address(controller));
raacGauge.setWeeklyEmission(weeklyEmission);
// Lock RAAC tokens to get veTokens
vm.startPrank(alice);
veToken.lock(LOCK_AMOUNT, YEAR * 4);
// Vote for gauge weights
controller.vote(address(raacGauge), 10000); // 100% to RAAC gauge
// Stake in gauges
raacGauge.stake(stakeAmount);
vm.stopPrank();
controller.distributeRewards(address(raacGauge));
// Advance time after distribute rewards
vm.warp(block.timestamp + 1 days);
uint256 aliceBalanceBefore = rewardToken.balanceOf(alice);
// claim rewards
vm.prank(alice);
raacGauge.getReward();
uint256 aliceBalanceAfter = rewardToken.balanceOf(alice);
console2.log("\nRAAC Gauge Rewards After Claim:");
console2.log("Alice Rewards:", aliceBalanceAfter - aliceBalanceBefore);
}
}
- Alice Rewards (Wrong Division): 4464285714 / 1e18 = 0.000000004464285714
+ Alice Rewards: 446428571428571426500000 / 1e18 = ~446428.57

Impact

  • Users receive drastically reduced rewards (by a factor of ~1e14)

  • For smaller stake amounts, rewards might be completely nullified due to precision loss

  • The boost mechanism's effectiveness is significantly diminished

  • All stakers are affected, with smaller stakers potentially receiving zero rewards

Tools Used

  • Manual code review

  • Foundry

Recommendations

The _applyBoost function should be modified to use the correct scaling factor:

return (baseWeight * boost) / 10_000;
Updates

Lead Judging Commences

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

BaseGauge reward calculations divide by 1e18 despite using 1e4 precision weights, causing all user weights to round down to zero and preventing reward distribution

Support

FAQs

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