Core Contracts

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

An attacker can steal the gauge stakers profit

Summary

earned function which is responsible for updating the commission of depositor from rewards rely only on veToken.balanceOf(account) which is unfair because everyone even who has any stake in the gauge can claim rewards.

Vulnerability Details

The only purpose of getReward function beside transferring earned rewards is to update user state and his rewards data by calling updateReward modifier.

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);
}
}
  • Inside _updateReward function we have earned that accrue user rewards each time called by him.

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);
}
}

The main reason behind the issue as i said is earned rely only veToken.balanceOf(account).

function earned(address account) public view returns (uint256) {
return (getUserWeight(account) *
(getRewardPerToken() - userStates[account].rewardPerTokenPaid) / 1e18
) + userStates[account].rewards;
}
function getUserWeight(address account) public view virtual returns (uint256) {
uint256 baseWeight = _getBaseWeight(account);
return _applyBoost(account, baseWeight);
}
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); <@ audit
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
);
return (baseWeight * boost) / 1e18;
}
  • The missing of time factor make this attack possible in single Tx.

Impact

  • Attacker can unfairly claim other users rewards even if he have any stake.

  • Attacker can steal other user profit in single Tx

Tools Used

Manual audit

Recommendations

I suggest taking another approach by ensuring users already have existing stakes in the gauge and implementing a time delay (or time-based mechanism) to prevent flash loan attacks.

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.