Core Contracts

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

Users can steal the reward of other users that are yet to claim their reward

Summary

The Base Guage has a staking section which allows users to stake and earn reward tokens but this is not correctly implemented and A user can take the reward earned by another user.

Vulnerability Details

The reward calculation does not save the Finish time of distribution hence users(especially the last user) who do not claim their rewards during the time period and wait for a second after finished time, will not be able to do so as this rewards will be stolen by another user

  1. During reward notification

    /**
    * @notice Notifies contract of reward amount
    * @dev Updates reward rate based on new amount
    * @param amount Amount of rewards to distribute //note
    */
    function notifyRewardAmount(uint256 amount) external override onlyController updateReward(address(0)) { // controller must be truste else we can do alot of harm NOTE
    if (amount > periodState.emission) revert RewardCapExceeded(); //do we decrease emission else why
    rewardRate = notifyReward(periodState, amount, periodState.emission, getPeriodDuration()); // change
    periodState.distributed += amount; // NOTE // update the amount we have distributed
    uint256 balance = rewardToken.balanceOf(address(this));
    if (rewardRate * getPeriodDuration() > balance) { // note revert no transfer before
    revert InsufficientRewardBalance();
    }
    @audit>>. lastUpdateTime = block.timestamp; // cache finished at also we are looking at a big ass bug check synthetic , users can still claim after finish other users will not be able to, stealing another persons rewarrd
    emit RewardNotified(amount);
    }

LastUpdatetime is saved

Reward rate is calculated based on the duration 7 days. e.g DISTRIBUTE 7000 USD for 7 days. 1000 USD will be distributed per day.

function notifyReward(
PeriodState storage state,
uint256 amount,
uint256 maxEmission,
uint256 periodDuration
) internal view returns (uint256) {
if (amount > maxEmission) revert RewardCapExceeded(); // double check this is not neccessary
if (amount + state.distributed > state.emission) {
revert RewardCapExceeded();
}
uint256 rewardRate = amount / periodDuration;
if (rewardRate == 0) revert ZeroRewardRate(); // see
return rewardRate;
}

But the Finished at is based on the LASTUPDATETIME set during the reward notification

/**
* @notice Gets end time of current period
* @return Period end timestamp
*/
function periodFinish() public view returns (uint256) {
return lastUpdateTime + getPeriodDuration(); // bug
}

But the LASTUPDATETIME is dynamic and keeps changing per reward distribution hence finished at keeps more allowing A user to take rewards even after reward distribution has ended.

// View functions
/**
* @notice Gets latest applicable reward time
* @return Latest of current time or period end
*/
function lastTimeRewardApplicable() public view returns (uint256) {
return block.timestamp < periodFinish() ? block.timestamp : periodFinish(); // bug finished period should be fixed
}

During every updatereward call, called during stake, getreward, checkpoint, withdraw.

// Internal functions
/**
* @notice Updates reward state for an account
* @dev Calculates and updates reward state including per-token rewards
* @param account Address to update rewards for
*/
function _updateReward(address account) internal {
rewardPerTokenStored = getRewardPerToken();
@audit>> lastUpdateTime = lastTimeRewardApplicable();
if (account != address(0)) {
UserState storage state = userStates[account];
state.rewards = earned(account); // we update not add can this be an issue test on calculate and see
state.rewardPerTokenPaid = rewardPerTokenStored;
state.lastUpdateTime = block.timestamp;
emit RewardUpdated(account, state.rewards);
}
}

After day 1 Alice calls and gets her share of the 1000 USD per day, gets 100 USD.

At day 8 After the duration of distribution has ended,

Alice calls to get reward,

since lastupdate time keeps updating and returning block.timestamp ( block.timestamp< lastupdate+7days) lastupdate was day 1.

there fore at day 8 Alice is still able to withdraw another 100 USD.

Until rewards become zero Alice can keep withdrawing other users shares,

Note even on day 8 if everyone decide to get their rewards the last user's call will revert and alice can continue to make call to take this rewards till the rewards become lesser than they have accumulated.

/**
* @notice Calculates current reward per token
* @return Current reward per token value
*/
@audit >> keeps changing>> function getRewardPerToken() public view returns (uint256) {
if (totalSupply() == 0) {
return rewardPerTokenStored;
}
@audit >> return rewardPerTokenStored + (
@audit >> time keeps changing>> (lastTimeRewardApplicable() - lastUpdateTime) * rewardRate * 1e18 / totalSupply() // bug before timing // now we decide to scale precion loss don happen for front
);
}
/**
* @notice Calculates earned rewards for account
* @param account Address to calculate earnings for
* @return Amount of rewards earned
*/
@audit >> function earned(address account) public view returns (uint256) {
return (getUserWeight(account) *
@audit >> depends on time >> (getRewardPerToken() - userStates[account].rewardPerTokenPaid) / 1e18 // bug dey give reward
) + userStates[account].rewards;
}

see synthetic implementation => https://github.com/Synthetixio/synthetix/blob/d7b5c386ffbd0a53a129cfbd8a683a6a3f62cc36/contracts/StakingRewards.sol#L129-L131.

/* ========== RESTRICTED FUNCTIONS ========== */
function notifyRewardAmount(uint256 reward) external onlyRewardsDistribution updateReward(address(0)) {
if (block.timestamp >= periodFinish) {
rewardRate = reward.div(rewardsDuration);
} else {
uint256 remaining = periodFinish.sub(block.timestamp);
uint256 leftover = remaining.mul(rewardRate);
rewardRate = reward.add(leftover).div(rewardsDuration);
}
// Ensure the provided reward amount is not more than the balance in the contract.
// This keeps the reward rate in the right range, preventing overflows due to
// very high values of rewardRate in the earned and rewardsPerToken functions;
// Reward + leftover must be less than 2^256 / 10^18 to avoid overflow.
uint balance = rewardsToken.balanceOf(address(this));
require(rewardRate <= balance.div(rewardsDuration), "Provided reward too high");
@audit>> lastUpdateTime = block.timestamp;
@audit>> saved >> periodFinish = block.timestamp.add(rewardsDuration);
emit RewardAdded(reward);
}

Impact

A user can claim rewards over the 7 days allotted rewards that they would effectively be stealing another user's reward.

Tools Used

Manual Review

Recommendations

Saved start-time and only calculated Period finish-at based on a fixed timestamp during notification or use synthetics implementation.

Updates

Lead Judging Commences

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

BaseGauge period end time miscalculation creates circular dependency between periodFinish() and lastUpdateTime, preventing periods from naturally ending and disrupting reward distribution

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

BaseGauge period end time miscalculation creates circular dependency between periodFinish() and lastUpdateTime, preventing periods from naturally ending and disrupting reward distribution

Support

FAQs

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