# GaslessReservoirEnabler: unvalidated module call drains WETH approvals

GaslessReservoirEnabler ran unvalidated module.call(data) with WETH whitelisted as a module, letting anyone invoke transferFrom to drain 466 victims' standing approvals for 8.72 WETH.

- Payout: Real-world hack
- Severity: high
- Program: GaslessReservoirEnabler
- Disclosed: 2026-09-21
- Write-up quality: 7/10
- Original: https://github.com/SunWeb3Sec/DeFiHackLabs/blob/main/src/test/2026-09/GaslessReservoirEnabler_exp.sol
- Library page: https://bugbountylibrary.com/r/dhl%3A20260921-gaslessreservoirenabler
- 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:** Solidity meta-transaction relayer contract (erc20WithTransfersAndExecute) on Polygon
**Root cause:** erc20WithTransfersAndExecute's ExecutionInfo loop calls module.call(data) after only checking moduleWhitelist[module] and isContract, never restricting data. WETH itself is whitelisted as a module, so caller-supplied transferFrom(victim, attacker, amount) executes with the enabler as msg.sender.
**Steps:**
1. Identify enabler 0x9B58fDAdc16E30fBA313E044bf9e88689C3F163e and confirm moduleWhitelist[WETH]==true via on-chain read
2. Find addresses holding standing WETH approvals to the enabler
3. Build ExecutionInfo[] with one entry per victim: `{module: WETH, data: transferFrom(victim, attacker, amount), value: 0}`
4. Call `erc20WithTransfersAndExecute([], executionInfos)` with empty erc20sTransfers
5. Enabler executes WETH.transferFrom as msg.sender, moving each victim's WETH to attacker
**How found:** Code review of the verified source: noticed the ExecutionInfo leg performs module.call(data) with fully caller-controlled data and only whitelist+isContract checks, then observed WETH was itself whitelisted as a module, turning the generic call primitive into arbitrary transferFrom over standing approvals.
**Exploitation:** Any unprivileged address submits a single call to erc20WithTransfersAndExecute with an empty transfers array and 466 ExecutionInfo entries, each targeting the whitelisted WETH module with transferFrom(victim, helper, amount) calldata. Because the enabler is msg.sender to WETH and victims held near-max standing approvals to it, all transfers succeed; 8.72 WETH (~$23.8K) drained across 466 victims in one tx. No signature, role, or setup required.
**Insight:** When a contract exposes a generic module.call(data) primitive, check whether any live token or high-value contract is itself on the module whitelist — that converts an innocuous 'execute' feature into arbitrary calldata against that contract, and any standing approvals to the caller contract become drainable.
