Open code423n4 opened 1 year ago
mattt21 marked the issue as sponsor disputed
mattt21 marked the issue as disagree with severity
mattt21 marked the issue as sponsor confirmed
Can the sponsor explain why they disagree with the severity?
The issue is valid in my opinion. The desired behavior from this line seems to be that rewards not distributed because there is no stakers are added to the rewards per seconds distributed when there are some. The fact that the first next staker could take all pending rewards is a bug.
An other mitigation could be to reset firstStakeTime
and replicate the initial behavior if at some point totalStake()
goes back to 0.
Picodes marked the issue as satisfactory
@Picodes Can you please have a look at L-07 from my QA report (https://github.com/code-423n4/2023-03-mute-findings/blob/main/data/HollaDieWaldfee-Q.md).
It talks about the same scenario where a user can get rewards without actually staking.
I did remark there that the sponsor should have a look and decide if they want to fix it. I did not see an immediate necessity to fix it and also had a thought there that the current behavior may be beneficial in some sense because it entices users to stake
@HollaDieWaldfee100 Yea that's the same issue, and you have brought up a fair point.
Since you didn't mention in your report, I'm not sure if you are aware that you can get this extra reward without staking (by staking and immediately withdrawing), and this is what made me believe that this behaviour is unintended. Furthermore, this vuln is exploitable as soon as the staking period starts since firstStakeTime doesn't actually do the job it's supposed to (someone can stake and immediately withdraw to set it early)
But I agree that the sponsor should have a say in this.
Picodes marked the issue as primary issue
Picodes marked the issue as selected for report
Indeed @HollaDieWaldfee100 your issue is a valid duplicate. In the absence of comment from the sponsor I'll keep Med severity, considering it isn't the desired behavior.
Lines of code
https://github.com/code-423n4/2023-03-mute/blob/main/contracts/amplifier/MuteAmplifier.sol#L95-L118
Vulnerability details
Proof of Concept
Consider the update modifier for the amplifier.
Suppose there's been a period with totalCurrentStake = 0, and a user stakes and immediately withdraws in the same transaction. When the user stakes, update doesn't do anything (including updating _mostRecentValueCalcTime) since totalCurrentStake = 0, and _userWeighted[account] gets set to _totalWeight (which hasn't been updated) in the stake function. https://github.com/code-423n4/2023-03-mute/blob/main/contracts/amplifier/MuteAmplifier.sol#L349
When the user withdraws, totalCurrentStake is no longer zero. Since _mostRecentValueCalcTime wasn't updated when the user staked (since totalCurrentStake was 0), the reward from the period with no stakers gets added to _totalWeight.
uint256 sinceLastCalc = block.timestamp.sub(_mostRecentValueCalcTime);
value = sinceLastCalc.mul(perSecondReward);
_totalWeight = _totalWeight.add(value.mul(10**18).div(totalCurrentStake));
(These are lines in the update modifier)As a result, this user who staked and immediately withdrew, got all the reward from the period with no stakers. See the reward calculation: https://github.com/code-423n4/2023-03-mute/blob/main/contracts/amplifier/MuteAmplifier.sol#L371
Observe that the exploit condition is met as soon as the staking period starts (as long as nobody stakes immediately). The code attempts to prevent this situation setting _mostRecentValueCalcTime to firstStakeTime the first time that update is invoked (which must be a stake call). https://github.com/code-423n4/2023-03-mute/blob/main/contracts/amplifier/MuteAmplifier.sol#L89-L91
However, this doesn't do anything since the user can first set the firstStakeTime by staking as soon as the staking period starts, and then make totalCurrentStake 0 by immediately withdrawing. In fact, observe that this takes away from other staker's rewards since perSecondReward is now lower. https://github.com/code-423n4/2023-03-mute/blob/main/contracts/amplifier/MuteAmplifier.sol#L98
uint256 perSecondReward = totalRewards.div(endTime.sub(firstStakeTime));
Please add the following test to the "advance to start time" context in amplifier.ts (add it here), and run with
npm run test-amplifier
.Result on my side:
This POC is a bit on the extreme side to get the point across. In the first test, the user stakes and then immediately unstakes, while in the second test, the user stakes for the entire period. In the end, the user gets roughly the same amount of reward.
Impact
After periods with no stakers, users can get reward without staking. This is also possible at the beginning of the staking period, and doing so then will reduce the reward for other users in the process.
Tools Used
Manual review, Hardhat
Recommended Mitigation Steps
Possible solution 1: set a minimum duration that a user must stake for (prevent them from staking and immediately withdrawing) Possible solution 2: always update _mostRecentValueCalcTime (regardless totalCurrentStake). i.e. move the following line out of the if statement. https://github.com/code-423n4/2023-03-mute/blob/main/contracts/amplifier/MuteAmplifier.sol#L109
Keep in mind that with solution 2, no one gets the rewards in periods with no stakers - this means that the rescueTokens function needs to be updated to get retrieve these rewards.