Arbitrary Call Token-Approval Drain / Router Approval Abuse

  • Name: arbitrary call token-approval drain / router approval abuse
  • Category: exploit pattern / arbitrary external call / token-approval abuse / router validation failure
  • Summary: Some swap routers, bridge aggregators, bridge-permissions helpers, and multicall surfaces ask users to approve them as ERC-20 spenders, then forward user-shaped target addresses or calldata into downstream integrations. If that execution path stops proving this step is really the quoted swap / bridge action, the attacker does not need custody compromise, forged governance, or a price bug. They can simply steer the already-approved protocol into calling transferFrom() on the token contract, or into some downstream execution path that does the same thing, and let the protocol’s own spender identity drain victim wallets. This note is also useful for preserving a three-step review ladder teams still blur together too often: the protocol is an approved spender, this exact route object is still allowed to exercise that spender authority, and that accepted route object still deserves this much downstream token-pull or settlement authority right now.
  • What it is:
    • A protocol contract is an approved ERC-20 spender for user wallets, often with infinite approvals left behind from earlier normal use.
    • The protocol exposes a generic swap, bridge, or execution path that forwards user-controlled target addresses, calldata, or adapter instructions.
    • Validation that should restrict those calls to approved DEXes, bridges, adapters, or selectors is missing, incomplete, or accidentally bypassed.
    • The attacker supplies calldata that does not perform the intended swap path at all; it instead invokes token transferFrom() or an equivalent spend path against already-approved victim wallets.
    • The protocol contract itself becomes the theft primitive because the token sees a legitimate approved spender making the call.
  • Typical preconditions:
    • Users have previously granted the router or aggregator allowances larger than the amount currently being transacted, often type(uint256).max.
    • The vulnerable path can reach arbitrary callTo targets or forward arbitrary callData without a strict allowlist.
    • The protocol does not bind call parameters tightly enough to the quoted route, expected token pair, or per-step adapter.
    • The attacker can cheaply discover wallets with lingering approvals and sufficient balances.
    • There is no secondary allowance scoping layer such as exact-amount pull patterns, short-lived permits, or selector-level filtering.
  • Common vulnerable surfaces:
    • DEX aggregators and bridge aggregators that accept raw swap-step arrays
    • router facets or modules added in upgrades without inheriting existing validation middleware
    • multicall / batching helpers that let users specify arbitrary external call targets
    • adapter frameworks where some facets enforce whitelists but newly added paths forget to do so
    • low-level swap helper libraries that both approve tokens and forward arbitrary calldata, assuming every caller already performed target / selector validation
    • token-spending helper contracts or permissions managers that are meant to pull only the current user’s quoted input token, but remain reachable with attacker-controlled from, to, token, or amount semantics
    • split execution pipelines where a proxy only whitelists the outer bridge / adapter, while inner calldata still chooses downstream targets and token-pull helper parameters
    • integrations approved by mutable address rather than pinned code identity, where a previously whitelisted proxy or router address can later resolve to attacker-controlled logic and turn old allowances into drains
  • Exploit sequence:
    1. The attacker finds a router path where user-controlled target addresses or calldata reach a low-level external call.
    2. They identify victim wallets that have already approved the router for a large token allowance.
    3. They craft route data that makes the router call the token contract itself, typically via transferFrom(victim, attacker, amount).
    4. The token accepts the spend because the caller is the already-approved router contract.
    5. The attacker repeats the process across many approved wallets and assets until the vulnerable path is paused or removed.
  • Variants:
    • Missing allowlist on a new facet or adapter — a newly deployed route or protocol-step helper skips the validation that older paths enforced; LI.FI’s GasZip facet is the clean example because public depositToGasZipERC20() called LibSwap.swap() directly instead of flowing through the shared SwapperV2._executeSwaps() / LibAllowList gate.
    • Opaque external-call bundle on an already-approved router — the router accepts a caller-shaped operation array or target / data object that is supposed to represent a real trade step, but nothing keeps that object from degenerating into a raw token call; Unizen’s v1 Trade Aggregator is the clean example because selector 0x1ef29a02 would accept attacker-shaped Call[] entries whose target and calldata directly invoked token transferFrom(...) against approved wallets.
    • Calldata validation without target validation — the router checks rough call shape but not whether the destination is a real approved integration.
    • Target allowlist without selector allowlist — a whitelisted contract can still be abused if arbitrary function selectors are reachable.
    • Outer allowlist, inner arbitrary call — the proxy only verifies the outer bridge / adapter, but attacker-controlled inner calldata still chooses a downstream target or helper function, as in Transit Swap.
    • Helper-contract reachability drift — the router itself need not call the token directly if attacker-shaped route data can reach an internal permissions helper, bridge helper, or token-pull utility that still honors arbitrary claim / pull semantics.
    • Offchain route-metadata policy oracle — mutable aggregator output such as router identity, approval target, or route-family metadata is imported onchain strongly enough that the metadata itself becomes live approval authority; Revert V3Utils is the clean example because a token contract could occupy the swapRouter slot once the API response crossed the trust boundary.
    • Spender-local generic execution surface — no rich route story is needed because the already-approved contract itself exposes a near-raw operation array, multicall, or callee.call() primitive; Seneca Chamber is the clean example because performOperations() left OPERATION_CALL reachable and let the spender contract speak directly in attacker-chosen token calldata.
    • Aggregator route / quote mismatch — the UI quotes a benign route, but settlement code does not cryptographically bind execution to that route.
    • Allowance-amplified draining — the protocol itself never custodies funds; the loss comes entirely from stale user approvals exposed by the arbitrary call bug.

