# Nimiq ERC20PermitHTLCHandler: paymaster-selection trust bug drains approvals

Nimiq HTLC handler's GSN forwarder execute() skipped signature/nonce checks, letting attacker-chosen paymaster drain victim's standing approvals for ~$50k.

- Payout: Real-world hack
- Severity: high
- Program: Nimiq
- Disclosed: 2026-09-16
- Write-up quality: 8/10
- Original: https://github.com/SunWeb3Sec/DeFiHackLabs/blob/main/src/test/2026-09/Nimiq_exp.sol
- Library page: https://bugbountylibrary.com/r/dhl%3A20260916-nimiq
- 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:auth-bypass vuln:business-logic

**Target:** ERC20PermitHTLCHandler (OpenGSN forwarder/paymaster) on Polygon
**Root cause:** The handler's execute() forwarder entrypoint discards the signature, domainSeparator, nonce and preconditions, decoding only request.data and calling openPrivate(request.from). Signature/nonce/precondition checks live solely in preRelayedCall(), which the hub runs on the attacker-named paymaster instead of the handler.
**Steps:**
1. Fork Polygon one block before setup tx 0xb067efae... at block 93930784.
2. Deploy EvilPaymaster whose preRelayedCall accepts unconditionally and getGasAndDataLimits returns loose limits.
3. Register relay: setRelayManagerOwner -> stakeForRelayManager(1 POL) -> authorizeHubByManager -> addRelayWorkers.
4. For each token build RelayRequest with request.from=victim, request.data=abi.encodeCall(open, id, token, amount, refund, CREATE2 recipient, hash, timeout, fee), relayData.paymaster=EvilPaymaster, relayData.forwarder=handler.
5. Call real RelayHub.relayCall() from worker EOA; hub calls EvilPaymaster (accepts) then handler.execute() which runs openPrivate(victim) -> token.transferFrom(victim, handler, amount).
6. Deploy precomputed CREATE2 recipient and call redeem(id, target, secret=0x01, fee) to pull drained tokens to attacker.
**How found:** Recognized that the handler is simultaneously its own forwarder, paymaster and recipient, and that execute() discards signature/domainSeparator/nonce while preRelayedCall() holds all verification. Since RelayHub lets any staked relay choose any (paymaster, forwarder) pair, pointing paymaster at an attacker contract bypasses verifyCallPrivate entirely.
**Exploitation:** Attacker stakes a relay manager with 1 POL, registers a worker, and submits forged RelayRequests through the real RelayHub naming their own always-accept paymaster and the victim handler as forwarder. The handler's execute() skips all verification and calls openPrivate(victim), pulling the victim's unlimited ERC20 approvals into HTLCs. Attacker then redeems each HTLC with a known secret to a CREATE2-deployed recipient, extracting ~$50,463 in USDC, USDT0 and USDC.e.
**Insight:** When a contract is both GSN forwarder and paymaster, check whether the forwarder entrypoint independently verifies signatures/nonces or delegates that to preRelayedCall — if the latter, any staked relay can substitute its own paymaster and skip verification. Also look for victims holding unlimited approvals to such forwarders.
