All incidents

LiFi router allowance-drain exploit steals approved holder tokens

Share
Mar 20, 2022 02:51 UTCAttackLoss: 202,062.15 USDC, 1,202.37 AUDIO +2 moreManually checked1 exploit txWindow: Atomic
Estimated Impact
202,062.15 USDC, 1,202.37 AUDIO +2 more
Label
Attack
Exploit Tx
1
Addresses
1
Attack Window
Atomic
Mar 20, 2022 02:51 UTC → Mar 20, 2022 02:51 UTC

Exploit Transactions

TX 1Ethereum
0x4b4143cbe7f5475029cf23d6dcbb56856366d91794426f2e33819b9b1aac4e96
Mar 20, 2022 02:51 UTCExplorer

Victim Addresses

0x5a9fd7c39a6c488e715437d7b1f3c823d5596ed1Ethereum

Loss Breakdown

202,062.15USDC
1,202.37AUDIO
0.944405GNO
22.95DPI

Similar Incidents

Root Cause Analysis

LiFi router allowance-drain exploit steals approved holder tokens

1. Incident Overview TL;DR

An unprivileged EOA exploited the LiFi diamond router on Ethereum mainnet by calling the public CBridgeFacet.swapAndStartBridgeTokensViaCBridge entry with attacker-crafted SwapData that caused the router to execute arbitrary ERC-20 transferFrom calls using long-standing user allowances. In a single transaction (0x4b41…4e96), tokens were drained from 20 unrelated holders across USDC, AudiusToken, GnosisToken, and SetToken and sent directly to an adversary EOA 0x8780…177e. The router exposed a public swap-and-bridge function that forwarded attacker-controlled SwapData into a generic LibSwap.swap primitive, which allowed arbitrary external calls (including ERC-20 transferFrom) to be executed under the router’s msg.sender with no check that the token owners matched the current caller or transaction receiver.

2. Key Background

LiFi operates a diamond-proxy router on Ethereum (0x5a9f…6ed1) that aggregates swaps and bridges across multiple underlying protocols via modular facets (including CBridgeFacet). Users typically approve the router as an ERC-20 spender once and then rely on router functions to perform swaps/bridges on their behalf. The CBridgeFacet exposes swapAndStartBridgeTokensViaCBridge, which is intended to swap a user’s tokens into a bridge asset and then initiate a CBridge transfer. Internally it defers all swap logic to a shared LibSwap.swap helper that takes arbitrary SwapData describing which contracts to call, which tokens to move, and the calldata to use. ERC-20 allowances enable a contract such as the LiFi router to call transferFrom on a user’s behalf. A secure router must ensure that it only consumes allowances belonging to the user who initiated the current action or a closely related controlled account; using allowances of unrelated third parties violates basic expectations of token custody.

3. Vulnerability Analysis & Root Cause Summary

CBridgeFacet.swapAndStartBridgeTokensViaCBridge forwards attacker-supplied SwapData into LibSwap.swap, which permits arbitrary external calls (including ERC-20 transferFrom) to be executed by the LiFi router under its own msg.sender using any allowances previously granted to the router, without verifying that the from-addresses correspond to the current caller or intended receiver. This design lets an attacker sweep tokens from any holder that has approved the router.

4. Detailed Root Cause Analysis

The LiFi diamond router 0x5a9f…6ed1 is implemented as a standard EIP-2535-style diamond with a fallback that dispatches function selectors to facet addresses stored in LibDiamond.DiamondStorage. CBridgeFacet is one such facet and exposes swapAndStartBridgeTokensViaCBridge(ILiFi.LiFiData, LibSwap.SwapData[] memory _swapData, CBridgeFacet.CBridgeData memory _cBridgeData) as a public function reachable via selector 0x01c0a31a. This function is designed to (1) read the router’s current balance of the bridge token (or ETH), (2) execute a sequence of swaps via LibSwap.swap for each entry in _swapData, (3) compute _cBridgeData.amount as the post-swap increase in the router’s balance of the bridge token, and (4) call _startBridge to approve the bridge and invoke ICBridge.send/sendNative.

