Description
The BaseGauge::earned
function calculates rewards based solely on user weight from boost calculations without considering the actual staked token balance. This allows users to earn disproportionate rewards by maintaining a high boost factor with minimal staked value, violating the core principle of proportional reward distribution based on capital commitment.
Proof of Concept
User A stakes 1 wei of tokens but holds significant veRAAC tokens for maximum boost
User B stakes 1000 tokens with no boost
Both users earn rewards based on boost-adjusted weight rather than actual stake
User A receives higher rewards than User B despite minimal stake
Test case demonstrating the issue (add this test case to test/unit/core/governance/gauges/BaseGauge.test.js
):
it("allows disproportionate rewards through boost without stake", async () => {
await veRAACToken.mint(user1.address, ethers.parseEther("10000"));
await veRAACToken.mint(user2.address, 0);
await rewardToken.mint(user1.address, 1);
await rewardToken.mint(user2.address, ethers.parseEther("1000"));
await rewardToken.connect(user1).approve(baseGauge.target, 1);
await rewardToken
.connect(user2)
.approve(baseGauge.target, ethers.parseEther("1000"));
await baseGauge.connect(user1).stake(1);
await baseGauge.connect(user2).stake(ethers.parseEther("1000"));
await baseGauge.notifyRewardAmount(ethers.parseEther("1000"));
await time.increase(7 * DAY);
const user1Rewards = await baseGauge.earned(user1.address);
const user2Rewards = await baseGauge.earned(user2.address);
console.log("user1Rewards: ", user1Rewards);
console.log("user2Rewards: ", user2Rewards);
expect(user1Rewards).to.be.gt(user2Rewards);
});
Update this function in MockBaseGauge.sol
to facilitate testing:
function _applyBoost(address account, uint256 baseWeight) internal view override returns (uint256) {
IERC20 veToken = IERC20(IGaugeController(controller).veRAACToken());
uint256 veBalance = veToken.balanceOf(account);
uint256 totalVeSupply = veToken.totalSupply();
uint256 MIN_BOOST = 10000;
uint256 MAX_BOOST = 25000;
if (totalVeSupply == 0) {
return MIN_BOOST;
}
uint256 votingPowerRatio = (veBalance * 1e18) / totalVeSupply;
uint256 boostRange = MAX_BOOST - MIN_BOOST;
uint256 boost = MIN_BOOST + ((votingPowerRatio * boostRange) / 1e18);
if (boost < MIN_BOOST) {
boost = MIN_BOOST;
}
if (boost > MAX_BOOST) {
boost = MAX_BOOST;
}
return baseWeight * boost / 1e4;
}
Impact
High Severity - Fundamentally breaks the staking incentive mechanism by allowing users to extract disproportionate rewards without meaningful capital commitment, leading to lost of trust in the protocol.
Recommendation
function earned(address account) public view returns (uint256) {
- return (getUserWeight(account) * (getRewardPerToken() - userStates[account].rewardPerTokenPaid) / 1e18)
+ return (_balances[account] * getUserWeight(account) * (getRewardPerToken() - userStates[account].rewardPerTokenPaid) / 1e36)
+ userStates[account].rewards;
}
uint256 public constant MIN_STAKE = 1e18;
function stake(uint256 amount) external {
require(amount >= MIN_STAKE, "Insufficient stake");
}
function getUserWeight(address account) public view virtual returns (uint256) {
uint256 baseWeight = _getBaseWeight(account);
uint256 boost = _applyBoost(account, baseWeight);
return (_balances[account] * boost) / 1e18;
}