Somebody asked Lido for their ETH back on 6 July 2024. Lido finalized it. The ETH has been payable ever since, and this morning it is still sitting there: 1,000 stETH, request #44851, isFinalized: true, isClaimed: false.
Nobody has to live like that. Five plugs in here queue the unstake, hold the ticket, and collect the moment the protocol clears it, and the rest of this is why they have to work that way.
We read all 134,486 requests in Lido's withdrawal queue directly off the contract this morning. 1,198 of them are worth a whole ETH or more, they belong to 1,161 different addresses, and together they hold 34,361 stETH that Lido has already set aside. The median one has been waiting 353 days. Five hundred and eighty-one have been waiting over a year. Nothing is wrong with any of them.
There are another 5,781 unclaimed requests below a tenth of an ETH. They come to 75 stETH between them, so they are not the story and we are leaving them out of the number.
Unstaking ETH takes two transactions. The gap between them belongs to nobody, and it is the reason the money stops.
You call requestWithdrawals and the queue mints you an NFT: a claim ticket for ETH that does not exist yet. Some days later Lido's oracle finalizes your request and the ETH becomes payable. Then nothing happens, because the second half needs you. You have to notice that finalization landed, come back, and call claimWithdrawal with the right request id.
The first half is the part you were motivated to do. The second half is a chore on a day you had no reason to mark.
Every unstaking design carries this shape and draws the gap differently. ether.fi mints a withdraw request NFT and pays out later. Rocket Pool skips the queue and burns rETH straight against whatever ETH is on hand. The wait exists because validators exit on the beacon chain's schedule rather than yours, and no amount of product polish removes it. What is missing is not a faster queue. It is something that remembers.
A signature covers one transaction. The request id you need in the second half does not exist when you sign the first half, and the transaction that learns it ends before the ETH is ever payable.
So the program has to write the ticket down somewhere that survives.
Twelve actions, one signed program, and it does something different depending on what it finds. Run it once and it queues the unstake and keeps the ticket. Leave it on a schedule and every later run reads the ticket back and does nothing, until the run that finds a finalized ticket claims the ETH and sets the stored id back to zero, so the next run starts a fresh cycle rather than chasing a ticket that has already burned.
Two of those twelve actions exist so you can look. The socket's stETH balance and the stored request id are both bubbled as outputs, which puts the program's state in front of you before you run anything: a stored id of zero means nothing is queued, and any other number is a live ticket with your ETH behind it. Storage nobody can see is a promise about what a program did last time. This one hands you the number.
ether.fi draws the same two halves, and its plug comes out the same twelve actions as Lido's, verb for verb. Rocket Pool comes out four, and only one of them touches the protocol.
rETH has no queue. You call burn and Rocket Pool pays you out of the collateral sitting in the rETH contract, at the exchange rate. While that collateral is there the exit is instant and there is no ticket to hold. While it is not, there is no exit.
At block 25,883,411 the contract's getTotalCollateral returned 30,513,992,069 wei, which is 0.0000000305 ETH against an exchange rate of 1.1708 ETH per rETH. Burning one rETH reverts with Insufficient ETH balance for exchange. We binary searched the ceiling against the live chain, calling burn from a wallet holding 34,210 rETH: the largest amount that clears is 4,871,855 wei, or 0.0000000049 rETH. Anything above that reverts.
A queue tells you to come back. Rocket Pool's answer to the same question today is no, at any size a holder cares about, with nothing to come back for. Exiting rETH at size means a DEX and the spread, which is the same wait priced in basis points instead of days. That is worth knowing before you pick where to stake, not after.
Hold the wrapped token and the plug unwraps before it queues, which is one more action and no more thinking.
Thirteen actions each, and the extra one is wrap_or_unwrap sitting ahead of the request, so wstETH becomes stETH and weETH becomes eETH before the queue ever sees them. Everything after that is the program above.
claimWithdrawal takes a number, and a number is easy to get wrong. The catalog does not let you.
The request_id input carries a withdrawal-position tag. What satisfies it is a real request_withdrawal output or one of the socket's own indexed queue positions. An arbitrary integer typed into that slot is a compile error before it is ever a transaction that burns the wrong ticket. You cannot guess your way in. The options the app offers are the socket's finalized, unclaimed requests, read off the withdrawal-queue index.
The stored ticket flows in because a value written by your own earlier run is exactly the case the tag rules allow, and nothing else about the slot is loosened to make it fit.
Compiled against the live pool it comes back parameterized with one open input and no step excluded, which is the compiler saying the program is whole and waiting on you for a number. 456,712 gas, at that block's own 0.0763 gwei base fee, is 0.0000348 ETH. Eight cents to collect a claim that has been ready for a year.
All five lower at the same block with nothing excluded from any of them. ether.fi comes back at 437,387 gas, the wstETH and weETH versions that unwrap first at 437,650 and 436,299, and the Rocket Pool burn at 281,408. Every one is a real lowering against the live pool rather than an estimate, and you can re-run any of them from the plug page.
The claim pays out as a bare native transfer, so the ETH lands in the socket with nothing attached to it, and the program stops there on purpose.
Fork one, tell it how much you want out, and run it. The first run queues your withdrawal and keeps the ticket. Put it on a schedule and you are done thinking about it, because the run that finds a finalized ticket is the run that collects.
What happens to the ETH afterwards is yours. Wrap it to WETH and it feeds any yield or loop card in the catalog, which is a separate program and a separate decision. This one exists to make sure the ETH is there to decide about.
Five hundred and eighty-one of these tickets have been waiting more than a year. Yours does not have to be one of them, and the difference is a program that remembers instead of a reminder you set for yourself.