Core Contracts

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

Incorrect reward distribution in `BaseGauge` due to `_updateReward`

Summary

BaseGauge's reward distribution ignores user participation and amounts, instead distributing rewards based only on time elapsed since last checkpoint.

Vulnerability Details

The reward calculation uses global time tracking and gauge weight instead of user participation.

Initial state with example values:

rewardRate = 100e18 reward tokens/sec
totalSupply = 1000e18 (1000 staking tokens)
gauge weight = 100e18 (can be seen that it is with 18 decimals from GaugeController calculations)
assuming the getUserWeight function is implemented correctly and a user has no boost
getUserWeight should return the gauge weight every time (for simplicity)
24 hours in seconds = 86400
25 hours in seconds = 90000
26 hours in seconds = 93600
50 hours in seconds = 180000
51 hours in seconds = 183600

User A first interaction with any function that has the updateReward modifier (24h):

rewardPerTokenStored = 0 + ((86400 - 0) * 100e18 * 1e18 / 1000e18) = 8640e18
earned = (100e18 * (8640e18 - 0) / 1e18) + 0 = 864000e18

User A calls getReward (which has the updateReward modifier) after 1h (25h):

rewardPerTokenStored = 8640e18 + ((90000 - 86400) * 100e18 * 1e18 / 1000e18) = 9000e18
earned = (100e18 * (9000e18 - 8640e18) / 1e18) + 864000e18 = 900000e18
900000e18 reward tokens can be withdrawn
userA.rewards = 0

User B calls getReward (which has the updateReward modifier) as their first interaction 1h later (26h):

rewardPerTokenStored = 9000e18 + ((93600 - 90000) * 100e18 * 1e18 / 1000e18) = 9360e18
earned = (100e18 * (9360e18 - 0) / 1e18) + 0 = 936000e18
936000e18 reward tokens can be withdrawn
userB.rewards = 0

User B calls getReward after a day (50h):

rewardPerTokenStored = 9360e18 + ((180000 - 93600) * 100e18 * 1e18 / 1000e18) = 18000e18
earned = (100e18 * (18000e18 - 9360e18) / 1e18) + 0 = 864000e18
864000e18 reward tokens can be withdrawn
userB.rewards = 0

User A calls getReward after 1h (51h):

rewardPerTokenStored = 18000e18 + ((183600 - 180000) * 100e18 * 1e18 / 1000e18) = 18360e18
earned = (100e18 * (18360e18 - 9000e18) / 1e18) + 0 = 936000e18
936000e18 reward tokens can be withdrawn
userA.rewards = 0

Issues:

  • rewardPerTokenStored is the same for everyone

  • First-time users getting all historical rewards

  • Subsequent claims get all rewards since last checkpoint regardless of participation

  • No tracking of actual participation periods

  • Staking/withdrawing tokens has no effect

Impact

Critical: Rewards are not distributed based on participation, leading to unfair distribution where only first users can claim rewards.

Recommendations

The reward distribution mechanism should be redesigned to reward users based on participation.

Updates

Lead Judging Commences

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

BaseGauge._getBaseWeight ignores account parameter and returns gauge's total weight, allowing users to claim rewards from gauges they never voted for or staked in

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

BaseGauge._getBaseWeight ignores account parameter and returns gauge's total weight, allowing users to claim rewards from gauges they never voted for or staked in

Support

FAQs

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