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.SwapDatareachedLibSwap.swap()directly instead of flowing throughSwapperV2._executeSwaps(), the shared middleware that enforcedLibAllowList.contractIsAllowed(...)andLibAllowList.selectorIsAllowed(...). That let the attacker route LI.FI’s spender authority away from legitimate DEX calls and toward raw ERC-20transferFrom()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()inGasZipFacet, which accepted a caller-suppliedLibSwap.SwapDatastruct and invokedLibSwap.swap(0, _swapData)directly. - In LI.FI’s hardened swap paths,
SwapperV2._executeSwaps()first checkedLibAllowList.contractIsAllowed(currentSwap.approveTo),LibAllowList.contractIsAllowed(currentSwap.callTo), andLibAllowList.selectorIsAllowed(bytes4(currentSwap.callData[:4]))before permitting execution. depositToGasZipERC20()bypassed that shared middleware entirely, which LI.FI later attributed to deployment-process human error.
- LI.FI added a new GasZip-related diamond facet at
- 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 calledLibSwap.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 riskin 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 masstransferFrom()execution primitive.
- LI.FI combined broad ERC-20 spender authority from prior user approvals with a generic low-level call surface inside
- Exploit path:
- The attacker identified that
GasZipFacet.depositToGasZipERC20()was public and accepted attacker-controlledLibSwap.SwapData. - 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 hitSwapperV2._executeSwaps()’s allowlist checks onapproveTo,callTo, and the first four calldata bytes. - They targeted wallets on Ethereum and Arbitrum that had previously granted LI.FI contracts large or effectively infinite token approvals.
- Rather than submitting calldata for a real DEX swap, the attacker crafted
_swapDatawhosecallToandcallDatainvoked ERC-20transferFrom(victim, attacker, amount). - 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.
- Tokens including USDC, USDT, and DAI were pulled directly from victim wallets into attacker-controlled addresses such as
0x7742ed59e9ecf1712bc4c6bdd0c526e903a7f2c8, with exploit execution also touching0xc74fc202a6d0cf7a0ac38e99607b8629cdf2cb10. - Once the issue was detected, LI.FI disabled the vulnerable facet across chains, which stopped further draining through that path.
- The attacker identified that
- 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 logicby itself. It was a low-level external call helper (LibSwap.swap()) that was reachable outside the sharedSwapperV2._executeSwaps()/LibAllowListgate. - The interesting architectural failure is not merely
facet omitted a check, buta 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 callandinfinite approvalare multiplicative risks. Either one alone is bad; together they become a mass wallet-drain path.
Three approval-funded route promotions this incident collapsed
-
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.
-
A caller-shaped GasZip helper invocation was allowed to count as a legitimate approval exercise for this exact route
depositToGasZipERC20()accepted attacker-shapedLibSwap.SwapDataand then calledLibSwap.swap()directly, outside the normalSwapperV2._executeSwaps()/LibAllowListfunnel.- That meant a generic
callToplus calldata blob could masquerade asthis is still the intended GasZip-related route objecteven though no shared path re-proved target family, selector family, or settlement meaning. - The real collapse happened here: LI.FI treated
helper received SwapDataas close enough tothis exact approval-funded route is still canonical.
-
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.
- Once the helper invocation was accepted, LI.FI’s spender identity still carried enough authority to call token contracts directly and make them honor
- 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
- Vulnerable GasZip facet:
- 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 publicdepositToGasZipERC20()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()/LibAllowListvalidation funnel that older paths depended on - Public helper reached the dangerous primitive directly:
depositToGasZipERC20()calledLibSwap.swap(0, _swapData)itself, so attacker-chosencallToandcallDatanever 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 riskwas only the blast-radius multiplier. The money path still started with a routing boundary failure: the helper accepted a swap object that no longer provedthis is an allowed GasZip-related steprather thanthis 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 bridgebutdid 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 fromis 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 withoutLibAllowList, 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
SwapDataor raw calldata for narrow helpers likedepositToGasZipERC20(). 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 onlyfeature 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, arbitraryapprove, arbitrarycallTo, 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 keepsswap to native for GasZip depositdistinct frommake 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 targetfromrouter 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 pathis 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 gasstill becamearbitrary token callonce the helper accepted genericSwapDatawithout 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?butdoes 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 muchfailures, 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 isevery 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 approvalsare best understood as blast-radius amplifiers, not standalone root causes. The exploit paid because the contract’s call discipline failed first.
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 a route-local authorization object that should have kept approved spender identity, victim payer scope, allowed target / selector set, and GasZip step meaning fused before the new helper path could exercise wallet allowance.
- Broader spender-boundary companion: spender-authority-truth-security-object — useful when the sharper question is why any LI.FI-owned spender surface should have been allowed to turn standing wallet approvals into attacker-shaped execution authority at all.
- Compare against adjacent router / integration drains: socket-wrappedtokenswapperimpl-performaction-arbitrary-call-approval-drain-2024-01-16, dough-finance-connectordeleverageparaswap-flashloanreq-unvalidated-paraswap-calldata-approval-drain-2024-07-12, unizen-trade-aggregator-v1-unverified-call-array-arbitrary-call-approval-drain-2024-03-08.
- Adjacent pattern: privileged-message-dispatcher-abuse — both involve overpowered generic call surfaces, but LI.FI monetized user allowances rather than protocol-owned admin authority.
Control surface
-
The decisive leverage sat in
depositToGasZipERC20(), the missing target / selector validation beforeLibSwap.swap(), and the fact that LI.FI contracts already held spender approval over user wallets. -
More specifically, LI.FI never kept one route-local authority object for
which helper path may spend,which external targets and selectors this GasZip step may touch, andwhat spender meaning user approval was supposed to authorizeonce the new facet bypassedSwapperV2._executeSwaps(). -
Once those pieces lined up, the attacker did not need price manipulation, governance power, or custody compromise. They only needed LI.FI to truthfully exercise the allowance it already had in the wrong place.
-
Sources:
- https://li.fi/knowledge-hub/incident-report-16th-july
- https://blog.verichains.io/p/lifi-attack-arbitrary-calls-and-infinite
- https://blog.solidityscan.com/li-fi-hack-analysis-521388128d22/
- https://github.com/lifinance/contracts/pull/700
- https://raw.githubusercontent.com/lifinance/contracts/9adde379d80c23fe0d3cc30ec0d0575a54c24c21/src/Facets/GasZipFacet.sol
- https://raw.githubusercontent.com/lifinance/contracts/9adde379d80c23fe0d3cc30ec0d0575a54c24c21/src/Helpers/SwapperV2.sol
- https://raw.githubusercontent.com/lifinance/contracts/main/src/Facets/GasZipFacet.sol
-
Last reviewed: 2026-08-04 UTC