LI.FI GasZipFacet Arbitrary Call / Infinite-Approval Drain (2024-07-16)

  • Name: LI.FI GasZipFacet arbitrary call / infinite-approval drain
  • Date: 2024-07-16
  • Protocol: LI.FI
  • Chain: Ethereum + Arbitrum
  • Category: router approval abuse / arbitrary external call / token-approval drain / upgrade regression
  • Loss: about $11.6 million across 153 wallets, per LI.FI’s incident report
  • Summary: Shortly after deploying a new GasZip diamond facet, LI.FI left a public helper path where attacker-controlled LibSwap.SwapData reached LibSwap.swap() directly instead of flowing through SwapperV2._executeSwaps(), the shared middleware that enforced LibAllowList.contractIsAllowed(...) and LibAllowList.selectorIsAllowed(...). That let the attacker route LI.FI’s spender authority away from legitimate DEX calls and toward raw ERC-20 transferFrom() calls against wallets that had already granted LI.FI large allowances. The protocol did not need to hold the victims’ funds in custody. It only needed to be an approved spender plus one swap helper that bypassed the normal route-authentication layer, so no route-local object kept approved spender identity, victim payer scope, allowed target / selector family, and GasZip-step settlement meaning fused above raw allowance.
  • Trigger / entry point:
    • LI.FI added a new GasZip-related diamond facet at 0xf28A352377663cA134bd27B582b1a9A4dad7e534; LI.FI’s incident report linked the in-development feature to PR #700.
    • The exploitable function was the public protocol-step helper depositToGasZipERC20() in GasZipFacet, which accepted a caller-supplied LibSwap.SwapData struct and invoked LibSwap.swap(0, _swapData) directly.
    • In LI.FI’s hardened swap paths, SwapperV2._executeSwaps() first checked LibAllowList.contractIsAllowed(currentSwap.approveTo), LibAllowList.contractIsAllowed(currentSwap.callTo), and LibAllowList.selectorIsAllowed(bytes4(currentSwap.callData[:4])) before permitting execution.
    • depositToGasZipERC20() bypassed that shared middleware entirely, which LI.FI later attributed to deployment-process human error.
  • Root cause:
    • LI.FI combined broad ERC-20 spender authority from prior user approvals with a generic low-level call surface inside LibSwap.swap().
    • The intended invariant was all user-shaped swap execution must pass through the shared allowlist gate; the new facet violated that architectural rule by exposing a public helper that called LibSwap.swap() directly.
    • Because ERC-20 tokens trust approved spenders rather than end-user intent, once LI.FI itself could be induced to call arbitrary token contracts, existing allowances became attacker-usable wallet access.
    • More precisely, the GasZip helper never preserved one narrower approval-funded route object proving which LI.FI spender surface was active, which victim wallet was funding this exact step, which external targets / selectors were canonical for the GasZip stage, and what recipient meaning the spend was supposed to have.
    • This was therefore not just an infinite approval risk in the abstract. It was an upgrade regression where one protocol-step helper escaped the parent router’s validation discipline and turned a legitimate spender contract into a mass transferFrom() execution primitive.
  • Exploit path:
    1. The attacker identified that GasZipFacet.depositToGasZipERC20() was public and accepted attacker-controlled LibSwap.SwapData.
    2. Instead of entering through LI.FI’s ordinary swap routes, they used that helper path, which called LibSwap.swap(0, _swapData) directly and therefore never hit SwapperV2._executeSwaps()’s allowlist checks on approveTo, callTo, and the first four calldata bytes.
    3. They targeted wallets on Ethereum and Arbitrum that had previously granted LI.FI contracts large or effectively infinite token approvals.
    4. Rather than submitting calldata for a real DEX swap, the attacker crafted _swapData whose callTo and callData invoked ERC-20 transferFrom(victim, attacker, amount).
    5. LI.FI executed those low-level calls from its own router / facet context, so the token contracts saw a legitimate approved spender making the transfer.
    6. Tokens including USDC, USDT, and DAI were pulled directly from victim wallets into attacker-controlled addresses such as 0x7742ed59e9ecf1712bc4c6bdd0c526e903a7f2c8, with exploit execution also touching 0xc74fc202a6d0cf7a0ac38e99607b8629cdf2cb10.
    7. Once the issue was detected, LI.FI disabled the vulnerable facet across chains, which stopped further draining through that path.
  • What the attacker needed to be true:
    • The new GasZip facet had to bypass the target / selector allowlist enforcement that older LI.FI paths relied on.
    • Victim wallets needed pre-existing approvals large enough for theft to be worthwhile; LI.FI said finite approvals were not affected.
    • The vulnerable contract needed to preserve its spender identity while making the attacker-chosen external call, so token contracts would honor the transfer.
    • Attackers needed a quick way to enumerate approved wallets and balances before LI.FI paused the facet and users revoked approvals.
  • Technical anatomy worth remembering:
    • The protocol was exploited as a spender, not as a custodian. Funds moved out of user wallets because LI.FI had already been granted approval.
    • The dangerous primitive was not swap logic by itself. It was a low-level external call helper (LibSwap.swap()) that was reachable outside the shared SwapperV2._executeSwaps() / LibAllowList gate.
    • The interesting architectural failure is not merely facet omitted a check, but a public protocol-step helper bypassed the one centralized path where LI.FI had concentrated target and selector validation.
    • Diamond / facet architectures make this class of failure easier to miss operationally: older facets can be hardened correctly while one new facet or helper reopens the generic-call surface.
    • This is why arbitrary call and infinite approval are multiplicative risks. Either one alone is bad; together they become a mass wallet-drain path.

