Core Contracts

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

Attacker can front-running notify reward in `BaseGauge` to get reward

Vulnerability Details

Function notifyRewardAmount()is called by controller to update reward rate. It will update reward rate right after function is called:

function notifyRewardAmount(uint256 amount) external override onlyController updateReward(address(0)) {
if (amount > periodState.emission) revert RewardCapExceeded();
rewardRate = notifyReward(periodState, amount, periodState.emission, getPeriodDuration()); // <--
periodState.distributed += amount;
uint256 balance = rewardToken.balanceOf(address(this));
if (rewardRate * getPeriodDuration() > balance) {
revert InsufficientRewardBalance();
}
lastUpdateTime = block.timestamp;
emit RewardNotified(amount);
}
/**
* @notice Notifies about new reward amount
* @param state Period state to update
* @param amount Reward amount
* @param maxEmission Maximum emission allowed
* @param periodDuration Duration of the period
* @return newRewardRate Calculated reward rate
*/
function notifyReward(
PeriodState storage state,
uint256 amount,
uint256 maxEmission,
uint256 periodDuration
) internal view returns (uint256) {
if (amount > maxEmission) revert RewardCapExceeded();
if (amount + state.distributed > state.emission) {
revert RewardCapExceeded();
}
uint256 rewardRate = amount / periodDuration;
if (rewardRate == 0) revert ZeroRewardRate();
return rewardRate;
}

User can freely stake and withdraw at any time:

function stake(uint256 amount) external nonReentrant updateReward(msg.sender) {
if (amount == 0) revert InvalidAmount();
_totalSupply += amount;
_balances[msg.sender] += amount;
stakingToken.safeTransferFrom(msg.sender, address(this), amount);
emit Staked(msg.sender, amount);
}
/**
* @notice Withdraws staked tokens
* @param amount Amount to withdraw
*/
function withdraw(uint256 amount) external nonReentrant updateReward(msg.sender) {
if (amount == 0) revert InvalidAmount();
if (_balances[msg.sender] < amount) revert InsufficientBalance();
_totalSupply -= amount;
_balances[msg.sender] -= amount;
stakingToken.safeTransfer(msg.sender, amount);
emit Withdrawn(msg.sender, amount);
}

Reward is calculated and updated in updateReward modifier, which based on reward rate:

modifier updateReward(address account) {
_updateReward(account);
_;
}

Reward calculation:

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);
}
}
function earned(address account) public view returns (uint256) {
return (getUserWeight(account) * //@note user weight based on veToken only
(getRewardPerToken() - userStates[account].rewardPerTokenPaid) / 1e18 // <--
) + userStates[account].rewards;
}

And checking condition in getReward()function can be easily bypassed with accounts that claim reward in gauge for the first time (lastClaimTime[msg.sender]will equal to 0 at the first time reward is claimed):

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

So attacker can gain profit by front-running reward notify, deposit then withdraw right after that without actually staking anything

Impact

Attacker can get reward without contributing to protocol

Recommendations

Using another mechanism to distribute reward

Updates

Lead Judging Commences

inallhonesty Lead Judge 7 months ago
Submission Judgement Published
Validated
Assigned finding tags:

BaseGauge reward system can be gamed through repeated stake/withdraw cycles without minimum staking periods, allowing users to earn disproportionate rewards vs long-term stakers

inallhonesty Lead Judge 7 months ago
Submission Judgement Published
Validated
Assigned finding tags:

BaseGauge reward system can be gamed through repeated stake/withdraw cycles without minimum staking periods, allowing users to earn disproportionate rewards vs long-term stakers

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.

Give us feedback!