Closed sherlock-admin2 closed 1 month ago
It isn't clear if the submission is referring to the dilution of stakers due to large stakers entering the farm or to an incorrect calculation of rewards. If the former, this is by design and is intrinsic to how the farm operates. If the latter, it seems the submitter may be confused as to what the rewardPerToken() represents. It is a global normalized reward accumulator that increases more slowly during periods of high total supply (high dilution) only to increase again at a faster rate once the total supply has reduced. There is nothing incorrect about multiplying that value by a staker's current balance to calculate the accumulated reward of a staker.
Squilliam
High
Malicious stakers will cause loss of reward funds for honest participants
Summary
The flawed reward calculation in the
StakingRewards
contract, which fails to account for time-weighted average stakes, will cause a significant loss of reward tokens for honest stakers. This flaw enables malicious users to briefly stake large amounts, withdraw most of their stake, and still earn rewards as if they had maintained their maximum stake throughout the entire period. Malicious actors can repeat this process over time and on different wallet addresses to claim disproportionately large rewards while maintaining minimal long-term stake.Root Cause
In
StakingRewards.sol
, theearned()
andrewardPerToken()
functions do not correctly account for changes in a user's staked amount over time. The reward calculation is based on the current stake multiplied by the difference inrewardPerToken()
since the last update, rather than considering the time-weighted average of the user's stake. This means that:ewardPerToken()
:The calculation uses the current _totalSupply, not accounting for how it might have changed over the period.
earned()
:This multiplies the current balance by the entire change in rewardPerToken(), even if the user's balance changed during that period.
This approach allows users to benefit from high reward rates achieved during periods of large stakes, even after they've withdrawn most of their stake.
StakingRewards::earned()
: https://github.com/sherlock-audit/2024-06-makerdao-endgame/blob/main/endgame-toolkit/src/synthetix/StakingRewards.sol?=plain#L92-L94StakingRewards::rewardPerToken()
: https://github.com/sherlock-audit/2024-06-makerdao-endgame/blob/main/endgame-toolkit/src/synthetix/StakingRewards.sol?=plain#L84-L90Internal pre-conditions
The StakingRewards contract needs to be deployed and active. The reward pool needs to be funded with a significant amount of reward tokens. The staking period needs to be ongoing.
External pre-conditions
None specific to this vulnerability.
Attack Path
Impact
This vulnerability leads to a significant loss of funds for honest users and undermines the integrity of the staking mechanism:
Direct Loss of Rewards(Funds) for Honest Users: Honest stakers receive fewer reward tokens(funds) than they should based on their stake and duration. The difference between expected rewards in a fair system and actual received rewards represents a direct loss of funds for honest participants. This loss is quantifiable in terms of the reward tokens not received that they should of received.
Cumulative Financial Impact: Over time, the loss for long-term stakers can be substantial. The consistent underpayment of rewards compounds, resulting in significant financial losses for dedicated participants.
Market Pressure: If widely exploited, the vulnerability could lead to increased selling pressure on the reward token as exploiters claim and potentially sell their disproportionate rewards. This could indirectly lead to a decrease in the reward token's value, further impacting honest users' returns.
Undermining Token Supply Reduction: The vulnerability negates the intended reduction of circulating supply. Users can stake tokens, quickly withdraw and sell them, while still earning rewards. This increases token velocity instead of reducing it, potentially leading to: Higher price volatility contrary to the protocol's stability objectives Increased selling pressure on the token - Increased selling pressure causes losses for the protocol, dev team, investors, and stakers.
Reasoning for High Severity Rating:
Assuming a user stakes 10,000 MKR (worth $10 million at $1000/MKR) for a month, expecting 1% rewards (100 MKR or $100,000). Due to this vulnerability, if exploiters collectively manipulate the pool such that honest users only receive 50% of their expected rewards, this user would lose 50 MKR ($50,000), which is a 50% loss on their expected rewards and 0.5% of their total staked value. This loss significantly exceeds the 5% threshold for high severity.
This vulnerability can be exploited continuously throughout the staking period.
Given the assumed $250 million maximal locked MKR, if this vulnerability leads to a 10% reduction in staking participation due to lost trust and reduced rewards for honest participants, it would result in a $25 million loss in locked value for the protocol, far exceeding the 5% threshold for high severity.
This vulnerability is inherent in the reward calculation mechanism and can be exploited under normal operating conditions, without requiring any specific external factors or unusual states.
PoC
Add the following test functions to
StakingRewards.t.sol
, in theStakingRewardsTest
contract:Run these tests with the following:
forge test --mt testPartialWithdrawalsAndRewardCalculations -vvv
forge test --mt testDetailedPartialWithdrawalsAndRewardCalculations -vvv
forge test --mt testRootCauseOfPartialWithdrawalVulnerability-vvv
forge test --mt testMultiplePartialWithdrawals -vvv
forge test --mt testExtremeWithdrawal -vvv
These tests reveal the following about the vulnerability:
testPartialWithdrawalsAndRewardCalculations
: This test failed with an assertion error, indicating that the earned rewards after a partial withdrawal were not less than double the rewards earned before the withdrawal, as expected. This suggests that the reward calculation is not correctly adjusting for partial withdrawals.testDetailedPartialWithdrawalsAndRewardCalculations
: This test provides more detailed logging, and it clearly shows the issue: Before withdrawal (mid-duration), the earned amount was 499999999999999867200. After the full duration, even with half the stake withdrawn, the earned amount was 999999999999999734400. This is exactly double the mid-duration amount, despite having only half the stake for the second half of the duration.testRootCauseOfPartialWithdrawalVulnerability
: This test passes, but it demonstrates the vulnerability: Two identical farms are set up, but one (rewardsB) has a partial withdrawal midway. At the end of the duration, both farms show the same earned amount (999999999999999734400). Both farms claim the same reward amount, despite rewardsB having half the stake for half the duration.testMultiplePartialWithdrawals()
: This test passed, but it reveals the vulnerability. User B, who made multiple partial withdrawals, still earned 89.8% of what User A earned (who maintained their full stake). This is much higher than expected, given that User B had less stake for a significant portion of the time. It shows that the contract is not correctly adjusting rewards for partial withdrawals.testExtremeWithdrawal()
: This test result clearly demonstrates the vulnerability in theStakingRewards
contract: User A initially staked a large amount and then immediately withdrew almost all of it, leaving only a tiny stake. User B staked only a tiny amount for the entire duration. Both users ended up with exactly the same rewards (999997000008999).The vulnerability lies in how the contract calculates rewards after partial withdrawals. It appears that when a user partially withdraws their stake, the contract does not properly adjust the reward calculation. As a result:
Mitigation
Implement one or more of the following:
Implement Time-Weighted Staking: Modify the reward calculation mechanism to use a time-weighted average of a user's stake. Track each user's stake changes over time, storing timestamps and amounts for each stake and withdrawal action. Update the earned() function to calculate rewards based on the time-weighted average stake rather than the current stake.
Introduce Reward Vesting: Implement a vesting period for rewards to discourage short-term staking behavior. Rewards earned could be released linearly over a set period (e.g., 30 days) after they are accrued. Early withdrawal of stakes could result in forfeiting unvested rewards or some type of penalty.
Snapshots for Reward Calculations: Implement a system of periodic snapshots of user stakes. Calculate rewards based on these snapshots rather than instantaneous balances. This approach can help mitigate the impact of rapid stake/unstake actions.
Dynamic Reward Rate Adjustment: Implement a mechanism that adjusts the reward rate based on the total staked amount and its stability over time. This could help balance rewards when there are significant fluctuations in the staking pool.
Incremental Reward Distribution: Instead of accumulating rewards and allowing claims at any time, distribute rewards incrementally (e.g., daily or weekly). This approach can help ensure that rewards more accurately reflect a user's stake over time.
Two-Tiered Staking System: Create two staking tiers: a flexible tier with lower rewards and a locked tier with higher rewards. The locked tier would require tokens to be staked for a minimum period, enforcing long-term participation for maximum rewards.