Three approval-funded route promotions this incident collapsed

  1. LI.FI still was a real approved spender for these wallets and assets

    • Users had previously granted LI.FI contracts broad ERC-20 allowances, often effectively infinite ones.
    • That only established the weakest fact: LI.FI can speak to token contracts as approved spender for these wallets.
    • It did not yet prove that this new GasZip helper path deserved to exercise that spender authority.
  2. A caller-shaped GasZip helper invocation was allowed to count as a legitimate approval exercise for this exact route

    • depositToGasZipERC20() accepted attacker-shaped LibSwap.SwapData and then called LibSwap.swap() directly, outside the normal SwapperV2._executeSwaps() / LibAllowList funnel.
    • That meant a generic callTo plus calldata blob could masquerade as this is still the intended GasZip-related route object even though no shared path re-proved target family, selector family, or settlement meaning.
    • The real collapse happened here: LI.FI treated helper received SwapData as close enough to this exact approval-funded route is still canonical.
  3. That accepted helper invocation still inherited broad downstream token-pull authority

    • Once the helper invocation was accepted, LI.FI’s spender identity still carried enough authority to call token contracts directly and make them honor transferFrom(victim, attacker, amount).
    • Healthy design would have narrowed that stage to GasZip-specific settlement targets, selector families, and recipient semantics, rather than letting the accepted helper invocation keep generic token-call reach.
    • This is why the incident was so expensive: a real approval lane, a counterfeit route object, and portfolio-scale downstream spend authority were all allowed to survive at once.
  • Representative transactions and addresses:
    • Vulnerable GasZip facet: 0xf28A352377663cA134bd27B582b1a9A4dad7e534
    • LI.FI router / spender address highlighted in public analysis: 0x1231deB6F5749Ef6ce6943A275A1D3E7486F4EaE
    • Attacker address highlighted by SolidityScan: 0x7742ed59e9ecf1712bc4c6bdd0c526e903a7f2c8
    • Exploit address highlighted by SolidityScan: 0xc74fc202a6d0cf7a0ac38e99607b8629cdf2cb10
    • Example exploit transaction: 0xd82fe84e63b1aa52e1ce540582ee0895ba4a71ec5e7a632a3faa1aff3e763873
    • Another reported exploit transaction: 0x65a92b189e4ae0b8a8a02cd59c5e9f6832586bd5167d41a24eb4f4d2ac692755
    • LI.FI told users to revoke approvals for at least these spender contracts: 0x1231deB6F5749Ef6ce6943A275A1D3E7486F4EaE, 0x341e94069f53234fE6DabeF707aD424830525715, 0xDE1E598b81620773454588B85D6b5D4eEC32573e, 0x24ca98fB6972F5eE05f0dB00595c7f68D9FaFd68
  • Response / mitigation:
    • LI.FI said it activated its incident-response plan immediately after detection and disabled the vulnerable facet across all chains.
    • The team publicly warned users to revoke approvals and later reported that only wallets with effectively infinite approvals were affected because exact finite approvals were LI.FI’s default path.
    • LI.FI’s incident report framed the failure as a deployment-review-process miss rather than a novel library flaw: the allowlist gate existed in SwapperV2, but the public depositToGasZipERC20() helper in the new facet never invoked it.
    • The post-incident GasZipFacet redesign in LI.FI’s repository removed that public ERC20 helper surface from the new version and instead pushed ERC20 entry through swapAndStartBridgeTokensViaGasZip() / _depositAndSwap(), i.e. back through the shared allowlist-enforcing swap pipeline.
    • The incident is a strong operational reminder that upgrade review needs to verify not just new business logic, but also whether every inherited security gate was actually wired in on the deployed path.

