Summary
A critical vulnerability has been identified in the reward distribution mechanism where users receive identical rewards regardless of their staked amounts due to improper boost calculation implementation. This leads to rewards being distributed equally among users instead of proportionally to their stakes, resulting in total distributed rewards exceeding the notified reward amount.
Vulnerability Details
The vulnerability stems from the boost calculation mechanism in the reward distribution system:
The base weight is derived from the gauge weight instead of individual user stakes:
function _getBaseWeight(address account) internal view virtual returns (uint256) {
return IGaugeController(controller).getGaugeWeight(address(this));
}
When _applyBoost
is called, the boost calculation always defaults to the minimum boost if the user's veToken balance or the total veToken supply is zero. As a result, boost
will always equal minBoost
under these conditions. This boost is then applied to baseWeight
, but instead of considering the user's individual weight, it simply scales the gauge’s weight, potentially leading to inaccurate weight distribution.
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();
uint256 boost = BoostCalculator.calculateBoost(
veBalance,
totalVeSupply,
params
);
return (baseWeight * boost) / 1e18;
}
This leads to:
All users receiving the same weight (10000) regardless of their stake
Equal reward distribution despite different stake amounts
Cumulative rewards exceeding the notified amount
POC
Paste the following code into the BaseGauge.test.js
file:
describe("Reward Distribution", () => {
beforeEach(async function () {
this.timeout(120000);
await rewardToken.mint(await baseGauge.getAddress(), ethers.parseEther("10000"));
await baseGauge.setEmission(ethers.parseEther("10000"));
await gaugeController.connect(user1).vote(await baseGauge.getAddress(), 10000);
await rewardToken.mint(user1.address, ethers.parseEther("10000"));
await rewardToken.connect(user1).approve(await baseGauge.getAddress(), ethers.parseEther("10000"));
await baseGauge.connect(user1).stake(ethers.parseEther("10000"));
await baseGauge.notifyRewardAmount(ethers.parseEther("1000"));
const rewardRate = await baseGauge.rewardRate();
console.log("reward rate", rewardRate.toString());
});
it.only("should calculate earned rewards", async () => {
const user3 = (await ethers.getSigners())[3];
await rewardToken.mint(user3.address, ethers.parseEther("5000"));
await rewardToken.connect(user3).approve(await baseGauge.getAddress(), ethers.parseEther("5000"));
await baseGauge.connect(user3).stake(ethers.parseEther("5000"));
await rewardToken.mint(user2.address, ethers.parseEther("2000"));
await rewardToken.connect(user2).approve(await baseGauge.getAddress(), ethers.parseEther("2000"));
await baseGauge.connect(user2).stake(ethers.parseEther("2000"));
await time.increase(7 * DAY);
await network.provider.send("evm_mine");
const earned3 = await baseGauge.earned(user1.address);
const earnedForUser3 = await baseGauge.earned(user3.address);
const earnedForUser2 = await baseGauge.earned(user2.address);
console.log("User 1 rewards at period end", earned3.toString());
console.log("User 3 rewards at period end", earnedForUser3.toString());
console.log("User 2 rewards at period end", earnedForUser2.toString());
});
Proof of Concept shows three users with different stakes receiving identical rewards:
User1: Staked 10,000 tokens, received 588 rewards
User3: Staked 5,000 tokens, received 588 rewards
User4: Staked 2,000 tokens, received 588 rewards
Total supply: 17,000 tokens
Reward per token: 58823529411764690
Initial reward notification: 1,000 tokens
Total rewards distributed: 1,764 tokens (588 * 3 users)
Impact
HIGH - The vulnerability results in:
Inequitable reward distribution
Over-distribution of rewards beyond the notified amount
Potential depletion of reward pools faster than intended
Economic impact due to incorrect reward allocation
Violation of stake-proportional reward distribution principle
Tools Used
Recommendations
Modify the base weight calculation to consider individual stake amounts:
function _getBaseWeight(address account) internal view virtual returns (uint256) {
uint256 userStake = _balances[account];
uint256 gaugeWeight = IGaugeController(controller).getGaugeWeight(address(this));
return userStake * gaugeWeight;
}
Add safety checks to prevent reward over-distribution:
function earned(address account) public view returns (uint256) {
uint256 calculatedReward =
uint256 remainingRewards = periodState.emission - periodState.distributed;
return calculatedReward > remainingRewards ? remainingRewards : calculatedReward;
}