Ether can stuck when an operators validators are removed due to an user front-running
When a full withdrawal occurs in the EigenPod, the excess amount can remain idle within the EigenPod and can only be swept by calling a function in the delegator contract of a specific operator. However, in cases where the owner removes all validators for emergencies or any other reason, a user can frontrun the transaction, willingly or not, causing the excess ETH to become stuck in the EigenPod. The only way to recover the ether would be for the owner to reactivate the validators, which may not be intended since the owner initially wanted to remove all the validators and now needs to add them again.\\nLet's assume a Layered Relay Token (LRT) with a beacon chain strategy and only two operators for simplicity. Each operator is assigned two validators, allowing each operator to stake 64 ETH in the PoS staking via the EigenPod.\\nThis function triggers a full withdrawal from the operator's delegator EigenPod. The `queueOperatorStrategyExit` function will withdraw the entire validator balance as follows:\\n```\\nif (validatorDetails.cap > 0 && newValidatorCap == 0) {\\n            // If there are active deposits, queue the operator for strategy exit.\\n            if (activeDeposits > 0) {\\n                -> operatorDetails.queueOperatorStrategyExit(operatorId, BEACON_CHAIN_STRATEGY);\\n                .\\n            }\\n           .\\n        } else if (validatorDetails.cap == 0 && newValidatorCap > 0) {\\n           .\\n        } else {\\n           .\\n        }\\n```\\n\\n`operatorDetails.queueOperatorStrategyExit` function will full withdraw the entire validator balance as follows:\\n```\\nfunction queueOperatorStrategyExit(IRioLRTOperatorRegistry.OperatorDetails storage operator, uint8 operatorId, address strategy) internal {\\n        IRioLRTOperatorDelegator delegator = IRioLRTOperatorDelegator(operator.delegator);\\n\\n        uint256 sharesToExit;\\n        if (strategy == BEACON_CHAIN_STRATEGY) {\\n            // Queues an exit for verified validators only. Unverified validators must by exited once verified,\\n            // and ETH must be scraped into the deposit pool. Exits are rounded to the nearest Gwei. It is not\\n            // possible to exit ETH with precision less than 1 Gwei. We do not populate `sharesToExit` if the\\n            // Eigen Pod shares are not greater than 0.\\n            int256 eigenPodShares = delegator.getEigenPodShares();\\n            if (eigenPodShares > 0) {\\n                sharesToExit = uint256(eigenPodShares).reducePrecisionToGwei();\\n            }\\n        } else {\\n            .\\n        }\\n        .\\n    }\\n```\\n\\nAs observed, the entire EigenPod shares are requested as a withdrawal, which is 64 Ether. However, a user can request a 63 Ether withdrawal before the owner's transaction from the coordinator, which would also trigger a full withdrawal of 64 Ether. In the end, the user would receive 63 Ether, leaving 1 Ether idle in the EigenPod:\\n```\\nfunction queueETHWithdrawalFromOperatorsForUserSettlement(IRioLRTOperatorRegistry operatorRegistry, uint256 amount) internal returns (bytes32 aggregateRoot) {\\n        .\\n        for (uint256 i = 0; i < length; ++i) {\\n            address delegator = operatorDepositDeallocations[i].delegator;\\n\\n            -> // Ensure we do not send more than needed to the withdrawal queue. The remaining will stay in the Eigen Pod.\\n            uint256 amountToWithdraw = (i == length - 1) ? remainingAmount : operatorDepositDeallocations[i].deposits * ETH_DEPOSIT_SIZE;\\n\\n            remainingAmount -= amountToWithdraw;\\n            roots[i] = IRioLRTOperatorDelegator(delegator).queueWithdrawalForUserSettlement(BEACON_CHAIN_STRATEGY, amountToWithdraw);\\n        }\\n        .\\n    }\\n```\\n\\nIn such a scenario, the queued amount would be 63 Ether, and 1 Ether would remain idle in the EigenPod. Since the owner's intention was to shut down the validators in the operator for good, that 1 Ether needs to be scraped as well. However, the owner is unable to sweep it due to MIN_EXCESS_FULL_WITHDRAWAL_ETH_FOR_SCRAPE:\\n```\\nfunction scrapeExcessFullWithdrawalETHFromEigenPod() external {\\n        // @review this is 1 ether\\n        uint256 ethWithdrawable = eigenPod.withdrawableRestakedExecutionLayerGwei().toWei();\\n        // @review this is also 1 ether\\n        -> uint256 ethQueuedForWithdrawal = getETHQueuedForWithdrawal();\\n        if (ethWithdrawable <= ethQueuedForWithdrawal + MIN_EXCESS_FULL_WITHDRAWAL_ETH_FOR_SCRAPE) {\\n            revert INSUFFICIENT_EXCESS_FULL_WITHDRAWAL_ETH();\\n        }\\n        _queueWithdrawalForOperatorExitOrScrape(BEACON_CHAIN_STRATEGY, ethWithdrawable - ethQueuedForWithdrawal);\\n    }\\n```\\n\\nWhich means that owner has to set the validator caps for the operator again to recover that 1 ether which might not be possible since the owner decided to shutdown the entire validators for the specific operator.\\nAnother scenario from same root cause: 1- There are 64 ether in an operator 2- Someone requests a withdrawal of 50 ether 3- All 64 ether is withdrawn from beacon chain 4- 50 ether sent to the users withdrawal, 14 ether is idle in the EigenPod waiting for someone to call `scrapeExcessFullWithdrawalETHFromEigenPod` 5- An user quickly withdraws 13 ether 6- `withdrawableRestakedExecutionLayerGwei` is 1 ether and `INSUFFICIENT_EXCESS_FULL_WITHDRAWAL_ETH` also 1 ether. Which means the 1 ether can't be re-added to deposit pool until someone withdraws.\\nCoded PoC:\\n```\\n// forge test --match-contract RioLRTOperatorDelegatorTest --match-test test_StakeETHCalledWith0Ether -vv\\n    function test_StuckEther() public {\\n        uint8 operatorId = addOperatorDelegator(reETH.operatorRegistry, address(reETH.rewardDistributor));\\n        address operatorDelegator = reETH.operatorRegistry.getOperatorDetails(operatorId).delegator;\\n\\n        uint256 TVL = 64 ether;\\n        uint256 WITHDRAWAL_AMOUNT = 63 ether;\\n        RioLRTOperatorDelegator delegatorContract = RioLRTOperatorDelegator(payable(operatorDelegator));\\n\\n        // Allocate ETH.\\n        reETH.coordinator.depositETH{value: TVL - address(reETH.depositPool).balance}();\\n\\n\\n        // Push funds into EigenLayer.\\n        vm.prank(EOA, EOA);\\n        reETH.coordinator.rebalance(ETH_ADDRESS);\\n\\n\\n        // Verify validator withdrawal credentials.\\n        uint40[] memory validatorIndices = verifyCredentialsForValidators(reETH.operatorRegistry, operatorId, 2);\\n\\n\\n        // Verify and process two full validator exits.\\n        verifyAndProcessWithdrawalsForValidatorIndexes(operatorDelegator, validatorIndices);\\n\\n        // Withdraw some funds.\\n        reETH.coordinator.requestWithdrawal(ETH_ADDRESS, WITHDRAWAL_AMOUNT);\\n        uint256 withdrawalEpoch = reETH.withdrawalQueue.getCurrentEpoch(ETH_ADDRESS);\\n\\n        // Skip ahead and rebalance to queue the withdrawal within EigenLayer.\\n        skip(reETH.coordinator.rebalanceDelay());\\n\\n        vm.prank(EOA, EOA);\\n        reETH.coordinator.rebalance(ETH_ADDRESS);\\n\\n        // Verify and process two full validator exits.\\n        verifyAndProcessWithdrawalsForValidatorIndexes(operatorDelegator, validatorIndices);\\n\\n        // Settle with withdrawal epoch.\\n        IDelegationManager.Withdrawal[] memory withdrawals = new IDelegationManager.Withdrawal[](1);\\n        withdrawals[0] = IDelegationManager.Withdrawal({\\n            staker: operatorDelegator,\\n            delegatedTo: address(1),\\n            withdrawer: address(reETH.withdrawalQueue),\\n            nonce: 0,\\n            startBlock: 1,\\n            strategies: BEACON_CHAIN_STRATEGY.toArray(),\\n            shares: WITHDRAWAL_AMOUNT.toArray()\\n        });\\n        reETH.withdrawalQueue.settleEpochFromEigenLayer(ETH_ADDRESS, withdrawalEpoch, withdrawals, new uint256[](1));\\n\\n        vm.expectRevert(bytes4(keccak256("INSUFFICIENT_EXCESS_FULL_WITHDRAWAL_ETH()")));\\n        delegatorContract.scrapeExcessFullWithdrawalETHFromEigenPod();\\n    }\\n```\\n
Make an emergency function which owner can scrape the excess eth regardless of `MIN_EXCESS_FULL_WITHDRAWAL_ETH_FOR_SCRAPE`
Owner needs to set the caps again to recover the 1 ether. However, the validators are removed for a reason and adding operators again would probably be not intended since it was a shutdown. Hence, I'll label this as medium.
```\\nif (validatorDetails.cap > 0 && newValidatorCap == 0) {\\n            // If there are active deposits, queue the operator for strategy exit.\\n            if (activeDeposits > 0) {\\n                -> operatorDetails.queueOperatorStrategyExit(operatorId, BEACON_CHAIN_STRATEGY);\\n                .\\n            }\\n           .\\n        } else if (validatorDetails.cap == 0 && newValidatorCap > 0) {\\n           .\\n        } else {\\n           .\\n        }\\n```\\n