Core Contracts

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

Users can manipulate their gauge rewards by increasing their veRAAC balance right before claiming, leading to unfair reward distribution

Summary

The earned() function calculates rewards based on the user's current weight, which depends on their veRAAC balance at the time of claiming. This allows users to temporarily boost their rewards by increasing their veRAAC balance just before claiming and reducing it afterward.

Vulnerability Details

The vulnerability exists in the earned() function:

function earned(address account) public view returns (uint256) {
@> return (getUserWeight(account) *
(getRewardPerToken() - userStates[account].rewardPerTokenPaid) / 1e18
) + userStates[account].rewards;
}
function getUserWeight(address account) public view virtual returns (uint256) {
uint256 baseWeight = _getBaseWeight(account);
@> return _applyBoost(account, baseWeight);
}
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); // Uses user's current veRAAC balance
uint256 totalVeSupply = veToken.totalSupply();
// Create BoostParameters struct from boostState
BoostCalculator.BoostParameters memory params = BoostCalculator.BoostParameters({
maxBoost: boostState.maxBoost,
minBoost: boostState.minBoost,
boostWindow: boostState.boostWindow,
totalWeight: boostState.totalWeight,
totalVotingPower: boostState.totalVotingPower,
votingPower: boostState.votingPower
});
uint256 boost = BoostCalculator.calculateBoost(
veBalance,
totalVeSupply,
params
);
return (baseWeight * boost) / 1e18;
}

The getUserWeight() function uses the current veRAAC balance to calculate the boost multiplier. This means that the reward calculation is based on the user's veRAAC balance at the moment of claiming, rather than throughout the earning period.

A malicious user can:

  1. Hold a minimal amount of veRAAC while rewards accrue

  2. Right before claiming:

    • Acquire a large amount of veRAAC

    • Get a much higher boost multiplier

This allows users to receive significantly more rewards than they should have earned based on their actual veRAAC commitment during the earning period.

Impact

This allows direct manipulation of reward distribution:

  • Users can extract more rewards than they should rightfully earn

  • Reduces rewards available to honest users who maintain consistent veRAAC holdings

  • The excess rewards claimed are permanently lost from the protocol

Tools Used

Manual review

Proof of Concept

First a bug in the BaseGauge::constructor() function need to be fixed, as this cause overflow in the calculations due to a incorrect assignment of boostState.minBoost:

- boostState.minBoost = 1e18;
+ boostState.minBoost = 10000;

Add the following test case to the test/unit/core/governance/gauges/GaugeController.test.js file:

it("reward boost manipulation", async () => {
// mint reward token to gauge to be able to distribute rewards
await rewardToken.mint(rwaGauge.target, ethers.parseEther("100000000000000000000"));
// mint veRAAC token to user to be able to stake and vote
await veRAACToken.mint(user1.address, ethers.parseEther("2000"));
// User1 vote for rwa gauge
await gaugeController.connect(user1).vote(await rwaGauge.getAddress(), 5000);
// User1 stake
await veRAACToken.connect(user1).approve(rwaGauge.target, ethers.parseEther("1000"));
await rwaGauge.connect(user1).stake(ethers.parseEther("1000"));
// Reward distribution
await gaugeController.distributeRewards(rwaGauge.target);
// advance time to the end of the period so no more rewards are distributed
const rwaGaugeDuration = await rwaGauge.getPeriodDuration();
await time.increase(rwaGaugeDuration);
const earnedUser1 = await rwaGauge.earned(user1.address);
expect(earnedUser1).to.be.gt(0);
// User1 purchased and locked raacTokens to get more veRAAC tokens before claiming
await veRAACToken.mint(user1.address, ethers.parseEther("10000"));
const earnedUser1AfterBoost = await rwaGauge.earned(user1.address);
expect(earnedUser1AfterBoost).to.be.gt(earnedUser1);
// User1 earned 30% more rewards
expect(earnedUser1AfterBoost).to.be.gt(earnedUser1 * 13n / 10n);
});

Recommendations

Track the user's veRAAC balance over time using a time-weighted average, similar to how vote weights are tracked.

Updates

Lead Judging Commences

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

BaseGauge::_applyBoost, GaugeController::vote, BoostController::calculateBoost use balanceOf() instead of getVotingPower() for vote-escrow tokens, negating time-decay mechanism

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

BaseGauge::_applyBoost, GaugeController::vote, BoostController::calculateBoost use balanceOf() instead of getVotingPower() for vote-escrow tokens, negating time-decay mechanism

Support

FAQs

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