Exchanges

The Queue That Never Asked Who Was Calling: An Audit of ether.fi's AtomicQueue Authorization Failure

0xCred

The Queue That Never Asked Who Was Calling

Fifteen point four five ETH. That is the figure SlowMist attached to ether.fi on September 11, and it has since been copied into every security roundup, every Telegram alert, every weekly digest where the reader's eye slides past it on the way to something louder. I have read it a dozen times now, and each reading makes it matter less to me as a sum and more as a signal.

Not because the loss is nothing. It is very much something to the person who owned those coins, and I am not in the habit of measuring other people's losses against my own comfort. But 15.45 ETH is not what a mature exploit looks like. It is what an exploit looks like when someone has found a door and is testing the hinges to see how quietly it opens.

What SlowMist described, in the compressed syntax researchers adopt when they are still choosing their words carefully, is an access-control failure inside ether.fi's AtomicQueue contract. Two functions — solve() and updateAtomicRequest() — reachable without any verification that the caller is the party the protocol assumes. That is the entire technical skeleton of the event. A queue that never asked who was calling.

I have been auditing contract logic since 2017, through the ICO mania, through DeFi Summer, through the long silence of 2022. The lesson that keeps repeating is that the vulnerabilities taking real money are almost never the elegant ones. They are the ones that were never considered at all — the doors that were never locked because nobody imagined anyone would try the handle.

The architecture underneath the incident

ether.fi occupies a specific and precarious place in the Ethereum stack. It began as a liquid staking protocol and extended into restaking, issuing a receipt token that lets a depositor keep both liquidity and exposure to validator rewards. That receipt token then travels. It moves into lending markets, into concentrated liquidity pools, into the collateral ledgers of other protocols written by other teams who trusted ether.fi's contracts because ether.fi was large, reputable, and audited.

An LRT is therefore not a product so much as a junction. Its security is not entirely its own. Every downstream integration is a bet that the upstream contract set behaves exactly as advertised, and none of those downstream teams re-audit their upstream dependency every time a new function ships.

Now look at how ether.fi handles the awkward part of being a junction: redemption and request fulfilment. Withdrawals from staked positions on Ethereum are not instantaneous. The beacon chain exit queue exists, it is slow, and the liquidity is real but it is not in the room. Every protocol offering a liquid representation of a slow asset has to reconcile the same contradiction — a fast promise settled against a slow reality.

The industry's dominant answer is the queue. A user creates a request. The request sits in a contract. Later, someone — a solver, a keeper, a bot, a permissioned operator — arrives and fulfils it. The design is not foolish. It exists for genuine reasons. It amortizes gas across many users. It pushes heavy computation off-chain where it is cheap. It produces atomicity: either the batch settles or the entire transaction reverts, leaving nothing half-done.

And it creates a temporal gap between the moment a user consents and the moment that consent is acted upon. That gap is where this incident lived.

AtomicQueue is ether.fi's version of the pattern. A request goes in. A designated solver comes along to process it. The solver is the trusted actor, and the safety of the whole design rests on a single assumption: that only the solver can call the function that solves.

That assumption was not enforced in code. An assumption living only in a design document is not a security control. It is a hope with good manners.

What solve() and updateAtomicRequest() actually gave an attacker

Here is the shape of the exploit as it can be reconstructed from the disclosure. It has two halves, and neither half is interesting alone. It is the combination that opens the vault.

The first half is solve(). In a correctly written implementation, solve() carries a modifier — onlyRole, onlyOwner, onlySolver, whichever name the team chose — that reverts any call from an address not granted the solver role. OpenZeppelin has shipped this primitive for years. It is roughly one line of code, and it is the kind of line a junior developer can write from memory. Inside ether.fi's AtomicQueue, that line appears to be absent. Any address can call solve(), and the contract will behave as though a legitimate solver has arrived.

The second half is updateAtomicRequest(). This function lets a request already sitting in the queue be modified. The intent is that the original requester, or an authorized party acting for them, can adjust parameters — amounts, recipient addresses, settlement terms. If the access check is missing or insufficient, anyone can reach into a pending request and rewrite it.

