Core Contracts

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

The reward calculation mechanism in `BaseGauge` uses inconsistent bases, leading to inaccurate reward distribution for users.

Summary

The reward calculation mechanism in BaseGauge uses inconsistent bases, leading to inaccurate reward distribution for users.

Vulnerability Details

The reward calculation process relies on different bases, resulting in discrepancies in the computed rewards for users.

In the _updateReward() function, the contract updates the stored rewardPerToken and the user's accumulated rewards:

modifier updateReward(address account) {
_updateReward(account);
_;
}
function _updateReward(address account) internal {
@> rewardPerTokenStored = getRewardPerToken();
lastUpdateTime = lastTimeRewardApplicable();
if (account != address(0)) {
UserState storage state = userStates[account];
@> state.rewards = earned(account);
state.rewardPerTokenPaid = rewardPerTokenStored;
state.lastUpdateTime = block.timestamp;
emit RewardUpdated(account, state.rewards);
}
}

The getRewardPerToken() function determines the per-token reward amount, using totalSupply() as the denominator:

function getRewardPerToken() public view returns (uint256) {
if (totalSupply() == 0) {
return rewardPerTokenStored;
}
return rewardPerTokenStored + (
@> (lastTimeRewardApplicable() - lastUpdateTime) * rewardRate * 1e18 / totalSupply()
);
}

However, when calculating the actual reward for a user in the earned() function, the contract uses getUserWeight(account) as the basis for calculation:

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

The getUserWeight(account) function determines the user's weighted staked amount, incorporating a boost factor:

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);
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;
}

Root Cause:

  1. getRewardPerToken() determines per-token rewards using totalSupply().

  2. earned(account) calculates rewards based on getUserWeight(account), which applies a boost mechanism and may not be proportional to totalSupply().
    As a result, if the ratio of getUserWeight(account) to totalSupply() changes over time, the reward distribution will be incorrect.

Poc

Add the following test to test/unit/core/governance/gauges/RAACGauge.test.js and execute it:

describe("Reward calculation error", () => {
it("Poc", async () => {
await veRAACToken.connect(user1).approve(raacGauge.getAddress(), ethers.MaxUint256);
await raacGauge.connect(user1).stake(ethers.parseEther("100"));
// user1 owns the entire stake amount
expect(await raacGauge.balanceOf(user1.address)).to.be.eq(await raacGauge.totalSupply());
// the total reward is 1000e18
await raacGauge.notifyRewardAmount(ethers.parseEther("1000"));
const user1BalanceStart = await rewardToken.balanceOf(user1.address);
await time.increase(WEEK);
await raacGauge.connect(user1).getReward();
const user1BalanceEnd = await rewardToken.balanceOf(user1.address);
console.log("Actual amount of reward received:",user1BalanceEnd - user1BalanceStart);
});
});

output:

RAACGauge
Reward calculation error
Actual amount of reward received: 189999999n

Impact

The mismatch between getUserWeight(account) and totalSupply() results in incorrect reward calculations. If the ratio of getUserWeight(account) to totalSupply() fluctuates, users may receive more or fewer rewards than expected, leading to unfair reward distribution.

Tools Used

Manual Review

Recommendations

Ensure that the reward calculation in earned(account) and getRewardPerToken() is based on the same unit of measurement. Either:

  1. Modify getRewardPerToken() to incorporate the boosted weight mechanism.

  2. Ensure that earned(account) considers totalSupply() as its basis for reward distribution.

Updates

Lead Judging Commences

inallhonesty Lead Judge 7 months ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement
inallhonesty Lead Judge 7 months ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

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

Give us feedback!