Core Contracts

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

Staked Token Balances Are Ignored in Reward Weight Calculations

BaseGauge.sol

https://github.com/Cyfrin/2025-02-raac/blob/89ccb062e2b175374d40d824263a4c0b601bcb7f/contracts/core/governance/gauges/BaseGauge.sol#L594

https://github.com/Cyfrin/2025-02-raac/blob/89ccb062e2b175374d40d824263a4c0b601bcb7f/contracts/core/governance/gauges/BaseGauge.sol#L218

Overview

In typical gauge or reward distribution contracts, a user’s staked token balance influences how many rewards they earn. However, BaseGauge calculates the user’s reward weight as follows:

function getUserWeight(address account) public view virtual returns (uint256) {
uint256 baseWeight = _getBaseWeight(account);
return _applyBoost(account, baseWeight);
}
function _getBaseWeight(address account) internal view virtual returns (uint256) {
// by default, returns global gauge weight from the gauge controller
return IGaugeController(controller).getGaugeWeight(address(this));
}

Key Observations:

  1. No Use of _balances[account]

    • The contract tracks staked tokens in _balances[account], updated by stake(...) and withdraw(...), but _getBaseWeight never references _balances[...].

  2. Global Instead of User-Specific

    • _getBaseWeight(account) simply calls getGaugeWeight(address(this)) on the gauge controller, returning a single gauge-wide figure, ignoring how many tokens that user staked.

  3. Reward Computations

    • The user’s “weight” is multiplied by (getRewardPerToken() - userStates[account].rewardPerTokenPaid) to compute earned(account). So if _getBaseWeight(account) is the same for everyone (or entirely ignoring staked amounts), the user’s actual stake is not factored into their share of rewards.

Consequences

  1. Staking Has No Effect
    Users can stake huge amounts of stakingToken in the gauge, but it won’t increase getUserWeight(...) or earned(...). The gauge effectively disregards each user’s staked balance, awarding rewards based on the gauge’s global weight from the controller plus a possible “veRAAC” boost.

  2. User Surprise or Loss
    A user who invests heavily, expecting a proportional share of rewards, will discover the contract’s reward logic does not consider their staked tokens. This can lead to confusion or feelings of a “broken” gauge system.

  3. Design Contradiction
    The code calls stake(...)/withdraw(...) and manages _totalSupply plus _balances[account], implying a typical “stake to earn” pattern. But because _getBaseWeight(account) never references _balances[account], the actual “stake to earn” flow is not implemented.

Proof / PoC

A minimal demonstration:

function testIgnoreStake() public {
// 1) userA stakes 100 tokens, userB stakes 10 tokens
baseGauge.stake(100e18); // userA
baseGauge.stake(10e18); // userB
// 2) The gauge calls getUserWeight(...) => calls _getBaseWeight(userA)
// returns gaugeController.getGaugeWeight(address(this)), ignoring userA's stake.
// => userA and userB get the same "baseWeight" if the boost is also the same.
// If there's no difference in veRAAC, userA and userB end up with identical "weight" => identical reward share
// even though userA staked 10x more tokens. Confirms staked tokens are not factored in.
}

No matter how many tokens userA or userB stake, they get the same base weight if the gauge controller’s weight is constant.

Recommendation

  1. Incorporate _balances[account]
    A typical gauge uses a user’s staked token share to determine reward distribution. For instance, rename _getBaseWeight(account) to something that multiplies or references _balances[account].

  2. Document/Remove Unused Staking
    If the gauge design only relies on veRAAC for distribution (and not the staked stakingToken amounts), remove or clarify the entire “stake/withdraw” logic. Right now, it confuses integrators into believing it’s standard “stake to earn.”

  3. Align with Child Contract
    If _getBaseWeight(account) is meant to be overridden by child contracts to factor in _balances[account], explicitly mention that in the doc. Possibly:

    function _getBaseWeight(address account) internal view virtual override returns (uint256) {
    return _balances[account];
    }

    or a combination of gauge weight and user stake.

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!