15.45 ETH. SlowMist flagged it. A handful of solver accounts on ether.fi's AtomicQueue contract had balances pulled out from under them by a caller with no right to touch them. No oracle failure. No flash loan. No governance capture. No leaked key. Just a function that forgot to ask who was calling it.
The dollar figure is a rounding error against protocol TVL. Whatever ETH trades at, the attacker walked away with less than the cost of a decent validator. That is why most readers will scroll past. The size of a loss measures the attacker's patience, not the severity of the defect. A missing access-control check is a class of bug, not an instance of one. ether.fi shipped one in AtomicQueue. The question nobody is asking loudly enough is how many other entry points assume the same thing and got lucky.
Where AtomicQueue sits
ether.fi is a liquid restaking protocol. Users deposit ETH, it gets staked and restaked, the protocol issues a receipt token. That receipt — eETH, with weETH as the wrapped non-rebasing version — is a liquid restaking token, an LRT. The product promise is composability: hold the receipt, keep the staking exposure, still deploy the receipt as collateral elsewhere.
Stack it up. L1 Ethereum and staking infrastructure at the bottom. ether.fi in the middle, converting staked ETH into a transferable claim. Downstream, lending markets and AMMs accept weETH as collateral. Pull out the middle layer and the structure stops clearing.
ether.fi's position in that stack is not small. It ranks among the larger LRT issuers, which means its eETH and weETH variants are integrated across major lending markets and AMMs. Score the peers for comparison — Renzo, KelpDAO, the rest of the restaking cohort. Every one of them runs an internal settlement layer of some kind.
AtomicQueue is an internal component of that middle layer. It handles atomic settlement — requests to exchange one asset for another, matched and executed in one transaction. No AMM pool, no slippage curve. A counterparty called a solver fills the request. Modules like this exist because they are cheaper and cleaner than routing size through a constant-product pool.
SlowMist's disclosure was specific. solve() does not verify that the caller is the solver it names. That is the whole bug. One missing comparison.
As of the material I reviewed, no confirmed post-incident report, no confirmed patch commit. Everything below is reconstruction from the disclosure, not the team's own words.
The mechanics: how an allowance becomes an attack surface
ERC-20 offers exactly one authorization primitive that matters here. approve() sets an allowance. transferFrom() lets the approved spender move tokens inside that allowance. The spender does not have to be the holder. That is by design. It is also a loaded gun handed to every contract at birth.
Intended flow in AtomicQueue: a user submits a request via updateAtomicRequest(); a solver fills it; solve() settles and calls want.transferFrom(solver, ...) to move the solver's tokens. The solver is a parameter. The caller is msg.sender. The contract assumes those are the same address. It never enforces it.
The exploit chain is four steps.
updateAtomicRequest() places no restriction on who can construct a request. An attacker builds an AtomicRequest with arbitrary fields.
The attacker sets the solver slot to a victim address — one that has previously approved the AtomicQueue contract to spend its tokens. That approval is the entry ticket.
The attacker calls solve(). Inside, finishSolve executes want.transferFrom(solver, ...). The solver variable now points at the victim. The contract pulls from the victim's balance, spending the victim's own allowance, into the settlement.
The attacker captures the output. 15.45 ETH, several victims, one transaction.
Two preconditions. The victim granted an allowance to AtomicQueue at some point. The attacker could name an arbitrary address in the solver slot. Neither is exotic. The first is what every user does before touching a new protocol. The second is what happens when a parameter is trusted instead of validated.
The blast radius deserves a precise boundary. AtomicQueue is not the eETH token contract. The vulnerability did not mint, burn, or reprice the receipt asset. Users who never granted an allowance to AtomicQueue had nothing to lose here, and weETH collateral sitting in Aave or Morpho was not impaired by this transaction. That distinction matters, because the reflex after any security headline is to assume the token is broken. It wasn't. The attack surface was an internal settlement path, and the victims were the addresses that had pre-authorized it.
The fix is one line. require(msg.sender == solver) at the top of solve(). Or delete the solver parameter and read msg.sender directly — strictly better, because it makes injection impossible instead of merely checked. One commit either way.
The fix isn't the story. Why it was needed is. In 2017, still a student in Dublin, I spent the final hour of the Status Network token sale reading its minting contract. I found an integer overflow in the supply function and reported it privately before mainnet. That was a wrong number. You find wrong numbers by reading arithmetic and asking what happens at the boundary. This was a wrong assumption. There is no wrong number to find. The function does exactly what its name says — it solves. It becomes dangerous only when you read it beside the function that calls it, and then beside the approval that predates both. Auditors miss these because the unit of review is the function and the defect lives at the seam. Pull-based authorization is a trust boundary, not a logic branch, and most audit hours go to logic branches.
Checking your own exposure
The chart is a map, not the territory. What actually happened is that an allowance granted months ago, possibly for a swap that already settled, sat live in a contract that could be told who to pull from. No expiry, no scope, no counterparty verification.
I learned this the expensive way in 2020. Running my SNX staking position, I calculated the collateralization ratio by hand on a local node before touching anything, and I kept the allowance wallet separate from the position wallet. Tedious. It is the only reason a bug like this would have cost me nothing. The practice is not sophisticated: a burner address holding only what the current transaction needs. Most people don't. Most people are the victims in disclosures like this one.
The check is short. Pull your address into revoke.cash, or read the allowance mapping on Etherscan against the AtomicQueue contract. Revoke anything you are not actively using. It costs gas. That gas is cheaper than the alternative.
The contrarian read
The dominant framing is that only users who approved were affected. Read that again. It quietly converts a protocol defect into a user error. Comfortable for protocols, and wrong. Approve is the only interface ERC-20 offers. Every router, vault, and staking contract requires it. Building on a primitive and then blaming users for using it is not a security posture. It is a liability transfer.
The second framing: the loss was small, so the market should shrug. The number is being used as an argument for calm. It is an argument for luck — a larger solver allowance, or a fatter cached balance, and this headline reads differently. It will shrug anyway. It always does. In 2022 I watched the same reflex through the Terra collapse — day one was analysis, day three was amnesia. Yield is just risk wearing a smiley face, and the smiley face always comes back.
There is a cleaner answer than require statements, and the industry keeps not adopting it. Scoped allowances. Per-transaction permits. Approvals that expire with the block. Permit2 exists. Teams skip it because approve is one line and Permit2 is a week of integration. The friction being skipped is exactly the friction that would have made this attack impossible rather than merely unprofitable.
Takeaway
In order. Revoke any outstanding allowance against ether.fi's AtomicQueue — revoke.cash, or the mapping on Etherscan. Watch protocol TVL on DefiLlama across seven days; a one-day dip is noise, a week is a trend. Watch for a post-incident report containing the commit hash of the patch, not just a statement. A patch without a hash is a claim.
Emotion is the only variable I cannot hedge. 15.45 ETH is not the point. The point is that a contract trusted with user allowances did not verify who was calling it, and that failure mode is not unique to ether.fi. The question isn't whether this gets fixed. It's how many other contracts are one require() statement away from the same headline — and how much liquidity is sitting behind that statement. Liquidity doesn't exist until you need it.