When users mint xezETH on L2 (layer 2), they need to deposit their ETH/WETH into the xRenzoDeposit contract. The assets are later bridged to L1 (layer 1, i.e. Ethereum) via Connext and deposited into restakeManager. At the end of this deposit, some amount of xezETH is burned on L1, but the burned amount may be less than the initially minted amount.
The issue arises because the burned amount of xezETH is calculated based on the amount of WETH bridged, using the price on L1 at restakeManager's deposit time. This price is likely to be higher than the initial minting price of xezETH on L2 due to the following reasons:
Over time, the price of ezETH with respect to ETH will increase as the Renzo protocol receives rewards from both EigenLayer and Renzo's operators.
There is a time lag between users depositing their ETH/WETH on L2 and the assets being received on L1 and deposited into the restakeManager contract.
If the burning price is higher than the minting price, the amount of xezETH burned is fewer than the amount of xezETH initially minted. As xezETH is a xERC20 token that can cross-chain with zero slippage via Connext, if users bridge all their xezETH from L2 to L1 and want to unwrap them to ezETH, there will not be enough ezETH in the Lockbox contract for them to unwrap.
Impact
More xezETH tokens are minted than intended, resulting in two issues:
The surplus supply of xezETH cannot be unwrapped to ezETH, and in the scenario where all users want to exit, the last users cannot redeem their xezETH into the underlying asset.
The actual value of a xezETH token is lower than a ezETH token, which opens up arbitrage opportunities not intended by the protocol.
I believe the risk rating for this issue is high because it violates an invariance: during the cross-chain scenario of the xezETH token, the minted amount should match the burned amount.
Proof of Concept
// Fetch price and timestamp of ezETH from the configured price feed
(uint256 _lastPrice, uint256 _lastPriceTimestamp) = getMintRate();
// Calculate the amount of xezETH to mint - assumes 18 decimals for price and token
uint256 xezETHAmount = (1e18 * amountOut) / _lastPrice;
When a user deposits native ETH into the xRenzoDeposit contract on Layer 2, the amount of minted xezETH is calculated using the price from getMintRate(), which gets the current mint rate from the oracle or the price previously set by the receiver.
// Calculate how much ezETH to mint
uint256 ezETHToMint = renzoOracle.calculateMintAmount(
totalTVL,
msg.value,
ezETH.totalSupply()
);
// Burn it - it was already minted on the L2
IXERC20(address(xezETH)).burn(address(this), xezETHAmount);
Later, the sweeper calls the xRenzoDeposit.sweep() function to bridge these assets to L1 via Connext. When L1 receives these assets, the xRenzoBridge.xReceive() function is triggered, depositing the assets into the restakeManager contract and receiving ezETH in return using the rate calculated in renzoOracle.calculateMintAmount(). Then, the same amount of xezETH is burned.
However, since the price of xezETH with respect to ETH is likely to increase over time from when xezETH is minted on L2 to when xezETH is burned on L1, the burned price may be higher than the mint price back on L2, resulting in fewer xezETH being burned than needed.
Tools Used
Manual Review
Recommended Mitigation Steps
During the L2 native restaking process, record the amount of minted xezETH on L2 in a variable and encode it into the Connext cross-chain message when bridging. When the L1 component receives the message, decode the amount of xezETH minted from the message and burn the exact same amount of xezETH. This ensures a balanced minting and burning of the xezETH xERC20 token.
Lines of code
https://github.com/code-423n4/2024-04-renzo/blob/519e518f2d8dec9acf6482b84a181e403070d22d/contracts/Bridge/L1/xRenzoBridge.sol#L193 https://github.com/code-423n4/2024-04-renzo/blob/519e518f2d8dec9acf6482b84a181e403070d22d/contracts/Bridge/L2/xRenzoDeposit.sol#L244-L245 https://github.com/code-423n4/2024-04-renzo/blob/519e518f2d8dec9acf6482b84a181e403070d22d/contracts/Bridge/L2/xRenzoDeposit.sol#L252-L253 https://github.com/code-423n4/2024-04-renzo/blob/519e518f2d8dec9acf6482b84a181e403070d22d/contracts/Bridge/L2/xRenzoDeposit.sol#L289-L301 https://github.com/code-423n4/2024-04-renzo/blob/519e518f2d8dec9acf6482b84a181e403070d22d/contracts/Bridge/L1/xRenzoBridge.sol#L139-L201 https://github.com/code-423n4/2024-04-renzo/blob/519e518f2d8dec9acf6482b84a181e403070d22d/contracts/RestakeManager.sol#L605-L609 https://github.com/code-423n4/2024-04-renzo/blob/519e518f2d8dec9acf6482b84a181e403070d22d/contracts/Bridge/L1/xRenzoBridge.sol#L193
Vulnerability details
Vulnerability Detail
When users mint
xezETH
on L2 (layer 2), they need to deposit their ETH/WETH into thexRenzoDeposit
contract. The assets are later bridged to L1 (layer 1, i.e. Ethereum) via Connext and deposited intorestakeManager
. At the end of this deposit, some amount ofxezETH
is burned on L1, but the burned amount may be less than the initially minted amount.The issue arises because the burned amount of
xezETH
is calculated based on the amount of WETH bridged, using the price on L1 atrestakeManager
's deposit time. This price is likely to be higher than the initial minting price ofxezETH
on L2 due to the following reasons:ezETH
with respect to ETH will increase as the Renzo protocol receives rewards from both EigenLayer and Renzo's operators.restakeManager
contract.If the burning price is higher than the minting price, the amount of
xezETH
burned is fewer than the amount ofxezETH
initially minted. AsxezETH
is a xERC20 token that can cross-chain with zero slippage via Connext, if users bridge all theirxezETH
from L2 to L1 and want to unwrap them toezETH
, there will not be enoughezETH
in the Lockbox contract for them to unwrap.Impact
More
xezETH
tokens are minted than intended, resulting in two issues:xezETH
cannot be unwrapped toezETH
, and in the scenario where all users want to exit, the last users cannot redeem theirxezETH
into the underlying asset.xezETH
token is lower than aezETH
token, which opens up arbitrage opportunities not intended by the protocol.I believe the risk rating for this issue is high because it violates an invariance: during the cross-chain scenario of the xezETH token, the minted amount should match the burned amount.
Proof of Concept
When a user deposits native ETH into the
xRenzoDeposit
contract on Layer 2, the amount of mintedxezETH
is calculated using the price fromgetMintRate()
, which gets the current mint rate from the oracle or the price previously set by the receiver.Later, the sweeper calls the
xRenzoDeposit.sweep()
function to bridge these assets to L1 via Connext. When L1 receives these assets, thexRenzoBridge.xReceive()
function is triggered, depositing the assets into therestakeManager
contract and receivingezETH
in return using the rate calculated inrenzoOracle.calculateMintAmount()
. Then, the same amount ofxezETH
is burned.However, since the price of
xezETH
with respect to ETH is likely to increase over time from whenxezETH
is minted on L2 to whenxezETH
is burned on L1, the burned price may be higher than the mint price back on L2, resulting in fewerxezETH
being burned than needed.Tools Used
Manual Review
Recommended Mitigation Steps
During the L2 native restaking process, record the amount of minted
xezETH
on L2 in a variable and encode it into the Connext cross-chain message when bridging. When the L1 component receives the message, decode the amount ofxezETH
minted from the message and burn the exact same amount ofxezETH
. This ensures a balanced minting and burning of thexezETH
xERC20 token.Assessed type
Token-Transfer