
Date: September 21, 2026
Chain: Polygon
Loss: about $23K in WETH and ZED, per SlowMist
Class: unvalidated calldata that spent users' standing token approvals
One Polygon transaction on September 21, 2026, carried 466 instructions, and every one of them said the same thing to the WETH contract: move this user's tokens to me. None of those users had signed anything. The transaction went through a contract called GaslessReservoirEnabler, which they'd approved in the past, and WETH saw the Enabler as the spender each time, so it paid.
SlowMist put the total at about $23,000 in WETH and ZED, taken from 997 token-holder addresses, and the attacker needed three transactions to reach them all. The Enabler would forward any call a caller wrote, as long as the target contract was on its whitelist, and WETH was on the whitelist. Because the Enabler never checked whose tokens a forwarded transferFrom was moving, it spent its users' approvals for anyone who asked.
GaslessReservoirEnabler is a gasless meta-transaction relayer on Polygon, deployed without a proxy. Its function erc20WithTransfersAndExecute takes two lists. One describes ERC-20 transfers pulled from the caller, the other describes module calls, each a target contract on the Enabler's moduleWhitelist plus the data to send it. Users who'd interacted with it had given it ERC-20 allowances, standing permissions in the token contract that let the Enabler move their tokens. According to the DeFiHackLabs proof of concept, hundreds of victims had near-maximum approvals in place from prior legitimate use.
An approval only records which contract may spend. Once a user approves the Enabler, the token will honor any transferFrom the Enabler sends on that user's account, so the Enabler itself is the only place where anyone can check that a spend is one the user actually wanted. The contract did have a signature-checked path for relayed transactions, executeMetaTransaction, and DeFiHackLabs notes that the attack never touched it.
DeFiHackLabs confirms the flaw against the verified source, the trace and direct on-chain reads. erc20WithTransfersAndExecute is external and nonReentrant, with no authentication. For each module call, an internal function, _executeInternal, checks that the module is on moduleWhitelist and is a contract, then calls the module with the caller's data. Nothing compares that data with the caller's identity. SlowMist's alert, as mirrored by KuCoin, describes the same gap: the function checked module addresses but didn't bind ERC-20 transferFrom instructions to an authorized asset owner.
WETH was on the whitelist, and an on-chain read at the fork block confirmed moduleWhitelist[WETH] was true. That meant a caller could name WETH as the module and pass transferFrom(victim, helper, amount) as the data. WETH sees the Enabler as the sender, checks the victim's allowance to the Enabler, finds it, and moves the tokens.
A whitelist like this answers which contract will be called, and the question that mattered was on whose behalf. It works like an office mailroom that will stamp and send any letter as long as it's addressed to an approved bank, without checking whether the person handing it over is the account holder named in it. Any contract that forwards calldata while holding user approvals has to bind every spend to the caller, a verified signature or a recorded intent, because the token can't tell those calls apart.
The documented exploit transaction, 0x2e47...fdee in block 94,199,850, called erc20WithTransfersAndExecute through selector 0x7dede4cf. Its list of ERC-20 transfers, the leg that would have pulled from the caller's own wallet, was empty. Its list of module calls held 466 entries, each naming WETH as the module with a value of zero and a transferFrom from a different victim to the attacker's helper contract. The trace shows 466 transferFrom subcalls under the Enabler.
DeFiHackLabs totals the WETH leg at 8.7199 WETH, about $23,800 at the fork block's price, which it says matches the reported loss of about $23,000. Its proof of concept makes a typed call to the deployed Enabler against the three largest real victims, using their actual balances and existing approvals, and drains 2.3546 WETH from them. SlowMist's alert names 0x46f5...79aa as the attacker and reports that the attacker consolidated the stolen WETH and ZED and deposited it into a bridge on Polygon.
The attacker's address history shows the documented transaction was the first of three. At 15:34:22 UTC, 0x2e47...fdee pulled 8.7199 WETH from 466 addresses into the helper contract at 0x26a3...62Eb. Thirty-one seconds later, 0xa1b1...ddf9 pulled 0.1304 WETH from 466 different addresses, and at 15:35:22 UTC, 0x0475...f886 pulled 81,210.29 ZED from 165 addresses, some of them already hit in the first two, and sold it into the ZED/WETH pool for 0.0041 WETH. Counted together, and after setting aside the pool, the three transactions took tokens from 997 distinct addresses, the figure SlowMist reported. At 15:35:43 UTC the attacker withdrew 8.8545 WETH from the helper, the three WETH amounts added together, and at 15:36:13 UTC sent the WETH through the contract Polygonscan labels Metamask: Meta Bridge, followed by about 1,993 POL in a second bridge transaction.
Olympix reads a contract the way an attacker does, as states, transitions and entry points, and takes in the context around it, here the approvals users grant the Enabler so it can relay their transactions, to infer the invariants the system depends on but never writes down. For this contract the invariant is one sentence: no call through the Enabler can move a user's tokens unless that user is the caller or authorized that spend.
The Enabler has two doors into the same approvals, a signed meta-transaction path and an unsigned batch path, and the engine maps both before deciding which to attack. Olympix's static analysis surfaces the signal on the second one: an external function with no authentication forwards caller-written calldata to an external contract, and the only check on that path concerns the target's address, never the arguments. When the target is a token the contract holds approvals for, that calldata can encode a spend from anyone.
BugPoCer takes that path and runs Counterexample-Guided Invariant Refutation from the Enabler's side of the approval. It reasons from what the contract is trusted to do with other people's allowances, then searches calls from an address that has signed nothing. The shortest break needs no setup at all: an empty transfer list plus one module call naming a whitelisted token, with transferFrom data pointing at a user who approved the Enabler.
The output is a counterexample: a fresh address calls erc20WithTransfersAndExecute, names WETH as the module and a victim as the source, and the victim's WETH arrives at the caller's helper. BugPoCer returns that sequence with the minimal conditions that trigger it and a generated Forge test that replays it, which is the September 21 drain produced in development instead of on Polygon. Because Olympix runs automatically in development, at every merge and before release, adding a token to a whitelist gets tested against every path that can reach it the day it happens.
GaslessReservoirEnabler checked the address it was calling and never the account it was spending from, and that gap was enough to move WETH and ZED out of hundreds of wallets. For any contract that holds approvals and forwards calls, every spend has to trace back to the person whose tokens move. To see how Olympix tests the forwarding and approval paths in your own contracts and hands back a runnable counterexample for any that spend for the wrong account, book a technical walkthrough.
Protocol: GaslessReservoirEnabler
Chain: Polygon
Date: September 21, 2026, 15:34:22 to 15:36:13 UTC
Loss: about $23K in WETH and ZED, per SlowMist (8.8545 WETH and 81,210.29 ZED from 997 addresses across three txs, read on-chain)
Attacker: 0x46f5...79Aa
Vulnerable contract (GaslessReservoirEnabler, verified): 0x9B58...163e
Whitelisted token used as module (WETH): 0x7ceB...f619
Example victim: 0x2f78...ac96
Attacker helper contract: 0x26a3...62Eb
ZED token: 0x5ec0...acdc
Exploit tx 1 (466 WETH victims, 15:34:22 UTC): 0x2e47...fdee
Exploit tx 2 (466 WETH victims, 15:34:53 UTC): 0xa1b1...ddf9
Exploit tx 3 (165 ZED victims, 15:35:22 UTC): 0x0475...f886
Withdrawal to attacker (8.8545 WETH): 0xe6e6...489b
Bridge deposit (WETH, via Metamask: Meta Bridge): 0x871f...e4e2
Block (first exploit tx): 94199850
SlowMist: GaslessReservoirEnabler exploit alert
KuCoin: SlowMist alert on GaslessReservoirEnabler with attacker and contract addresses
DeFiHackLabs: GaslessReservoirEnabler exploit proof of concept