Core Contracts

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

Missing `BaseGauge::distributionCap` validation leads to over-emission of rewards

Summary

The BaseGauge contract includes a distributionCap state variable that is intended to limit the total rewards distributed across all periods. However, while this cap can be set by the FEE_ADMIN role through BaseGauge::setDistributionCap, the contract never actually validates distributions against this cap. This allows the distribution of more rewards then the max allowed.

Vulnerability Details

The BaseGauge::notifyReward function, which handles reward validation, checks against period-specific caps but fails to verify that the total distribution stays under the global distributionCap.

function notifyReward(
PeriodState storage state,
uint256 amount,
uint256 maxEmission,
uint256 periodDuration
) internal view returns (uint256) {
if (amount > maxEmission) revert RewardCapExceeded();
if (amount + state.distributed > state.emission) {
revert RewardCapExceeded();
}
@> //@audit: missing check
uint256 rewardRate = amount / periodDuration;
if (rewardRate == 0) revert ZeroRewardRate();
return rewardRate;
}

Impact

The lack of distributionCap validation means the intended global limit on reward distribution is not enforced. This leads to over-emission of rewards beyond the intended maximum undermines the economic model and token distribution plan of the protocol. The FEE_ADMIN role's ability to set a distribution cap becomes ineffective.

Add Foundry to the project following this procedure

Create a file named RaacGauge.t.sol and copy/paste this:

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;
import {Test, console2} from "forge-std/Test.sol";
import {RAACGauge} from "../contracts/core/governance/gauges/RAACGauge.sol";
import {GaugeController} from "../contracts/core/governance/gauges/GaugeController.sol";
import {IGaugeController} from "../contracts/interfaces/core/governance/gauges/IGaugeController.sol";
import {IGauge} from "../contracts/interfaces/core/governance/gauges/IGauge.sol";
import {MockToken} from "../contracts/mocks/core/tokens/MockToken.sol";
contract RaacGaugeTest is Test {
MockToken public rewardToken;
MockToken public veRAACToken;
GaugeController public gaugeController;
RAACGauge public raacGauge;
bytes32 public constant CONTROLLER_ROLE = keccak256("CONTROLLER_ROLE");
bytes32 public constant EMERGENCY_ADMIN = keccak256("EMERGENCY_ADMIN");
bytes32 public constant FEE_ADMIN = keccak256("FEE_ADMIN");
address owner = makeAddr("owner");
address alice = makeAddr("alice");
address bob = makeAddr("bob");
uint256 public WEEK = 7 * 24 * 3600;
uint256 public WEIGHT_PRECISION = 10000;
function setUp() public {
rewardToken = new MockToken("Reward Token", "RWD", 18);
veRAACToken = new MockToken("veRAAC Token", "veRAAC", 18);
// Setup initial state
veRAACToken.mint(alice, 200 ether);
veRAACToken.mint(bob, 1000 ether);
rewardToken.mint(alice, 1000 ether);
rewardToken.mint(bob, 1000 ether);
gaugeController = new GaugeController(address(veRAACToken));
vm.warp(block.timestamp + 3 weeks);
raacGauge = new RAACGauge(address(rewardToken), address(veRAACToken), address(gaugeController));
// Setup roles
raacGauge.grantRole(raacGauge.CONTROLLER_ROLE(), owner);
vm.startPrank(alice);
rewardToken.approve(address(raacGauge), type(uint256).max);
veRAACToken.approve(address(raacGauge), type(uint256).max);
vm.stopPrank();
vm.startPrank(bob);
rewardToken.approve(address(raacGauge), type(uint256).max);
veRAACToken.approve(address(raacGauge), type(uint256).max);
vm.stopPrank();
// Add gauge to controller
gaugeController.grantRole(gaugeController.GAUGE_ADMIN(), owner);
gaugeController.addGauge(address(raacGauge), IGaugeController.GaugeType.RAAC, WEIGHT_PRECISION);
raacGauge.grantRole(raacGauge.FEE_ADMIN(), owner);
// Move time forward
vm.warp(block.timestamp + 1 weeks);
// Set initial gauge weight through voting
vm.prank(alice);
gaugeController.vote(address(raacGauge), WEIGHT_PRECISION);
// Set emission rate
vm.prank(owner);
raacGauge.setWeeklyEmission(10000 ether);
// Transfer reward tokens to gauge for distribution
rewardToken.mint(address(raacGauge), 100000 ether);
vm.prank(owner);
raacGauge.setBoostParameters(25000, 10000, WEEK);
// Set initial weight after time alignment
vm.prank(address(gaugeController));
raacGauge.setInitialWeight(5000); // 50% weight
vm.warp(block.timestamp + 1);
console2.log("\nContracts:");
console2.log("rewardToken: ", address(rewardToken));
console2.log("veRAACToken: ", address(veRAACToken));
console2.log("raacGauge: ", address(raacGauge));
console2.log("");
}
function test_canEarnMoreRewardsThenDistributionCap() public {
// Alice stakes: 100 tokens
vm.prank(alice);
raacGauge.stake(100 ether);
uint256 aliceBalanceAfter = veRAACToken.balanceOf(alice);
assertEq(aliceBalanceAfter, 100 ether);
vm.prank(address(gaugeController));
raacGauge.notifyRewardAmount(1000 ether);
vm.prank(alice);
raacGauge.voteEmissionDirection(5000);
vm.prank(owner);
raacGauge.setDistributionCap(1000);
vm.warp(block.timestamp + 1 weeks / 2);
uint256 earnedRewards = raacGauge.earned(alice);
assertLt(raacGauge.distributionCap(), earnedRewards);
console2.log("distributionCap: ", raacGauge.distributionCap());
console2.log("earnedRewards: ", earnedRewards);
console2.log("----------------");
}
}

Run forge test --match-test test_canEarnMoreRewardsThenDistributionCap -vv

Logs:
Contracts:
rewardToken: 0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f
veRAACToken: 0x2e234DAe75C793f67A35089C9d99245E1C58470b
raacGauge: 0x5991A2dF15A8F6A256D3Ec51E99254Cd3fb576A9
distributionCap: 1000
earnedRewards: 11248999
----------------
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 11.18ms (1.35ms CPU time)

The test shows that can be distributed to Alice more rewards than the max setted up.

Tools Used

Manual review

Recommendations

Track the distributed rewards across the periods and add a validation for distributionCap in the BaseGauge::notifyReward function.

Updates

Lead Judging Commences

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

BaseGauge lacks enforcement of both distributionCap and MAX_REWARD_RATE limits

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

BaseGauge lacks enforcement of both distributionCap and MAX_REWARD_RATE limits

Support

FAQs

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

Give us feedback!