Closed sherlock-admin4 closed 5 months ago
2 comment(s) were left on this issue during the judging contest.
0xmystery commented:
invalid because user.rewardSettled is universally linked to pool.accPointsPerShare
0xreadyplayer1 commented:
The Watson has clearly demonstrated definite loss of funds scenario - that's why i belive the issue is a valid high according to sherlock rules.
cryptic
high
Rewards are not distributed correctly, causing users to lose rewards
Summary
Rewards are calculated based off the number of blocks users have deposited for. The problem is that the
lastRewardBlock
is updated globally for all users rather than for individual users, causing rewards to be distributed incorrectly.Vulnerability Detail
Let's assume Bob and Alice each have deposited
5e18 wstETH
into thewstETH
pool.Assume the following values:
pool.allocPoint = 1000
totalAllocPoint = 10000
wstETH pool.amount = 10e18 (5e18 each)
pointsPerBlock = 2e18
pool.accPointsPerShare = 2e18
user.rewardSettled == user.rewardDebt
Assume
10 blocks
have passed since both users have deposited their tokens. Bob decides to withdraw his5e18 wstETH
.SophonFarming.sol#L699-L742
Firstly, the pool accounting is updated.
SophonFarming.sol#L411-L435
Note that at the end of the function,
pool.lastRewardBlock
is set to the currentblock.number
. Since rewards are distributed based off how many blocks users have deposited for, this will make the next user lose a majority of their rewards. The correct solution is to update thelastRewardBlock
for individual users, rather than for all users at once.Continuing with the example:
Recall that
10 blocks
have passed, thereforeblockMultiplier = 10e18
:SophonFarming.sol#L344-L346
We have
pointReward = 10e18 * 2e18 * 1000 / 10000 = 2000000000000000000000000000000000000
=>pool.accPointsPerShare = 2000000000000000000000000000000000000 / 10e18 + 2e18 = 2200000000000000000
Now, back to withdraw function.
user.rewardSettled = userAmount * pool.accPointsPerShare / 1e18
=>user.rewardSettled = 5e18 * 2200000000000000000 / 1e18 = 11000000000000000000
Bob's reward points are settled at
11e18
.Assume 1 block has passed (total of
11 blocks
since Alice's first deposit), now Alice decides to withdraw her5e18 wstETH
.Again, a call to
updatePool()
first.Since
pool.lastRewardBlock
was just updated 1 block ago during Bob's withdrawal,blockMultiplier
is now1e18
. This is incorrect, as Alice has deposited for 11 blocks, which should give ablockMultiplier
value of11e18
.The problem was that when Bob called withdraw, the
pool.lastRewardBlock
was updated to theblock.number
at that time, and theblockMultiplier
is calculated based off how many blocks have passed sincepool.lastRewardBlock
.The changes to
lastRewardBlock
in that case should have only applied to Bob, not Alice. Now Alice only gets rewarded for depositing for1 block
, rather than the full11 blocks
, whereas Bob got rewarded for the full10 blocks
.Alice in this case suffers and loses rewards.
Impact
Loss of rewards for users.
Code Snippet
https://github.com/sherlock-audit/2024-05-sophon/blob/main/farming-contracts/contracts/farm/SophonFarming.sol#L699-L742
https://github.com/sherlock-audit/2024-05-sophon/blob/main/farming-contracts/contracts/farm/SophonFarming.sol#L411-L435
https://github.com/sherlock-audit/2024-05-sophon/blob/main/farming-contracts/contracts/farm/SophonFarming.sol#L344-L346
Tool used
Manual Review
Recommendation
The recommendation is to update the
lastRewardBlock
for individual users, for example:Of course, this is a change that would require reimplementation of many of the core functionalities. But this way, users will have the correct
lastRewardBlock
updated for their respective reward. Coming back to the example from above, after Bob's withdrawal, Alice'slastRewardBlock
would correctly be 11 blocks ago and only Bob'slastRewardBlock
would be 1 block ago.