Exploit post-mortems
/
QNT Reserve Pool
Ethereum
Access control

QNT Reserve Pool's $125K Loss and How Olympix Would Have Prevented It

Loss
Loss
Exploit date
April 28, 2026

For years, a simple rule held on Ethereum: if a call comes from an ordinary wallet address, the person holding that wallet's private key sent it. A QNT investment pool built its admin check on that rule. EIP-7702, the Pectra-era standard that lets an ordinary account (an EOA) take on smart contract code, quietly changed it. On April 28, 2026, an attacker used that change, and one contract with no caller check, to make the pool believe its own admin was asking for its QNT.

The drain landed in a single transaction at 13:19:59 UTC in block 24978818. The pool sent 1,974.546547972979064297 QNT to the attacker, which DARKNAVY's analysis values at about $124.9K. SlowMist's alert the next day reported 1,988.5 QNT, roughly 54.93 ETH, but the Transfer event on-chain shows the smaller figure, and that's the one used here.

A Pool That Trusted an Address

The victim was an investment pool contract holding QNT, with deposits, withdrawals, whitelist management, fee distribution and one powerful admin function. According to DARKNAVY's read of the unverified bytecode, that function, forwardTransaction(), takes a target address, a gas amount and arbitrary calldata, then makes the external call. Its only guard is an admin check: the pool looks up the caller in its participant records and requires a non-zero admin flag.

That check is strong as long as admin addresses behave like ordinary wallets. One of the pool's registered admins was an EOA. Before EIP-7702, the only way to send a call from that address was to sign it with the key. So "the caller is an admin address" and "the admin approved this call" meant the same thing.

DARKNAVY found that this admin EOA had been delegated under EIP-7702, with its on-chain code pointing to a separate implementation contract. Once that delegation was live, anyone could call the code at the admin's address, and whatever that code called next would see the admin as msg.sender. The pool's trust was now only as strong as the access control in the delegated code and in anything that code trusted.

The Open Door at the Start of the Chain

The delegated implementation exposes an executeBatch() function that runs a list of operations from the admin's address. Its source isn't verified, so DARKNAVY couldn't confirm its access control from code. The contract also exposes an isOwner() function, and the attacker went out of their way to route through a third contract instead of calling executeBatch() directly. DARKNAVY reads both signals as evidence that executeBatch() likely checks for an authorized owner, and that the third contract was likely registered as one.

That third contract is where the chain broke. It's a small, unverified utility contract with a public batch() function. Based on DARKNAVY's decompilation, batch() takes a list of target addresses and a matching list of calldata and calls each target in turn, reverting if any call fails. There's no owner check, no allowlist and no restriction on who can call it.

Put the pieces together and the chain looks like this. Anyone can call batch(). batch() can call the admin EOA's executeBatch(). executeBatch() runs operations as the admin. The pool accepts admin calls to forwardTransaction(), which will call the QNT token with any calldata. Each later link in the chain checked its caller, and each of those checks passed because the link before it was trusted. Only the first link checked nothing.

One Contract Creation, Six Hops

The attacker's EOA sent a single contract-creation transaction. According to DARKNAVY's call trace, the new contract deployed a second helper and triggered it. That helper called batch() with one target, the admin EOA, and one payload, an executeBatch() call. The admin EOA ran that batch, which held a single operation: call the pool's forwardTransaction() with the QNT token as the target and a transfer of 1,974.546547972979064297 QNT to the attacker as the data.

The pool checked whether its caller was an admin. It was. The pool then called the QNT token, and the token moved the full amount to the attacker's EOA. DARKNAVY's evidence includes both the pool's forwardTransaction event and the QNT Transfer event in the same receipt.

There was no flash loan, no price manipulation and no swap. The transaction used 2,270,882 gas and cost about 0.004 ETH. The attack's only real requirement was knowing which addresses trusted which.

What Happened After

SlowMist flagged the incident publicly on April 29. As of November 12, 2026, the pool's operator hadn't published a post-mortem, and no recovery or return of the QNT had been announced.

How Olympix Would Have Caught the Open Entry Point

Olympix reads a system as states, transitions and entry points, then infers the invariants the system depends on. For this pool, the invariant is written into its own admin check: only a legitimate admin should be able to trigger forwardTransaction(), and so only a legitimate admin should be able to move the pool's QNT. The interesting question is every path by which a call can arrive at the pool carrying an admin's address.

That question only gets answered if the delegate contracts are part of the analysis. Had the team included the batch contract and the delegated wallet code in scope alongside the pool, static analysis would have surfaced the key signal at the start of the chain: a public, payable batch() that makes arbitrary calls to caller-chosen targets, with no restriction on msg.sender, sitting in front of a contract that runs operations as a pool admin.

BugPoCer would have taken it from there. It starts from the invariant and searches for a sequence that breaks it. A caller the pool has never seen calls batch() with the admin EOA as the target and an executeBatch() payload, the admin's code calls forwardTransaction(), and the pool transfers QNT out. BugPoCer would have returned that sequence as a counterexample, with the minimal preconditions it depends on, such as the admin flag on the delegated EOA and whatever authorization the batch contract held in the wallet code, plus a generated Forge test that replays the transfer. That test would have been the April 28 drain, produced in development, while the fix was still one caller check on batch().

Because the engine runs at every merge and before release, the moment an admin address picked up new delegated code, or a helper contract picked up new authority, the same invariant would have been checked again against the new paths.

The pattern isn't new. It's the same kind of open public entry point that let an outsider upgrade a game service and pull approved WETH from The Red Village's players for $80.7K in September. EIP-7702 just gives it a new place to hide.

Delegation Changes What an Address Means

EIP-7702 is a real improvement for users, and it's here to stay. It also means an address on an allowlist can now be code, and that code may trust other code. Any protocol that checks msg.sender against a list of admin or operator addresses should ask which of those addresses can now carry delegated code, and what that code will run on someone else's behalf.

For builders, the practical rule is to treat every contract that can act as an admin as an admin contract itself. Helpers, batchers and routers registered as owners of a privileged wallet need the same caller checks as the wallet, and they belong in scope whenever the system they feed is reviewed. If your team uses delegated accounts in privileged roles and wants to see how those paths would be mapped, book a technical walkthrough.

On-Chain References

Attacker EOA: 0xf560...fa01

Attack Contract 1: 0xee03...e49f

Attack Contract 2: 0x9af7...7ea2

Victim Pool: 0xdd4f...e16a

EIP-7702 Admin EOA: 0xc6dd...13f4

EIP-7702 Implementation: 0x9553...688e

Batch Contract (Unguarded batch()): 0x044d...057c

QNT Token: 0x4a22...4675

Exploit Transaction: 0xef99...684f

See what Olympix finds on your own code

Send us a repo that's already been audited, and we'll run Olympix at the same commit and show you what comes back.