Closed sherlock-admin2 closed 3 months ago
This is a limitation of the Synthetix staking contract. As per the rules, "Any issue that exists in the original non-Maker Syntetix staking rewards contract is out of scope." In practice, the total supply of any of the tokens used as staking token is going to be less than 10^12 10^18 and the reward rate (a value that is adjustable by governance) is going to be (much) greater than 10^18/(243600), hence even if the update occurs every block (12 s), the rewardPerTokenStored will not stop increasing.
chaduke
High
Loss of rewards due to frequent call of modifier
updateReward()
.Summary
The code will be deployed on Ethereum. As a result, the average block time is 12 seconds, resulting in five blocks per minute. Given the huge number of users in Makerdao and five functions in
StakingRewards
that has the modifierupdateReward()
, the chance that there is someone who will call a function with theupdateReward()
per block is pretty high (only need five users per minute). Therefore, the tokens to be claimed might be a small value (accumulated in 12 seconds), due to rounding down error, this might lead to zero increase ofrewardPerTokenStored
, loss of rewards for all users. This will occur more frequently when _supply becomes larger, as the number of users increases.The loss of rewards has high probability to occur by itself (zero cost) and affect each participant (might near 100% loss). Therefore, I mark this finding as high.
Root Cause
The accumulation of rewards is performed by
updateReward()
, a modifier for the five reward-related functions.https://github.com/sherlock-audit/2024-06-makerdao-endgame/blob/main/endgame-toolkit/src/synthetix/StakingRewards.sol#L191C2-L199C6
updateReward()
callsrewardPerToken()
to calculate the newrewardPerTokenStored
:https://github.com/sherlock-audit/2024-06-makerdao-endgame/blob/main/endgame-toolkit/src/synthetix/StakingRewards.sol#L84-L90
However, if
updateReward()
is called too often, then due to round down error,(((lastTimeRewardApplicable() - lastUpdateTime) * rewardRate * 1e18) / _totalSupply)
might be zero, leading to no increase forrewardPerTokenStored
and the loss of rewards for participants (rewardPerTokenStored
is updated at L192).Internal pre-conditions
_totalSupply
is a large number, will contribute easier loss of rewards due to easier rounding down to zero error.External pre-conditions
Frequent call of any function with the modifier
updateReward()
, which is expected due to the large number of MakerDao users and only five blocks per minute for Ethereum and the five functions in the RewardsStaking contract.Attack Path
No malicious code needs to be deployed. It will occur by itself when the user base increases and the
_totalSupply
increases, leading to HIGH probability of more frequent call of a function (five of such functions) with the modifierupdateReward()
.Impact
Loss of rewards due to frequent call of any function with the modifier
updateReward()
.PoC
consider the case of using NGT/NST as rewards/stake tokens, both of them have18 decimals.
Suppose_totalSupply = 100, 000, 000 ether and the amount of NGT rewards claimed
(((lastTimeRewardApplicable() - lastUpdateTime) * rewardRate
is smaller than 100,000, 000, say 99,999,999.Then, we have
99,999,999 * e18 / 100, 000, 000 * e18 = 0
. Therefore, no increase forrewardPerTokenStored
, the rewards accumulated are lost for all.In the following, we show that:
1) we set the total amount of reward tokens to be result.tot = 1.5 ether / 100000. 2) Bob stakes 100,000,000 ether staking tokens, as a result we have _totalSupply = 100000000000000000000000000; 3) rewardRate: 8267195 4) Each 12 seconds, we only accumulate 99206340 reward tokens; 5) After 12 seconds, when Bob calls getReward(), he gets no rewards at all! This is due the the rounding down error, and the accumulated reward of 99206340 are lost due to no increase of
rewardPerTokenStored
. 6) runforge test --match-test testReward1 -vv
. 7) Install the Dsstest package under lib from https://github.com/makerdao/dss-vest. 8) change result.tot = 1.5 ether / 100000.Mitigation
Manage
dustRewards
as those accumulated rewards that have not been accounted for inrewardPerTokenStored
. In this way, no accumulated rewards will be lost even they are small.