Core Contracts

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

Invariant distributionCap of BaseGauge can be breached due to a lack of check when notifying rewards

Summary

When calling BaseGauge::notifyRewardAmount, the contract does not check that the total amount to be distributed does not exceed the distributionCap limit.

Vulnerability Details

The [distributionCap limit](https://github.com/Cyfrin/2025-02-raac/blob/89ccb062e2b175374d40d824263a4c0b601bcb7f/contracts/core/governance/gauges/BaseGauge.sol#L73) is a protocol invariant that limits the amount that can be distributed in a period:

/// @notice Cap on reward distribution amount
uint256 public distributionCap;

The problem is that the BaseGauge contract checks against the emission cap instead of distribution cap when a new reward notification is sent at https://github.com/Cyfrin/2025-02-raac/blob/89ccb062e2b175374d40d824263a4c0b601bcb7f/contracts/core/governance/gauges/BaseGauge.sol#L384:

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();
}
uint256 rewardRate = amount / periodDuration;
if (rewardRate == 0) revert ZeroRewardRate();
return rewardRate;
}

The net effect is that more rewards can be distributed in a period than determined by the project admins, breaking a protocol invariant.

There is a duplicate of the same check at

if (amount > maxEmission) revert RewardCapExceeded();

and

if (amount > periodState.emission) revert RewardCapExceeded();

For a POC, add this test to test/unit/core/governance/gauges/BaseGauge.test.js::Ln134:

describe("Distribution Cap", () => {
beforeEach(async () => {
const newCap = ethers.parseEther("1000");
await baseGauge.setDistributionCap(newCap);
});
it("should breach distribution caps", async () => {
expect(await baseGauge.distributionCap()).to.equal(newCap);
// Setup rewards
await rewardToken.mint(await baseGauge.getAddress(), ethers.parseEther("10000"));
// Set emission cap before notifying rewards
await baseGauge.setEmission(ethers.parseEther("10000"));
// Set initial weights and vote to enable rewards
await gaugeController.connect(user1).vote(await baseGauge.getAddress(), 5000);
await baseGauge.notifyRewardAmount(ethers.parseEther("1500"));
});
});

Impact

The protocol invariant that limits the maximum amount to be distributed in a period is broken.

Tools Used

Manual review

Recommendations

Replace https://github.com/Cyfrin/2025-02-raac/blob/89ccb062e2b175374d40d824263a4c0b601bcb7f/contracts/core/governance/gauges/BaseGauge.sol#L384-L386:

if (amount + state.distributed > distributionCap) {
revert DistributionCapExceeded();
}
Updates

Lead Judging Commences

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

BaseGauge lacks enforcement of both distributionCap and MAX_REWARD_RATE limits

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