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: }