The values used in the deposit calculation are also not affected by any pending withdrawals, but the current TVL and the current ezETH supply are used.
Suppose the value of the collateral token to be withdrawn has deviated from the average collateral value, i.e. the price of ezETH, since the withdrawal request was made. If it is greater than the average then upon claim() correspondingly more value will be removed from the TVL than ezETH burned from the total supply, and the price of ezETH will decrease. Conversely, if the value of the collateral token has become less than the average of the collateral values then the price of ezETH will increase.
This can be abused in all kinds of ways. It gives an attacker some measure of control over the price of ezETH. He can make withdrawal requests and then choose to claim them such that the price is manipulated in his favour, depending on the integrations with Renzo. The attacker can frontrun another claim (not necessarily his own) with a deposit such that he experiences an immediate profit from the price movement.
For example, let's say 2 ezETH has been minted against 1 tokA and 1 tokB, and that all current prices are 1. Suppose 0.5 ezETH is requested to be withdrawn against tokA. This will schedule the burning of 0.5 ezETH and the transfer of 0.5 tokA. Suppose then the price of tokA increases to 1.1 and the price of tokB increases to 1.25. Before claim() the price of ezETH is (1.1 + 1.25) / 2 = 1.175, but after claim() the price would be (0.55 + 1.25) / 1.5 = 1.2. Now, if an attacker deposits 1 ETH's worth (of any token) just before claim() is called, he will receive 1 / 1.175 ≈ 0.8512 ezETH. After claim() the price will then be (0.55 + 1.25 + 1) / 2.3512 ≈ 1.1910. Immediately withdrawing this again would give him 0.8512 * 1.1910 ≈ 1.014 ETH worth of collateral.
Note that by sandwiching the claim() with a deposit and withdrawal the attacker was able to reduce the price of ezETH (up to future additional price changes) and extract a profit.
The more volatile the collateral tokens, and especially if they move in opposite directions, the more profit can be extracted.
Recommended Mitigation Steps
Consider calculating the amount of collateral token to be transferred at the time of claim() instead. It could then be possible to also choose which token to withdraw from in claim(). That is, a withdraw request would simply be a request to withdraw an amount of ezETH, with the rest to be decided upon claim().
Lines of code
https://github.com/code-423n4/2024-04-renzo/blob/519e518f2d8dec9acf6482b84a181e403070d22d/contracts/Withdraw/WithdrawQueue.sol#L246 https://github.com/code-423n4/2024-04-renzo/blob/519e518f2d8dec9acf6482b84a181e403070d22d/contracts/Withdraw/WithdrawQueue.sol#L299-L309
Vulnerability details
Impact
Claiming withdrawals may change the ezETH price. This can be used extract profit from the protocol, at the cost of other holders.
Proof of Concept
The price of ezETH is essentially
TVL / totalSupply
, i.e. depositingd
worth of collateral will mintd * totalSupply / TVL
ezETH (RenzoOracle.calculateMintAmount()
). When requesting to withdraww
ezETH the reverse calculation is made, i.e.w * TVL / totalSupply
(RenzoOracle.calculateRedeemAmount()
). These values are set at the time of the withdrawal request, such that the amount of collateral token and amount of ezETH to burn are fixed. Only later uponclaim()
are the tokens transferred and the ezETH burned.The values used in the deposit calculation are also not affected by any pending withdrawals, but the current TVL and the current ezETH supply are used.
Suppose the value of the collateral token to be withdrawn has deviated from the average collateral value, i.e. the price of ezETH, since the withdrawal request was made. If it is greater than the average then upon
claim()
correspondingly more value will be removed from the TVL than ezETH burned from the total supply, and the price of ezETH will decrease. Conversely, if the value of the collateral token has become less than the average of the collateral values then the price of ezETH will increase.This can be abused in all kinds of ways. It gives an attacker some measure of control over the price of ezETH. He can make withdrawal requests and then choose to claim them such that the price is manipulated in his favour, depending on the integrations with Renzo. The attacker can frontrun another claim (not necessarily his own) with a deposit such that he experiences an immediate profit from the price movement.
For example, let's say
2
ezETH has been minted against1
tokA and1
tokB, and that all current prices are1
. Suppose0.5
ezETH is requested to be withdrawn against tokA. This will schedule the burning of0.5
ezETH and the transfer of0.5
tokA. Suppose then the price of tokA increases to1.1
and the price of tokB increases to1.25
. Beforeclaim()
the price of ezETH is(1.1 + 1.25) / 2 = 1.175
, but afterclaim()
the price would be(0.55 + 1.25) / 1.5 = 1.2
. Now, if an attacker deposits1
ETH's worth (of any token) just beforeclaim()
is called, he will receive1 / 1.175 ≈ 0.8512
ezETH. Afterclaim()
the price will then be(0.55 + 1.25 + 1) / 2.3512 ≈ 1.1910
. Immediately withdrawing this again would give him0.8512 * 1.1910 ≈ 1.014
ETH worth of collateral. Note that by sandwiching theclaim()
with a deposit and withdrawal the attacker was able to reduce the price of ezETH (up to future additional price changes) and extract a profit. The more volatile the collateral tokens, and especially if they move in opposite directions, the more profit can be extracted.Recommended Mitigation Steps
Consider calculating the amount of collateral token to be transferred at the time of
claim()
instead. It could then be possible to also choose which token to withdraw from inclaim()
. That is, a withdraw request would simply be a request to withdraw an amount of ezETH, with the rest to be decided uponclaim()
.Assessed type
MEV