All incidents

DFX flash LP mint exploit

Share
Nov 10, 2022 19:27 UTCAttackLoss: 99,866.26 USDC, 2,283,092,402.04 XIDRPending manual check1 exploit txWindow: Atomic
Estimated Impact
99,866.26 USDC, 2,283,092,402.04 XIDR
Label
Attack
Exploit Tx
1
Addresses
1
Attack Window
Atomic
Nov 10, 2022 19:27 UTC → Nov 10, 2022 19:27 UTC

Exploit Transactions

TX 1Ethereum
0x6bfd9e286e37061ed279e4f139fbc03c8bd707a2cdd15f7260549052cbba79b7
Nov 10, 2022 19:27 UTCExplorer

Victim Addresses

0x46161158b1947d9149e066d6d31af1283b2d377cEthereum

Loss Breakdown

99,866.26USDC
2,283,092,402.04XIDR

Similar Incidents

Root Cause Analysis

DFX flash LP mint exploit

1. Incident Overview TL;DR

An attacker exploited the DFX Finance XIDR/USDC Curve pool at 0x46161158b1947d9149e066d6d31af1283b2d377c in Ethereum block 15941704 through a single transaction, 0x6bfd9e286e37061ed279e4f139fbc03c8bd707a2cdd15f7260549052cbba79b7. The attacker-controlled helper contract borrowed pool assets with flash(), deposited into the same pool during the flash callback to mint overstated LP shares, then burned those LP shares after the flash settled and withdrew more XIDR and USDC than it had economically added. The root cause was not a pricing oracle issue or privileged access issue. It was a liquidity-accounting bug: flash() exposed a reentrant callback before flash liabilities were settled, and proportionalDeposit() minted LP shares from a transient, flash-depressed liquidity snapshot instead of settled net liquidity.

2. Key Background

DFX used a proxy pool at 0x46161158b1947d9149e066d6d31af1283b2d377c backed by the verified Curves implementation at 0x17af88bcc6590bbad6ec29e4ba63e132cb572326. The pool served two assets in this incident: XIDR at 0xebf2096e01455108badcbaf86ce30b6e5a72aa52 and USDC at 0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48.

The verified implementation exposes flash(address,uint256,uint256,bytes), deposit(uint256,uint256), and withdraw(uint256,uint256) on the same contract. The verified source also shows deposit() and withdraw() are nonReentrant, while flash() performs token transfers and then invokes IFlashCallback(msg.sender).flashCallback(...) before its post-callback balance checks. That callback surface matters because LP minting and burning both depend on pool liquidity snapshots.

The attacker did not need any privileged role. The helper contract 0x6cfa86a352339e766ff1ca119c8c40824f41f22d was created by EOA 0x14c19962e4a899f29b3dd9ff52ebfb5e4cb9a067, and the same EOA later submitted the exploit transaction.

3. Vulnerability Analysis & Root Cause Summary

The vulnerability is an accounting attack caused by mixing flash-loan state with LP mint pricing. In the verified DFX source, flash() transfers assets out and calls back into the attacker before the loan has been repaid. During that callback, the attacker can call deposit() on the same pool. deposit() routes into ProportionalLiquidity.proportionalDeposit(), which computes minted LP from the caller's intended deposit value divided by the current gross-liquidity snapshot returned by getGrossLiquidityAndBalancesForDeposit(). Because the flash loan has temporarily removed pool assets, that _oGLiq snapshot is artificially low, so the same deposit mints too many LP shares. After the callback ends and flash() finishes, the attacker burns those fresh LP shares through withdraw(), which prices redemption against restored live balances via proportionalWithdraw(). The result is deterministic over-redemption of pool assets and direct attacker profit.

4. Detailed Root Cause Analysis

The core invariant is that LP shares minted in one transaction must correspond to net liquidity actually added to the pool after accounting for liabilities that are still open in that same transaction. DFX broke that invariant by letting a flash-borrower observe and act on a temporary balance state.

Independent evidence from the exploit trace shows the exact sequence:

helper -> pool.flash(2311627906953628 XIDR, 99400000000 USDC)
       -> helper.flashCallback(...)
       -> pool.deposit(200000000000000000000000, deadline)
       -> mint 387023837944937266146579 LP to helper
       -> pool.withdraw(387023837944937266146579, deadline)

