Core Contracts

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

Incorrect gauge period's end time may cause the rewards to be distributed over a longer time period.

Summary

The gauge's rewards are distributed per 'period' (7 or 30 days depending on gauge type).
Because BaseGauge::periodFinish doesn't return the correct period's end time, rewards from the current period are accrued indefinitely.

Vulnerability Details

lastUpdateTime is the last time when rewardPerTokenStored, the rewards per token accumulator was updated. Every time an user or controller interacts with the gauge it's updated via updateReward modifier.

modifier updateReward(address account) {
_updateReward(account);
_;
}
function _updateReward(address account) internal {
rewardPerTokenStored = getRewardPerToken();
@> lastUpdateTime = lastTimeRewardApplicable();
if (account != address(0)) {
UserState storage state = userStates[account];
state.rewards = earned(account);
state.rewardPerTokenPaid = rewardPerTokenStored;
state.lastUpdateTime = block.timestamp;
emit RewardUpdated(account, state.rewards);
}
}

The periodFinish() uses same lastUpdateTime to compute the end time of current period. As long as any user/controller interacted with the gauge in the last period time (in the last 7 or 30 days depending on gauge type), the periodFinish will be in the future.
periodFinish is used in lastTimeRewardApplicable -> getRewardPerToken to calculate the current reward per token.

function periodFinish() public view returns (uint256) {
@> return lastUpdateTime + getPeriodDuration();//@audit-issue: endPeriod will be in the future if users interacted with the gauge in last 'periodDuration'
}
function lastTimeRewardApplicable() public view returns (uint256) {
@> return block.timestamp < periodFinish() ? block.timestamp : periodFinish(); // @audit will return block.timestamp, prolonging the `period` indefinitely
}
function getRewardPerToken() public view returns (uint256) {
if (totalSupply() == 0) {
return rewardPerTokenStored;
}
return rewardPerTokenStored + (
@> (lastTimeRewardApplicable() - lastUpdateTime) * rewardRate * 1e18 / totalSupply()
);
}
function _updateReward(address account) internal {
@> rewardPerTokenStored = getRewardPerToken();// save accumulator value
lastUpdateTime = lastTimeRewardApplicable();
...
}

The latest applicable reward time is prolonged indefinitely. When admin doesn't starts a new period and adds new rewards, the last rewards are spread over a longer time interval than the desired period.
Last users to claim rewards will be unable to do so due to insufficient reward balance.

Impact

The latest applicable reward time may be prolonged indefinitely. Rewards are not distributed as intended.

Tools Used

Recommendations

When a new period is started the periodState.periodStartTime is set. Use this variable to get the period end time.

function periodFinish() public view returns (uint256) {
- return lastUpdateTime + getPeriodDuration();
+ return periodState.periodStartTime+ getPeriodDuration();
}
Updates

Lead Judging Commences

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

BaseGauge period end time miscalculation creates circular dependency between periodFinish() and lastUpdateTime, preventing periods from naturally ending and disrupting reward distribution

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

BaseGauge period end time miscalculation creates circular dependency between periodFinish() and lastUpdateTime, preventing periods from naturally ending and disrupting reward distribution

Support

FAQs

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

Give us feedback!