Core Contracts

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

Boost Miscalculation Leads to Excess Distribution

Summary

A critical vulnerability has been identified in the reward distribution mechanism where users receive identical rewards regardless of their staked amounts due to improper boost calculation implementation. This leads to rewards being distributed equally among users instead of proportionally to their stakes, resulting in total distributed rewards exceeding the notified reward amount.

Vulnerability Details

The vulnerability stems from the boost calculation mechanism in the reward distribution system:

  1. The base weight is derived from the gauge weight instead of individual user stakes:

function _getBaseWeight(address account) internal view virtual returns (uint256) {
return IGaugeController(controller).getGaugeWeight(address(this));
}
  1. When _applyBoost is called, the boost calculation always defaults to the minimum boost if the user's veToken balance or the total veToken supply is zero. As a result, boost will always equal minBoost under these conditions. This boost is then applied to baseWeight, but instead of considering the user's individual weight, it simply scales the gauge’s weight, potentially leading to inaccurate weight distribution.

function _applyBoost(address account, uint256 baseWeight) internal view virtual returns (uint256) {
if (baseWeight == 0) return 0;
IERC20 veToken = IERC20(IGaugeController(controller).veRAACToken());
uint256 veBalance = veToken.balanceOf(account);
uint256 totalVeSupply = veToken.totalSupply();
// other code
uint256 boost = BoostCalculator.calculateBoost(
veBalance,
totalVeSupply,
params
);
return (baseWeight * boost) / 1e18;
}

This leads to:

  • All users receiving the same weight (10000) regardless of their stake

  • Equal reward distribution despite different stake amounts

  • Cumulative rewards exceeding the notified amount

POC

Paste the following code into the BaseGauge.test.js file:

describe("Reward Distribution", () => {
beforeEach(async function () {
this.timeout(120000); // Increase timeout to 120 seconds
// Setup rewards
await rewardToken.mint(await baseGauge.getAddress(), ethers.parseEther("10000"));
// Set emission cap before notifying rewards
await baseGauge.setEmission(ethers.parseEther("10000"));
// Set initial weights and vote to enable rewards
await gaugeController.connect(user1).vote(await baseGauge.getAddress(), 10000);
// Stake tokens for user1
await rewardToken.mint(user1.address, ethers.parseEther("10000"));
await rewardToken.connect(user1).approve(await baseGauge.getAddress(), ethers.parseEther("10000"));
await baseGauge.connect(user1).stake(ethers.parseEther("10000"));
// Notify rewards after staking
await baseGauge.notifyRewardAmount(ethers.parseEther("1000"));
const rewardRate = await baseGauge.rewardRate();
console.log("reward rate", rewardRate.toString());
});
it.only("should calculate earned rewards", async () => {
const user3 = (await ethers.getSigners())[3];
await rewardToken.mint(user3.address, ethers.parseEther("5000"));
await rewardToken.connect(user3).approve(await baseGauge.getAddress(), ethers.parseEther("5000"));
await baseGauge.connect(user3).stake(ethers.parseEther("5000"));
await rewardToken.mint(user2.address, ethers.parseEther("2000"));
await rewardToken.connect(user2).approve(await baseGauge.getAddress(), ethers.parseEther("2000"));
await baseGauge.connect(user2).stake(ethers.parseEther("2000"));
// Move time forward by 7 days to allow rewards to accrue
await time.increase(7 * DAY);
await network.provider.send("evm_mine");
const earned3 = await baseGauge.earned(user1.address);
const earnedForUser3 = await baseGauge.earned(user3.address);
const earnedForUser2 = await baseGauge.earned(user2.address);
console.log("User 1 rewards at period end", earned3.toString());
console.log("User 3 rewards at period end", earnedForUser3.toString());
console.log("User 2 rewards at period end", earnedForUser2.toString());
});

Proof of Concept shows three users with different stakes receiving identical rewards:

  • User1: Staked 10,000 tokens, received 588 rewards

  • User3: Staked 5,000 tokens, received 588 rewards

  • User4: Staked 2,000 tokens, received 588 rewards

Total supply: 17,000 tokens
Reward per token: 58823529411764690
Initial reward notification: 1,000 tokens
Total rewards distributed: 1,764 tokens (588 * 3 users)

Impact

HIGH - The vulnerability results in:

  1. Inequitable reward distribution

  2. Over-distribution of rewards beyond the notified amount

  3. Potential depletion of reward pools faster than intended

  4. Economic impact due to incorrect reward allocation

  5. Violation of stake-proportional reward distribution principle

Tools Used

  • Manual code review

Recommendations

  1. Modify the base weight calculation to consider individual stake amounts:

function _getBaseWeight(address account) internal view virtual returns (uint256) {
uint256 userStake = _balances[account];
uint256 gaugeWeight = IGaugeController(controller).getGaugeWeight(address(this));
return userStake * gaugeWeight;
}
  1. Add safety checks to prevent reward over-distribution:

function earned(address account) public view returns (uint256) {
uint256 calculatedReward = // current calculation
uint256 remainingRewards = periodState.emission - periodState.distributed;
return calculatedReward > remainingRewards ? remainingRewards : calculatedReward;
}
Updates

Lead Judging Commences

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