Submitted by LessDupes, also found by SBSecurity, peanuts, guhu95 (1, 2), bill, ilchovski, and RamenPeople
The WithdrawQueue contract allows users to withdraw their funds in various tokens, including liquid staking derivatives (LSDs) such as stETH. The withdraw() function calculates the amount of the specified _assetOut token equivalent to the ezETH being withdrawn using the renzoOracle.lookupTokenAmountFromValue() function. This amount is then stored in the amountToRedeem field of a new WithdrawRequest struct, which is added to the users withdrawRequests array and the tokens claimReserve.
When the user later calls claim(), the contract transfers the amountToRedeem to the user via the IERC20.transfer() function.
However, this implementation does not properly handle rebasing tokens like stETH. The stETH balance of the WithdrawQueue can change between the time a withdrawal is recorded and when it is claimed, even though the contracts stETH shares remain constant.
If the stETH balance decreases during this period due to a rebasing event (e.g., a slashing of the staked ETH), the amountToRedeem stored in the WithdrawRequest may exceed the contracts actual stETH balance at the time of claiming. As a result, the withdrawal can fail or result in the user receiving a larger share of the total protocol reserves than intended.
The issue can be illustrated by comparing the behavior of withdrawals for non-rebasing and rebasing LSDs:
The current withdrawal mechanism for rebasing tokens like stETH can lead to:
We can validate the vulnerability through a Foundry test case POC. This test case will simulate the exploit scenario and confirm the issue by performing the following actions:
The PoC can be run in Foundry by using the setup and mock infra provided here.
To address the issue of unfair distribution of funds when withdrawing rebasing tokens like stETH, the WithdrawQueue contract should store and transfer the users withdrawal as stETH shares instead of a fixed stETH amount.
When a user initiates a withdrawal with stETH as the _assetOut, the contract should convert the calculated amountToRedeem to stETH shares using the stETH.getSharesByPooledEth() function:
The resulting sharesAmount should be stored in the WithdrawRequest struct instead of the amountToRedeem.
When the user calls claim(), the contract should transfer the stETH shares directly to the user using the stETH.transferShares() function:
By storing and transferring stETH shares instead of a fixed stETH amount, the contract ensures that each user receives their fair share of the stETH balance, regardless of any rebasing events that occur between the time of the withdrawal request and the claim.
To implement this mitigation, the contract should:
Note that this mitigation is specific to stETH and may need to be adapted for other rebasing tokens that use a similar shares-based system.
Furthermore, the claimReserve and withdrawalBufferTarget for stETH would also need to be stored in shares and converted to underlying in TVL and withdraw buffer calculations, respectively.
alcueca (judge) commented:
jatinj615 (Renzo) acknowledged