Combine them, and the primitive is complete. An attacker calls updateAtomicRequest() to reshape a request — or simply creates one — pointing settlement at an address they control. Then they call solve() to execute it. The contract performs exactly what it was told to do. It is not malfunctioning. It is obeying.

The third ingredient is the one that makes it pay, and it lives in the user's wallet rather than the protocol's repository: the ERC-20 approval.

When a user interacts with a DeFi contract for the first time, the interface asks them to approve token spending. Almost always the request is for the maximum value, because setting a finite allowance means paying gas again later, and every product team has concluded that friction is the enemy of conversion. So the approval is signed once and never revisited. It does not expire. It does not check a calendar. It does not notice that the protocol has upgraded, that a new function has been deployed, or that the meaning of the contract it points at has quietly changed.

That approval is a standing consent, and it is the true subject of this story. Not the 15.45 ETH. The standing consent.

Attacking a wallet does not require compromising it. It requires a contract holding an approval, plus a function in that contract willing to move funds on command. ether.fi's AtomicQueue, as described, supplied the second half of that sentence. The victims supplied the first — unknowingly, months or years earlier, on some unremarkable Tuesday, clicking through a modal they did not read.

Reconstructing the path in sequence

It helps to lay the mechanics out as a sequence, because the sequence is what makes the failure legible.

An address holds a standing approval to the AtomicQueue contract. That address may never have interacted with ether.fi's front end in months. It may have moved on, forgotten the allowance, or assumed that revocation was something only paranoid people did.

The attacker identifies this. The identification is not difficult — allowances are public on-chain state, and tooling for enumerating them is widely available and cheap to run.

The attacker calls updateAtomicRequest() to create or modify a request whose settlement terms route value toward an address they control. No permission is checked. The contract accepts the modification as legitimate.

The attacker then calls solve(). No solver role is checked. The contract treats the hostile caller as an authorized operator and executes the settlement against the standing approval.

The transaction is atomic. It either completes or reverts. There is no partial state, no half-drained position, no opportunity for a monitoring bot to interrupt mid-flight.

The attacker receives the funds. Chain analysts later observe a figure around 15.45 ETH and publish it. The protocol, to its credit, is informed before publication.

Nothing in that sequence required a private key, a phishing site, a malicious browser extension, or a compromised endpoint. The attack surface was a permission the user granted voluntarily and a function signature that never asked who was knocking. That is the entire story, and it should frighten people more than a bridge hack, because it does not depend on a novel cryptographic break. It depends on a line of code that was not written.

An audit memory that will not leave me alone

In 2017, during the ICO rush, I was contracted to review the smart contract logic for a data-provenance project calling itself TruthChain. The founders wanted a mainnet launch inside three weeks to catch the market. I found five critical issues, all touching the exposure of user metadata, and I refused to sign the report.

The conversation that followed was not about cryptography. It was about timing. One of the founders — an intelligent, genuinely committed person — told me the market would not wait, that the vulnerabilities were theoretical, that nobody would bother attacking a project that small. I told him the size of the target is not what determines whether a door gets opened. The door gets opened because it is a door.

I left that engagement without the fee. I have never once regretted it.

What I carried out of 2017 into every review since is a specific discipline. I stopped asking whether a given function should be restricted, and started asking who the contract believes is calling. The second question is the one that gets skipped. Teams document the roles they intend — solver, guardian, admin, requester — then implement ninety percent of the intent and forget the modifier on the function they were most confident about.

Confidence is where the lock goes missing. The function everyone "knows" is protected is the function nobody re-reads.

AtomicQueue fits that pattern exactly. It is not a subtle failure. It is the absence of a modifier that a reviewer asking the adversarial question would have found in minutes.

The number that should worry you is not 15.45

The roundups report the loss as a fact about ether.fi's security posture. It is not. It is a fact about the exploit's economics.

When an access-control hole exists in a queue contract, the maximum extractable value is not bounded by the contract's own balance. It is bounded by the sum of every outstanding approval any address has ever granted to that contract. That set includes the protocol treasury, every integrated router, every downstream vault, and every individual who approved once and forgot.

