The getRewardPerToken
function in the BaseGauge contract contains three critical flaws that together result in unfair and inaccurate reward calculations:
First Depositor Advantage: When no tokens are staked, rewardPerTokenStored
is zero. Thus, the very first depositor locks in a baseline of zero, which can lead to disproportionately high rewards relative to later depositors.
Outdated Total Supply Usage: The reward calculation uses the total supply from before the current deposit or withdrawal is applied. This means that when a user deposits a large amount, the current transaction is not factored in, leading to an overestimation of rewards for early depositors and a lower reward per token for later deposits.
Failure to Clear Outdated Reward State: After a reward period expires, the accumulated rewardPerTokenStored
is never cleared or reset. Consequently, users who maintain their stake across multiple periods continue to benefit from the outdated, higher reward per token, even when they should not be accruing additional rewards.
Flaw 1: First Depositor Advantage
When the contract's total supply is zero, getRewardPerToken
simply returns rewardPerTokenStored
(which is initially 0). The first depositor, therefore, starts with a very low baseline for userStates[account].rewardPerTokenPaid
. Since subsequent rewards are calculated as the difference between the current getRewardPerToken()
and the stored baseline, the first depositor effectively receives a higher reward per token compared to later depositors.
Flaw 2: Outdated Total Supply Usage
The reward per token is computed as:
However, the totalSupply() used in this calculation is from before the current stake or withdrawal is processed (since state updates occur later). For example, if a user deposits a large amount, the calculation uses the previous total supply, neglecting the new tokens, thus resulting in an inflated reward per token. This unfairly benefits users depositing earlier and penalizes those depositing later.
Flaw 3: Failure to Clear Outdated Reward State
There is no mechanism in place to reset or clear rewardPerTokenStored after a reward period expires. As a result, if a user stays staked across multiple reward periods without an intervening withdrawal, they continue to earn rewards based on an outdated (and likely inflated) rewardPerTokenStored value. This causes rewards from past periods to carry over, allowing users to accrue more rewards than intended over time.
Unfair Reward Distribution:
Early depositors secure an unfair advantage by locking in a lower reward baseline and benefiting from an outdated total supply calculation. This leads to significant disparities in reward allocation.
Economic Imbalance:
The combination of an inflated reward rate for early users and continued accrual from previous periods can result in substantial misallocation of rewards, undermining the protocol’s intended economic incentives.
Discouragement of New Depositors:
New users may be discouraged from staking if they perceive that the reward mechanism is biased toward early depositors, potentially reducing overall participation in the system.
First Depositor Advantage:
User A deposits 100 tokens when totalSupply() is 0, so rewardPerTokenStored is 0.
User A's reward baseline is set at 0.
As rewards start accumulating, User A benefits from a very high differential because the baseline is so low.
Outdated Total Supply Usage:
User B deposits 100 tokens after User A, when totalSupply() is 100.
The reward calculation uses 100 (the pre-deposit supply) instead of 200, resulting in a lower incremental reward per token for User B.
Failure to Clear Reward State:
User A remains staked across multiple reward periods.
Since rewardPerTokenStored is not reset after each period, User A continues to accrue rewards based on an outdated and high reward per token value, further amplifying their advantage.
Note: I am not able to provide a complete detailed explanation here, but if given the chance, I will explain each flaw one by one with a proof-of-concept or examples as needed.*
Manual Code Review: We analyzed the getRewardPerToken function, along with related state update mechanisms in the _updateReward modifier and stake/withdraw functions, to identify how the timing of state updates and total supply calculations lead to these flaws.
Address First Depositor Advantage:
Initialize rewardPerTokenStored appropriately or adjust the reward baseline for the first depositor immediately upon their deposit, so that the baseline reflects a fair starting point.
Update Total Supply Usage:
Modify the order of operations so that the total supply used in the reward calculation includes the current transaction. This might involve updating the user's stake and the global total supply before calling the reward update logic, ensuring that new deposits are properly factored into the calculation.
Reset Reward State After Period Expiry:
Implement a mechanism to clear or reset rewardPerTokenStored after the reward period ends. This ensures that rewards from past periods do not inadvertently accumulate and inflate future rewards.
The contest is live. Earn rewards by submitting a finding.
This is your time to appeal against judgements on your submissions.
View preliminary resultsAppeals are being carefully reviewed by our judges.
The contest is complete and the rewards are being distributed.