The seed trace records the flash transfers out of the pool, the callback-time deposit, the LP mint, and the later withdrawal in the same transaction:

0x4616...377C::flash(..., 2311627906953628, 99400000000, ...)
  emit Transfer(pool -> helper, 2311627906953628 XIDR)
  emit Transfer(pool -> helper, 99400000000 USDC)
0x6cFa...F22D::flashCallback(...)
  0x4616...377C::deposit(200000000000000000000000, ...)
  emit Transfer(0x0 -> helper, 387023837944937266146579 LP)
0x4616...377C::withdraw(387023837944937266146579, ...)
  emit Transfer(pool -> helper, 2283092402041452 XIDR)
  emit Transfer(pool -> helper, 99866263271 USDC)

The verified DFX source supports the causal explanation. flash() is exposed on the pool contract and invokes IFlashCallback(msg.sender).flashCallback(...) without nonReentrant on the flash() function itself. deposit() and withdraw() each route into the proportional liquidity library, and ProportionalLiquidity.proportionalDeposit() computes LP output from getGrossLiquidityAndBalancesForDeposit() and mints immediately from that snapshot. In other words, the attacker is allowed to mint LP while the pool is still missing the assets that were just flashed out.

The balance-diff evidence confirms that this accounting mismatch translated into real loss. The pool's USDC balance fell from 448042647980 to 348176384709, a delta of -99866263271. The helper's USDC balance rose from 99398834232 to 198665097503, a delta of +99266263271, while the submitting EOA only paid gas. The auditor also tracked the XIDR side: the pool's XIDR balance fell by 2283092402041452, and the helper's XIDR holdings increased from 2050000000000000 to 4319138913669499.

5. Adversary Flow Analysis

The attacker first deployed a helper contract, then used it as the sole execution surface for the exploit. The helper borrowed both pool assets via flash(), which immediately reduced the pool balances used by DFX's gross-liquidity accounting. While still inside the callback window, the helper called deposit(200000e18, deadline) against the same pool and transferred in the required XIDR and USDC, but the pool priced LP issuance against the temporarily depressed _oGLiq snapshot. That minted 387023837944937266146579 LP shares to the helper.

After the callback returned, flash() completed its repayment checks and the pool balances were back to their live state. The helper then burned all newly minted LP through withdraw(). Because withdrawal pricing used the restored pool balances, the helper received 2283092402041452 XIDR and 99866263271 USDC from the pool and ended with net profit in both assets.

6. Impact & Losses

The exploit directly drained both assets in the DFX XIDR/USDC pool. The measured losses are:

  • USDC: 99866263271 raw units, decimal = 6
  • XIDR: 2283092402041452 raw units, decimal = 6

These values are evidenced by the collector balance-diff artifact for USDC and the block-bounded XIDR balance changes cited in the root cause analysis. The immediate victim was the DFX XIDR/USDC Curve pool contract, and the attacker retained the withdrawn assets in the helper contract after the LP round-trip completed.

7. References

  • Exploit transaction: 0x6bfd9e286e37061ed279e4f139fbc03c8bd707a2cdd15f7260549052cbba79b7
  • Helper creation transaction: 0x4a3dd10795e8dd0955b87f9a5b740dae3d160cd27bd448f889aa3465412b0f54
  • Victim proxy: 0x46161158b1947d9149e066d6d31af1283b2d377c
  • Verified implementation: 0x17af88bcc6590bbad6ec29e4ba63e132cb572326
  • Seed transaction metadata: /workspace/session/artifacts/collector/seed/1/0x6bfd9e286e37061ed279e4f139fbc03c8bd707a2cdd15f7260549052cbba79b7/metadata.json
  • Seed verbose trace: /workspace/session/artifacts/collector/seed/1/0x6bfd9e286e37061ed279e4f139fbc03c8bd707a2cdd15f7260549052cbba79b7/trace.cast.log
  • Seed balance diff: /workspace/session/artifacts/collector/seed/1/0x6bfd9e286e37061ed279e4f139fbc03c8bd707a2cdd15f7260549052cbba79b7/balance_diff.json
  • Verified DFX code pages: https://etherscan.io/address/0x46161158b1947d9149e066d6d31af1283b2d377c#code and https://etherscan.io/address/0x17af88bcc6590bbad6ec29e4ba63e132cb572326#code