Pattern tags

  • arbitrary-call-token-approval-drain
  • Upgrade regression reopened a generic spender call surface: the new GasZip facet inherited LI.FI’s swap libraries and wallet-approval blast radius, but not the shared SwapperV2._executeSwaps() / LibAllowList validation funnel that older paths depended on
  • Public helper reached the dangerous primitive directly: depositToGasZipERC20() called LibSwap.swap(0, _swapData) itself, so attacker-chosen callTo and callData never had to survive the platform’s normal target-and-selector gate
  • Existing wallet approvals became live theft authority: users had already approved LI.FI as spender, so once the helper could be steered into raw ERC-20 calls, the protocol’s legitimate spender identity became the drain primitive
  • Diamond / facet modularity amplified review risk: one newly deployed protocol-step helper quietly widened execution authority even though adjacent LI.FI facets still enforced the intended routing policy

Why the exploit worked

  • LI.FI’s safety story depended on one architectural rule: user-shaped swap execution had to enter through the shared allowlist middleware before any low-level external call happened. The new facet broke that rule, so one helper path regained arbitrary-call power while still wearing LI.FI’s already-approved spender identity.
  • The attacker did not need custody compromise, governance, or a pricing bug. They only needed one public path where LI.FI would speak arbitrary calldata from an address wallets had already approved to spend on their behalf.
  • Infinite approval risk was only the blast-radius multiplier. The money path still started with a routing boundary failure: the helper accepted a swap object that no longer proved this is an allowed GasZip-related step rather than this is just some call LI.FI is now willing to make.
  • The facet architecture mattered operationally because the dangerous primitive lived in reusable shared libraries. That made it easy for a new entrypoint to look like an ordinary extension while silently bypassing the one place where target and selector constraints were normally enforced.
  • The remediation path in the later GasZipFacet is the clean design clue: ERC-20 entry moved back behind _depositAndSwap() and the inherited allowlist gate, which is exactly where wallet-funded route authority should have remained all along.

Detection and audit pivots

  • Grep every approval-consuming entrypoint for direct LibSwap.swap(...) reachability or any other low-level call helper that can be touched without crossing the one shared allowlist / policy function first.
  • Diff new facets, adapters, and convenience helpers against the legacy hardened path. The key question is not only does this compile and bridge but did this new entrypoint actually inherit the same target, selector, and spender-scope checks as the old one?
  • Treat public protocol-step helpers as first-class wallet-drain surfaces whenever the contract commonly holds user approvals. If a helper can accept user-shaped callTo, approveTo, or raw calldata, review it as if it were a direct token-pull primitive.
  • Monitor for approval-consuming transactions whose downstream calls target ERC-20 token contracts directly rather than expected DEXes, bridges, or approved adapters. For LI.FI-style routers, spender contract directly called token.transferFrom(...) is the key anomaly.
  • Keep an inventory of which deployed facets or router modules can currently exercise the same spender addresses users approve. In modular systems, incident response needs to answer which helper paths can spend with this approval? before attackers do.

