Calculated from recorded token losses using historical USD prices at the incident time.
0xc6c3331fa8c2d30e1ef208424c08c039a89e510df2fb6ae31e5aa40722e28fd60x3a23f943181408eac424116af7b7790c94cb97a5EthereumOn Ethereum mainnet block 19021454, transaction 0xc6c3331fa8c2d30e1ef208424c08c039a89e510df2fb6ae31e5aa40722e28fd6 exploited Socket's SocketGateway at 0x3a23F943181408EAC424116Af7b7790c94Cb97a5 through route 406, which resolved to WrappedTokenSwapperImpl at 0xCC5fDA5e3cA925bd0bb428C8b2669496eE43067e. The attacker EOA 0x50df5a2217588772471b84adbbe4194a2ed39066 deployed helper contracts and used the route to make SocketGateway execute arbitrary USDC transferFrom calls against users that had previously approved SocketGateway as spender. The seed balance diff shows the attacker gained 2569980189709 raw USDC units, or 2569980.189709 USDC, while paying 0.406150234359359672 ETH in gas.
The root cause is an arbitrary token-call primitive inside WrappedTokenSwapperImpl::performAction. In the wrapped-token-to-native branch, the route first performs safeTransferFrom(msg.sender, socketGateway, amount) and then forwards attacker-controlled calldata through fromToken.call(swapExtraData). Because the route is executed by from , that low-level token call runs with . Any user who had already approved to spend USDC therefore became drainable by an unprivileged attacker.
delegatecallSocketGatewaymsg.sender == SocketGatewaySocketGatewaySocketGateway is a routing contract that dispatches user requests to route implementation contracts. The verified SocketGateway source shows two relevant execution surfaces: getRoute(uint32) returns the route implementation for a route id, and the fallback handler treats the first four calldata bytes as a route id, then delegatecalls into the mapped route. At the exploit block, the selected route id was 406, encoded on-chain as the first four bytes 0x00000196.
function getRoute(uint32 routeId) public view returns (address) {
return addressAt(routeId);
}
fallback() external payable {
address routeAddress = addressAt(uint32(msg.sig));
result := delegatecall(gas(), routeAddress, 0, sub(calldatasize(), 4), 0, 0)
}
USDC at 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 uses the standard spender-based allowance model. The trace shows the attacker repeatedly queried balanceOf(victim) and allowance(victim, SocketGateway) before each drain attempt. This matters because if SocketGateway is the runtime caller of USDC.transferFrom, any pre-existing allowance[victim][SocketGateway] authorizes the spend.
The attack required no privileged roles, no private keys, and no protocol-admin transaction. The only prerequisite was a victim account with positive USDC balance and a positive USDC allowance to SocketGateway, a condition created by normal prior use of Socket routes.
The vulnerability is an application-layer arbitrary external call under a shared spender identity. WrappedTokenSwapperImpl was meant to wrap tokens into native ETH or vice versa, but its ERC20-to-native path does not validate that swapExtraData actually represents a legitimate wrapper action. Instead, it forwards arbitrary attacker-controlled calldata directly to the token contract.
The critical code is the ERC20 branch of WrappedTokenSwapperImpl::performAction:
_initialBalanceTokenOut = address(socketGateway).balance;
ERC20(fromToken).safeTransferFrom(
msg.sender,
socketGateway,
amount
);
(bool success, ) = fromToken.call(swapExtraData);
if (!success) {
revert SwapFailed();
}
_finalBalanceTokenOut = address(socketGateway).balance;
require(
(_finalBalanceTokenOut - _initialBalanceTokenOut) == amount,
"Invalid wrapper contract"
);
This creates a precise invariant violation: a route that is supposed to perform wrapping may instead invoke any token selector under SocketGateway authority. The exploit sets amount = 0, which makes the initial safeTransferFrom(msg.sender, socketGateway, amount) a harmless zero-value transfer from the attack helper. That bypasses the intended funding step while still reaching fromToken.call(swapExtraData). The attacker then encodes swapExtraData as USDC.transferFrom(victim, attacker, drainAmount), so the raw token call consumes the victim's pre-existing allowance to SocketGateway. The final ETH-balance check also passes trivially because both the expected and actual ETH delta are zero.
The exploit flow is deterministic and directly visible in the collected trace and balance diff. The transaction sender 0x50df5a2217588772471b84adbbe4194a2ed39066 created helper contracts 0xf2d5951bb0a4d14bdcc37b66f919f9a1009c05d1 and 0xd2bc9a9c2c39b8693ed4b2b72469032e87ed7f4a, then immediately entered the draining loop. For each victim, the helper first queried the victim's USDC balance and its allowance to SocketGateway.
One representative trace segment for victim 0x7d03149A2843E4200f07e858d6c0216806Ca4242 shows the full abuse path:
FiatTokenV2_2::allowance(0x7d03149A..., SocketGateway) -> 115792089237316195423570985008687907853269984665640564039457584007913029639935
SocketGateway::fallback(0x00000196...)
WrappedTokenSwapperImpl::performAction(
fromToken = USDC,
toToken = NATIVE,
amount = 0,
receiver = 0x856da0aCbfF24Fd61A470023E8A5dAE8FC45bde8,
metadata = 0x...1b3b,
swapExtraData = 0x23b872dd...
)
FiatTokenV2_2::transferFrom(0xd2bc9A9c..., SocketGateway, 0)
FiatTokenV2_2::transferFrom(0x7d03149A..., 0x50DF5a22..., 656424984436)
The key observations are:
0x00000196 reaches SocketGateway fallback, confirming route 406 usage.performAction is called with amount = 0, so the helper-to-gateway transfer is a no-op.swapExtraData begins with selector 0x23b872dd, the ERC20 transferFrom(address,address,uint256) selector.FiatTokenV2_2::transferFrom drains the real victim address into the attacker EOA.This pattern repeats across 127 victim addresses. The seed balance diff aggregates the outcome: the attacker EOA starts at zero USDC and ends with 2569980189709, while the negative USDC deltas across victims sum to the exact same value. The attack therefore did not rely on hidden value sources or privileged settlement. It is a straight conversion of third-party allowance[victim][SocketGateway] into attacker-controlled transfers.
The exploitable preconditions are also explicit:
1. route 406 must still map to WrappedTokenSwapperImpl;
2. the target victim must have positive USDC balance;
3. the target victim must have positive USDC allowance to SocketGateway;
4. the attacker must choose drainAmount <= min(balanceOf(victim), allowance(victim, SocketGateway)).
Under those conditions, the exploit succeeds for any unprivileged actor able to submit a normal transaction.
The adversary strategy was a single-transaction allowance sweep. The EOA 0x50df5a2217588772471b84adbbe4194a2ed39066 paid gas and received the stolen funds. The two helper contracts existed only to package the victim list and loop over repeated drain attempts.
The end-to-end execution flow was:
USDC.balanceOf(victim);USDC.allowance(victim, SocketGateway);SocketGateway fallback with route id 406, amount = 0, and swapExtraData = abi.encodeWithSelector(transferFrom, victim, attacker, drainAmount).The trace shows that the inner helper repeatedly executed SocketGateway::fallback(0x00000196...) frames followed by nested FiatTokenV2_2::transferFrom(victim, attacker, amount) calls. No victim signatures, approvals, or per-victim interactions occurred inside the exploit transaction itself. All spend authority was inherited from approvals that already existed before block 19021454.
The measurable impact is a direct theft of USDC from 127 accounts that had approved SocketGateway. The seed transaction balance diff records the attacker's final gain and the victims' aggregate loss as the same raw value:
{
"attacker": "0x50df5a2217588772471b84adbbe4194a2ed39066",
"token": "USDC",
"before": "0",
"after": "2569980189709",
"delta": "2569980189709"
}
Expressed with USDC's 6 decimals, the loss is 2569980.189709 USDC. Gas cost was borne separately by the attacker EOA in ETH and does not offset the victims' token loss. The scope of impact is any user address that both held USDC and had granted SocketGateway a usable allowance during the vulnerable route configuration.
0xc6c3331fa8c2d30e1ef208424c08c039a89e510df2fb6ae31e5aa40722e28fd6 metadata and trace.SocketGateway source for fallback route dispatch and getRoute.WrappedTokenSwapperImpl source for the unvalidated fromToken.call(swapExtraData) path.FiatTokenV2_2 source artifact showing spender-based allowance behavior.