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. There is a plug at the end of this that queues the unstake, holds the ticket, and collects the moment Lido clears it, and the rest of this is why it has 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 has this shape. ether.fi mints a withdraw request NFT and pays later. Rocket Pool burns rETH against available liquidity. The queue exists because validators exit on the beacon chain's schedule, not yours, and no amount of product polish removes the wait. 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.
That is the whole plug. Eleven steps, one signed program, and it does something different depending on what it finds:
0 take unstake_amount
1 read stored request id, plug scope
2 if it equals 0
3 request that much stETH out of Lido → requestId
4 store requestId
5 else
6 claim withdrawal requestId → ETH arrives
7 clear the stored id
8 read the socket's own ETH balance → balance
9 stake that balance back into stETH
10 end
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 the moment Lido finalizes, that run claims, reads what landed, and stakes it again. Then it clears the slot, so the next run starts the cycle rather than chasing a ticket that has already burned.
claimWithdrawal takes a number, and a number is easy to get wrong. The catalog does not let you.
The request_id input carries standard:position:lido:withdrawal, so the only things that satisfy it are a real request_withdrawal output or one of the socket's own indexed queue positions, which means an arbitrary integer typed into that slot is a compile error rather than a transaction that burns the wrong ticket. You cannot guess your way in. The option set the app offers is the socket's finalized, unclaimed requests, read from the withdrawal-queue index rather than typed in.
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. Fill the amount in and the same program lowers to executable at 521,627 gas. At this morning's base fee that is a few cents. The claim it collects has been ready for a year.
The claim pays out as a bare native transfer, so there is no return value on it to work from. The balance read is the bridge: the socket asks what it now holds and the stake is sized from that word. Because the read is scoped to the socket rather than to a wallet, the program is funding itself and nothing has to be pulled in from outside to make the stake go through.
The plug is on plug.to. Fork it, tell it how much stETH 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 and restakes.
Twelve hundred addresses have not come back in 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.
Unstake From Lido, Claim It When It Clears, Restake It