<!-- canonical: https://0xsimao.com/findings/renzo-eth-withdrawals-delegator-reentrant -->

# ETH withdrawals from EigenLayer always fail due to `OperatorDelegator`'s nonReentrant `receive()`

Crit/High · Code4rena · Liquid restaking · 30th April 2024

Finding H-03 of the Renzo competition.

- Protocol: https://code4rena.com/audits/2024-04-renzo
- Codebase: https://github.com/0xsimao/2024-04-renzo/tree/519e518f2d8dec9acf6482b84a181e403070d22d
- Source: https://github.com/code-423n4/2024-04-renzo-findings/issues/368

---

<https://github.com/code-423n4/2024-04-renzo/blob/519e518f2d8dec9acf6482b84a181e403070d22d/contracts/Delegation/OperatorDelegator.sol#L269>

<https://github.com/code-423n4/2024-04-renzo/blob/519e518f2d8dec9acf6482b84a181e403070d22d/contracts/Delegation/OperatorDelegator.sol#L501>

### Vulnerability details

The [`OperatorDelegator.completeQueuedWithdrawal()`](https://github.com/code-423n4/2024-04-renzo/blob/519e518f2d8dec9acf6482b84a181e403070d22d/contracts/Delegation/OperatorDelegator.sol#L265) function is used by admins to finalize previously initiated withdraws of shares from EigenLayer.

We note that both this and the OperatorDelegator's `receive()` functions are `nonReentrant`:

```Solidity
File: OperatorDelegator.sol
265:     function completeQueuedWithdrawal(
266:         IDelegationManager.Withdrawal calldata withdrawal,
267:         IERC20[] calldata tokens,
268:         uint256 middlewareTimesIndex
269:     ) external nonReentrant onlyNativeEthRestakeAdmin {
270:         uint256 gasBefore = gasleft();
271:         if (tokens.length != withdrawal.strategies.length) revert MismatchedArrayLengths();
272: 
273:         // complete the queued withdrawal from EigenLayer with receiveAsToken set to true
274:         delegationManager.completeQueuedWithdrawal(withdrawal, tokens, middlewareTimesIndex, true);
---
501:     receive() external payable nonReentrant {
502:         // check if sender contract is EigenPod. forward full withdrawal eth received
503:         if (msg.sender == address(eigenPod)) {
504:             restakeManager.depositQueue().forwardFullWithdrawalETH{ value: msg.value }();
```

However, the `receive()` function is normally called by the `EigenPod` in the call stack originated by the L274 `completeQueuedWithdrawal()` when `receiveAsTokens == true` like in this case. This particular instance of reentrancy is not only acceptable but also required to allow ETH redemptions from EigenLayer. However, the `nonReentrant` modifier prevents it.

### Impact

All withdrawals that include any amount of ETH will be permanently stuck in EigenLayer and won't be redeemable. Only amounts coming from new deposits can be redeemed and the team will have no way to fill the withdrawal queues. To unblock them, the team will necessarily have to upgrade `OperatorDelegator`.

### Proof of Concept

To prove the concept, it's sufficient to upgrade `OperatorDelegator` on a mainnet fork and initiate a withdrawal that has ETH among the withdrawn strategies.

While it would be too bulky to provide a coded PoC, you can find in [this GH Gist](<https://gist.github.com/assets/145972240/0f2500e1-1f93-4daf-8a5f-509da79f2e96>) the Foundry traces of such failed call on a mainnet fork.

### Tools Used

Foundry

### Recommended Mitigation Steps

Consider removing `nonReentrant` from OperatorDelegator's `receive`, or applying the modifier only in case `msg.sender != eigenPod`.

### Assessed type

Reentrancy

---

Related findings:

- [xBundle(...) and xReceive(...) should have nonReentrant modifiers](https://0xsimao.com/findings/fuji-finance-bundle-non-reentrant-modifiers): Fuji Finance
- [ERC20AssetPool and ERC721AssetPool should have the nonReentrant modifier as ERC721 and some tokens have callbacks](https://0xsimao.com/findings/singularity-erc20-erc721-reentrant-callbacks): Singularity
- [Missing nonReentrant functionality in some execution paths](https://0xsimao.com/findings/yieldoor-iii-reentrant-functionality-execution-paths): Yieldoor LoopedVault Update
- [Claiming will fail for Ole and D1MemeToken if the overfunded ETH reverts](https://0xsimao.com/findings/districtone-meme-overfunded-eth-reverts): DistrictOne
