Two Sweeps Pointed at the Same Address
Constraints, episode 9

The tracing was done and the report was already with law enforcement when the client told me the part that reopened the case. His wallet had been emptied weeks earlier. The seed phrase had leaked, the balance was gone before anyone noticed, and that is normally where the conversation ends.
Part of his ETH had never been in the wallet. It was staked.
Ethereum runs on two halves. The execution layer is the one everybody sees, where transactions are sent and balances move. The consensus layer is the ledger of validators, the machines that lock up ETH to secure the network and get paid for it. Staked ETH is credited to a validator over there, and it comes back to an ordinary address only when the validator leaves and the protocol pays it out, which is slow by design and visible to everyone.
So there was money with a legitimate owner, untouched, scheduled to arrive in an address whose key a thief also held. The thief had to do nothing. His software was already waiting.
The machine on the other side
A sweeper bot is a script with one instruction: watch this address, and move anything of value out of it the moment it appears. Leaked keys are collected in bulk, from phishing kits, fake wallet apps, a seed phrase photographed into a cloud backup, and each one is handed to the same loop. The loop costs almost nothing to run, so it can wait for years.
This account was close to ideal for one. A compromised key cannot be un-compromised. The payout address was not going to change either: once a validator’s withdrawal credentials are set, pointing them at a clean address is not something the protocol offers today, whatever may come later. And the schedule was public, so both sides were reading the same screen.
Quality varies at the other end. Cheap sweepers take the native ETH and leave the tokens, which is why victims sometimes find their ERC-20 balances untouched inside an account that empties itself every time it is funded. The better ones bring their own gas and take those too.
Sending gas is the trap that makes most rescues fail. Moving a token takes a transaction, and a transaction takes ETH sitting in the same account, so rescuing a token means funding the account that holds it. That funding is exactly what the bot is watching for. Plenty of people meet their sweeper by feeding it, and watch the gas leave for another address seconds later.
The tools for the ordinary case
The stack I use when the account holds assets it cannot move and the gas has to come from outside is Dark Florist’s, open source and free.
The Interceptor is a browser extension that sits between the dApp and the wallet. It simulates every transaction before you sign it and says in plain language what it will do. Its simulation stack runs several transactions in sequence, as if they had been mined one after another, so you can see whether step three still works after steps one and two changed the state. It also lets you browse as any address without holding its keys, which is how a rescue gets built for an account you cannot sign for yet.
Bouquet turns that simulation into a bundle. It lays out the sequence with values and fees, takes the signing keys and keeps them in the browser, and asks you to top up a temporary funding account with what the bundle needs plus a margin for a rising base fee. Rescue bundles go out through the relay only, so the funding is never exposed in the public mempool on its own, and nothing can be slipped between the funding and the rescue sweep. It also refuses to work on a stale picture, rejecting an import that did not come cleanly out of the simulation and warning when the transactions it holds no longer match what the simulation shows. With one attempt available, the refusal is the feature.
The rest of the stack has the same shape, small interfaces with no backend to trust: Lunaria for sending tokens, NFT Sender for NFTs, Petal Lock for immutable ENS subnames, Horswap as a censorship resistant interface to Uniswap.
The money arrives without a transaction
Here the case stopped resembling anything I had done before.
The consensus layer does not pay out by sending a transaction. The protocol raises the balance of the destination address directly, while the block is being built. No sender, no gas, no code running at the receiving end, nothing sitting in the queue of pending transactions where everyone watches for prey. Ethereum’s own documentation has a name for this way of paying validators out, and the name is the sweep: the same word the industry uses for what the thief’s software does to a compromised account.
The fight happens in the next block
The absence of a transaction cuts both ways. There is nothing to front-run, and nothing to get ahead of: withdrawals from the consensus layer are credited after all the transactions in the block that carries them, so that ETH cannot be spent inside that block by anyone, thief included. Everything is decided at the top of the next one.
And we did not know what the bot could see. A basic one reads the pending transactions in the public mempool, which is the thing you can hide from. A better one also reads each block once it is confirmed, reacting to the balance itself and not only to somebody’s stated intentions, and it can follow the exit queue and see the payout coming roughly when we did.
The gas trap did not apply here, and that helped less than it sounds. What was landing was ETH, and ETH pays for its own movement, so there was no funding to smuggle in and nothing to wrap it with. One transfer, out of an address that two parties can sign for, and the only thing left to win was position in the block after the credit.
You can still stop broadcasting. Instead of entering the public mempool, the transaction goes privately to a builder through a relay, inside a bundle aimed at a specific block. That matters because you cannot line up a public transaction for money that has not arrived yet: execution clients reject it from their transaction pools. A bundle can wait for the block where the money will be. It is never gossiped across the public network while it waits, and if it is not included it simply expires without being mined, so the attacker has nothing to see and nothing to react to.
The saved fee is not the point. Losing the block costs everything, because there is one credit and whoever moves first keeps it.
So the choice was between two risks. Public, and be outbid inside the mempool by a bot that answers in milliseconds and pays more. Private, and depend on a builder carrying your bundle winning that particular block, against an adversary who may be reading the balance rather than the mempool, and who can stay public with an enormous tip that any builder in the market will take.
How many blocks to cover, what to pay for each attempt, whether to stay private or go loud: those were the real decisions, there was no time to work through them properly, and I did not have the repetitions behind me to make them fast.
Where I stopped
Flashbots is the research organisation formed to study and mitigate MEV, the value extracted by whoever decides the order of transactions inside a block. Much of the infrastructure a rescue like this leans on came from there: the bundle model, MEV-Boost, Protect, and now BuilderNet. Whitehat rescues have lived in its orbit from the beginning.
I introduced the client to them and stepped back. Every rescue I was familiar with had the same shape: the assets are still sitting in the account, and the opponent is a bot with a faster reaction time. This one was different in the parts that decide the outcome. The money arrives once. How the wallet was being watched, and what triggered the signature, we never knew. A miss cannot be taken back. Those are things you get right by having done them often, and I had not.
I checked when the redemption came due. The money got out in time.
Two sweeps were pointed at the same address, and only one of them was going to end up with the money. Method includes knowing which part of a problem you can time, and handing the rest to somebody who can.
Next: Episode #10: The Magic of Flash Loans (Part 1): Capital Was Never the Constraint