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 4 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 4 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.