Transit Swap Routing-Bridge claimTokens() Arbitrary-Call Approval Drain (2022-10-01)
- Name: Transit Swap routing-bridge
claimTokens()arbitrary-call approval drain - Date: 2022-10-01
- Protocol: Transit Swap / Transit Finance
- Chain: BNB Smart Chain + Ethereum
- Category: router approval abuse / arbitrary external call / token-approval drain / route-validation failure
- Loss: about
$21Minitially drained from approved wallets; public analysis later described more than$23Mof unauthorized transfers before most funds were returned - Summary: Transit Swap split swap execution across a routing proxy, route-bridge contracts, and a permissions manager that held users’ token approvals. The routing proxy only checked lengths and that the chosen bridge contract was whitelisted; the route bridge then parsed attacker-controlled calldata into both a downstream target and call payload. By pointing that downstream target back at Transit Swap’s own permissions manager and encoding a forged
claimTokens()call, the attacker made Transit use its pre-existing wallet approvals to call arbitrary tokentransferFrom(victim, attacker, amount)flows. Users were not drained because Transit lost custody; they were drained because Transit had already been approved as spender and its route-validation chain accepted attacker-shaped execution data. - Trigger / entry point:
- Public incident reporting and SlowMist’s decompilation-based analysis place the key approved-spender surface at Transit Swap’s permissions-management contract
0xeD1afC8C4604958C2F38A3408Fa63B32E737C428. - SlowMist describes the live execution chain as
routing proxy (0x8785bb...) -> whitelisted route bridge (0x0B4727...) -> permissions manager (0xeD1afC...). - When users swapped through Transit, the routing proxy chose a whitelisted route-bridge contract and passed user-supplied route bytes into it.
- The route bridge’s parsed execution path then decided both which downstream contract to call and which bytes to send; SlowMist’s writeup specifically describes attacker-controlled
exchange contractandredemption databeing recovered from the upstream route bytes and later consumed by the bridge helper’scallBytespath. - The vulnerable chain accepted attacker-controlled data deeply enough that the attacker could redirect execution back into Transit Swap’s own permissions manager rather than into a legitimate swap venue.
- Public incident reporting and SlowMist’s decompilation-based analysis place the key approved-spender surface at Transit Swap’s permissions-management contract
- Root cause:
- Transit combined broad wallet-spender authority with a multi-contract execution pipeline that validated structure more than meaning.
- The intended invariant was effectively
user route data may only resolve to approved swap integrations and may only pull the caller's own intended input token for the quoted trade. - In practice, the routing proxy only enforced shallow checks like route-data length and outer bridge allowlisting, while the bridge and permissions-manager path still honored attacker-chosen downstream targets and token-transfer parameters.
- The key missed boundary was that
the outer bridge address is whitelistedwas treated as if it also authenticated the bridge’s innerexchange contract, selector family, and redemption semantics. It did not. - That meant Transit could be tricked into using its own allowance authority to transfer tokens from arbitrary approved users directly to the attacker.
- Exploit path:
- The attacker identified wallets on Ethereum and BNB Chain that had already approved Transit Swap’s permissions manager as ERC-20 spender.
- They called Transit Swap with crafted route data that passed the proxy’s superficial checks: expected-length fields plus selection of an outer route-bridge address already present in the whitelist mapping.
- Inside the bridge path, attacker-controlled bytes were parsed into an inner
exchange contractand calldata without proving that they described a real DEX interaction. - SlowMist’s reconstruction describes the bridge helper’s
callBytespath consuming that attacker-shaped inner target and redemption payload. - Instead of pointing at a normal swap venue, the attacker pointed the parsed target back at the permissions manager contract and designated the redemption payload to call
claimTokens(). - The forged
claimTokens()call then made the permissions manager invoke tokentransferFrom()with attacker-chosen token, victim, recipient, and amount parameters. - Because victims had already approved the permissions manager, the token contracts honored those pulls and transferred funds directly from victim wallets to the attacker.
- The attacker repeated the pattern across approved wallets until Transit paused contracts, security teams traced the operator, and most funds were later returned.
- What the attacker needed to be true:
- Victims needed live ERC-20 approvals to Transit Swap’s permissions manager.
- The routing proxy needed to accept attacker-supplied route bytes so long as they matched expected length / shape constraints and selected a whitelisted bridge.
- The route bridge needed to parse attacker-controlled target addresses and calldata without binding them to an approved swap venue or selector.
- The permissions manager’s
claimTokens()path needed to keep honoring attacker-controlledtoken,from,to, andamountsemantics once reached through the bridge.
- Technical anatomy worth remembering:
- This was not a single-contract
router.call(userData)toy example. The dangerous primitive was spread across a proxy, a bridge adapter, and a permissions helper, which made the approval-drain surface easier to miss. - The permissions manager was supposed to be a narrow input-token pull helper. Once reachable with attacker-chosen parameters, it became a generic wallet-drain primitive.
- The exploit preserved a distinct approval-drain variant:
whitelisted outer bridge + attacker-shaped inner target/redemption bytes + approved helper contract, not justnew facet forgot the allowlist. - The cleanest preserved failure boundary is
authenticated outer bridge != authenticated downstream spend meaning: allowlisting0x0B4727...did not prove whatcallByteswould later tell the permissions manager to do with wallet approvals. - The sharper positive-design frame is approval-funded route authority as a single security object: Transit had wallet-spender authority, but no narrower route-local object kept proxy-selected bridge, inner target, payer semantics, selector family, and token-pull meaning fused across the execution chain.
- Closed-source / unverified deployed contracts materially worsened response and review, because outside defenders had to decompile bytecode before they could explain the exploit path.
- This was not a single-contract
- Representative transactions and addresses:
- Main attacker address on Ethereum and BNB Chain:
0x75f2aba6a44580d7be2c4e42885d4a1917bffd46 - Vulnerable permissions-management contract:
0xeD1afC8C4604958C2F38A3408Fa63B32E737C428 - First publicly highlighted Ethereum attack transaction:
0xba75ad7a43e784f51fe777d749fc55ae10f1df2bcb01cde97641613b19acb6ec - Example Ethereum attack transaction cited by SlowMist:
0x743e4ee2c478300ac768fdba415eb4a23ae66981c076f9bff946c0bf530be0c7 - Example BNB Chain attack transaction cited by SlowMist:
0x181a7882aac0eab1036eedba25bc95a16e10f61b5df2e99d240a16c334b9b189 - Return / consolidation address reported by REKT:
0xD989f7B4320c6e69ceA3d914444c19AB67D3a35E
- Main attacker address on Ethereum and BNB Chain:
- Response / mitigation:
- Transit paused affected contracts and publicly warned users to revoke approvals on Ethereum and BNB Chain.
- SlowMist, PeckShield, and Bitrace publicly assisted with tracing, attribution pressure, and recovery coordination.
- Most stolen funds were eventually returned after the attacker began negotiating and sending assets back to the designated recovery address.
- The durable engineering lesson is to bind route bytes to approved targets and selectors at every hop, and to keep token-pull helpers from accepting attacker-controlled
from/tosemantics through generic upstream call paths.
Pattern tags
- arbitrary-call-token-approval-drain
- Outer-allowlisted bridge still hid an inner arbitrary call: Transit’s route proxy only proved
this bridge contract is whitelisted, while the bridge’s parsedexchange contractandredemption datastill remained attacker-authored execution policy - A token-pull helper quietly became the real theft primitive:
claimTokens()was supposed to be a narrow permissions-manager helper, but once attacker-shaped route bytes could reach it with arbitrarytoken,from,to, andamountsemantics, it functioned as a generic wallet-drain surface - Policy was split across proxy, bridge, and helper instead of one authenticated route object: each contract looked narrower in isolation, but the combined
proxy -> bridge -> permissions managerchain already had enough unbound authority to spend every standing user approval - Closed-source deployment increased the review gap: defenders had to decompile bytecode before they could even reconstruct which contract in the route chain really held spender power and where call meaning went unconstrained
Why the exploit worked
- Transit’s architecture already put its permissions manager inside users’ allowance tables, which meant route parsing was not just a swap-quality concern. It was the real wallet-spend boundary. Once attacker-supplied bytes could survive from the route proxy through the bridge and into
claimTokens(), Transit itself became the trusted spender that token contracts were willing to honor. - The dangerous primitive was not only
arbitrary external callin the abstract. It was the specific chainuser data -> whitelisted outer bridge -> parsed inner target -> parsed redemption bytes -> permissions helper -> token.transferFrom(...), where every hop preserved enough attacker control to keep changing what the route meant. - Transit therefore failed at a sharper question than
did we whitelist the bridge?The live question should have beendoes this exact route-local object still prove who may pay, which helper may pull, which downstream target family is canonical, and what this token movement is supposed to mean?It did not. - SlowMist’s reconstruction makes the preserved lesson unusually clean: the bridge helper’s
callBytespath was willing to parse attacker-providedexchange contractandredemption data, so the route never stopped being attacker-authored just because one outer contract address had passed an allowlist check. - This is why Transit matters as more than an early approval drain. It preserves the
outer adapter looked authenticated, inner spend meaning was notvariant that later incidents often disguise behind more modern router, facet, or adapter terminology.
Detection and audit pivots
- Trace route data all the way from user calldata to the final token-moving primitive. For Transit-style systems, the critical review question is not only
what bridge is selected?butwhich later parser decides target, selector, payer, and recipient semantics after that outer bridge is chosen? - Treat permissions helpers like
claimTokens()as first-class wallet-drain surfaces whenever they can be reached from public routing paths. Review them as if the contract already has victim approvals, because in production it often does. - Search for multi-hop validation gaps: route proxy checks length and bridge allowlist, bridge checks caller, helper checks caller, but nobody proves one shared object for
exact payer + exact token + exact target + exact selector + exact recipient meaning. - Add a negative test for the specific Transit shape: can a whitelisted outer bridge still be induced to parse an inner target equal to the protocol’s own permissions helper and redemption bytes equal to
claimTokens(...)over an arbitrary victim? If yes, the architecture is already a wallet drainer. - Inventory any closed-source or unverified production contracts that sit on approval-funded paths. If incident responders would need decompilation to answer
which contract can spend with this approval?, review and operational response are already lagging the risk surface.
Failure-prevention checklist
- Bind route admission and helper execution into one authenticated route object: payer, token, amount, outer bridge, inner target family, selector family, and recipient semantics should remain one proof, not several loosely related checks across several contracts.
- Do not let public routing bytes choose both the bridge path and the later token-pull meaning. If a bridge helper parses inner
exchange contractandredemption data, those fields need the same allowlist and semantic constraints as the outer route object. - Keep token-pull helpers narrow by construction. A function like
claimTokens()should derivefrom,to, token, and amount from authenticated route state or local accounting, not from attacker-shaped upstream calldata. - Reuse the same target / selector validation on nested execution that already exists for outer adapters.
Outer bridge is approvedis not enough if the bridge can still dispatch arbitrary inner calls. - Collapse policy layers operationally as well as logically: maintain a live map of which proxy, bridge, and helper contracts together exercise each spender address users may approve, so emergency disablement can target the whole approval-funded chain at once.
- Prefer verified source and deployment-time attestation for approval-consuming contracts. If decompilation is required to understand the live spend path, the protocol is already asking defenders to race the attacker under worse conditions.
- Add route-authenticity tests that try helper substitution, inner-target substitution, direct token calls, and
claimTokens()-style parameter spoofing through every bridge / adapter path, not just through the main router entrypoint. - Pair path validation with user-approval blast-radius controls: exact-amount approval defaults, approval expiry or reset paths, route-local pauses, and faster spender-inventory tooling all reduce loss if one nested call boundary still fails.
Audit prompts
- Which exact contract in the route chain is the approved spender from the token’s perspective, and what upstream components are allowed to tell it whom to pull from and where to send funds?
- After the route proxy selects an allowlisted bridge, what exact proof still constrains the bridge’s parsed inner target and redemption bytes to a canonical swap / bridge meaning?
- Can any public or user-shaped calldata path make the bridge call the protocol’s own permissions helper or another token-pull helper with attacker-controlled parameters?
- Are caller checks being confused with route-authentication checks? In Transit,
only approved proxy / bridge may call this helperwas weaker thanonly an authenticated route with canonical payer and recipient semantics may exercise this helper. - If a helper such as
claimTokens()is meant to pull only the current caller’s input token, where is that identity and amount derived, and can attacker-shaped calldata override it anywhere upstream? - Which nested adapters, bridge helpers, or parser functions can reinterpret route bytes after the outer router has already accepted them, and do those nested surfaces inherit the same allowlist and selector rules?
- If a copycat exploit began now, how quickly could operators list every spender address, proxy, bridge, and helper in the same approval-funded execution family and disable the whole chain before another sweep?
- Are unverified or closed-source contracts sitting on approval-funded paths preventing reviewers from proving the live route authority object without bytecode decompilation?
Lessons that should already be standard
- Allowlisting the outer adapter is not the same thing as authenticating the route. If inner calldata can still choose target and selector meaning, the route is still attacker-authored where it matters.
- Caller restrictions on helpers are not enough.
Only the protocol's bridge may call this functiondoes not help if the bridge itself can be steered into calling it with arbitrary victim and recipient semantics. - Permissions helpers are custody-adjacent systems. If users have approved them, then every upstream parser, proxy, and bridge that can reach them sits on the wallet-spend boundary whether the product team thinks of it that way or not.
- Multi-contract modularity can hide arbitrary-call power instead of reducing it. A proxy, adapter, and helper can each look locally narrow while jointly forming a generic wallet-drain pipeline.
- Closed-source approval-funded code is a security liability even when the exploit is eventually reversible. Defenders lost time because they first had to reconstruct the spend path before they could explain or bound it.
- Infinite or lingering approvals were the blast-radius multiplier, not the first mistake. The decisive failure was that Transit never kept spender authority bound to one coherent route story across nested execution.
- Review has to follow authority, not product labels.
routing proxy,bridge, andpermissions managersound like separate modules, but if together they can tell a token contracttransferFrom(victim, attacker, amount), they are one security object.
Why this incident matters
- It preserves an early large-scale approval-drain case where the bug lived in route parsing across several contracts, not just in one obvious router function.
- It shows that allowlisting only the outer bridge contract is not enough if the inner calldata can still choose arbitrary downstream targets and function calls.
- The preserved healthy-side lesson is sharper than
validate bridge calldata better: Transit needed one route-local authority object that kept spender identity, payer scope, whitelisted bridge meaning, inner target / selector family, andclaimTokens()token-pull semantics fused across the wholeproxy -> bridge -> helperchain before any allowance-backed execution happened. - It is also a useful warning about security review shape: when policy is split across proxy, adapter, and helper contracts, each component can look locally narrow while the combined route object is already broad enough to drain every wallet that approved the helper.
- It broadens the arbitrary-call approval-drain family beyond Dexible’s direct route array and Socket / LI.FI’s upgraded module regressions into a
proxy -> bridge -> helperexecution-chain failure where each contract looked narrower than the combined authority object it actually formed.
Internal linkages
- Core recurring pattern: arbitrary-call-token-approval-drain.
- Positive-design companion: approval-funded-route-authority-truth-security-object — the clean follow-up when the sharper question is what one route-local object should have kept spender identity, payer scope, whitelisted bridge meaning, inner target selection, selector family, and token-pull semantics fused above the user’s standing approval.
- Broader spender-boundary companion: spender-authority-truth-security-object — useful when the sharper question is why any already-approved permissions helper should be reachable through attacker-shaped route metadata at all.
- Adjacent pattern: privileged-message-dispatcher-abuse — both revolve around generic execution machinery that becomes more powerful than the surrounding product story suggests.
- Adjacent pattern: callback-origin-authentication-failure — both can drain approved wallets, but Transit was an arbitrary-call route-validation failure rather than a spoofed AMM settlement callback.
Control surface
-
The decisive leverage sat in Transit Swap’s route proxy, whitelisted bridge adapters, and permissions manager together rather than in any one isolated contract.
-
More precisely, the control surface was the full
proxy -> bridge -> permissions helperinterpretation chain: outer route admission, inner target selection, redemption-data meaning, and finalclaimTokens()token-pull execution. -
The deeper missed control was the absence of one shared route-authentication funnel proving
this exact wallet-funded route may traverse this exact bridge, call this exact downstream target / selector family, and consume this exact token-pull authority against this exact payerbefore any helper exercised wallet allowance. -
Once that execution chain let attacker-supplied bytes choose both the downstream target and the
claimTokens()parameters, Transit became a truthful but attacker-directed spender against every wallet that had approved it. -
Sources:
-
Last reviewed: 2026-07-24 UTC