Core Contracts

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

periodState.distributed overestimates amount and may exhaust limit sooner than intended

Description

notifyRewardAmount() increments periodState.distributed by amount:

File: contracts/core/governance/gauges/BaseGauge.sol
349: /**
350: * @notice Notifies contract of reward amount
351: * @dev Updates reward rate based on new amount
352: * @param amount Amount of rewards to distribute
353: */
354: function notifyRewardAmount(uint256 amount) external override onlyController updateReward(address(0)) {
355: if (amount > periodState.emission) revert RewardCapExceeded();
356:
357: rewardRate = notifyReward(periodState, amount, periodState.emission, getPeriodDuration());
358:@---> periodState.distributed += amount;
359:
360: uint256 balance = rewardToken.balanceOf(address(this));
361: if (rewardRate * getPeriodDuration() > balance) {
362: revert InsufficientRewardBalance();
363: }
364:
365: lastUpdateTime = block.timestamp;
366: emit RewardNotified(amount);
367: }

However the actual distribution figure is rewardRate * getPeriodDuration() which is calculated inside notifyReward() and can be a few wei less due to truncation via division:

File: contracts/core/governance/gauges/BaseGauge.sol
377: function notifyReward(
378: PeriodState storage state,
379: uint256 amount,
380: uint256 maxEmission,
381: uint256 periodDuration
382: ) internal view returns (uint256) {
383: if (amount > maxEmission) revert RewardCapExceeded();
384: if (amount + state.distributed > state.emission) {
385: revert RewardCapExceeded();
386: }
387:
388:@---> uint256 rewardRate = amount / periodDuration;
389: if (rewardRate == 0) revert ZeroRewardRate();
390:
391: return rewardRate;
392: }

Impact

periodState.distributed (named state.distributed above) exhausts the limit checked on L384 sooner than it should and rewards are capped prematurely.

Mitigation

File: contracts/core/governance/gauges/BaseGauge.sol
349: /**
350: * @notice Notifies contract of reward amount
351: * @dev Updates reward rate based on new amount
352: * @param amount Amount of rewards to distribute
353: */
354: function notifyRewardAmount(uint256 amount) external override onlyController updateReward(address(0)) {
355: if (amount > periodState.emission) revert RewardCapExceeded();
356:
357: rewardRate = notifyReward(periodState, amount, periodState.emission, getPeriodDuration());
- 358: periodState.distributed += amount;
+ 358: periodState.distributed += rewardRate * getPeriodDuration();
359:
360: uint256 balance = rewardToken.balanceOf(address(this));
361: if (rewardRate * getPeriodDuration() > balance) {
362: revert InsufficientRewardBalance();
363: }
364:
365: lastUpdateTime = block.timestamp;
- 366: emit RewardNotified(amount);
+ 366: emit RewardNotified(rewardRate * getPeriodDuration());
367: }
Updates

Lead Judging Commences

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

BaseGauge::notifyRewardAmount increments periodState.distributed with full amount regardless of actual distribution timing, causing reward accounting discrepancies

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

BaseGauge::notifyRewardAmount increments periodState.distributed with full amount regardless of actual distribution timing, causing reward accounting discrepancies

Support

FAQs

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

Give us feedback!