Core Contracts

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

Any user can claim reward from gauge, even though user not staked anything.

Summary

An user can get reward, by calling getReward() function.

Contract - BaseGauge.sol

function getReward() external virtual nonReentrant whenNotPaused updateReward(msg.sender) {
if (block.timestamp - lastClaimTime[msg.sender] < MIN_CLAIM_INTERVAL) {
revert ClaimTooFrequent();
}
lastClaimTime[msg.sender] = block.timestamp;
UserState storage state = userStates[msg.sender];
@-> uint256 reward = state.rewards;
if (reward > 0) {
state.rewards = 0;
uint256 balance = rewardToken.balanceOf(address(this));
if (reward > balance) {
revert InsufficientBalance();
}
rewardToken.safeTransfer(msg.sender, reward);
emit RewardPaid(msg.sender, reward);
}
}

function flow

1 .getReward() -> 2 .updateReward(msg.sender) -> 3 ._updateReward(account)

function _updateReward(address account) internal {
rewardPerTokenStored = getRewardPerToken();
lastUpdateTime = lastTimeRewardApplicable();
if (account != address(0)) {
UserState storage state = userStates[account];
state.rewards = earned(account);
state.rewardPerTokenPaid = rewardPerTokenStored;
state.lastUpdateTime = block.timestamp;
emit RewardUpdated(account, state.rewards);
}
}

4 .earned() -> NON-ZERO

function earned(address account) public view returns (uint256) {
return (getUserWeight(account) *
// here - start again
(getRewardPerToken() - userStates[account].rewardPerTokenPaid) / 1e18
) + userStates[account].rewards;
}

5 . getRewardPerToken() ->
NON-ZERO -

  • as the return value (rewardPerTokenStored) isn't user dependent.

  • (lastTimeRewardApplicable() - lastUpdateTime) * rewardRate * 1e18 / totalSupply() always non-zero.

function getRewardPerToken() public view returns (uint256) {
if (totalSupply() == 0) {
return rewardPerTokenStored;
}
return rewardPerTokenStored + (
(lastTimeRewardApplicable() - lastUpdateTime) * rewardRate * 1e18 / totalSupply()
);
}

6 .getUserWeight() ->
NON-ZERO -

  • As _applyBoost() is non-zero. see point 8.

function getUserWeight(address account) public view virtual returns (uint256) {
uint256 baseWeight = _getBaseWeight(account);
return _applyBoost(account, baseWeight);
}

7 . _getBaseWeight(account) ->
NON-ZERO -

  • This will be non-zero value as it's returning gauge weight of Gauge contract, not user.

function _getBaseWeight(address account) internal view virtual returns (uint256) {
return IGaugeController(controller).getGaugeWeight(address(this));
}

8 . _applyBoost(account, baseWeight); -> NON-ZERO The only way to return 0 is if veRAAC balance of account is 0.

NOTE: It's possible that veRAAC balance of user is non-zero, but he haven't staked any veRAAC in guage contract.

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();
// Create BoostParameters struct from boostState
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
);
// $mynotes - weight is in 1e18 format so it's correct.
return (baseWeight * boost) / 1e18;
}

since, earned() is non - zero, so state.rewards is also non-zero, which means non-zero reward is being tranfered to user.

Vulnerability Details

It means any user who have veRAAC balance, but not staked in gauge can still earn rewards, without staking.

Impact

  • User who haven't staked veRAAC are also claiming the reward, which is unethical for user who deposited in Gauge contract.

  • This could possibly drain out whole RAAC balance of Gauge contract.

Tools Used

Manual

Recommendations

Implement a check, to ensure only stakers can claim reward.

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.