# New Market Trading: SquidRouterModule missing caller check drains Safes

NMT's SquidRouterModule trusted a caller-supplied sourceAddress string and never checked msg.sender==delegate, letting anyone drive Safe swaps and drain 88 Safes of ~$3.98M.

- Payout: Real-world hack
- Severity: critical
- Program: New Market Trading
- Credited: DeFiHackLabs
- Disclosed: 2026-05-25
- Write-up quality: 8/10
- Original: https://github.com/SunWeb3Sec/DeFiHackLabs/blob/main/src/test/2026-05/NewMarketTrading_exp.sol
- Library page: https://bugbountylibrary.com/r/dhl%3A20260525-newmarkettrading
- Tags: domain:web3 impact:funds-loss kind:incident platform:defihacklabs sev:critical tech:evm tech:solidity technique:chained technique:source-review vuln:access-control vuln:auth-bypass

**Target:** Gnosis Safe module (SquidRouterModule) on Ethereum/Base/Arbitrum
**Root cause:** The module's _executeWithToken override validated a caller-supplied sourceAddress string against the public squidRouter address instead of verifying the real Axelar gateway, and _handleActions never required msg.sender == delegate, so the public expressExecuteWithToken path executed arbitrary payload actions on any Safe.
**Steps:**
1. Call `expressExecuteWithToken(commandId, sourceChain, "0xce16F69375520ab01377ce7B88f5BA8C48F8D666", payload, symbol, 0)` on module `0x1f1d37a3Bf840e35c6a860c7C2dA71Fe555123ca` with amount=0 so safeTransferFrom costs nothing.
2. Set sourceAddress to the public squidRouter address string to pass `require(srcAddress == squidRouter)`.
3. Craft payload = abi.encode(module, victimSafe, realDelegate, params) where realDelegate (e.g. `0x0f7aAa84...`) already holds APPROVE+SWAP permission on the Safe.
4. params.actions = [ERC20_APPROVE(USDC, permit2, max), PERMIT2_APPROVE(USDC, universalRouter), UNI_V3_SWAP_EXACT_IN(USDC->attacker 'u' token, amountOutMin=0)] with isStrict.
5. Module transfers 0 tokens to Safe then runs actions; Safe approves and swaps its entire USDC into attacker-owned Uniswap V3 pool at fee 500.
**How found:** Reading the verified source: the express path performs no gateway validation, and the override compares a caller-supplied sourceAddress string to the public squidRouter constant — a 'master password printed in the source code'. Attacker likely monitored permission contracts for months waiting for sufficient value.
**Exploitation:** No flash loan or capital needed: amount=0 makes the express transfer a no-op. Attacker supplied a real public delegate address that already had permissions, so the module's permission check passed while msg.sender was the attacker. The Safe's USDC was swapped via Permit2/UniversalRouter into an attacker-controlled V3 pool for a worthless 'u' token, then routed to DAI. Repeated across 88 Safes on Ethereum, Base, and Arbitrum in ~2 hours; ~$3.98M consolidated to 3.07M DAI. Seed capital 2.1 ETH from Tornado Cash.
**Escalation:** Scaled the same forged-payload call across 88 victim Safes on three chains simultaneously, consolidating proceeds into DAI in a single wallet.
**Insight:** On Safe modules and Axelar express-executable integrations, check whether authorization compares caller-supplied strings/addresses from the payload rather than msg.sender or gateway validation; a public constant like a router address used as a require() target is a free bypass, and any delegate address with standing permissions becomes a usable puppet.
