The getRewardPerToken function in the reward distribution mechanism calculates rewards based on the time elapsed since the last update (lastTimeRewardApplicable() - lastUpdateTime). If no time has passed (e.g., block.timestamp is equal to lastUpdateTime), the rewards calculated will be zero. This results in no rewards being distributed to users, even if they have staked tokens and are eligible for rewards.
This also allows users who claim rewards earlier to receive disproportionately more rewards, even if they have staked fewer tokens. This occurs due to the way rewardPerTokenStored is updated when a user calls getReward(). As a result, users who claim rewards later receive fewer rewards, leading to an unfair distribution of rewards.
The issue arises because the getRewardPerToken function relies on the time elapsed since the last update to calculate rewards. Specifically:
lastTimeRewardApplicable(): Returns the current timestamp (block.timestamp) if it is less than the periodFinish time, otherwise returns periodFinish.
lastUpdateTime: The timestamp of the last reward update.
If block.timestamp is equal to lastUpdateTime (i.e., no time has passed since the last update), the term (lastTimeRewardApplicable() - lastUpdateTime) becomes zero. As a result, the reward calculation returns rewardPerTokenStored, and no additional rewards are accrued. This is exactly what happens when raacGauge::notifyRewardAmount is called. The lastUpdateTime is updated to the current timestamp.
Another issue arises because the getReward() function updates the rewardPerTokenStored value for the calling user, but this update affects the reward calculations for all users. Specifically:
getReward() Function:
When a user calls getReward(), the _updateReward function is invoked, which updates the rewardPerTokenStored value.
This update is based on the current time and the rewardRate, and it affects the reward calculations for all users. As a result, if user1 calls getReward() before user2, the rewardPerTokenStored value is updated to reflect the rewards accrued up to that point.
When user2 later calls getReward(), the rewardPerTokenStored value has already been updated, and user2 receives fewer rewards than they should, even if they have staked more tokens.
Example Scenario:
user1 stakes 100 tokens.
user2 stakes 200 tokens.
Rewards are notified, and time passes.
user1 calls getReward() first, updating rewardPerTokenStored.
user2 calls getReward() later and receives fewer rewards than they should, even though they have staked more tokens.
These tests were run in the RAACGauge.test.js file in the "Reward Distribution" describe block
These are the logs from each test:
Unfair Reward Distribution: Users who claim rewards earlier receive disproportionately more rewards, even if they have staked fewer tokens. This undermines the fairness of the protocol and discourages users from participating. It also creates incentives to game the system and can create gas wars to be first to call raacGauge::getReward after an amount of time has elapsed. A user can also unfairly dilute another user's rewards by front running their raacGauge::getReward call.
Economic Inefficiency: The protocol's reward distribution mechanism becomes inefficient, as it does not accurately reflect the contributions of users based on their staked amounts.
Manual Review, Hardhat
To fix this issue, the reward distribution mechanism should be modified to ensure that rewards are distributed fairly based on the staked amounts of users, regardless of when they claim their rewards. This can be achieved by:
Separate Reward Tracking
Track rewards for each user separately, without updating the global rewardPerTokenStored value when a user claims rewards.
This ensures that the reward calculations for one user do not affect the reward calculations for other users.
Ensuring that the earned function calculates rewards based on the user's staked amount and the time elapsed since the last update, without being affected by other users' actions.
The contest is live. Earn rewards by submitting a finding.
This is your time to appeal against judgements on your submissions.
Appeals are being carefully reviewed by our judges.