# EtherFi AtomicQueue: missing solver access control drains allowances

EtherFi AtomicQueue.solve lacked access control on the caller-supplied solver, letting an attacker name any allowance-holding victim as solver and drain their standing liquidETH allowances.

- Payout: Real-world hack
- Severity: high
- Program: EtherFiAtomicQueue
- Credited: DeFiHackLabs
- Disclosed: 2026-09-09
- Write-up quality: 8/10
- Original: https://github.com/SunWeb3Sec/DeFiHackLabs/blob/main/src/test/2026-09/EtherFiAtomicQueue_exp.sol
- Library page: https://bugbountylibrary.com/r/dhl%3A20260909-etherfiatomicqueue
- Tags: domain:web3 impact:funds-loss kind:incident platform:defihacklabs sev:high tech:evm tech:solidity technique:chained technique:source-review vuln:access-control vuln:business-logic

**Target:** AtomicQueue.solve() on Ethereum mainnet
**Root cause:** solve() takes a caller-supplied `solver` address with no check that solver == msg.sender, no signature, and no registration/consent. It transfers the user's offer to solver, calls solver.finishSolve, then pulls want from solver to the user using assetsToUser derived from the user's own AtomicRequest.
**Steps:**
1. Pick a victim address holding a standing liquidETH allowance to AtomicQueue (leftover from legitimate queue use).
2. Compute pullable = min(liquidETH.balanceOf(victim), liquidETH.allowance(victim, queue)).
3. Mint a worthless FakeOffer token to self and approve AtomicQueue.
4. Call `updateAtomicRequest(fakeOffer, liquidETH, {deadline: max, atomicPrice: pullable, offerAmount: 1e18, inSolve: false})` so assetsToUser == atomicPrice == pullable.
5. Call `solve(fakeOffer, liquidETH, [attacker], "", victim)` — victim is named as solver, so AtomicQueue pulls `pullable` liquidETH from victim via its standing allowance, paying victim only 1e18 worthless FakeOffer.
6. Repeat per victim; cash out drained liquidETH to native ETH via Uniswap V4 pools (fee 100/1, 4500/90, 4050/81).
**How found:** Code review of AtomicQueue.solve revealed the `solver` parameter is fully caller-controlled with no msg.sender check, signature, or registration; combined with the fact that users leave standing ERC-20 allowances to the queue, an attacker can name any allowance-holder as solver and have the queue pull their funds.
**Exploitation:** Attacker contract mints a worthless ERC-20, registers a self AtomicRequest with atomicPrice equal to each victim's pullable liquidETH, then calls solve() naming the victim as solver. AtomicQueue's standing allowance to the victim is abused to transferFrom liquidETH out of the victim; the victim receives only 1e18 of the fake token. Drained liquidETH is swapped to native ETH on Uniswap V4 and sent to the attacker EOA. 14.4455 liquidETH drained from 9 real victims, cashed out to ~15.45 ETH.
**Escalation:** Looped over 9 distinct victim addresses, each with pre-existing allowances, to aggregate ~15.45 ETH; cashed out across three V4 pools to limit slippage.
**Insight:** When a function accepts an address parameter that will be used as the source of a transferFrom (or as a callback target), check whether that address is bound to msg.sender. Standing allowances to a shared contract are a shared pool of pullable funds — any unbound `solver`/`recipient`/`payer` parameter turns them into a drain primitive.