Three approval-route questions reviewers should not collapse

1. Is the protocol really an approved spender for these wallets and assets at all?

  • This is the raw wallet approval / permit / standing spender reach exists question.
  • It answers whether the router, adapter, bridge helper, or settlement contract can cause the token to honor a transferFrom() if it ever speaks the right calldata.
  • It does not yet answer whether the current route object deserves to exercise that spender reach.

2. Does this exact route object get to exercise that spender authority?

  • This is the narrower quoted route, target family, selector family, payer scope, and nested helper meaning stayed fused question.
  • It is the review step Transit, Revert, Unizen, Socket, Dough, and LI.FI lost in different ways: the approved spender was real, but the route object that reached it stopped proving this is still the intended swap / bridge / leverage / deleverage action.
  • A system can have a legitimate approved spender and still fail here because route metadata, operation arrays, inner calldata, helper parameters, or new entrypoints silently reinterpret that spender reach into a different execution story.

3. Even if the route object is authentic, how much downstream authority should it still have right now?

  • This is the which recipients, token-pull semantics, callback freedoms, nested calls, and live spend budget survive once the route has been accepted question.

  • Healthy execution should not treat valid route object exists as equivalent to that route may now speak arbitrary calldata, touch arbitrary helpers, or retain ordinary authority under degraded validation assumptions.

  • The important control is not just a route passed validation once; it is the accepted route now carries only the downstream authority that exact route meaning actually needs.

  • Detection heuristics:

    • Trace every path where route structs, swap-step arrays, operation arrays, or adapter data influence low-level .call, delegatecall, or generic library dispatch.
    • Ask whether the spender contract can ever be induced to call an ERC-20 token contract directly with attacker-chosen calldata.
    • Also ask the less obvious sibling question: can attacker-shaped route data reach an internal permissions helper, bridge helper, or token-pull utility that will itself call transferFrom() with attacker-controlled arguments?
    • Check whether mutable offchain metadata such as router identity, approval target, route id, or integration family is imported as live policy for an already-approved spender. Revert’s swapRouter path is the model case: the exploit existed because API-returned metadata could still redefine the spender’s external call surface.
    • Check whether the spender contract remains dangerous even without any honest UI or quote server at all. Seneca’s performOperations() shape is the model case: once the approved spender itself exposes a generic execution surface, the wallet-drain path survives even if there is no rich routing story to spoof.
    • Compare old and new router facets during upgrades: security middleware often lives in helper functions that one new path forgets to invoke.
    • Specifically grep for protocol-step helpers or convenience entrypoints that call the low-level swap / router library directly instead of entering through the one shared route-validation function; LI.FI’s depositToGasZipERC20() is the canonical public helper escaped the allowlist pipeline example.
    • Inventory whether the protocol commonly receives infinite approvals; if yes, any arbitrary external call is automatically a wallet-drain risk, not just a protocol-balance risk.
    • Look for routes whose execution is not tightly bound to an allowlisted target set, allowlisted selectors, exact input token, exact output token, quoted recipient, expected spender identity, and route-stage meaning.
    • Treat whitelisted address reasoning as incomplete if the approved integration is an upgradeable proxy or other mutable code pointer.
  • Defenses:

    • Enforce per-route allowlists for both target contracts and callable selectors on every router / facet / adapter path.
    • Bind nested execution too, not just the outer adapter: if a bridge or swap helper parses inner calldata, that inner target / selector set needs the same constraints.
    • Prefer exact-amount approvals, Permit2-style scoped approvals, or per-transaction permits over persistent infinite allowances.
    • Keep validation centralized and mandatory so newly added modules cannot accidentally skip it.
    • Reject route steps that call the input token contract itself unless that behavior is explicitly required and separately constrained.
    • Constrain internal token-pull helpers so they cannot be reached with attacker-controlled from / to semantics through generic upstream call paths.
    • Treat upgradeable or proxy integrations as code-identity problems, not just address-allowlist problems; if an approved spender route depends on a mutable endpoint, pin or re-check code identity.
    • Add emergency facet / adapter disable switches and rapid approval-revocation guidance because wallet-drain exploits spread faster than protocol-custody drains.

