Core Contracts

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

Gauge stakes have no effect on rewards

Summary

Staking tokens has no effect on reward distribution, as rewards are based solely on the user's ve token balance.

Vulnerability Details

Inside the gauges, users can stake tokens using stake:

/**
* @notice Stakes tokens in the gauge
* @param amount Amount to stake
*/
function stake(uint256 amount) external nonReentrant updateReward(msg.sender) {
if (amount == 0) revert InvalidAmount();
_totalSupply += amount;
_balances[msg.sender] += amount;
stakingToken.safeTransferFrom(msg.sender, address(this), amount);
emit Staked(msg.sender, amount);
}

As you can see, the contract tracks the balance of the user in _balances[].

The problem here is that despite _balances being incremented every time a user stakes, it's never used during reward calculations. What actually matters for the reward distribution is the user's veToken balance. Somehow, both the stake and withdraw functions don't affect how the rewards are distributed.

Proof of Concept

To test the scenario, please create a file named GaugeStakes.test.js in the following path: test/unit/core/governance/gauges/, and paste the following test code into it:

import { time } from "@nomicfoundation/hardhat-network-helpers";
import { expect } from "chai";
import hre from "hardhat";
const { ethers } = hre;
describe("RAACGauge", () => {
let raacGauge;
let gaugeController;
let veRAACToken;
let rewardToken;
let stakingToken;
let owner;
let user1;
let user2;
let snapshotId;
const WEEK = 7 * 24 * 3600;
const WEIGHT_PRECISION = 10000;
beforeEach(async () => {
snapshotId = await network.provider.send('evm_snapshot');
[owner, user1, user2,] = await ethers.getSigners();
const MockToken = await ethers.getContractFactory("MockToken");
rewardToken = await MockToken.deploy("Reward Token", "RWD", 18);
veRAACToken = await MockToken.deploy("veRAAC Token", "veRAAC", 18);
stakingToken = await MockToken.deploy("Stakign Token", "STK", 18);
await veRAACToken.mint(user1.address, ethers.parseEther("1000"));
await veRAACToken.mint(user2.address, ethers.parseEther("1000"));
await stakingToken.mint(user1.address, ethers.parseEther("1000"));
await stakingToken.mint(user2.address, ethers.parseEther("1000"));
const GaugeController = await ethers.getContractFactory("GaugeController");
gaugeController = await GaugeController.deploy(await veRAACToken.getAddress());
const currentTime = BigInt(await time.latest());
const nextWeekStart = ((currentTime / BigInt(WEEK)) + 3n) * BigInt(WEEK);
await time.setNextBlockTimestamp(Number(nextWeekStart));
await network.provider.send("evm_mine");
const RAACGauge = await ethers.getContractFactory("RAACGauge");
raacGauge = await RAACGauge.deploy(
await rewardToken.getAddress(),
await stakingToken.getAddress(),
await gaugeController.getAddress()
);
await raacGauge.grantRole(await raacGauge.CONTROLLER_ROLE(), owner.address);
await rewardToken.connect(user1).approve(raacGauge.getAddress(), ethers.MaxUint256);
await rewardToken.connect(user2).approve(raacGauge.getAddress(), ethers.MaxUint256);
await gaugeController.grantRole(await gaugeController.GAUGE_ADMIN(), owner.address);
await gaugeController.addGauge(await raacGauge.getAddress(), 0, WEIGHT_PRECISION);
await time.increase(WEEK);
await gaugeController.connect(user1).vote(await raacGauge.getAddress(), WEIGHT_PRECISION);
await raacGauge.setWeeklyEmission(ethers.parseEther("10000"));
await rewardToken.mint(raacGauge.getAddress(), ethers.parseEther("100000"));
await raacGauge.setBoostParameters(
25000,
10000,
WEEK
);
await raacGauge.setInitialWeight(5000);
await network.provider.send("evm_mine");
});
afterEach(async () => {
await network.provider.send('evm_revert', [snapshotId]);
});
describe("Reward Distribution", () => {
beforeEach(async () => {
await stakingToken.connect(user1).approve(raacGauge.getAddress(), ethers.MaxUint256);
await raacGauge.connect(user1).stake(ethers.parseEther("1000"));
await raacGauge.connect(user1).voteEmissionDirection(5000);
});
it("Stakes are effectless", async () => {
await raacGauge.notifyRewardAmount(ethers.parseEther("1000"));
await time.increase(WEEK / 2);
await raacGauge.connect(user1).getReward();
await raacGauge.connect(user2).getReward();
const balance1 = await rewardToken.balanceOf(user1.address);
const balance2 = await rewardToken.balanceOf(user2.address);
console.log("User1 Reward Balance:", balance1.toString());
console.log("User2 Reward Balance:", balance2.toString());
expect(balance1).to.be.gt(0);
expect(balance2).to.be.gt(0);
});
});
});

Run the test:

npm run test:unit:governance -- --grep "Stakes are effectless"

Result:

RAACGauge
Reward Distribution
User1 Reward Balance: 8749999
User2 Reward Balance: 8749999
✔ Stakes are effectless (1846ms)
1 passing (23s)

As you can see, user2 didn't have any stakes, while user1 had 1000e18 stakes. But at the end of the day, they both received 8749999 rewards, since they both had 1000e18 veToken balance.

Impact

Stakes have no effect on rewards.

Tools Used

  • VSCode

  • Hardhat

Recommendations

Ensure that the staking amount is integrated into the reward calculation logic to accurately reflect user contributions.

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!