Core Contracts

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

`BaseGauge::earned` function updates the amount of rewards incorrectly

The BaseGauge::earned function calculates rewards using the user’s boosted weight without verifying if the user has any tokens staked. In particular, since the getUserWeight function returns a nonzero value based solely on the base weight and minimum boost (even if no tokens are locked), an attacker can claim rewards without participation by exploiting this miscalculation.

Vulnerability Details

The earned function is implemented as:

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

Here, getUserWeight computes the boosted weight as:

function getUserWeight(address account) public view virtual returns (uint256) {
uint256 baseWeight = _getBaseWeight(account);
return _applyBoost(account, baseWeight);
}

However, the base weight (from the controller) multiplied by the minimum boost is returned regardless of whether the user has actually staked any tokens. As a result, an account that has not participated in the gauge (has no locked tokens) still obtains a positive weight, and thus accumulates rewards over time.
Please also take into consideration that the initial amount for minBoost set in the constructor parameters is 1e18 which is higher than the max boost, leading to wrong calculation.

The provided PoC demonstrates that a user (e.g., Bob) who hasn’t staked any tokens can still accrue and claim rewards:

Proof Of Concept
Add this under the Reward Distribution describe in BaseGause.test.js and execute with npx hardhat test --grep "user can manipulate gauge rewards"

it("user can manipulate gauge rewards", async () => {
await rewardToken.mint(user1.address, ethers.parseEther("1000"));
await rewardToken.connect(user1).approve(await baseGauge.getAddress(), ethers.parseEther("1000"));
await baseGauge.connect(user1).stake(ethers.parseEther("1000"));
await time.increase(DAY);
// bob hasn't staked anything
const [bob] = await ethers.getSigners();
const userWeight = await baseGauge.getUserWeight(bob.address);
expect(userWeight).to.be.gt(0);
const getRewardPerToken = await baseGauge.getRewardPerToken();
expect(getRewardPerToken).to.be.gt(0);
const initEarned = await baseGauge.earned(bob.address);
expect(initEarned).to.be.gt(0);
await time.increase(DAY);
const secondEarned = await baseGauge.earned(bob.address);
expect(secondEarned).to.be.gt(initEarned);
// Bob can claim rewards even when he didn't stake any tokens into the contract
await baseGauge.connect(bob).getReward();
const bobBalance = await rewardToken.balanceOf(bob.address);
expect(bobBalance).to.be.gt(0);
});

Impact

  • Unjust Reward Claim: Users with no staked tokens can claim rewards, undermining the incentive model.

Tools Used

  • Manual Code Review

  • Unit Testing

Recommendations

  1. Incorporate Stake Verification:

  • Modify the weight calculation logic in getUserWeight to account for the actual amount staked (or locked) by the user. The function should return zero if the user has not staked any tokens.

  1. Reward Calculation Adjustment:

  • Alternatively, adjust the earned function to multiply the difference in reward per token by the user's staked balance, ensuring rewards are proportional to participation.

Updates

Lead Judging Commences

inallhonesty Lead Judge 4 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 4 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.