Core Contracts

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

Period calculation in `BaseGauge` contract adds extra cycles leading to permanent loss of user rewards

Summary

In BaseGauge::updatePeriod and BaseGauge::setInitialWeight, the period advancement logic skips two entire distribution cycles by adding 2 periods. This creates permanent gaps in reward distribution where users irreversibly lose their entitled rewards for the skipped periods.

Vulnerability Details

The issue occurs in the period advancement logic:

function updatePeriod() external override onlyController {
// ...
uint256 nextPeriodStart = ((currentTime / periodDuration) + 2) * periodDuration;
// ...
}
function setInitialWeight(uint256 weight) external onlyController {
// ...
uint256 nextPeriodStart = ((currentTime / periodDuration) + 2) * periodDuration;
//...
}

The + 2 forces a skip of two entire reward periods.

POC

To use foundry in the codebase, follow the hardhat guide here: Foundry-Hardhat hybrid integration by Nomic foundation

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import {ERC20Mock} from "../../../../contracts/mocks/core/tokens/ERC20Mock.sol";
import {FeeCollector} from "../../../../contracts/core/collectors/FeeCollector.sol";
import {Treasury} from "../../../../contracts/core/collectors/Treasury.sol";
import {RAACToken} from "../../../../contracts/core/tokens/RAACToken.sol";
import {veRAACToken} from "../../../../contracts/core/tokens/veRAACToken.sol";
import {GaugeController, IGaugeController} from "../../../../contracts/core/governance/gauges/GaugeController.sol";
import {RAACGauge} from "../../../../contracts/core/governance/gauges/RAACGauge.sol";
import {RWAGauge} from "../../../../contracts/core/governance/gauges/RWAGauge.sol";
import {Test, console} from "forge-std/Test.sol";
contract TestSuite is Test {
FeeCollector feeCollector;
Treasury treasury;
RAACToken raacToken;
veRAACToken veRAACTok;
GaugeController gaugeController;
RAACGauge raacGauge;
RAACGauge raacGauge2;
RWAGauge rwaGauge;
ERC20Mock rewardToken;
address repairFund;
address admin;
uint256 initialSwapTaxRate = 100; //1%
uint256 initialBurnTaxRate = 50; //0.5%
uint256 initialWeight = 5000;
function setUp() public {
repairFund = makeAddr("repairFund");
admin = makeAddr("admin");
rewardToken = new ERC20Mock("Reward Token", "RWT");
treasury = new Treasury(admin);
raacToken = new RAACToken(admin, initialSwapTaxRate, initialBurnTaxRate);
veRAACTok = new veRAACToken(address(raacToken));
feeCollector = new FeeCollector(address(raacToken), address(veRAACTok), address(treasury), repairFund, admin);
vm.startPrank(admin);
raacToken.setFeeCollector(address(feeCollector));
raacToken.setMinter(admin);
gaugeController = new GaugeController(address(veRAACTok));
raacGauge = new RAACGauge(address(rewardToken), address(raacToken), address(gaugeController));
rwaGauge = new RWAGauge(address(rewardToken), address(raacToken), address(gaugeController));
gaugeController.addGauge(address(raacGauge), IGaugeController.GaugeType.RAAC, initialWeight);
gaugeController.addGauge(address(rwaGauge), IGaugeController.GaugeType.RWA, initialWeight);
vm.stopPrank();
}
function testPeriodIsSkipped() public {
uint256 maxEmission = raacGauge.MAX_WEEKLY_EMISSION();
uint256 initialStartTime = 7 days;
uint256 duration = 7 days;
// Emission rewards are distributed to gauge
vm.startPrank(address(gaugeController));
// first emission
rewardToken.mint(address(raacGauge), maxEmission);
raacGauge.notifyRewardAmount(maxEmission);
// period ends and updatePeriod is called
vm.warp(initialStartTime + duration);
raacGauge.updatePeriod();
// second emission
raacGauge.notifyRewardAmount(maxEmission);
// the second period should end after duration passes but updatePeriod is called and always reverts
vm.warp(block.timestamp + duration);
vm.expectRevert();
raacGauge.updatePeriod();
vm.stopPrank();
(,, uint256 distributed, uint256 periodStartTime) = raacGauge.periodState();
console.log("Distributed amount: ", distributed);
console.log("Period start time: ", periodStartTime); //period start time is 2 periods ahead
// notifyRewardAmount reverts with RewardCapExceeded and users miss out on the skipped period rewards
vm.prank(address(gaugeController));
vm.expectRevert();
raacGauge.notifyRewardAmount(maxEmission);
}
}

Impact

Users permanently lose two full periods of rewards in every period transition and lost rewards cannot be recovered as they are tied to specific time periods

Tools Used

Manual review, foundry test suite

Recommendations

  1. Advance to immediate next period:

- uint256 nextPeriodStart = ((currentTime / periodDuration) + 2) * periodDuration;
+ uint256 nextPeriodStart = (currentTime / periodDuration) * periodDuration;
Updates

Lead Judging Commences

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

BaseGauge::updatePeriod uses ((currentTime / periodDuration) + 2) calculation causing entire reward periods to be skipped, resulting in permanent loss of user rewards

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

BaseGauge::updatePeriod uses ((currentTime / periodDuration) + 2) calculation causing entire reward periods to be skipped, resulting in permanent loss of user rewards

Support

FAQs

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

Give us feedback!