Core Contracts

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

`BaseGauge::rewardPerTokenStored` never decreases despite less available rewards, leading to inflated user rewards and reverts when claiming

Summary

The BaseGauge contract's `rewardPerTokenStored` only increases and never decreases, even when there are less rewards available to distribute. This leads to inflated reward calculations and failed claim attempts.

Vulnerability Details

The issue exists in the getRewardPerToken() function where rewardPerTokenStored is only ever increased:

function getRewardPerToken() public view returns (uint256) {
if (totalSupply() == 0) {
return rewardPerTokenStored;
}
@> return rewardPerTokenStored + ((lastTimeRewardApplicable() - lastUpdateTime) * rewardRate * 1e18 / totalSupply());
}

The function:

1. Always adds to the previous `rewardPerTokenStored` value

2. Never considers actual available rewards

3. Doesn't decrease when reward rate or available rewards decrease

POC

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;
RAACGauge raacGauge2;
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));
raacGauge2 = 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(raacGauge2), IGaugeController.GaugeType.RAAC, initialWeight);
gaugeController.addGauge(address(rwaGauge), IGaugeController.GaugeType.RWA, initialWeight);
vm.stopPrank();
}
function testRewardPerTokenStoredNeverDecreasesEvenWhenTheresLessRewardsAvailableToDistribute() public {
address Alice = makeAddr("Alice");
uint256 stakeAmount = 100e18;
uint256 largeRewardAmount = 1000e18;
uint256 smallRewardAmount = 10e18;
// Setup
vm.startPrank(admin);
raacToken.mint(Alice, stakeAmount);
rewardToken.mint(address(raacGauge), largeRewardAmount + smallRewardAmount);
vm.stopPrank();
// Alice stakes tokens
vm.startPrank(Alice);
raacToken.approve(address(raacGauge), stakeAmount);
raacGauge.stake(stakeAmount);
vm.stopPrank();
// First distribution with large rewards
vm.startPrank(address(gaugeController));
raacGauge.notifyRewardAmount(largeRewardAmount);
vm.stopPrank();
// Record initial state
uint256 initialRewardPerToken = raacGauge.rewardPerTokenStored();
uint256 initialRewardRate = raacGauge.rewardRate();
// Let time pass to accumulate rewards
vm.warp(block.timestamp + 1 days);
// Second distribution with much smaller rewards
vm.startPrank(address(gaugeController));
raacGauge.notifyRewardAmount(smallRewardAmount);
vm.stopPrank();
// Record state after smaller distribution
uint256 newRewardPerToken = raacGauge.rewardPerTokenStored();
uint256 newRewardRate = raacGauge.rewardRate();
console.log("Initial reward per token:", initialRewardPerToken);
console.log("New reward per token:", newRewardPerToken);
console.log("Initial reward rate:", initialRewardRate);
console.log("New reward rate:", newRewardRate);
// Despite much lower rewards, rewardPerTokenStored keeps increasing
assertGt(newRewardPerToken, initialRewardPerToken);
// Let more time pass
vm.warp(block.timestamp + 1 days);
// Try to claim rewards
vm.startPrank(Alice);
uint256 earnedRewards = raacGauge.earned(Alice); // a precision bug here causes this test to fail.
console.log("Earned rewards shown:", earnedRewards);
console.log("Actual reward token balance:", rewardToken.balanceOf(address(raacGauge)));
// Claim should fail due to insufficient balance
vm.expectRevert();
raacGauge.getReward();
vm.stopPrank();
}
}

Impact

The bug causes:

1. Users to see inflated reward amounts they can't actually claim

2. Failed transactions when trying to claim rewards due to insufficient balance

3. DOS as users waste gas on failed claim attempts

4. Loss of trust in the reward system

Tools Used

Manual review, foundry test suite

Recommendations

Track actual available rewards and use it to cap `rewardPerTokenStored` or add a mechanism to decrease `rewardPerTokenStored` when rewards decrease

Updates

Lead Judging Commences

inallhonesty Lead Judge 4 months ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement
Assigned finding tags:

BaseGauge::rewardPerTokenStored never decreases despite lower available rewards, causing inflated reward calculations and failed claim attempts due to insufficient bala

rewardPerTokenStored monotonically increasing is correct behavior, not a bug

inallhonesty Lead Judge 4 months ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement
Assigned finding tags:

BaseGauge::rewardPerTokenStored never decreases despite lower available rewards, causing inflated reward calculations and failed claim attempts due to insufficient bala

rewardPerTokenStored monotonically increasing is correct behavior, not a bug

Support

FAQs

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