Core Contracts

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

Inflated reward distribution due to misalignment of boosted balances

Summary

The getRewardPerToken function does not account for total boosted balances, leading to an inflated reward per token. This causes early claimers to receive more rewards than they should, at the expense of later claimers.

Vulnerability Details

The BaseGauge::getRewardPerToken calculates the current reward per token as follows:

function getRewardPerToken() public view returns (uint256) {
if (totalSupply() == 0) {
return rewardPerTokenStored;
}
return rewardPerTokenStored + (
(lastTimeRewardApplicable() - lastUpdateTime) * rewardRate * 1e18 / totalSupply()
);
}

The rewardRate (per second) is multiplied by deltaTime, scaled by 1e18 and divided by `totalSupply()`, which represents the total staked tokens.

The earned function then calculates a user’s accumulated rewards. For simplicity let's consider userStates[account].rewardPerTokenPaid == 0 and userStates[account].rewards == 0.

The earned function is simplified to userWeight * rewardPerToken / 1e18

function earned(address account) public view returns (uint256) {
return (getUserWeight(account) *
(getRewardPerToken() - userStates[account].rewardPerTokenPaid) / 1e18
) + userStates[account].rewards;
}

In getUserWeight a multiplier factor is applied to user's weight such that boostedWeight >= weight,

Thus, the effective formula for calculating rewards becomes: boostedReward * rewardPerToken / 1e18.

Note: current implementation may be confusing because the boost is incorrectly applied to user's weight. Instead it should have been applied to user's staked amount in gauge to encourage liquidity provision. This issue is reported in a separate submission and mentioned here to add some clarity.

The problem lies in the calculation of rewardPerToken. When computing rewardPerToken, the contract uses the flat, non-boosted totalSupply. However, when computing a user's rewards, the boosted weight is applied. This discrepancy results in an overestimation of rewardPerToken, causing early claimers to receive disproportionately high rewards while depleting the available rewards for later claimers.

For example, assume:

  • gauge has 1000 reward tokens to be distributed

  • user A stakes 100 tokens with a 1.5x boost

  • user B stakes 100 tokens with no boost
    Total staked (totalSupply) = 200 (but total boosted weight = 250).

    The rewardPerToken is calculated as 1000 / 200 = 5. (for the entire period).

  • Afte the period ended, user B claims rewards:

    userB rewards = 100 * 5 = 500, claiming half of the rewards, when, in fact it should have claimed only

    100 * (1000 / 250) = 400.

Impact

The system does not fairly distribute rewards based on actual liquidity provision.

Users who claim earlier receive an inflated portion of the rewards pool at the expense of late claimers.

Tools Used

Recommendations

To fix this issue, getRewardPerToken should consider the total boosted weight instead of the flat totalSupply. This ensures that reward distribution aligns correctly with users' weighted contributions.

Updates

Lead Judging Commences

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

BaseGauge::earned calculates rewards using getUserWeight instead of staked balances, potentially allowing users to claim rewards by gaining weight without proper reward checkpoint updates

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

BaseGauge::earned calculates rewards using getUserWeight instead of staked balances, potentially allowing users to claim rewards by gaining weight without proper reward checkpoint updates

Support

FAQs

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

Give us feedback!