What sound design looks like

  • The protocol should derive one route-local authorization object that fuses together the user whose assets may be spent, the exact input token and amount, the allowed target contracts, the allowed selectors, the expected downstream counterparties, the recipient semantics, and the expiry / replay boundary. Approval-funded route authority truth as a single security object is the positive-design companion for this spender privilege plus route shape must stay one object requirement.
  • A healthy router does not treat token approval exists as ambient authority to execute arbitrary downstream calldata. Approval is only one ingredient; the route executor should still prove that every external call belongs to the quoted swap / bridge / repay path and cannot be repurposed into wallet theft.
  • Healthy review keeps three questions separate on purpose: is this spender really approved, is this exact route object still entitled to exercise that spender authority, and what exact downstream calls or token-pull rights survive after that route is accepted.
  • Offchain route metadata and onchain operation selectors should stay descriptive, not sovereign. If API-returned router fields, approval-target fields, or generic operation codes can themselves redefine who the approved spender talks to, the protocol has already promoted integration plumbing into wallet-spend authority.
  • Validation has to survive composition. If the outer adapter is allowlisted but inner calldata can still choose a downstream helper, token-pull primitive, or mutable integration endpoint, the route is not actually constrained.
  • Accepted route objects should get only route-shaped downstream authority. A quoted bridge step should not inherit arbitrary recipient freedom, arbitrary token-call freedom, or generic helper reach just because the router contract itself is approved.
  • The safest architectures narrow spender authority and execution authority at the same time: exact-amount permits, exact target / selector envelopes, immutable or code-pinned integrations, and no public helper that can bypass the one shared validation path.
  • Upgrade discipline matters as much as steady-state logic. A newly added facet, convenience helper, or protocol-step entrypoint should inherit the same route-binding guarantees as the original router, or it should not be able to spend user-approved funds at all.

Failure-prevention checklist

  • Define one mandatory validation funnel for all approval-consuming execution paths. No facet, helper, adapter, or convenience function should be able to reach low-level external calls without crossing the same target-and-selector policy gate.
  • Authenticate the full route shape: spender contract, token-in, amount-in, target contracts, selectors, recipients, refund semantics, and any nested adapters or bridge helpers. If an inner payload can choose a second target, validate that second target too.
  • Keep a deny-by-default policy for raw token calls. Routers should reject steps that invoke the input token contract or any generic token-pull primitive unless that behavior is explicitly required and separately constrained.
  • Treat every approval-consuming helper as a wallet-drain surface. Review public swap, bridge, repay, claim, pull, performAction, and multicall-style functions with the assumption that the contract already has victim allowances.
  • Pair route validation with narrow approval scope: exact-amount approvals, expiring permits, Permit2 allowances with per-call bounds, or approval reset patterns after use.
  • Pin integration code identity where possible. If the allowlist names an upgradeable proxy, verify implementation hash or other code identity too; otherwise approved target can silently become approved attacker logic.
  • Test bypasses created by new modules. Any newly added facet or entrypoint should have regression tests proving it cannot call the low-level swap library, adapter dispatcher, or token-pull helper without the shared policy checks firing first.
  • Model nested calldata explicitly. If the protocol wraps aggregators, bridges, or leverage helpers, fuzz the full tree of downstream call targets and selectors instead of only the outermost route object.
  • Instrument wallet-exposure inventory so incident response can quickly answer which spender addresses hold broad user approvals and which functions can exercise them.
  • Preserve the three-step review ladder in tests and audits: spender reach exists, this exact route object may exercise it, and that accepted route only gets this narrower downstream authority should be checked separately rather than flattened into one vague router is approved conclusion.
  • Rehearse kill switches and user messaging. Approval drains propagate across user wallets faster than most TVL exploits, so disable switches and revoke-guidance playbooks need to be ready before deployment.

