On Ethereum at 2026-05-25T06:17:59Z, an attacker used New Market Trading’s SquidRouterModule to execute unauthorized swaps against a victim Safe through an access control bypass. The vulnerable path was the module’s Axelar express entrypoint, which accepted attacker-supplied payload data and trusted a forged delegate identity after only checking that a string-form sourceAddress matched Squid’s router address. In this transaction, the victim Safe at 0xc52950d522034a558903cc409c8bbf1f4decc62e lost 0.25361701 WBTC, 0.293599251 wTAO, and 0.022100612795638534 WETH, and received only 0.568933475584054988 u in return. The attacker exploited the module to impersonate a permissioned delegate and route those assets through Uniswap V3 pools using three fake expressExecuteWithToken calls with zero bridged amount.

Root Cause

Vulnerable Contract

SquidRouterModule at 0x1f1d37a3bf840e35c6a860c7c2da71fe555123ca was the primary vulnerable contract. It is not a proxy, and the source is verified on-chain. The victim Safe that actually held the drained assets was 0xc52950d522034a558903cc409c8bbf1f4decc62e.

Vulnerable Function

The vulnerable execution path was expressExecuteWithToken(bytes32,string,string,bytes,string,uint256) (0xe4a974cc), which reaches the internal function _executeWithToken(bytes32,string,string,bytes,string,uint256) and then _processPayload(IERC20,uint256,bytes).

Vulnerable Code

function _executeWithToken(
    bytes32,
    string calldata,
    string calldata sourceAddress,
    bytes calldata payload,
    string calldata tokenSymbol,
    uint256 amount
) internal override {
    // Verify source chain sender address
    address srcAddress = Strings.parseAddress(sourceAddress);
    require(srcAddress == squidRouter, InvalidSourceAddress(srcAddress)); // <-- VULNERABILITY

    IERC20 token = IERC20(_getTokenAddress(tokenSymbol));

    _processPayload(token, amount, payload);
}

function _processPayload(
    IERC20 bridgedToken,
    uint256 bridgedTokenAmount,
    bytes calldata payload
) internal {
    (address module, address safe, address delegate, ActionsExecutionParams memory params) = abi.decode(
        payload,
        (address, address, address, ActionsExecutionParams)
    );

    require(module == address(this), InvalidModuleAddress(module));

    // Send all bridged tokens to the safe
    bridgedToken.safeTransfer(safe, bridgedTokenAmount);

    _handleActions(safe, delegate, params); // <-- VULNERABILITY
}

Why It’s Vulnerable

Expected behavior: the module should only execute destination-chain actions for payloads that were authenticated by Axelar or Squid, and it should derive the acting delegate from a trusted execution context rather than from arbitrary payload bytes. If the entrypoint is permissionless express execution, the module still must verify that the payload corresponds to a legitimate cross-chain message and that the delegate identity was not attacker-chosen.

Actual behavior: _executeWithToken only parses the caller-supplied sourceAddress string and checks that it equals the immutable squidRouter address. _processPayload then decodes (module, safe, delegate, params) directly from attacker-controlled payload bytes and passes that delegate into _handleActions, which in turn uses it for every hasPermission check. In this transaction the attacker supplied sourceAddress = "0xce16F69375520ab01377ce7B88f5BA8C48F8D666", tokenSymbol = "WETH", and amount = 0, yet still reached the approval, Permit2, wrap, and swap actions because the payload also embedded the victim Safe and a delegate address that already had APPROVE, SWAP, and WRAP permissions on the module.

Normal flow vs attack flow: under a legitimate bridge flow, Squid would encode the destination payload and Axelar would later deliver bridged funds before the module executed the post-bridge actions. Here, the attacker skipped any real bridge settlement and called expressExecuteWithToken directly three times with commandId = 0x0, an empty source-chain string, and zero bridged amount. Because the contract trusted the payload’s delegate and safe fields, it treated those fake express calls as authorized post-bridge executions and spent the victim Safe’s local assets through execTransactionFromModule.

Attack Execution

High-Level Flow

  1. The attacker sent a transaction to a New Market Trading wrapper account that then self-routed three internal calls into SquidRouterModule.
  2. Each internal call invoked expressExecuteWithToken with a forged Squid source address, zero bridge amount, and a payload naming the victim Safe plus a permissioned delegate.
  3. The module accepted the forged delegate identity, checked permissions against that delegate, and treated the payload as a valid post-bridge action bundle.
  4. The module caused the victim Safe to approve Permit2 and Uniswap’s Universal Router for WBTC, wTAO, and WETH, and to wrap native ETH into WETH for the final leg.
  5. The Universal Router swapped each asset through a *-u Uniswap V3 pool, leaving the victim Safe with only small amounts of u while the valuable assets exited to the pools.