LibSwap.swap is a generic helper used across LiFi facets. For each SwapData entry it receives, it (a) ensures the router holds at least fromAmount of sendingAssetId, pulling additional tokens from msg.sender via LibAsset.transferFromERC20 if necessary, (b) sets an allowance for approveTo over the sendingAssetId, and then (c) performs a low-level call to callTo with arbitrary callData and value msg.value. Crucially, LibSwap imposes no restrictions on callTo or callData beyond type signatures; it does not require that callTo be a trusted DEX, nor does it decode callData to verify that the called function and its parameters conform to an expected swap.

In the exploit transaction, the attacker constructs SwapData entries that cause LibSwap.swap to skip any meaningful swap and instead call ERC-20 token contracts directly with transferFrom(from, to, amount) calldata, where from is set to a series of unrelated addresses that had previously approved 0x5a9f…6ed1 for USDC, AudiusToken, GnosisToken, or SetToken, and to is set to the adversary EOA 0x8780…177e. Because these transferFrom calls are executed by the LiFi router as msg.sender, they succeed as long as the victims’ allowances to the router are sufficient. There is no check in CBridgeFacet or LibSwap that the from-address matches the transaction sender 0xc6f2…d76 or the LiFiData.receiver, and no whitelist limits which token contracts or function selectors may be targeted.

The prestateTracer output and balance_diff.json demonstrate that, starting from σ_B with long-standing user allowances in place, executing swapAndStartBridgeTokensViaCBridge with the attacker-crafted SwapData deterministically results in net outflows from 20 victim holders and matching inflows to 0x8780…177e across the four tokens. The broken invariant is therefore directly attributable to LibSwap.swap’s ability to issue arbitrary external calls using the router’s allowances, combined with CBridgeFacet’s exposure of this primitive via a public, unprivileged entry point.

// LibSwap.swap (excerpt)
function swap(bytes32 transactionId, SwapData calldata _swapData) internal {
    uint256 fromAmount = _swapData.fromAmount;
    uint256 toAmount = LibAsset.getOwnBalance(_swapData.receivingAssetId);
    address fromAssetId = _swapData.sendingAssetId;
    if (!LibAsset.isNativeAsset(fromAssetId) && LibAsset.getOwnBalance(fromAssetId) < fromAmount) {
        LibAsset.transferFromERC20(_swapData.sendingAssetId, msg.sender, address(this), fromAmount);
    }
    if (!LibAsset.isNativeAsset(fromAssetId)) {
        LibAsset.approveERC20(IERC20(fromAssetId), _swapData.approveTo, fromAmount);
    }
    (bool success, bytes memory res) = _swapData.callTo.call{ value: msg.value }(_swapData.callData);
    if (!success) {
        string memory reason = LibUtil.getRevertMsg(res);
        revert(reason);
    }
}

Caption: LibSwap.swap executes a low-level call to attacker-chosen callTo with arbitrary callData using the diamond router as msg.sender, after optionally pulling tokens from msg.sender and granting allowances.

{
  "token": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
  "holder": "0x878099f08131a18fab6bb0b4cfc6b6dae54b177e",
  "before": "0",
  "after": "202012284580",
  "delta": "202012284580"
}

Caption: Excerpt from balance_diff.json for tx 0x4b41…4e96 showing the profit EOA 0x8780…177e receiving 202,012.28458 USDC (202012284580 raw units) during the exploit transaction.

5. Adversary Flow Analysis