Audit prompts

  • Which public or externally reachable functions can cause the protocol to make an external call while acting as an approved ERC-20 spender for user wallets?
  • Does every such path pass through the same target, selector, recipient, and nested-calldata validation layer, or do some helpers call the low-level swap / bridge library directly?
  • Can attacker-controlled route data ever make the contract call the token contract itself, or an internal helper that will eventually call transferFrom() with attacker-chosen from, to, or amount values?
  • If an outer adapter is allowlisted, what prevents inner calldata from choosing an unreviewed downstream target or helper function?
  • Are target allowlists paired with selector allowlists and code-identity checks, or can a mutable proxy / upgraded endpoint preserve its allowlisted address while changing behavior?
  • Where is the exact relationship enforced between quoted route, signed intent, and actual execution path? Could the UI describe one route while settlement honors another?
  • Do any newer facets, modules, or convenience entrypoints bypass the historical validation middleware by calling shared execution helpers directly?
  • Which spender addresses retain the largest aggregate user approvals today, and what is the shortest code path from each of those contracts to a raw external call?
  • If an exploit began now, how quickly could operators disable the vulnerable execution surface and publish an authoritative spender-revocation list?
  • Are tests asserting the negative property that token transferFrom(victim, attacker, ...) cannot be reached through any public router path even when the contract already has victim allowance?

Lessons that should already be standard

  • Infinite approvals are usually not the root cause; they are the blast-radius multiplier. The money path still starts with a protocol that failed to keep spender authority bound to one narrow execution story.
  • Router security is not just did we whitelist some addresses. Real safety requires binding exact targets, exact selectors, nested execution, code identity, and recipient semantics all the way down.
  • Reviewers should keep three questions separate: the spender can reach funds, this exact route object may exercise that reach, and that accepted route still deserves this exact downstream call and recipient authority. Most approval drains collapse two or three of those into one vague router is approved claim.
  • Generic helper paths are not harmless ergonomics when the contract is an approved spender. A convenience function that bypasses one shared validation funnel can instantly convert a router into a wallet drainer.
  • Upgrades reopen old trust boundaries. Teams often harden the original swap path, then ship a new facet or integration helper that quietly regains arbitrary-call power.
  • Approval drains should be treated operationally like wallet-wide incidents, not just protocol-balance incidents. Response speed, spender inventory, and revoke guidance are part of the security model.
  • We do not custody user funds is not a defense if the protocol can still spend them. Spender authority plus route ambiguity is already a custody-adjacent risk surface.
  • Approval-drain review should separate three cousins that all feel like arbitrary call: route metadata became policy (Revert), the approved spender exposed a near-raw execution primitive (Seneca), or a new helper bypassed shared middleware (LI.FI). The exploit code differs, but the preserved design lesson is the same: spender authority was never kept narrower than reachable execution authority.