Detailed Call Trace

  • 0x9bdc730183821b6bb2b51be30b77c964fa645b91 called 0xe1d5fcfbba4d46f4937de369de415dd7e2d3265a with unresolved selector 0xf8c8f0a3.
  • That wrapper account made three self-calls with unresolved selector 0x672d4f99, each preparing one asset-specific execution leg.
  • For the first leg, the wrapper called 0x1f1d37a3bf840e35c6a860c7c2da71fe555123ca via CALL into expressExecuteWithToken(bytes32,string,string,bytes,string,uint256) (0xe4a974cc).
    • The module queried Axelar’s token registry with isCommandExecuted(bytes32) and tokenAddresses(string).
    • The module performed transferFrom(address,address,uint256) and transfer(address,uint256) on WETH with zero amount because the forged express call declared amount = 0.
    • The module checked hasPermission(address,address,(address,string)) on PermissionsManager for delegate 0x352c6a9f59357457b83d97e33ce28b333a7a1f3c.
    • The module called the victim Safe 0xc52950d522034a558903cc409c8bbf1f4decc62e through execTransactionFromModule(address,uint256,bytes,uint8) (0x468721a7) to approve WBTC for Permit2.
    • It called the Safe again to set the Permit2 approval for the Universal Router.
    • It checked SWAP permission for the same forged delegate.
    • It called the Safe again to invoke Uniswap Universal Router execute(bytes,bytes[],uint256) (0x3593564c).
    • The router called the WBTC/u Uniswap V3 pool 0x52821a7b74a9388000fc846e1d4fb9c48afcbfe6, which executed swap(address,bool,int256,uint160,bytes) (0x128acb08), transferred 0.253489591314883698 u back to the Safe, and pulled 0.25361701 WBTC from the Safe through Permit2 during uniswapV3SwapCallback(int256,int256,bytes).
  • The second leg repeated the same pattern for 0x77e06c9eccf2e797fd462a92b6d7642ef85b0a44 (wTAO):
    • permission check for the forged delegate,
    • Safe approval to Permit2,
    • Permit2 approval to the Universal Router,
    • Universal Router execution against pool 0x5065302afc91306c2919342766661a0d01464360,
    • output of 0.293354590493101066 u,
    • input of 0.293599251 wTAO.
  • The third leg again called expressExecuteWithToken with forged metadata and zero bridged amount, but first used the victim Safe to wrap 0.022100612795637319 ETH into WETH via deposit() (0xd0e30db0).
    • After a WRAP permission check, the module used the Safe to approve WETH for Permit2 and then the Universal Router.
    • After another SWAP permission check, it used the Safe to call Universal Router execute.
    • The router swapped through pool 0x97aca462462549e218bfb16ccff403739cd5b688, sending back 0.022089293776070224 u and consuming 0.022100612795638534 WETH.

Financial Impact

This transaction spent 0.25361701 WBTC, 0.293599251 wTAO, and 0.022100612795638534 WETH from the victim Safe at 0xc52950d522034a558903cc409c8bbf1f4decc62e. In exchange, the Safe received only 0.568933475584054988 u across the three swaps, so the practical loss was the full market value of the WBTC, wTAO, and WETH removed from the Safe. The immediate loser in this transaction was the Safe user, not a protocol treasury or AMM LP set. The attacker did not receive a direct payout in this same transaction, indicating this hash captured an unauthorized conversion step within the broader exploit rather than the final profit-extraction transfer.

Evidence

  • Transaction hash: 0x2d52984706d5ac567d554d40a62beeeda9e3901dd3847e93dd2a3117902abfeb
  • Status: success
  • Block number: 25170475
  • Block timestamp: 2026-05-25T06:17:59Z
  • Attacker EOA: 0x9bdc730183821b6bb2b51be30b77c964fa645b91
  • Vulnerable module: 0x1f1d37a3bf840e35c6a860c7c2da71fe555123ca
  • Victim Safe: 0xc52950d522034a558903cc409c8bbf1f4decc62e
  • Forged delegate used in permission checks: 0x352c6a9f59357457b83d97e33ce28b333a7a1f3c
  • Forged Squid source address string supplied to each express call: 0xce16F69375520ab01377ce7B88f5BA8C48F8D666
  • Each forged expressExecuteWithToken call used commandId = 0x0, empty source-chain string, token symbol WETH, and amount = 0.
  • The module emitted repeated ActionExecuted and PermissionedApprovalExecuted events for the victim Safe and forged delegate before each swap leg.
  • Permit2 approval events show the Safe approving the Universal Router with effectively unlimited allowance for WBTC, wTAO, and WETH during this transaction.