Failure-prevention checklist

  • Treat every new facet, protocol-step helper, or adapter as a new wallet-spend surface when the parent router already sits in user allowance tables. Review should begin from what approvals does this code inherit immediately on deployment?, not from is this feature narrow in product terms?
  • Keep one shared execution funnel for all approval-consuming swap paths and make bypasses impossible by construction. If LibSwap.swap(...) is dangerous without LibAllowList, helper code should not be able to reach it directly from a public entrypoint.
  • Bind helper purpose, target family, selector family, payer scope, and recipient semantics into one route-local authorization object before any attacker-shaped bytes can reach low-level execution.
  • Prefer typed protocol-step arguments over generic SwapData or raw calldata for narrow helpers like depositToGasZipERC20(). If a generic structure is unavoidable, prove onchain that the target, selector, asset path, and recipient still match the helper’s advertised purpose.
  • Keep stage-scoped permissions narrower than raw spender authority. A helper intended only to swap into native gas for GasZip should not inherit generic power to call arbitrary token contracts just because the router is already approved.
  • Pair architectural review with deployment review. LI.FI’s own incident report matters because the validation layer existed in the codebase but not on the newly deployed path; release checks must verify security middleware is wired in here too, not only feature matches the PR.
  • Maintain emergency-disable inventory for all modules and facets sharing the same spender addresses, and rehearse chain-wide disablement before production incidents force that answer in real time.
  • Add negative tests that assert every new helper rejects direct ERC-20 transferFrom, arbitrary approve, arbitrary callTo, arbitrary selector changes, and any route where approved spender identity outlives the exact intended settlement story.

Audit prompts

  • Which exact public functions can currently reach LibSwap.swap(...) or equivalent low-level call helpers without first passing through the router’s shared allowlist gate?
  • For a helper like depositToGasZipERC20(), what onchain proof keeps swap to native for GasZip deposit distinct from make any arbitrary ERC-20 call from LI.FI's approved spender address?
  • If a new facet is added tomorrow, what deployment-time checklist proves it inherited the same approveTo, callTo, and selector-family validation as the older hardened routes?
  • What narrower state object binds payer scope, target family, selector family, asset path, and recipient meaning after the router has already been approved by the wallet?
  • Can any helper that is meant to be protocol-specific gas / bridge plumbing ever call token contracts directly, and if yes, what prevents that surface from becoming a transferFrom(...) primitive against pre-approved wallets?
  • How quickly can the team enumerate every live spender address and every facet / module that can exercise each one if a copycat exploit begins before users revoke approvals?
  • Are code review and release review explicitly checking for shared security middleware bypassed by new convenience helper, or do they still assume architectural invariants survive automatically through modular upgrades?
  • If the helper were exploited today, would monitors distinguish expected DEX / bridge target from router called token.transferFrom(...) fast enough to pause the path before a second drain wave spreads?

Lessons that should already be standard

  • We already have an allowlist in the shared path is not a safety property if a new public helper can route around it. In modular routers, architectural invariants have to be inherited mechanically, not socially.
  • Infinite approvals were the blast-radius multiplier, not the original sin. The decisive failure was that one GasZip helper regained arbitrary-call power while still carrying LI.FI’s spender identity into user wallets.
  • Narrow product purpose does not imply narrow execution authority. Swap to native and deposit gas still became arbitrary token call once the helper accepted generic SwapData without proving the route meaning stayed intact.
  • Deployment-process mistakes are protocol-design facts when modular upgrades can instantly inherit wallet authority. If a release review can forget one security gate, the system must assume that failure mode and constrain helpers accordingly.
  • Router, facet, and helper inventories are part of wallet safety. Teams need a standing answer to which modules can spend with this approval today? before attackers or frantic users ask it during an incident.
  • Approval-drain incidents are often route-authentication failures in disguise. The allowance only made theft possible; the missing proof about what that allowance was still allowed to do made it profitable.
  • The healthier design question is not only is this target allowed? but does this exact helper still prove one coherent settlement story from approved spender to final external call?

Why this incident matters

  • It is a canonical example of how aggregator-style arbitrary-call bugs monetize stale user approvals instead of draining protocol TVL.
  • The note belongs in the same family as other router can call too much failures, but here the key lesson is sharper: one missing validation step turned LI.FI’s spender privilege into direct wallet access.
  • The preserved healthy-side boundary is not only every swap helper must inherit the allowlist; it is every approval-consuming helper must still prove one route-local authority object binding approved spender surface, victim payer scope, canonical target / selector family, and GasZip-step settlement meaning before any wallet-funded call executes.
  • It also shows why infinite approvals are best understood as blast-radius amplifiers, not standalone root causes. The exploit paid because the contract’s call discipline failed first.

Internal linkages

Control surface