Where this pattern stops

  • Classify it here when the decisive bug is approved spender still had too much reachable execution freedom. If the attacker needed a generic router path, dynamic adapter, route array, low-level swap helper, or another attacker-shaped external-call surface to steer an already-approved protocol into token calls it should never have made, this note should do the analytical work.

  • Narrow to callback-origin authentication failure when the dangerous primitive is a spoofed AMM settlement callback rather than a generic arbitrary-call surface. Sushi RouteProcessor2, ParaSwap Augustus V6, and Primitive still end in approved-token theft, but the sharp question there is why did the contract believe this fake pool / callback sender was the real settlement counterparty?

  • Narrow to approval-by-address / mutable-code execution when the route was nominally allowlisted, but the approved address later resolved to different code. Furucombo sits at that edge: the user-approved / whitelisted endpoint drifted under a mutable proxy boundary rather than through an openly arbitrary call path.

  • Narrow to approval-rights exposure when no attacker-routed swap calldata, adapter steering, or generic arbitrary-call surface was needed. Bancor’s public safeTransferFrom() path and Beanstalk’s transferTokenFrom(..., fromMode = EXTERNAL, ...) near miss belong there because the approval-consuming helper itself lost its caller boundary. The outcome looks similar, but the mechanism is already-approved spender exposed its own pull primitive, not router validation failed.

  • High-signal incidents:

    • [[../../../incidents/approvals-wallets-integrations/transit-swap-routing-bridge-claimtokens-arbitrary-call-approval-drain-2022-10-01|Transit Swap routing-bridge claimTokens() arbitrary-call approval drain (2022-10-01)]] — the proxy only whitelisted the outer bridge, but attacker-shaped inner calldata redirected execution into Transit Swap’s own approved permissions manager and turned claimTokens() into arbitrary victim transferFrom().
    • [[../../../incidents/approvals-wallets-integrations/dexible-selfswap-router-validation-approval-drain-2023-02-17|Dexible selfSwap router-validation failure / approval drain (2023-02-17)]] — a user-defined route array let arbitrary callers choose router, spender, and raw routerData, so Dexible approved attacker-chosen spenders and executed attacker-chosen calls against victim-approved tokens.
    • [[../../../incidents/approvals-wallets-integrations/revert-finance-v3utils-dynamic-swaprouter-arbitrary-call-approval-drain-2023-02-18|Revert Finance V3Utils dynamic swapRouter arbitrary-call approval drain (2023-02-18)]] — Revert’s atomic-swap helper trusted router metadata from an external aggregator strongly enough that a token contract itself could occupy the router slot, turning V3Utils’ leftover user allowances into direct transferFrom() theft.
    • [[../../../incidents/approvals-wallets-integrations/seneca-chamber-performoperations-arbitrary-call-approval-drain-2024-02-28|Seneca Chamber performOperations() arbitrary-call approval drain (2024-02-28)]] — a per-collateral CDP contract left generic OPERATION_CALL execution reachable, so attackers encoded token transferFrom(victim, attacker, amount) directly and used Chamber’s existing collateral allowances to drain undeployed wallet balances.
    • [[../../../incidents/approvals-wallets-integrations/socket-wrappedtokenswapperimpl-performaction-arbitrary-call-approval-drain-2024-01-16|Socket WrappedTokenSwapperImpl performAction() arbitrary call / approval drain (2024-01-16)]] — a newly added route module forwarded attacker-controlled swapExtraData into a low-level call, turning SocketGateway’s existing wallet approvals into direct transferFrom() theft.
    • approval drain (2024-03-08) — the already-approved Unizen router accepted attacker-shaped Call[] entries behind selector 0x1ef29a02, so token contracts like VRA and DMTR could be called directly with transferFrom(victim, router, amount) calldata and the router’s own spender identity became the theft primitive.
    • [[../../../incidents/approvals-wallets-integrations/dough-finance-connectordeleverageparaswap-flashloanreq-unvalidated-paraswap-calldata-approval-drain-2024-07-12|Dough Finance ConnectorDeleverageParaswap flashloanReq() unvalidated ParaSwap calldata / approval drain (2024-07-12)]] — Dough’s public deleverage helper let attacker-shaped ParaSwap bytes survive deep enough to become a raw token call, so a flash-loan-assisted unwind withdrew collateral from victim DSAs and then transferFrom()-pulled that collateral to the attacker.
    • infinite-approval drain (2024-07-16) — the public protocol-step helper depositToGasZipERC20() accepted attacker-controlled SwapData and called LibSwap.swap() directly, bypassing the shared SwapperV2._executeSwaps() / LibAllowList target-and-selector gate and turning user allowances into direct transferFrom() theft.

Internal linkages

Control surface