Dexible selfSwap Router-Validation Failure / Approval Drain (2023-02-17)
- Name: Dexible
selfSwaprouter-validation failure / approval drain - Date: 2023-02-17
- Protocol: Dexible
- Chain: Ethereum + Arbitrum
- Category: router approval abuse / arbitrary external call / token-approval drain / route-validation failure
- Loss: about 1.5 million on Ethereum and about $450,000 on Arbitrum
- Summary: Dexible’s v2
selfSwap()flow let any caller supply arbitrary route targets, spender addresses, and calldata. The implementation then approved the attacker-chosen spender and executedrr.router.call(rr.routerData)for each route step without proving that the target was a legitimate DEX or that the calldata matched an intended swap. That meant an attacker could make Dexible call token contracts directly withtransferFrom(victim, attacker, amount)payloads. Because victims had already approved Dexible as spender, Dexible’s own allowance authority became the theft primitive. - Trigger / entry point:
- Public exploit writeups identified Dexible’s vulnerable v2 contract at
0xde62e1b0edaa55aac5ffbe21984d321706418024; Sourcify-verified source for the v2 implementation at0x33e690aea97e4ef25f0d140f1bf044d663091dafpreserves the critical logic. Dexible.selfSwap()accepted aSwapTypes.SelfSwapstruct whoseroutesarray let the caller choose each route’srouter,spender,routeAmount, and rawrouterData.SwapHandler.fill()then looped those route entries, calledsafeApprove(rr.spender, rr.routeAmount.amount), and executed(bool s, ) = rr.router.call(rr.routerData).- No onchain allowlist or selector binding forced
rr.routerto be a real DEX orrouterDatato encode a legitimate swap.
- Public exploit writeups identified Dexible’s vulnerable v2 contract at
- Root cause:
- Dexible treated attacker-supplied route instructions as if they were trustworthy swap policy, but
selfSwap()exposed that generic execution surface directly to arbitrary callers. - The intended invariant was effectively
the router path may only execute approved swap integrations for the declared trade; the deployed code only enforced broad shape constraints like fee-token validity and then trusted arbitrary route targets, spender identities, and calldata. - In practice the user-supplied
routesarray became a live onchain route-policy oracle forwho gets approval,which contract is called, andwhat that call is allowed to mean, even though Dexible already held wallet-spender authority. - Because Dexible already sat in many users’ ERC-20 allowance tables, the missing route validation turned a normal aggregator contract into a wallet-drain primitive.
- This was not a custody compromise or oracle failure. It was a call-surface authorization failure amplified by stale approvals.
- Dexible treated attacker-supplied route instructions as if they were trustworthy swap policy, but
- Exploit path:
- The attacker identified wallets that had already approved Dexible’s contracts as ERC-20 spenders.
- They called the vulnerable v2
selfSwap()path and supplied a craftedroutesarray. selfSwap()wrapped that attacker-controlled data into a genericSwapRequestand forwarded it intofill().preCheck()only enforced fee-token relationships and transferred the first route amount from the attacker; it did not authenticate downstream route targets.- Inside
fill(), Dexible approved the attacker-chosenspenderand then executed an arbitrary low-level call to the attacker-chosenrouterusing attacker-chosen calldata. - By pointing
routerat token contracts and encodingtransferFrom(victim, attacker, amount)inrouterData, the attacker made Dexible spend victims’ approved balances directly. - The attacker repeated the pattern across approved wallets on Ethereum and Arbitrum until the contracts were paused and users began revoking approvals.
- What the attacker needed to be true:
- Victims needed existing token approvals to Dexible large enough to monetize.
selfSwap()andfill()had to remain live long enough to sweep approved wallets.- Dexible could not enforce an allowlist for route targets or a selector / semantic check on
routerData. - The attacker needed only minimal legitimate input for the route setup; the real value came from the victims’ pre-existing approvals, not from attacker-owned inventory.
- Technical anatomy worth remembering:
- The dangerous line was not some obscure math bug; it was the generic execution pair of
approve(attacker-chosen spender)followed bycall(attacker-chosen target, attacker-chosen calldata)inside an approved router. selfSwap()is the preserved lesson here. A convenience path that lets users bring their own route data silently inherits the full trust burden of a generic execution engine.- Dexible is the user-supplied-route cousin of the later Revert-style family: instead of mutable offchain aggregator metadata crossing the trust boundary, the route array itself crossed it and directly became wallet-funded execution policy.
- The exploit family later reappeared in Socket and LI.FI, but Dexible is an earlier clean example of the
route meaning was never authenticated before an approved contract executed itvariant. - The protocol did not need to hold victim funds in escrow. The victims’ wallets were the balance sheet, and Dexible’s allowance slot was the authorization boundary that failed.
- The dangerous line was not some obscure math bug; it was the generic execution pair of
- Representative transactions and addresses:
- Attacker address used across public writeups:
0x684083f312ac50f538cc4b634d85a2feafaab77a - Vulnerable v2 proxy / entry contract:
0xde62e1b0edaa55aac5ffbe21984d321706418024 - Sourcify-verified v2 implementation preserving
selfSwap()andfill():0x33e690aea97e4ef25f0d140f1bf044d663091daf - Example Ethereum exploit transaction:
0x138daa4cbeaa3db42eefcec26e234fc2c89a4aa17d6b1870fc460b2856fd11a6 - Example heavily impacted victim highlighted in public coverage: BlockTower-associated address
0x58f5f0684c381fcfc203d77b2bba468ebb29b098
- Attacker address used across public writeups:
Three approval-route promotions this incident collapsed
-
Dexible already held real spender reach into victim wallets
- Users had granted Dexible live ERC-20 allowances, often well beyond the amount involved in any one honest swap.
- That meant the raw
approved spender can ask tokens to honor transferFrom(...)precondition was already true before the attacker touched route calldata.
-
Attacker-shaped route data was still promoted into canonical swap authority
selfSwap()accepted caller-chosenrouter,spender, androuterDatafields and forwarded them intofill()as if they still described one legitimate route step.- Dexible therefore promoted
some user-supplied route object existsintothis exact route object may exercise Dexible's spender authority, even though the route had never been bound to an allowlisted DEX, selector family, or recipient meaning.
-
That accepted route object still inherited ordinary downstream approval-and-call power
- Once the route survived ingress,
fill()gave it two authority edges in sequence: approve the attacker-chosenspender, then call the attacker-chosenrouterwith attacker-chosen calldata. - That final promotion is what turned stale wallet approvals into immediate theft. A route object that should only have described one swap step was still allowed to exercise near-generic downstream execution authority against victim-approved balances.
- Once the route survived ingress,
- Response / mitigation:
- Dexible paused affected contracts after the exploit and warned users through its front end and public channels.
- The practical user mitigation was immediate approval revocation, because the exploit monetized lingering wallet approvals rather than assets already escrowed inside Dexible.
- The engineering fix for this family is to make route validation mandatory at the contract boundary: allowlisted targets, allowlisted selectors, tighter route binding, and narrower approval scope.
Pattern tags
- arbitrary-call-token-approval-drain
- User-supplied route array became live approval policy:
selfSwap()accepted attacker-chosenrouter,spender,routeAmount, and rawrouterData, so Dexible let mutable route description choose both who received approval and who got called approve(...)plus arbitrarycall(...)lived in the same approved spender:SwapHandler.fill()first granted the attacker-shapedspenderallowance over the route amount and then executedrr.router.call(rr.routerData)without proving that either value belonged to a legitimate swap integration- Existing wallet approvals funded the theft path: victims had already approved Dexible, so once the route array could redefine downstream execution meaning, Dexible’s own spender identity became the mass-drain primitive
- Aggregator flexibility outran route authentication: Dexible treated
bring your own routeas a product feature, but never preserved one narrower route-local object tying payer scope, approved router target, approved spender target, selector family, and final recipient semantics together before execution
Why the exploit worked
- Dexible’s architecture already put it inside users’ allowance tables, which meant route validation was not a nice-to-have parser concern. It was the real custody boundary. Once
selfSwap()let arbitrary callers author the route array, Dexible gave attacker input the right to choose both who would receive approval and which contract Dexible itself would talk to next. - The dangerous primitive was not one line in isolation, but the paired sequence of
safeApprove(rr.spender, rr.routeAmount.amount)followed byrr.router.call(rr.routerData). Those two steps only make sense when one authenticated route object proves that the spender, router, calldata, payer, and recipient all belong to the same quoted swap story. Dexible never built that object. - The attacker therefore did not need to counterfeit liquidity, manipulate prices, or compromise admins. They only needed Dexible to truthfully exercise the allowance authority it already had while speaking attacker-shaped calldata to attacker-shaped targets.
- Dexible is useful because the route-policy oracle was visible and explicit: the exploit did not require offchain metadata corruption or a hidden helper. The mutable route array itself crossed the trust boundary and became onchain execution policy.
- That makes Dexible a clean early exemplar of the broader approval-funded route problem later seen in Revert, Socket, and LI.FI: route meaning remained more flexible than spender authority was allowed to be.
Detection and audit pivots
- Trace every path where user-provided route arrays or swap-step structs influence both an approval target and a later external call target. The specific red flag is
router,spender, or equivalent fields surviving from calldata intoapprove(...)and.call(...)without a shared allowlist or route commitment. - Audit
bring your own routeconvenience functions as wallet-drain surfaces, not just swap helpers. If the contract commonly holds user approvals, any user-authored integration tuple should be reviewed as potential delegatedtransferFrom()authority. - Add a negative test for the exact failure shape: can a token contract itself occupy the
routerslot and can attacker-craftedrouterDatamake the already-approved contract executetransferFrom(victim, attacker, amount)? If that test can ever pass, the router is already a wallet drainer. - Treat approval-target choice as route authority, not plumbing. If user input can choose
spenderseparately fromrouter, review whether the contract is letting attacker input define two different external authority edges in the same route step. - Monitor for aggregator execution where downstream targets are token contracts rather than expected DEX routers, and for route plans where the approval target is not the same small immutable set of known integrations. In Dexible-style systems, those are high-signal anomaly pivots.
Failure-prevention checklist
- Force every approval-consuming swap path through one mandatory validation funnel. No
selfSwap(), helper, facet, or protocol-step shortcut should be able to reachapprove(...)plus low-level external call execution without the same route-policy checks firing. - Bind one route-local authority object before execution: payer, token in, amount in, approved spender, approved router target, approved selector family, recipient semantics, and replay / expiry boundary should travel together rather than as loosely related fields.
- Deny token contracts and generic token-pull helpers as route targets by default. If the protocol ever needs to call them directly, that path should live behind a separate, tightly scoped flow rather than inside ordinary swap routing.
- Do not let callers choose
spenderandrouterindependently unless the protocol can prove the pair belongs to one reviewed integration step. A healthy router should not hand attacker input two separate authority edges in the same route entry. - Pair route validation with narrow approval scope: exact-amount pulls, short-lived permits, or approval-reset patterns reduce the blast radius when one route path slips.
- Pin integration identity at the code level where possible. If a route allowlist only names addresses, an upgradeable or repointed endpoint can silently turn an approved route into attacker logic later.
- Add negative tests for the exact Dexible failure shape: token contract occupying the
routerslot, attacker calldata encodingtransferFrom(victim, attacker, amount), and the router already holding victim allowance. - Keep spender-exposure inventory and emergency disable paths ready. For approval drains, fast route shutdown and fast revoke guidance are part of the design, not just incident response polish.
Audit prompts
- Which public entrypoints can make the contract both approve a downstream spender and then execute a low-level external call while acting as an already-approved wallet spender?
- Can user-controlled route structs choose
router,spender, and raw calldata independently, or does one authenticated route object bind those fields together? - Can a token contract itself occupy the route target slot, or can route calldata reach an internal helper that ultimately performs
transferFrom()with attacker-shaped arguments? - Do convenience paths such as
selfSwap()bypass the main integration-validation middleware that more curated swap flows use? - Where is the exact relationship enforced between quoted trade meaning and executed calldata meaning? Could the UI describe one swap while settlement honors a completely different external call story?
- Are route targets allowlisted only by address, or also by selector family and code identity?
- If the protocol already sits in users’ allowance tables, what is the shortest path from that spender identity to an arbitrary
.call(...)under attacker input? - During incident response, can operators quickly answer which contracts hold the largest aggregate user approvals and which reachable functions can exercise them?
Lessons that should already be standard
Bring your own routeis not harmless UX flexibility when the contract is an approved spender. It is a generic execution engine unless route meaning is authenticated onchain.- Approval drains are not meaningfully separate from custody risk. If the protocol can already pull from user wallets, route validation is a wallet-custody boundary.
approve(attacker-shaped spender)pluscall(attacker-shaped target, attacker-shaped calldata)inside the same approved contract is already enough to classify the path as a potential wallet drainer.- Mutable route description should never outrun spender authority. The route array is not just metadata; in designs like Dexible it becomes the real policy object deciding who the approved contract may talk to and why.
We do not hold user depositsis not a defense when lingering approvals are the balance sheet. User-wallet exposure inventory and revoke playbooks belong in the protocol security model.- Router hardening has to survive convenience helpers and future modules. One flexible shortcut can silently undo the validation discipline of the main path.
Why this incident matters
- It is one of the cleanest pre-2024 examples of an aggregator draining user wallets by abusing allowance authority rather than by losing custody or compromising admins.
- It shows how
bring your own routeproduct flexibility can silently collapse into arbitrary-call execution if the route is not authenticated onchain. - The preserved healthy-side lesson is sharper than
validate routers better: Dexible needed one route-local authority object that kept payer scope, approved targets, approved selectors, spender identity, and recipient semantics fused above raw wallet allowance before anyapprove + callpair was allowed to execute. - Dexible is also a useful family anchor because the policy object was visibly the user-supplied route array itself. Later cases such as Revert, Socket, and LI.FI changed the packaging — offchain metadata, nested helper calldata, or skipped middleware — but kept the same underlying mistake of letting mutable route meaning outrun wallet-spender authority.
Internal linkages
- Core recurring pattern: arbitrary-call-token-approval-drain.
- Positive-design companion: approval-funded-route-authority-truth-security-object — the sharper follow-up is what route-local authorization object should have bound Dexible’s approved spender surface, victim payer scope, allowed route targets/selectors, and final recipient meaning before
selfSwap()could spend any wallet allowance. - Broader spender-boundary companion: spender-authority-truth-security-object — useful when the sharper question is not just router validation but why any already-approved contract may exercise a wallet allowance at all.
- Adjacent pattern: privileged-message-dispatcher-abuse — both revolve around overpowered generic execution surfaces, but Dexible monetized wallet approvals rather than protocol-owned admin privileges.
Control surface
-
The decisive leverage sat in Dexible’s
selfSwap()route submission surface, the user-suppliedroutesarray acting as live route policy,SwapHandler.fill()’s unconditional approve-plus-call sequence, and the fact that users had already granted Dexible spender authority. -
The deeper missed control was the absence of one shared route-authentication funnel that proved
this exact wallet-funded route step may approve this exact spender and call this exact target/selector for this exact settlement meaning. -
Once route data was treated as trusted execution intent instead of hostile input, Dexible itself became the attacker-authorized caller that token contracts were willing to honor.
-
Sources:
- https://rekt.news/dexible-rekt
- https://blog.solidityscan.com/dexible-hack-analysis-never-blindly-trust-smart-contracts-c2aee7943c1a/
- https://repo.sourcify.dev/contracts/full_match/1/0x33e690aea97e4ef25f0d140f1bf044d663091daf/sources/contracts/dexible/Dexible.sol
- https://repo.sourcify.dev/contracts/full_match/1/0x33e690aea97e4ef25f0d140f1bf044d663091daf/sources/contracts/dexible/baseContracts/SwapHandler.sol
- https://www.coindesk.com/tech/2023/02/17/blocktower-capital-loses-15m-in-defi-market-aggregator-dexible-exploit-blockchain-data/
-
Last reviewed: 2026-08-02 UTC