The PriorityPool
directs user requests to WithdrawPool if queued assets in it are unable to cover the withdrawal amount. When WithdrawalPool
gets assets through deposit()
function, it triggers _finalizeWithdrawals
to cover the queued requests and pushes a WithdrawalBatch into withdrawalBatches which is then used to cover the withdrawals of a user. The WithdrawBatch has the following properties,
indexOfLastWithdrawal
stakePerShares
The issue lies in the stakePerShares
that it stores. This represents the the exchange rate of LSTs per underlying shares
at the time of this batch.
The protocol clearly states that lst is a rebasing token so the shares should be worth more tokens after each rebase and
new value is reflected in their token balance. The rebasing can happen any time and the amount user withdraws CAN GO UP OR DOWN depending on the TIMING of when the withdraw()
is triggered BECAUSE the RATE CAN CHANGE depending on the current totalShares and totalStaked that StakingPool keeps track of and user SHOULD get amount based on the current rate at which withdraw()
takes place, NOT at the rate when WithdrawPool has enough assets to cover the requests due to assets entering the pool through deposit(), or leaving pool, or by some other means.
In WithdrawPool::_finalizeWithdrawals
, the rate is cached,
This old outdated rate is used directly in WithdrawPool::withdraw()
,
If the stakePerShares value is cached at the time of batch, the requests WithdrawalBatch covers gets their tokens at this rate no matter when the withdraw()
gets triggered. So if withdraw()
is called at a later date, users would potentially get tokens at the rate fixed in WithdrawalBatch, not the current rate, resulting in users getting more or less tokens than intended.
PriorityPool queues request of UserA for 50 LINK (asset token) in WithdrawalPool.
UserB deposits 50 LINK via PriorityPool::deposit() and it gets deposited to WithdrawPool because there are queued requests there. This calls _finalizeWithdrawals
internally and WithdrawBatch is created since the deposit fully covers the queued amount. Now the current rate (stakePerShares) is 2e18 per shares and it is cached in WithdrawBatch.
UserC donates 300 tokens and the rate rises to 2003e15 per shares
UserA calls WithdrawPool::withdraw()
and gets tokens at 2e18 instead of 2003e15.
Tokens UserA receives: 50e18
Tokens UserA should've received: 50075000000000000000
UserA is robbed of 75e15 amount.
The following functions are added in WithdrawPool for testing purposes only. They're used in test added as Proof-Of-Code.
Place the following test in withdraw-pool.test.ts
Users receive tokens at the outdated rate instead of the current rate, depriving them of potential token gains.
Manual Review & Hardhat testing.
Consider one of the following,
Remove stakePerShares property from WithdrawalBatch and using current rate when withdraw() is triggered.
2- Raise totalShares when tokens are donated.
The contest is live. Earn rewards by submitting a finding.
This is your time to appeal against judgements on your submissions.
Appeals are being carefully reviewed by our judges.