If that sum is large — and for an entity of ether.fi's scale it almost certainly is — then 15.45 ETH represents roughly the cost of a probe. An attacker running an efficient bot does not stop at 15.45 ETH when the ceiling is eight figures. They stop because the exploit is fragile, or because they were still calibrating, or because they sold the technique rather than operating it, or because something interrupted them. None of those explanations are comforting.

There is a second reading, and it is the one I find more professionally useful. The extraction may have been capped by the design itself. AtomicQueue is, by construction, atomic. A solver executes a batch, and if the batch does not balance, the whole transaction reverts. That property — the thing making the queue valuable to legitimate users — also constrains what a hostile caller can do inside a single call. An attacker cannot partially drain and stroll away. They must complete a valid settlement.

So the modest figure may be evidence of an unadvertised safety property: the queue's atomicity imposed a ceiling that the missing access control would otherwise have removed. Rigor is not the absence of failure but the shape of it. The bug was real. The blast radius was narrowed by architecture the team almost certainly did not design as a defense.

That is not absolution. It is an observation about where safety actually comes from. It comes from layering, and layering is an accident more often than it is a plan.

Consent that never expires

I want to linger on the approval primitive, because the industry has never properly audited it, and ether.fi is only the most recent evidence.

The Queue That Never Asked Who Was Calling: An Audit of ether.fi's AtomicQueue Authorization Failure

An ERC-20 approval is a bearer instrument. It does not identify a purpose. It does not identify a spender beyond an address. It carries no expiration, no scope, no revocation trigger, and no notification path back to the person who signed it. It was designed in 2015 for simple, synchronous, single-asset interactions — a router pulling tokens to complete a swap, then done.

Then the industry built asynchronous infrastructure on top of it. Queues. Vaults. Solver networks. Restaking layers. Bridges with multi-week exit windows. Every one of these systems asks for a permission at the beginning of a relationship whose execution happens much later, in a contract that may by then be a different contract, running code that did not exist when the user clicked Approve.

The gap between when consent is granted and when consent is exercised is now measured in months. Nothing in the permission primitive acknowledges that gap. Nothing warns the user when the spender's behavior changes. Nothing revokes on inactivity. The drawer stays unlocked, and the check stays signed.

This is not an ether.fi problem. It is a primitive problem that ether.fi happened to expose. The honest version of the lesson is not that ether.fi should have added onlyRole. It is that we have built a financial system whose safety depends on an authorization mechanism never designed to be long-lived, and we have not replaced it.

Every user interacting with a queue-based protocol leaves a signed blank check in a drawer and trusts that no one will open it. Some of them are right. The ones who are wrong find out on a Tuesday.

The Queue That Never Asked Who Was Calling: An Audit of ether.fi's AtomicQueue Authorization Failure

The illusion that one audit is enough

There is a reflex in this industry that fires whenever a contract fails: hire more auditors. I do not oppose audits — I have performed them for most of my career. But audits have become a ritual of absolution rather than a process of inquiry, and this incident is a useful place to examine the gap.

A conventional audit reviews code against a stated threat model. That model typically assumes trusted actors behave. Solvers are trusted. Admin keys are held by the protocol. The attacker is external to the role hierarchy. Under that frame, a missing modifier on solve() is not obviously a finding, because the reviewer's attention is on what happens when the intended party calls the function, not on what happens when a stranger walks up to it.

The finding surfaces only if the reviewer's core posture is adversarial toward the protocol's own assumptions. Not "does this work?" but "what if the wrong person calls this?" Most engagements are scoped to the first question, because the first question is what the client asked.

I have written before that the loudest voice is rarely the most aligned, and it applies to security culture as much as governance. Protocols that talk most about their audit reports tend to be the ones whose audits were scoped to confirm what the team already believed about itself. The quiet teams who maintain living threat models, who treat every external call as hostile, who write invariants and fuzz them until they break — those teams rarely make news. Silence is a poor marketing asset, which is precisely why it is such a reliable quality signal.

If AtomicQueue had been reviewed by someone asking the adversarial question, the missing check would have taken minutes to find. Which suggests the review either did not happen, or happened inside a frame where the question was never asked. Either possibility is worth more attention than the 15.45 ETH.

The legal shadow nobody in the disclosure thread mentioned

I need to raise something uncomfortable, because it is directly relevant and because the industry habitually skips past it.