The adversary prepared an EOA pair, used prior user-granted allowances to the LiFi router as the value source, and executed a single unprivileged swapAndStartBridgeTokensViaCBridge call that swept tokens from many holders into a profit EOA; subsequent Uniswap V3 swaps and minor ETH transfers simply reshaped and paid for this stolen portfolio.

  • Stage: Adversary preparation and funding

    • Txs: transfer tx 0x5aa9215e3c07c115eb774427e8f9f1102817da2b803f5cc5dc412e7f9742bcea in block 14420272 on Ethereum (chainid 1)
    • Effect: EOA 0xc6f2…d76 appears with prior ETH funding and performs a small ETH transfer to 0xa532…0b7d; this establishes 0xc6f2…d76 as an active EOA with sufficient ETH to initiate the later exploit.
  • Stage: Exploit transaction draining allowances

    • Txs: swap+bridge+arbitrary-transferFrom tx 0x4b4143cbe7f5475029cf23d6dcbb56856366d91794426f2e33819b9b1aac4e96 in block 14420687 on Ethereum (chainid 1)
    • Effect: 0xc6f2…d76 calls swapAndStartBridgeTokensViaCBridge on LiFi router 0x5a9f…6ed1 with crafted SwapData, causing the router to execute a long sequence of ERC-20 transferFrom calls that move USDC, AudiusToken, GnosisToken, and SetToken balances from 20 unrelated holders into 0x8780…177e while optionally initiating a CBridge transfer of a portion of USDC.
  • Stage: Post-exploit consolidation and swaps

    • Txs: transfer tx 0x5c7422eed874e732cdce3c460e27005a170980a6072a583fb8bed47c2b78a874 in block 14420704 on Ethereum (chainid 1); swap tx 0xec62bd31bd78f32aff9668450c862e6b06976486c844b43dd7a87f2455ab919f in block 14420724 on Ethereum (chainid 1); approve tx 0x3c8bd98eb45b62442ffe8ec02f82df8500d94cbfbdee06c3e31395c9ad1d4af9 in block 14420728 on Ethereum (chainid 1); swap tx 0x5ad07a8f1b60a479c1a5448a85e9955da0c32984e2d0a337599aca9179740326 in block 14420747 on Ethereum (chainid 1)
    • Effect: Immediately after the exploit, 0xc6f2…d76 sends 0.939593388109634662 ETH to 0x8780…177e, which then uses Uniswap V3 multicall swaps via the 0x68b3…fc45 router and an approval to USDT (0xdac1…3ec7) to rebalance and potentially cash out part of the stolen portfolio. These actions do not change the fact that the adversary’s profit was already realized in the exploit transaction itself.

6. Impact & Losses

The exploit drained tokens from 20 distinct holder addresses across four ERC-20 contracts: approximately 202,062.148578 USDC, 1202.371620631794480684 AUDIO, 0.944405031229340416 GNO, and 22.950860845096132852 DPI-equivalent SetToken. These losses are concentrated among individual users who had previously interacted with the LiFi router and granted it allowances. The LiFi router contract itself does not lose value but functions as the unwitting executor of the attacker’s arbitrary transferFrom calls, damaging user trust and requiring off-chain recovery efforts.

Token-level loss overview:

  • USDC: 202,062.148578 (202062148578 raw units, 6 decimals)
  • AUDIO: 1202.371620631794480684 (1202371620631794480684 raw units, 18 decimals)
  • GNO: 0.944405031229340416 (944405031229340416 raw units, 18 decimals)
  • DPI: 22.950860845096132852 (22950860845096132852 raw units, 18 decimals)

7. References

  • [1] Exploit transaction trace (callTracer) and state diff: artifacts/root_cause/data_collector/iter_1/tx/1/0x4b4143cbe7f5475029cf23d6dcbb56856366d91794426f2e33819b9b1aac4e96/trace_callTracer.json
  • [2] ERC-20 balance diffs for exploit transaction: artifacts/root_cause/seed/1/0x4b4143cbe7f5475029cf23d6dcbb56856366d91794426f2e33819b9b1aac4e96/balance_diff.json
  • [3] LiFi diamond, CBridgeFacet, and LibSwap source code: artifacts/root_cause/data_collector/iter_1/contract/1/0x73a499e043b03fc047189ab1ba72eb595ff1fc8e/source/
  • [4] USDC (FiatTokenV2_2) compiled artifact with storage layout: artifacts/root_cause/seed/1/0x43506849d7c04f9138d1a2050bbf3a0c054402dd/out/FiatTokenV2_2.sol/FiatTokenV2_2.json
  • [5] Normal transaction lists for 0xc6f2…d76, 0x8780…177e, and 0x5a9f…6ed1: artifacts/root_cause/data_collector/iter_2/tx/1/