Core Contracts

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

Incorrect reward calculation based on boost without staked balance enables disproportionate reward distribution

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

  1. User A stakes 1 wei of tokens but holds significant veRAAC tokens for maximum boost

  2. User B stakes 1000 tokens with no boost

  3. Both users earn rewards based on boost-adjusted weight rather than actual stake

  4. 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 () => {
// Setup boosts
await veRAACToken.mint(user1.address, ethers.parseEther("10000")); // Max boost
await veRAACToken.mint(user2.address, 0); // No boost
// User1 stakes 1 wei, User2 stakes 1000 tokens (in the test, rewardToken is used as the staking token)
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"));
// Distribute rewards
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);
// User1 with 1 wei stake but max boost earns more than User2
expect(user1Rewards).to.be.gt(user2Rewards);
});

Update this function in MockBaseGauge.sol to facilitate testing:

// Override boost calculation for 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;
}
// Calculate voting power ratio with higher precision
uint256 votingPowerRatio = (veBalance * 1e18) / totalVeSupply;
// Calculate boost within min-max range
uint256 boostRange = MAX_BOOST - MIN_BOOST;
uint256 boost = MIN_BOOST + ((votingPowerRatio * boostRange) / 1e18);
// Ensure boost is within bounds
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

  • Primary Fix: Modify reward calculation to include staked balance:

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;
}
  • Alternative: Implement minimum stake requirements in BaseGauge::stake:

uint256 public constant MIN_STAKE = 1e18; // 1 token
function stake(uint256 amount) external {
require(amount >= MIN_STAKE, "Insufficient stake");
// ... existing logic ...
}
  • Secondary: Combine boost with absolute stake in weight calculation:

function getUserWeight(address account) public view virtual returns (uint256) {
uint256 baseWeight = _getBaseWeight(account);
uint256 boost = _applyBoost(account, baseWeight);
return (_balances[account] * boost) / 1e18;
}
Updates

Lead Judging Commences

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