In August 2022, the United States sanctioned the Tornado Cash contracts, and by extension treated the act of publishing immutable code as a sanctionable activity. That precedent has not been reversed. If anything it has spread — into developer liability discussions in Europe, into MiCA implementation debates, into the quiet advice lawyers now give anyone shipping permissionless infrastructure.

This matters here because both stories share a premise. Both proceed from the assumption that code, once deployed, is a thing that acts — and that the people who wrote it bear responsibility for what it does when someone else operates it.

An attacker calling solve() on a public contract is doing something the contract permits. The contract, as deployed, has no concept of an unauthorized caller. If we continue down the path of holding developers liable for behaviors their code allows, then every missing modifier becomes a legal exposure and not merely a technical one. And if we continue treating code as speech, we must accept that some speech is dangerous.

Last year I worked with a European legal team to draft a framework on ethical staking governance, precisely because this tension is arriving faster than the industry is preparing for it. Our conclusion, stated plainly in the document, was that solver-based architectures need enumerated role definitions with on-chain, auditable grants — not because regulators demanded it, but because liability attaches to ambiguity. A protocol that cannot produce a list of who is authorized to act on user funds has no defense when someone acts without authorization.

I do not have a clean resolution to that tension. I do know the resolution will not come from people posting exploit screenshots. It will come from people reading statutes, and it will arrive whether the industry is ready or not.

Who is the solver, and can we prove it?

There is a question hiding underneath the missing modifier that deserves more attention than it has received.

In the original design intent, "solver" referred to a known operator. A team, a partner, a whitelisted keeper. Increasingly, though, solvers are autonomous agents — bots running strategies, MEV searchers, AI-driven executors that decide in milliseconds whether to fulfil a request. By 2026 this is no longer a hypothetical. It is the default.

When the solver is a bot, "who is calling" stops being an identity question answered by a role registry and becomes a provenance question. Which agent? Acting under whose authority? On behalf of which principal? An onlyRole modifier answers the first sub-question and nothing else. It tells you an address was whitelisted once. It does not tell you whether the agent behind that address is still acting in the interest of the people whose funds it touches.

This is the same gap I have been working on outside my writing. The project I lead builds zero-knowledge verification of human presence, originally to keep DAO governance from being flooded by automated accounts. The underlying primitive generalizes further than governance. It answers a question smart contracts cannot currently ask: is the entity invoking this function a verifiable, accountable party — human or institution — rather than an unattributed process?

I am not proposing that every solver be human. That would be absurd; the latency demands preclude it, and no one has proposed a queue that clears without automation. I am proposing that "authorized solver" should be a claim with evidence attached, not a line in a deployment script. The ether.fi incident is what you get when the industry skips that step: a privileged function reachable by anyone with a script and forty seconds of patience.

The fragmentation problem hiding behind the restaking boom

There is a structural reason this class of bug keeps appearing in restaking-layer protocols specifically, and it deserves naming.

Since the restaking narrative took hold, the number of liquid restaking and liquid staking tokens has multiplied far beyond the number of distinct users who want them. Dozens of receipts, all pointing at the same underlying ETH, all competing for the same liquidity, all integrated into the same handful of lending markets and pools. The aggregate user base has not grown proportionally. What has grown is surface area.

Every one of those protocols needs a queue. Every queue needs a solver role. Every solver role needs an access check. And every access check is a place where a team moving at narrative speed might skip a line.

The Queue That Never Asked Who Was Calling: An Audit of ether.fi's AtomicQueue Authorization Failure

The result is that liquidity is not being scaled. It is being sliced, and each slice is a new contract with a new function signature and a new opportunity for exactly the mistake SlowMist flagged. I have argued for years that stacking restaking layers on an unchanged user base is division rather than growth. This incident is a concrete cost of that division: more code, more roles, more permissions, more doors.

There is a related and less-discussed consequence. Latency is everything in execution, and solvers exist partly to compete on it. But the more the industry routes order flow through off-chain solver competition, the more it recreates the very conditions that make centralized venues unbeatable — because market makers will not leave quotes exposed on-chain to be front-run by anyone who can read the mempool. Queue architectures are not a decentralization victory. They are an admission that execution requires a private lane, dressed in the language of permissionless settlement. The missing modifier is what happens when that admission goes unexamined.

