code-423n4 / 2024-04-renzo-findings

11 stars 8 forks source link

During L2 native restaking, fewer xezETH are burned than needed, resulting in some xezETH being minted without corresponding asset-backed value #136

Closed howlbot-integration[bot] closed 5 months ago

howlbot-integration[bot] commented 5 months ago

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 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:

  1. 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.
  2. 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:

  1. 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.
  2. 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.

Assessed type

Token-Transfer

c4-judge commented 5 months ago

alcueca marked the issue as satisfactory