QNT Pool Drain via EIP-7702 Admin EOA Delegation
On Ethereum mainnet, transaction 0xef9994ac862318ccf3ebdb66c181bb159651373b945aea59a966608d7b98684f succeeded at block 24978818 on 2026-04-28T13:19:59Z. The attacker deployed two helper contracts and exploited the public batch(address[],bytes[]) function on legacy contract 0x044dc3e39c566a95011e272ec800dbd2cc9c057c to route a call through an EIP-7702 delegated EOA (0xc6ddf907). Because 0xc6ddf907 was a registered admin of QNT pool 0xdd4f556e1b42d9b29294a7eeb6d6a5059bbbe16a, the call passed the pool’s _isAdmin(msg.sender) check on forwardTransaction(), causing QNT.transfer(attacker, 1974.5465 QNT). The victim pool lost 1,974.546547972979064297 QNT, estimated at approximately $124.9K.
Root Cause
Vulnerable Contract 1: Batch Executor
- Batch contract:
0x044dc3e39c566a95011e272ec800dbd2cc9c057c. - Source status: unverified on Etherscan. Decompiled via Dedaub (user-provided reconstruction).
- Bytecode size: 1,418 bytes — minimal utility contract.
// AI source reconstruction by app.dedaub.com
// 2026.04.29 01:23 UTC
pragma solidity 0.8.31;
error BatchRevert(uint256 index, address recipient, bytes returnData);
contract Contract {
function batch(address[] calldata recipients, bytes[] calldata data) public payable {
require(recipients.length == data.length);
for (uint256 i = 0; i < recipients.length; i++) {
(bool success, bytes memory returnData) = recipients[i].call(data[i]); // <-- NO ACCESS CONTROL
if (!success) {
revert BatchRevert(i, recipients[i], returnData);
}
}
}
receive() external payable {}
}
The batch() function is public with no access control — no onlyOwner, no whitelist, no require(msg.sender == ...). Any caller can specify any target address and any calldata, and the contract will execute recipients[i].call(data[i]) without restriction.
Vulnerable Contract 2: QNT Pool
- QNT pool:
0xdd4f556e1b42d9b29294a7eeb6d6a5059bbbe16a. - Source status: unverified on Etherscan. Decompiled via Dedaub (user-provided reconstruction).
- Contract type: investment pool with deposit/withdrawal, whitelist management, fee distribution, and an admin-only
forwardTransactionfunction. - Bytecode size: ~14 KB, 20+ public functions.
The critical function is forwardTransaction (selector 0x4d5a8e10):
function _0x4d5a8e10(address target, uint256 gasAmount, bytes calldata data) external {
require(_isAdmin(msg.sender)); // <-- access control: checks admin flag
require(stor_0_0_0 != 1); // pool must not be in 'failed' state
emit EventA0e077e8(target, gasAmount, data);
uint256 g = gasAmount > 0 ? gasAmount : gasleft();
(bool ok,) = target.call{gas: g}(data); // <-- arbitrary external call
require(ok);
}
function _isAdmin(address a) internal view returns (bool) {
return (_getParticipantInfo[a].flags & 0xff) != 0; // bottom byte of flags != 0
}
The forwardTransaction() function does have access control — require(_isAdmin(msg.sender)). The admin check looks up the caller in the _getParticipantInfo mapping and verifies the bottom byte of the flags field is non-zero. During the exploit, msg.sender was 0xc6ddf907, which was a registered admin of the pool. The check passed, and the function executed QNT.transfer(attacker, 1974.5 QNT) via the arbitrary external call.
This means the exploit relied on being able to make calls appear to originate from the admin EOA 0xc6ddf907.
EIP-7702 Delegated Admin EOA: The Missing Link
- Delegated EOA:
0xc6ddf90790b433743bd050c1d1d45f673a3413f4. - On-chain bytecode:
0xef010095538e1c40e82dbc9dfb2f7f88580d0d0824688e.0xef01= EIP-7702 delegation designator.0x00= version byte.0x95538e1c40e82dbc9dfb2f7f88580d0d0824688e= implementation address.
- Implementation:
0x95538e1c40e82dbc9dfb2f7f88580d0d0824688e(unverified, ~4 KB).- Key selectors:
0x34fcd5be=executeBatch((address,uint256,bytes)[]),0x2f54bf6e=isOwner(address). - Source status: unverified — the
executeBatchaccess control behavior cannot be confirmed from source. The presence ofisOwner(address)suggests owner-based access control, meaningexecuteBatchlikely requires the caller to be an authorized owner.
- Key selectors:
Before EIP-7702, 0xc6ddf907 was a plain EOA. Only the holder of its private key could sign transactions from it. The pool’s require(_isAdmin(msg.sender)) was secure because it relied on the assumption that only the EOA owner could call functions “from” that address.
After EIP-7702 delegation, 0xc6ddf907 behaves as a smart contract: anyone can call its functions, and those calls execute with 0xc6ddf907 as msg.sender to downstream contracts.
Why It’s Vulnerable
Expected behavior: the QNT pool’s forwardTransaction() is admin-only. Only the private key holder of admin addresses can invoke it. The admin EOA 0xc6ddf907 was trusted to only call forwardTransaction for legitimate operations.
Actual behavior: EIP-7702 delegation broke the trust assumption that “only the private key holder of address X can send transactions from X.” After delegation, the EOA’s code can be invoked by any caller. When the batch contract called executeBatch() on the EIP-7702 implementation, the implementation executed the requested operation from 0xc6ddf907’s context — so msg.sender to the QNT pool was 0xc6ddf907, a registered admin. The pool’s _isAdmin(0xc6ddf907) check passed because the address is indeed an admin — but the call was initiated by the attacker, not by the admin.
The role of the batch contract: The attacker went through the batch contract rather than calling 0xc6ddf907.executeBatch() directly. This is significant — the EIP-7702 implementation has isOwner(address), suggesting executeBatch checks whether msg.sender is an authorized owner. If so, the batch contract (0x044dc3e3) was likely registered as an owner of the EIP-7702 wallet, making it a necessary stepping stone. The attacker could not call executeBatch directly from their deployed contract because it was not an authorized caller. The batch contract, with its missing access control, became the entry point that the attacker exploited to reach the authorized executeBatch.
The attack chain combined three weaknesses:
| Component | Weakness | Role in Attack |
|---|---|---|
Batch contract 0x044dc3e3 | No access control on batch() | Entry point: anyone can route calls through it to reach authorized executors |
EIP-7702 account 0xc6ddf907 | EIP-7702 delegation + admin of pool | Delegation turns trusted admin EOA into callable contract; its admin status on the pool is weaponized |
QNT pool 0xdd4f556e | forwardTransaction trusts callers by address | Admin address-based trust is broken when the admin EOA becomes a delegated contract |
Source caveat: the EIP-7702 implementation at 0x95538e1c remains unverified. The isOwner(address) selector and the attacker’s choice to route through the batch contract strongly suggest owner-based access control on executeBatch, but this cannot be confirmed without the implementation source. If executeBatch has no access control, the batch contract would not be strictly necessary, but the core finding — EIP-7702 delegation breaking address-based trust — remains unchanged.
Attack Execution
High-Level Flow
- The attacker EOA
0xf5604f65submitted a contract-creation transaction deployingAttackContract1(0xee039cb7). AttackContract1deployedAttackContract2(0x9af7eb6c) viaCREATE.AttackContract1calledAttackContract2via selector0x0ce23abbto trigger the exploit.AttackContract2calledbatch()on the unpermissioned batch contract, targeting the EIP-7702 account0xc6ddf907withexecuteBatch()calldata.- The batch contract forwarded the call to
0xc6ddf907, which (via EIP-7702 delegation) executedexecuteBatch()with one operation: call QNT pool0xdd4f556ewithforwardTransaction(QNT, 0, transfer(attacker, 1974.5 QNT)). - The QNT pool’s
forwardTransaction()checked_isAdmin(0xc6ddf907)— passed, since0xc6ddf907is a registered admin — and executedQNT.transfer(attacker, 1974546547972979064297).
Detailed Call Trace
- Root: attacker EOA
0xf5604f65CREATEsAttackContract10xee039cb7.- Path
.0:AttackContract1CREATEsAttackContract20x9af7eb6c(init bytecode 8,597 bytes, runtime 16,890 bytes). - Path
.1:AttackContract1->AttackContract2,CALL, selector0x0ce23abb(4-byte trigger function, no arguments).- Path
.1.0:AttackContract2-> batch contract0x044dc3e3,CALL, selector0xf38f59d7=batch(address[],bytes[]). Input: 740 bytes.recipients = [0xc6ddf90790b433743bd050c1d1d45f673a3413f4]data = [executeBatch(...)]- Path
.1.0.0: batch contract -> EIP-7702 account0xc6ddf907,CALL, selector0x34fcd5be=executeBatch((address,uint256,bytes)[]). Input: 484 bytes.msg.senderto EIP-7702 impl =0x044dc3e3(batch contract). IfexecuteBatchchecksisOwner(msg.sender), the batch contract must be a registered owner.operations = [(0xdd4f556e, 0, forwardTransaction(...))]- Path
.1.0.0.0:0xc6ddf907-> QNT pool0xdd4f556e,CALL, selector0x4d5a8e10=forwardTransaction(address,uint256,bytes). Input: 228 bytes.msg.sender=0xc6ddf907— registered admin,_isAdmincheck passes.target = 0x4a220e6096b25eadb88358cb44068a3248254675(QNT token)gasAmount = 0(usesgasleft())data = 0xa9059cbb000...6b0a5687c913387de9=transfer(0xf5604f65..., 1974546547972979064297)- Path
.1.0.0.0.0: QNT pool -> QNT token,CALL,transfer(address,uint256). Output: 32 bytes (success).
- Path
- Path
Calldata Breakdown
The outermost transaction is a contract creation (9,989 bytes init code). The nested exploit calldata:
Layer 1 — batch(address[],bytes[]) at batch contract (740 bytes):
0xf38f59d7
recipients.length = 1
recipients[0] = 0xc6ddf90790b433743bd050c1d1d45f673a3413f4
data[0] = <executeBatch calldata>
Layer 2 — executeBatch((address,uint256,bytes)[]) at EIP-7702 account (484 bytes):
0x34fcd5be
operations.length = 1
operations[0]:
target = 0xdd4f556e1b42d9b29294a7eeb6d6a5059bbbe16a (QNT pool)
value = 0
data = <forwardTransaction calldata>
Layer 3 — forwardTransaction(address,uint256,bytes) at QNT pool (228 bytes):
0x4d5a8e10
target = 0x4a220e6096b25eadb88358cb44068a3248254675 (QNT token)
gasAmount = 0 (uses gasleft())
data = transfer(0xf5604f6545d5827a01801ffa5c48f5c61258fa01, 1974546547972979064297)
The transfer amount 0x6b0a5687c913387de9 = 1974546547972979064297 = 1974.546547972979064297 QNT (18 decimals).
Financial Impact
| Metric | Value |
|---|---|
| Token drained | 1,974.546547972979064297 QNT |
| Estimated USD | ~$124.9K |
| Gas used | 2,270,882 |
| Gas price | 1.7735 gwei |
| Gas cost | 0.004027 ETH (~$6.40) |
| Attack complexity | Single transaction, no flash loan, no AMM interaction |
No flash loan, AMM swap, oracle manipulation, or price feed interaction appears in this transaction. The loss comes entirely from the EIP-7702 delegation enabling impersonation of a trusted admin address.
Evidence
- Exploit receipt status:
0x1. - Contract created in exploit receipt:
AttackContract10xee039cb73872827a5118cdd67e0b24e45651e49f. - Receipt log
0x1f1: event from QNT pool0xdd4f556e(topic0xa0e077e8...) — matchesEventA0e077e8(address, uint256, bytes)emitted byforwardTransaction(). Decoded data: target = QNT token, gasAmount = 0, data =transfer(attacker, 1974546547972979064297). This confirmsforwardTransaction()executed and the_isAdmin(msg.sender)check passed. - Receipt log
0x1f2: QNTTransfer(address,address,uint256)(topic0xddf252ad...) from0xdd4f556eto0xf5604f65, amount1974546547972979064297. - EIP-7702 delegation verification: bytecode at
0xc6ddf907is0xef0100+95538e1c40e82dbc9dfb2f7f88580d0d0824688e, confirming EIP-7702 delegation. - Admin verification: the trace shows
0xc6ddf907callingforwardTransaction()on the QNT pool, and the pool accepted the call (did not revert). The Dedaub decompilation confirmsforwardTransactionrequires_isAdmin(msg.sender), proving0xc6ddf907is a registered admin. - Selector verification:
batch(address[],bytes[]) = 0xf38f59d7,executeBatch((address,uint256,bytes)[]) = 0x34fcd5be,forwardTransaction(address,uint256,bytes) = 0x4d5a8e10,transfer(address,uint256) = 0xa9059cbb. - Source caveat: batch contract and QNT pool decompiled via Dedaub (user-provided). EIP-7702 implementation (
0x95538e1c) remains unverified;executeBatchaccess control behavior is inferred from theisOwner(address)selector and the attacker’s routing choice.
Related URLs
- Exploit transaction: https://etherscan.io/tx/0xef9994ac862318ccf3ebdb66c181bb159651373b945aea59a966608d7b98684f
- Batch contract: https://etherscan.io/address/0x044dc3e39c566a95011e272ec800dbd2cc9c057c
- QNT pool (drained): https://etherscan.io/address/0xdd4f556e1b42d9b29294a7eeb6d6a5059bbbe16a
- EIP-7702 admin EOA: https://etherscan.io/address/0xc6ddf90790b433743bd050c1d1d45f673a3413f4
- EIP-7702 implementation: https://etherscan.io/address/0x95538e1c40e82dbc9dfb2f7f88580d0d0824688e
- Attacker EOA: https://etherscan.io/address/0xf5604f6545d5827a01801ffa5c48f5c61258fa01
- AttackContract1: https://etherscan.io/address/0xee039cb73872827a5118cdd67e0b24e45651e49f
- AttackContract2: https://etherscan.io/address/0x9af7eb6c701b33c52c8b5050fbbba9faafaa7ea2
- QNT token: https://etherscan.io/address/0x4a220e6096b25eadb88358cb44068a3248254675