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
- The attacker sent a transaction to a New Market Trading wrapper account that then self-routed three internal calls into
SquidRouterModule. - Each internal call invoked
expressExecuteWithTokenwith a forged Squid source address, zero bridge amount, and a payload naming the victim Safe plus a permissioned delegate. - The module accepted the forged delegate identity, checked permissions against that delegate, and treated the payload as a valid post-bridge action bundle.
- 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.
- The Universal Router swapped each asset through a
*-uUniswap V3 pool, leaving the victim Safe with only small amounts ofuwhile the valuable assets exited to the pools.
Detailed Call Trace
0x9bdc730183821b6bb2b51be30b77c964fa645b91called0xe1d5fcfbba4d46f4937de369de415dd7e2d3265awith unresolved selector0xf8c8f0a3.- 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
0x1f1d37a3bf840e35c6a860c7c2da71fe555123caviaCALLintoexpressExecuteWithToken(bytes32,string,string,bytes,string,uint256)(0xe4a974cc).- The module queried Axelar’s token registry with
isCommandExecuted(bytes32)andtokenAddresses(string). - The module performed
transferFrom(address,address,uint256)andtransfer(address,uint256)on WETH with zero amount because the forged express call declaredamount = 0. - The module checked
hasPermission(address,address,(address,string))onPermissionsManagerfor delegate0x352c6a9f59357457b83d97e33ce28b333a7a1f3c. - The module called the victim Safe
0xc52950d522034a558903cc409c8bbf1f4decc62ethroughexecTransactionFromModule(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
SWAPpermission 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/
uUniswap V3 pool0x52821a7b74a9388000fc846e1d4fb9c48afcbfe6, which executedswap(address,bool,int256,uint160,bytes)(0x128acb08), transferred0.253489591314883698 uback to the Safe, and pulled0.25361701 WBTCfrom the Safe through Permit2 duringuniswapV3SwapCallback(int256,int256,bytes).
- The module queried Axelar’s token registry with
- 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
expressExecuteWithTokenwith forged metadata and zero bridged amount, but first used the victim Safe to wrap0.022100612795637319 ETHinto WETH viadeposit()(0xd0e30db0).- After a
WRAPpermission check, the module used the Safe to approve WETH for Permit2 and then the Universal Router. - After another
SWAPpermission check, it used the Safe to call Universal Routerexecute. - The router swapped through pool
0x97aca462462549e218bfb16ccff403739cd5b688, sending back0.022089293776070224 uand consuming0.022100612795638534 WETH.
- After a
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
expressExecuteWithTokencall usedcommandId = 0x0, empty source-chain string, token symbolWETH, andamount = 0. - The module emitted repeated
ActionExecutedandPermissionedApprovalExecutedevents 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.