Socket WrappedTokenSwapperImpl performAction() Arbitrary Call / Approval Drain (2024-01-16)
- Name: Socket WrappedTokenSwapperImpl
performAction()arbitrary call / approval drain - Date: 2024-01-16
- Protocol: Socket
- Chain: Ethereum
- Category: router approval abuse / arbitrary external call / token-approval drain / route-validation failure
- Loss: about 3.4 million across roughly 230 wallets
- Summary: Socket added a new wrapped-token swap route whose
performAction()function forwarded user-controlledswapExtraDatainto a low-level call without validating that the calldata actually represented a legitimate wrap / unwrap action. The attacker used SocketGateway’s dynamic route dispatch to reach that route, then made the route call token contracts with attacker-chosen calldata such astransferFrom(). Because victims had already approved the SocketGateway contract as spender, the protocol’s own allowance authority became the theft primitive. - Trigger / entry point:
- Socket’s gateway contract on Ethereum (
0x3a23F943181408EAC424116Af7b7790c94Cb97a5) dynamically dispatched route-specific actions through a fallback selector path. - Three days before the exploit, admin transaction
0x1df44e224c7a715da25fa33dcad2ca3a930d1a4dafd263e61c07b52673d505f4added a new route at0xCC5fDA5e3cA925bd0bb428C8b2669496eE43067e. - The newly added module, commonly described as
WrappedTokenSwapperImpl, exposedperformAction()for native-token / wrapped-token conversions. - In that route, attacker-controlled
swapExtraDatareached an external.call()path without sufficient validation of target, selector, or semantic intent.
- Socket’s gateway contract on Ethereum (
- Root cause:
- Socket combined broad spender authority from old user approvals with a generic route-dispatch surface and a newly added module that trusted calldata too much.
- The intended invariant was effectively
this route may only do a narrow wrap / unwrap action for the requested amount; the deployed code instead let user-supplied calldata decide what function the token contract would execute. - The route also relied on a balance-change check around the wrap / unwrap flow, but public analysis noted that this did not exclude a malicious path where the attacker could satisfy the check while still transferring
0 WETHin. - The deeper missed boundary was not merely
validate performAction() better, butkeep one route-local authority object binding gateway spender identity, victim payer scope, the fallback-selected route id, wrapped-token route meaning, and the exact target / selector family allowed for that routebefore any allowance-backed execution occurred. - Socket therefore lost two layers of meaning at once: the gateway’s selector-to-route registry already said
0x00000196 means this wrapped-token helper, but the helper still let attacker-shapedswapExtraDataredefine what economic action that route id meant onchain. - Once the route could be steered into arbitrary token calls, pre-existing approvals to SocketGateway became direct wallet-drain authority.
- Exploit path:
- The attacker funded EOA
0x50df5a2217588772471b84adbbe4194a2ed39066and deployed helper contracts including0xa5f1e956d7085998ac65fb87c18dc498481f1c8e. - Through SocketGateway, the attacker called the fallback-dispatched route path associated with selector
0x00000196; public reconstructions describe that selector as resolving through the gateway fallback into route slot406, where the newly added wrapped-token route lived. - The route’s
performAction()function accepted attacker-craftedswapExtraDataand forwarded it into a low-level call on the token side. - Instead of performing a legitimate wrap / unwrap step, the malicious payload used the ERC-20
transferFromselector0x23b872ddagainst tokens already approved to SocketGateway. - Because the token contracts saw the already-approved SocketGateway as caller, they honored the transfers and pulled assets directly from victim wallets.
- The attacker first drained about
$2.5MofUSDCfrom roughly 127 victims, then repeated the pattern againstWETH,USDT,WBTC,DAI, andMATICin a second exploit wave. - Socket disabled the vulnerable route with admin transaction
0xac75adcc1cb3fef158c4f200c48fcbcbb9b6ce3250bdf3751d6231d41a9e604b, stopping further abuse through that path.
- The attacker funded EOA
- What the attacker needed to be true:
- Users needed existing approvals to the SocketGateway contract large enough to make theft worthwhile.
- The fallback dispatch inside SocketGateway had to expose the newly added wrapped-token route before the route was disabled.
performAction()had to trust attacker-controlledswapExtraDataenough to reach arbitrary token-call behavior.- The route’s post-call balance checks had to be satisfiable even when the routed call was malicious rather than a real wrap / unwrap.
- The attacker needed quick wallet discovery so they could sweep approved balances before revocations and route disablement spread.
- Technical anatomy worth remembering:
- This was not a bridge-verifier failure or custody compromise. The protocol was exploited as an approved spender sitting in user wallets’ allowance tables.
- The bug lived in a newly added route module, not in the abstract idea of routing itself. Operationally,
new route introduced without full input validationis the preserved lesson. - Dynamic dispatch made the exploit path less obvious: the attacker did not need a named high-level swap function so long as gateway fallback routing could still reach the bad module.
- Socket’s selector-to-route registry did not fail in the narrow sense; the failure was that the selected route still let attacker bytes redefine route meaning after dispatch. That is the sharper authority lesson: route id, route purpose, target family, and selector family had to stay one object.
- The route was supposed to be narrow-purpose wrap / unwrap plumbing. Once it accepted generic calldata, that narrow helper effectively became an arbitrary-call adapter.
- The sharper positive-design frame is approval-funded route authority as a single security object: SocketGateway had raw wallet allowance, but no narrower route-local object kept payer scope, fallback-selected route identity, allowed wrapped-token counterparties, selector family, and step meaning fused once
swapExtraDatacrossed the route boundary. - The incident is also a useful bridge back to spender authority truth as a single security object:
users approved SocketGatewayandthis exact fallback-routed module may exercise that approval for this exact purpose right nowwere different questions, and the second one went unproven. - The incident is a clean cousin to LI.FI’s later GasZip facet bug: in both cases, a router that already held user approvals became a mass wallet-drain primitive after one path skipped or weakened call validation.
- Representative transactions and addresses:
- Attacker EOA:
0x50df5a2217588772471b84adbbe4194a2ed39066 - SocketGateway contract:
0x3a23F943181408EAC424116Af7b7790c94Cb97a5 - Vulnerable route contract:
0xCC5fDA5e3cA925bd0bb428C8b2669496eE43067e - Example attacker contract:
0xa5f1e956d7085998ac65fb87c18dc498481f1c8e - Route-addition transaction:
0x1df44e224c7a715da25fa33dcad2ca3a930d1a4dafd263e61c07b52673d505f4 - First exploit transaction:
0xc6c3331fa8c2d30e1ef208424c08c039a89e510df2fb6ae31e5aa40722e28fd6 - Second exploit transaction:
0x591d054a9db63f0976e533f447df482bed5f24d7429646570b2108a67e24ce54 - Route-disable transaction:
0xac75adcc1cb3fef158c4f200c48fcbcbb9b6ce3250bdf3751d6231d41a9e604b
- Attacker EOA:
- Response / mitigation:
- Socket disabled the vulnerable route within minutes of the exploit once users and monitoring systems surfaced the attack.
- Socket’s first public incident notice explicitly framed the blast radius as wallets with infinite approvals to Socket contracts and said the affected contracts had already been paused, which is the right operational reminder that spender-surface inventory and kill ability matter as much as root-cause debugging in the first minutes.
- Public writeups describe direct user communications, an incident war room, and an onchain message from the admin wallet to the attacker seeking recovery.
- The practical user mitigation was the same one this exploit family almost always forces: revoke approvals immediately, because the danger sat in lingering allowances rather than in funds escrowed inside the protocol.
- The larger engineering fix is to make route modules prove their legitimacy at the call boundary instead of trusting
extraDatato remain well-behaved.
Pattern tags
- arbitrary-call-token-approval-drain
- New route module silently widened spender authority: Socket’s route registry still pointed at a specific wrapped-token helper, but that helper let attacker-shaped
swapExtraDataredefine what the route was economically allowed to do after dispatch - Route meaning and downstream call meaning came apart: selector
0x00000196authenticateduse the wrapped-token route, yet the route never preserved a second narrower object proving which token target and selector family still counted aswrapped-token behavior - Existing wallet approvals became live theft authority: users had already approved SocketGateway, so once the wrapped-token helper could speak arbitrary ERC-20 calldata, the protocol’s legitimate spender identity became the drain primitive
- Post-call balance checks were weaker than semantic call validation: Socket still inspected balance movement around the helper path, but the attacker could satisfy that coarse accounting shape while making a downstream call whose real meaning was
transferFrom(victim, attacker, amount)rather thanwraporunwrap
Why the exploit worked
- Socket’s security story relied on an unstated but crucial architectural rule: a route id selected through gateway fallback should still commit the downstream helper to one narrow target / selector family and one narrow payment meaning. The wrapped-token module broke that rule by accepting attacker-controlled
swapExtraDatathat could redefine the downstream call after dispatch. - The attacker did not need custody compromise, governance, or a pricing bug. They only needed SocketGateway to keep its already-approved spender identity while one new route helper stopped proving that the downstream token call was actually a canonical wrap / unwrap step.
- This is why the incident is more instructive than the short label
arbitrary calldata bug. The economically decisive miss was route-local authority collapse:users approved SocketGateway,fallback selected the wrapped-token route, andthis exact route step may exercise wallet allowance for this exact selector familywere treated as if they were the same fact when they were not. - The route’s balance-delta checks did not rescue the design because they only asked whether balances changed in a compatible-looking way, not whether the call that caused the change still belonged to the approved wrapped-token settlement story. In approval-funded routers, semantic target / selector commitment has to happen before low-level balance accounting can matter.
- The clean positive-design clue is the one Socket lacked: a route-local authorization object that keeps gateway spender identity, victim payer scope, route id, allowed wrapped-token counterparties, allowed selectors, and step meaning fused before any external call executes. Without that middle object, a narrow helper became a mass wallet drainer as soon as one route started trusting attacker bytes.
Detection and audit pivots
- Grep every route module reachable from gateway fallback or dynamic dispatch for low-level
.call(...), decodedextraData, or attacker-controlled target / calldata pairs that can survive past route selection. The key question is not onlycan this route be reached?butcan it redefine its own meaning after the router picked it? - Review newly added adapters and helper routes as first-class spender surfaces whenever the parent gateway already holds user approvals. If a module can receive arbitrary
swapExtraData,callData, or inner-route blobs, audit it like a wallet-drain primitive rather than a narrow utility. - Test the negative property that a wrapped-token or bridge helper cannot directly or indirectly call ERC-20
transferFrom(...)on arbitrary token contracts even when the gateway already has victim allowance. For Socket-style systems,approved gateway plus route-local arbitrary token callis the critical exploit shape. - Compare post-call accounting checks against semantic call commitments. If the route only verifies resulting balances, ask what attacker-chosen downstream selectors could still satisfy those coarse checks while performing a different economic action.
- Maintain an inventory of which route ids, fallback selectors, or registry slots currently map to modules that can exercise the same spender address users approve. Incident response needs to answer
which route-local helpers can spend with this allowance today?before an attacker does.
Failure-prevention checklist
- Treat every new route module as a new spender surface when the parent gateway already sits in user allowance tables. Security review should begin from
what wallet authority does this route inherit immediately on deployment?, not fromis this helper logically narrow? - Bind route id, canonical wrapped-token counterparties, permitted selector family, payer semantics, and amount semantics into one route-local authorization object before any user-shaped bytes cross into low-level call territory.
- Prefer typed route arguments over opaque
swapExtraDatablobs for narrow helpers like wrap / unwrap modules. If opaque bytes are unavoidable, decode and prove exact target, selector, recipient, and amount meaning onchain before execution. - Keep stage-scoped permissions narrower than raw router allowance. A route chosen for
wrap native into WETHshould not inherit generic power to call the token contract with any ERC-20 selector just because the gateway is already an approved spender. - Pair semantic validation with negative tests: assert that every wrap / unwrap route rejects direct
transferFrom, arbitraryapprove, arbitrary recipient change, and any target outside the exact wrapped-token family the route claims to support. - Require deployment-time route registration review to include emergency disable readiness, route-to-module inventory updates, and alerting for any newly enabled path that can exercise an already-approved spender address.
- Audit balance-delta checks as downstream sanity checks only. Never let
post-call balances look plausiblesubstitute for proving that the executed call still belonged to the intended settlement story. - Simulate copycat and second-wave behavior during incident drills: once one allowance-consuming route is exploitable, responders need precomputed answers for
which other live routes share this spender surfaceandhow fast can we disable them all.
Audit prompts
- Which exact modules can exercise the spender address users approve today, and where is that inventory maintained at deploy / upgrade time?
- After fallback or registry dispatch selects a route id, what local state proves the route may only call a specific target and selector family rather than any calldata blob the user supplied?
- For every helper that accepts
swapExtraData,callData, or equivalent opaque bytes, what onchain checks prove recipient, token counterparty, amount semantics, and function selector still match the advertised route purpose? - Can a route intended only for wrap / unwrap semantics ever make an ERC-20
transferFrom(...),approve(...), or arbitrary.call(...)against a token contract already trusting the gateway as spender? - Are post-call balance checks catching semantic misuse, or could an attacker still satisfy them while executing a different economic action such as draining a victim wallet through the gateway’s allowance?
- What is the operational path to disable one bad route immediately, and how quickly can the team enumerate sibling routes that inherit the same approved spender surface?
- During code review of new routes, who verifies that
narrow helper purposeandactual reachable selector familyremain the same object after all decoding, fallback dispatch, and module indirection layers? - If the gateway already holds broad approvals, what route-local proof keeps
approved spender identityseparate fromattacker-chosen execution authorityat the final external-call boundary?
Lessons that should already be standard
Router already has approvalis not a background condition; it is the main blast-radius fact. Every new helper path inheriting that spender identity deserves wallet-drain-level review.- Route dispatch is not enough. Selecting the correct module first does not prove the module still executes only the narrow economic action the route id was supposed to mean.
- Opaque calldata is custody-critical whenever it crosses from a user-facing router into a low-level external call. If the bytes can redefine target, selector, or recipient meaning, they sit directly on the spend-authority boundary.
- Balance checks without semantic commitment are weak medicine. They can confirm that assets moved, but not that the movement still belonged to the authorized settlement story.
- Emergency disablement is part of the design, not just operations polish. Socket’s quick route pause mattered because the exploitable surface was one newly added module hanging off a gateway with broad existing approvals.
- Approval-drain incidents are often route-authentication incidents in disguise. The allowance tables only made the loss possible; the missing route-local proof about what that allowance was allowed to do made it profitable.
- New route modules should be reviewed as inheriting the full social and operational blast radius of the parent gateway, even when the product story says they are only adding a tiny wrap / unwrap convenience.
Why this incident matters
- It is one of the clearest examples of an aggregator / bridge-adjacent router turning stale approvals into direct wallet theft without ever taking custody of the victims’ funds.
- It shows how easy it is for a helper module with a very narrow product purpose to inherit the full blast radius of a generic router once calldata validation slips.
- The preserved healthy-side lesson is sharper than
validate calldata: once a gateway already has spender authority, every route field that can redefine counterparty, selector family, or settlement meaning is itself part of the approval object and has to stay narrower than the raw allowance. - It also broadens the arbitrary-call approval-drain family beyond LI.FI’s missing allowlist pattern into the
new route module with incompletely validated user inputvariant.
Internal linkages
- Core recurring pattern: arbitrary-call-token-approval-drain.
- Positive-design companion: approval-funded-route-authority-truth-security-object — the missing middle layer was one route-local authorization object that should have kept spender identity, victim payer scope, wrapped-token route meaning, and allowed target / selector families fused above the gateway’s standing approvals.
- Broader spender-boundary companion: spender-authority-truth-security-object — useful when the sharper question is why any already-approved gateway or module should be allowed to exercise wallet allowance from attacker-shaped route bytes at all.
- Compare against adjacent router / integration drains: dough-finance-connectordeleverageparaswap-flashloanreq-unvalidated-paraswap-calldata-approval-drain-2024-07-12, li-fi-gaszipfacet-arbitrary-call-infinite-approval-drain-2024-07-16, unizen-trade-aggregator-v1-unverified-call-array-arbitrary-call-approval-drain-2024-03-08.
- Adjacent pattern: privileged-message-dispatcher-abuse — both revolve around generic execution surfaces, but Socket monetized user allowances rather than protocol-owned admin authority.
Control surface
-
The decisive leverage sat in SocketGateway’s route dispatch, the newly added
WrappedTokenSwapperImpl.performAction()module, and the fact that users had already granted spender authority to the gateway. -
The deeper missed control was the lack of a narrower route-local authority object proving
this exact gateway-approved route may call this exact wrapped-token target / selector family for this exact payer and wrap/unwrap purposebefore any allowance-backed execution happened. -
Once that route accepted attacker-shaped calldata, Socket did not need to misprice assets or lose custody keys. It only needed to exercise the allowance authority it already possessed in the wrong code path.
-
Sources:
-
Last reviewed: 2026-07-22 UTC