Platform: Code4rena
Start Date: 16/12/2021
Pot Size: $100,000 USDC
Total HM: 21
Participants: 25
Period: 7 days
Judge: alcueca
Total Solo HM: 12
Id: 66
League: ETH
Rank: 6/25
Findings: 2
Award: $4,781.66
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: kenzo
Also found by: UncleGrandpa925
UncleGrandpa925
Users' rewards in Wrapped JLP will be miscalculated & lost. Every interaction with WJLP (wrap, unwrapFor...) will trigger the bug.
Function _userUpdate
in WJLP.sol
So the nature of this WJLP is simply a wrap of the JLP, and it itself maintains certain information regarding the rewards that each user is entitled to. This information must be updated whenever there is a change in balances of any users or any users who want to redeem their JOE rewards. The function that was used to do this is _userUpdate
This function has a very similar implementation to how TraderJoe (MasterchefV2) did it, but there is a subtle but significant difference: Whenever TraderJoe updates the pending
and rewardDebt
information, they always immediately transfer out all pending rewards. However, in YETI's contracts, the rewards is not transferred out immediately but instead accumulated and only transfer when the user calls claimReward
.
As such, this leads to a miscalculation of users' rewards, and eventually, users will lose their rewards.
Line 252 of WJLP.sol
, the unclaimedJOEReward
should be accumulated (+=
) instead of just reassigning like TraderJoe, OR the reward should be transferred out immediately.
For simplicity, I will ignore the 1e12 division.
Let's consider a new user who has 0 WJLP.
Let's denote the action of calling wrap(X, msg.sender, msg.sender, msg.sender)
as deposit X
At T0 user.amount = 0, accJoePerShare=1, user.rewardDebt = 0
At T1 (a while after T0): user deposits 100 JLP
At T2 (a while after T1): user deposits 100 JLP
=> If user claims reward here, he will get 100
At T3 (shortly after T2): user deposits 100 JLP
=> If the user claims reward here, he will get 2, so he lost 98 units of rewards and it's not recoverable.
To mitigate, either change the formula of line 252 to user.unclaimedJOEReward = unclaimedJOEReward.add(user.amount.mul(accJoePerShare).div(1e12).sub(user.rewardDebt));
, or simply do it as JOE, transfer out all the accuredReward at every updates
#0 - kingyetifinance
2022-01-07T07:55:11Z
Duplicate #141
🌟 Selected for report: cmichel
Also found by: UncleGrandpa925
UncleGrandpa925
Users' rewards in Wrapped JLP will be miscalculated. Hackers can exploit this to steal users' rewards. All WJLP's unwrapFor transactions will trigger the bug.
Function unwrapFor
in WJLP.sol
So the nature of this WJLP is simply a wrap of the JLP, and it itself maintains certain information regarding the rewards that each user is entitled to. This information must be updated whenever there is a change in balances of any users or any users who want to redeem their JOE rewards. The function that was used to do this is _userUpdate
As such, it can be observed that _userUpdate
was called in wrap
(line 136, right before mint
). However, no _userUpdate
was called in unwrapFor
(line 164 there is nothing). Therefore, whenever users unwrap assets, post unwrapping, they will still receive the rewards as if they have never unwrapped.
As such, this leads to a miscalculation of users' rewards. Also, since the rewardData is not updated when unwrapFor is called, hackers can do a flashloan to pretend to wrap a huge amount of assets, the immediately unwrap it. He then will be entitled to almost all rewards available (Example below).
For simplicity, I will ignore the division to 1e12. Let's consider a new user who has 0 WJLP.
At T0 user.amount = 0, accJoePerShare=1, user.rewardDebt = 0
At T1
user wrap(1000000, msg.sender, msg.sender, msg.sender)
the user immediately unwrapFor(msg.sender,1000000)
With the current code, user.unclaimedJOEReward, user.amount, user.rewardDebt won't be changed at all
As such, after the unwrap is completed,
Note that at this point, the user still has 0 WJLP
At T2 (long after T1)
user wrap(1, msg.sender, msg.sender, msg.sender)
So, user just deposit 1 unit of JLP in and he receives the rewards for whatever total amount he has deposited earlier.
To mitigate, just add the missing _userUpdate
to line 164.
#0 - kingyetifinance
2022-01-07T07:57:47Z
Duplicate #209 . This is intended behavior but due to it being implemented not properly in borrowerOperations withdraw collateral, it is a bug still where users will not get their rewards updated in calls from this function.