The contrarian angle: what if the audit was never the point

The dominant response to this incident will be a call for better audits. It is the correct-sounding response, requires no behavior change from anyone, and will accomplish very little.

Consider the actual incentive structure. A team shipping a queue-based LRT is racing a narrative. Narratives do not reward caution, because caution is invisible until something breaks. The audit is a cost center with a certificate at the end. The certificate feeds marketing. Marketing attracts deposits. Deposits increase the value of the standing approvals — which increases the prize for the next attacker.

Nothing in that loop penalizes the missing modifier at the moment it is written. The penalty arrives all at once, later, on a random day, and even then it is measured in a number small enough to be absorbed and forgotten. Until that asymmetry changes — until the cost of skipping one line lands on the team that skipped it rather than on the wallets that trusted it — the same incident will recur with a different protocol's name in the headline.

There is a second contrarian observation, and it is about us. The reflex is to blame the team. The team does bear responsibility. But the residual risk in this exploit sat in wallets, not in the protocol treasury. Every person holding an outstanding infinite approval to a queue contract is holding an unhedged position in someone else's code quality, and almost none of them know it.

Security education has failed here, and it failed because the people who need it most are least likely to read a thread about revocation. The tools exist. Revocation dashboards exist. Block explorers surface allowances. None of it appears in the onboarding flow, because onboarding is optimized by the same conversion logic that produced the infinite approval to begin with.

The loop closes on itself. The friction that would make users safer is removed to increase adoption, which enlarges the pool attackers draw from, which produces incidents that reduce adoption anyway. We are running an experiment on ourselves and calling the results growth.

A note on responsible disclosure as ritual

One more thing before I close, because it deserves saying plainly.

SlowMist's disclosure was responsible in the procedural sense. The team was notified before publication. That is the right practice and I do not question the intent behind it.

But responsible disclosure has evolved into something serving reputations more than users. A researcher notifies a team, waits a specified window, then publishes. The team gets a chance to patch and control the narrative. The researcher gets credit. The users — who hold the standing approvals — get a tweet and, if they are fortunate, a blog post written in the passive voice.

What users actually need is a mechanism. A registry of affected contracts. A machine-readable warning wallet software can display. A revocation prompt that appears without being sought. We have the technical capacity for all of it. We lack the coordination, because coordination is unglamorous and disclosure is a career asset.

I am not calling for the end of responsible disclosure. I am calling for its completion. A disclosure ending at publication is an announcement. A disclosure ending with every affected approval revoked is a remedy. We keep producing announcements and calling them diligence.

What revocation actually looks like

I want to end the practical portion with something concrete, because analysis without an action is entertainment.

If you have ever interacted with a queue-based protocol — not ether.fi specifically, but any of them — go look at your outstanding allowances. Not the ones you remember granting. The ones you do not. The list will be longer than you expect, and some of the entries will point at contracts you cannot identify by name, deployed by projects you followed for three weeks during a narrative.

Revoke what you do not actively use. It costs gas and it is boring and nobody will congratulate you. But the entire premise of the exploit class we have been examining is that the drawer stays unlocked because unlocking it is inconvenient. The single most effective security measure available to an individual depositor in this market is also the least discussed, because it produces no yield and generates no content.

That is not a coincidence. It is the same reason the missing modifier was never added. The work that prevents the incident is invisible, and invisible work does not win narratives.

Takeaway

ether.fi will likely patch the modifier within days. The queue will resume processing. The roundups will move on to the next incident, because there is always a next incident, and because 15.45 ETH is small enough to be forgotten before the next quarterly chart is published.

But the thing actually compromised on September 11 was not the contract. It was the assumption — held quietly across the entire restaking stack, and by everyone who ever clicked Approve without reading — that a permission granted once is a permission understood.

Solitude is the only auditor that never sleeps. Code is law, but conscience is the interpreter. And the question I keep returning to, sitting with that number, is this: if the missing line was worth only fifteen ETH to the attacker who found it first, how many other lines are missing in queues nobody has found yet — and who, precisely, is still holding the approval to that door?