Fantasm Finance XFTM mint() Missing FTM-Input Validation / Collateral-Pool Drain (2022-03-09)
- Name: Fantasm Finance XFTM
mint()missing FTM-input validation / collateral-pool drain - Date: 2022-03-09
- Protocol: Fantasm Finance
- Chain: Fantom
- Category: missing collateral-input validation / undercollateralized synthetic mint / issuance-redemption solvency asymmetry / collateral-pool drain
- Loss: about
1008.498875252390151 ETHbridged out by the attacker, roughly$2.62Mat the time; Fantasm later said it whitehacked another935,415 FTMfrom the remaining collateral pool for user compensation - Summary: Fantasm’s synthetic-FTM mint path was supposed to require both native
FTMand protocol tokenFSMas backing for newXFTM, with the exact split determined by the live collateral ratio. But the pool’s publicmint(uint256 _fantasmIn, uint256 _minXftmOut)path only enforced theFSMside. It set_ftmIn = msg.value, passed that intocalcMint(_ftmIn, _fantasmIn), and later wrapped whatever_ftmInhappened to be, but it never checked that the caller had actually supplied the FTM amount the mint economics assumed. At the time of the exploit the collateral ratio was very high, so a small amount ofFSMcould legitimately justify a largeXFTMmint only if the caller also supplied the dominantFTMleg. The attacker simply omitted thatFTMleg, minted outsizedXFTM, calledcollect()to receive it, sold the syntheticXFTMinto realFTM, bought moreFSM, and repeated the loop until the pool’s real collateral was gone. - Trigger / entry point:
- Fantasm’s core mint surface was the Fantom pool contract
0x880672ab1d46d987e5d663fc7476cd8df3c9f937. - The attacker used contract
0x944b58c9b3b49487005cead0ac5d71c857749e3eto automate repeated mint / collect / dump cycles. - Public postmortem and third-party reproductions agree that the exploit path hinged on
mint()accepting only the algorithmicFSMcomponent while failing to enforce the matching native-FTMcontribution.
- Fantasm’s core mint surface was the Fantom pool contract
- Root cause:
- Fantasm modeled
XFTMissuance as a two-legged backing problem: some value had to arrive as nativeFTM, and the remainder as burnableFSM, with the split determined by the current collateral ratio. - The public
mint()path did not actually prove that both legs entered. It recorded_ftmIn = msg.value, fed that intocalcMint(), and verified only that enoughFSMhad been provided for the computedminFantasmInrequirement. - In the live system,
calcMint()used_fantasmIn, theFSMprice, and the term(COLLATERAL_RATIO_MAX - collateralRatio)to determine_xftmOut, so when the collateral ratio was high the caller could mint a large amount ofXFTMwith a smallFSMinput if the dominantFTMside was also supplied. - Because
mint()never rejected theFTM == 0case when a large collateralized mint still went through, Fantasm let users print a claim on the reserve while paying only the tiny algorithmic side of the backing equation. - The redeem / exit side stayed economically live: freshly minted
XFTMcould still be sold into realFTM, so the broken issuance side and still-honest exit side created a direct drain loop.
- Fantasm modeled
- Exploit path:
- The attacker funded on BNB Chain via Tornado Cash, swapped into
USDC, and bridged that capital to Fantom through Celer. - On Fantom they swapped into
FTM, then into enoughFSMto start the exploit loop. - Using attack contract
0x944b58c9b3b49487005cead0ac5d71c857749e3e, they called Fantasm’smint()with meaningfulFSMinput but without supplying the matching nativeFTMcollateral the economic model assumed. mint()set_ftmIn = msg.value, calledcalcMint(_ftmIn, _fantasmIn), required only that_minFantasmIn <= _fantasmIn, wrapped the zero or trivial_ftmIn, and credited the caller with unclaimedXFTM.- The attacker then called
collect()to receive the mintedXFTM. - They sold that
XFTMfor realFTM, bought moreFSM, and repeated the cycle at a larger size. - Repetition mattered because the bug was not a single free withdrawal; it was a broken mint price that let the attacker recursively scale from a small seed into millions of dollars of synthetic claims.
- Fantasm and CertiK both described the attacker as ultimately minting and dumping roughly
2.8M XFTM, converting the extractedFTMintoETH, bridging thatETHto Ethereum through Celer, and then laundering through Tornado Cash.
- The attacker funded on BNB Chain via Tornado Cash, swapped into
- What the attacker needed to be true:
- Fantasm had to keep the
XFTMissuance formula live even when the required nativeFTMleg was not actually enforced. - The collateral ratio had to be high enough that a relatively small
FSMpayment could justify a very largeXFTMamount if the missingFTMside went unchecked. - The protocol needed a working
collect()path so minted-but-unclaimedXFTMcould be turned into transferable inventory. - There had to still be usable onchain liquidity or protocol exit paths that valued
XFTMagainst realFTM. - No reserve-outflow cap, mint cap keyed to actual collateral intake, or same-block circuit breaker could interrupt the recursive
mint -> collect -> dump -> rebuy FSMloop.
- Fantasm had to keep the
- Technical anatomy worth remembering:
- This was not primarily an oracle exploit. The key failure sat inside Fantasm’s own issuance boundary: the protocol’s mint function treated the
FSMside as sufficient evidence that the whole collateral package had been paid. - The useful formula lens is the one CertiK highlighted:
_xftmOut = (_fantasmIn * _fantasmPrice * COLLATERAL_RATIO_MAX * (PRECISION - mintingFee)) / PRECISION / (COLLATERAL_RATIO_MAX - collateralRatio) / PRICE_PRECISION. WhencollateralRatiois high, the denominator becomes small, so smallFSMinput can imply largeXFTMoutput — but only if the correspondingFTMcollateral also arrives. - Fantasm therefore broke on the issuance side and lost money on the exit side. The bug printed undercollateralized synthetic supply; later swaps merely monetized that printed fiction.
- The exploit is a clean example of why fractional / algorithmic backing systems need one explicit proof object for total collateral intake rather than separate
native legandburn legassumptions spread across helper logic. - It also shows how recursive looping can turn a seemingly small
missing requireinto complete reserve depletion once each cycle buys the attacker more of the scarce input token needed for the next mint.
- This was not primarily an oracle exploit. The key failure sat inside Fantasm’s own issuance boundary: the protocol’s mint function treated the
- Representative transactions and addresses:
- Vulnerable Fantasm pool:
0x880672ab1d46d987e5d663fc7476cd8df3c9f937 - Attacker contract:
0x944b58c9b3b49487005cead0ac5d71c857749e3e - Representative exploit-loop txs cited in contemporaneous technical analysis:
0x64da8b8043b14fe93f7ab55cc56ccca2d190a59836a3f45dbb4b0a832e329cac,0xa84d216a1915e154d868e66080c00a665b12dab1dae2862289f5236b70ec2ad9 - BNB Chain funding txs from Tornado Cash:
0x11407dd11dd028cfefdb24c9bde9163b4d10e3338e62c74ae2f5f3c2ec863133,0xeffe3e64b2df1e80d5282a55bd12a7e89cd088616d85274bb1589b3b96526f28 - BNB Chain bridge-into-Fantom tx:
0x2b5f61778881958d98935cdfcbcb97a7de8405b1d059c931f02461776ad9e007 - Fantom-side Celer receipt tx:
0x39be07303cf562407c857d9b43077f99bd63ee46b1a95d5cfe5947ee3c106422 - Ethereum-side recipient / laundering address highlighted by Fantasm:
0x47091E015b294B935BAbDA2d28aD44e3Ab07ae8D
- Vulnerable Fantasm pool:
- Response / mitigation:
- Fantasm paused protocol activity, patched the vulnerable code path, and sent the patched code back to its auditor.
- The team said it whitehacked
935,415 FTMfrom the remaining pool balances and planned user compensation using that recovered collateral plus accumulated protocol fees. - Fantasm also halted
FSMfarm emissions, stopped furtherXFTMminting, and moved toward a relaunch / token-replacement plan rather than trying to keep the broken system live.
Three collateral-bundle promotions this incident collapsed
One small algorithmic-side payment reached a live mint path.The attacker could still bringFSMto Fantasm’s payablemint()entrypoint even when the matching native-FTMleg that the quote economically assumed had not been supplied.That partial payment was over-promoted into full synthetic-backing truth.Oncemint()accepted theFSMside and threadedmsg.valuethroughcalcMint(), the protocol behaved as though the wholeFTM + FSMcollateral package required by the live collateral ratio had arrived, rather than preservingonly one leg is provenas a blocking state.That accepted backing truth still inherited live claim-creation and reserve-exit authority.The protocol then let the attackercollect()real transferableXFTM, and downstream markets still honored that synthetic against realFTM, so the bad publication immediately became recursively drainable reserve exposure.
Pattern tags
- Core recurring pattern: issuance-redemption-solvency-asymmetry.
- Missing native-collateral receipt proof /
mint()computedXFTMfrom a two-leggedFTM + FSMbacking model but only enforced theFSMleg onchain. - Reserve-backed claim undercharge / the issuance side accepted a tiny algorithmic contribution as if the dominant reserve-backed leg had also arrived, then left the exit side economically real.
- Actual-receipt boundary failure /
msg.valuewas read into_ftmIn, wrapped, and emitted, but there was no stronger proof that the native-asset contribution required by the pricing model had actually been supplied in the dangerousFTM == 0shape.
Why the exploit worked
- Fantasm’s mint formula already treated
XFTMas one reserve-backed claim object with two funding legs: nativeFTMsupplied immediately andFSMburned for the algorithmic remainder. But the publicmint()path only enforced one side of that object. Once_minFantasmIn <= _fantasmInpassed, the protocol behaved as if the whole collateral package had arrived even when the dominant native leg had not. - The live collateral ratio made that omission economically explosive. CertiK’s reproduced formula shows
collateralRatiositting in the denominator of_xftmOut; when the ratio is high, a relatively smallFSMamount can imply a much largerXFTMmint, but only if the pairedFTMcontribution is real. Fantasm therefore turned a missing input check into systematic underpricing of reserve-backed claims. - The native-asset boundary was weaker than the economic story the contract told.
mint()read_ftmIn = msg.valueand later wrapped_ftmIn, but it never rejected the shape where the calculated mint still made sense only under a nonzeroFTMcontribution. In practice, the protocol confusedcaller invoked the payable mint surfacewiththe native collateral leg required by this quote was actually paid. - The extraction path stayed clean because issuance and monetization were decoupled.
collect()converted the freshly printedXFTMinto transferable inventory, and downstream liquidity still treated that synthetic as redeemable against realFTM. The protocol therefore undercharged on issuance while still honoring the printed claim against real reserves on exit. - Recursive scaling finished the job. Each undercollateralized mint produced more saleable
XFTM, which produced more realFTM, which bought moreFSM, which let the attacker return to the broken issuance surface at larger size. The exploit was not one free mint; it was a loop that compounded the same solvency asymmetry until the reserve pool emptied.
Detection and audit pivots
- Treat multi-leg backing formulas as one solvency object, not two adjacent checks. If issuance math depends on both a native-asset contribution and a burn / transfer side contribution, ask where each leg is independently proven and where the protocol rejects impossible combinations like
dominant native leg omitted but quote still accepted. - Compare the quote engine to the receipt engine. Fantasm’s dangerous split was that
calcMint()modeled the full backing mix whilemint()only enforced theFSMside strongly enough to stop underpayment. Any protocol where the pricing helper knows more about required value intake than the public entrypoint enforces deserves direct adversarial testing. - Audit native-asset paths with the same skepticism usually reserved for weird ERC-20 semantics.
msg.valueis only a raw environmental input; if the business logic requires a specific nonzero native contribution, the contract should encode that requirement explicitly instead of hoping the surrounding formula makes the impossible branch unattractive. - Test recursive monetization, not just one-shot mint validity. A synthetic, LP share, or reserve-backed wrapper bug may look like
slightly underpriced issuancein isolation but become catastrophic once the output token can be sold, rehypothecated, or used to buy the scarce input for the next round. - Ask whether the exit side still honors stricter truth than the mint side. Fantasm drained because the system’s sell / reserve side still treated
XFTMas fully meaningful after the mint side had already stopped proving full backing.
Failure-prevention checklist
- Force quote-time and receipt-time collateral proofs to converge. If mint economics assume both a native-asset leg and a burn / transfer leg, the state-changing entrypoint must reject the call unless both legs are actually present in the same transaction and in the same proportions the quote modeled.
- Treat native-asset inputs as first-class backing proofs, not implicit environment values.
msg.valueshould be checked against the economically required collateral amount, not merely passed through as one convenient variable in later math. - Keep one explicit
total backing arrivedinvariant per issuance path. A protocol should not let helper logic separately assumethe FSM side was paidandthe FTM side must therefore also be fine; mint finalization should prove the whole backing package before claim creation. - Cap mint-side blast radius by actual collateral intake, not by formula output alone. Per-call mint ceilings, reserve-outflow throttles, and collateral-ratio-aware same-block guards help stop
underpaid mint -> immediate dump -> rebuy scarce input -> repeatloops from scaling to pool exhaustion. - Test zero-value and low-value native-input branches directly. If a mint formula becomes more favorable as a ratio moves, fuzz the exact states where
required reserve leg should dominateand verify the contract still fails closed when that leg is omitted. - Keep issuance and monetization containment coupled. If a reserve-backed claim can be minted incorrectly, pause and transfer controls should already be able to stop the downstream sell / collect / redeem rail that would convert the printed claim into stronger reserve assets.
- Treat recursive path profitability as part of severity review. A bug that undercharges by a small amount per mint may still be existential if the output token can be sold to buy the missing scarce input for the next iteration.
Audit prompts
- Where, exactly, does the protocol prove the full collateral bundle assumed by the mint formula, and does that proof happen in the same function that finalizes claim issuance?
- If
msg.value == 0ormsg.valueis materially below the reserve leg implied by the quote, which line fails, and is that failure explicit rather than accidental? - Does the public entrypoint enforce every asset leg the pricing helper modeled, or does some part of the backing requirement live only inside comments, docs, or economic intuition?
- Can a caller satisfy the small algorithmic leg, skip the dominant reserve leg, and still receive a claim whose downstream markets honor it as fully backed?
- What invariant test proves that claim supply cannot rise faster than the reserve-backed portion of collateral intake across all mint paths, not just the intended happy path?
- If a bad mint happened right now, which liquidity pools, redemption routes, or sibling markets would still let the attacker sell the printed claim into stronger assets before operators can react?
- Does the protocol ever confuse
environmental input observedwitheconomically sufficient payment received, especially on native-asset paths where there is no ERC-20transferFrom()to make the receipt boundary obvious? - Have reviewers simulated recursive
mint -> collect -> dump -> rebuy scarce inputloops, or only one-shot underpayment cases that understate the true drain path?
Lessons that should already be standard
- Fractional or algorithmic backing systems should not spread one solvency proof across multiple assumptions. If two assets jointly back one claim, the protocol needs one explicit proof object that both arrived before minting completes.
payableis not validation. Readingmsg.valueinto a variable does not prove the caller supplied the economically required native-asset leg.- Quote logic is not settlement logic. A formula can be correct and still lethal if the entrypoint that uses it does not prove the assets the formula assumed.
- The dangerous question is often not
can this mint too much once?butcan the printed claim be sold to finance the next round?Recursive monetization should be a default review lens for reserve-backed issuance bugs. - Reserve-backed claim systems should assume the sell side will stay honest longer than the mint side during an incident. That asymmetry is exactly why undercollateralized issuance bugs drain real reserves so quickly.
- Missing input checks on the dominant collateral leg are not minor hygiene bugs in partially collateralized systems; they are direct authorization failures over who gets to create money-good claims.
- Native-asset branches deserve the same adversarial rigor as nonstandard ERC-20 branches.
No transferFrom to inspectis a reason to add explicit receipt assertions, not a reason to rely on ambient context.
Why this incident matters
- Fantasm is a crisp example of
issuance side undercharges, exit side still honors real valuein a fractional-algorithmic synthetic design. - The bug was not exotic: the protocol simply failed to prove that the dominant collateral leg had arrived before it minted claims on the reserve.
- It is also a useful reminder that in partially collateralized systems, the dangerous edge is often not just the pricing formula itself but the boundary that is supposed to bind every priced component to real asset intake.
Internal linkages
- Core recurring pattern: issuance-redemption-solvency-asymmetry.
- Adjacent backing-proof lens: balance-delta-provenance-failure — useful when the sharper question is how a protocol should prove that the assets its issuance math assumes actually arrived.
- Positive-design companions: actual-receipt-accounting-and-token-semantics-admission-discipline and reserve-backed-claim-truth-security-object — Fantasm’s preserved lesson is that the quote surface, native-asset receipt proof, and reserve-backed claim quantity belonged to one security object, not two loosely coordinated legs.
- Useful contrast: meta-pool-mpeth-erc4626-mint-without-asset-transfer-unbacked-mint-pool-drain-2025-06-17 — both incidents let the issuance side print claims without full backing, but Meta Pool failed through inherited-standard boundary drift while Fantasm failed through a missing collateral-input check in a bespoke mint path.
- Useful reserve-arb cousin: nirvana-finance-ana-buy-side-pricing-input-manipulation-treasury-drain-2022-07-28 — both incidents broke the protocol’s own buy-side pricing / issuance boundary and then monetized the resulting claims against a still-real reserve.
Control surface
-
The decisive leverage sat in Fantasm’s
mint()/calcMint()boundary: what exact mix ofFTMandFSMwas supposed to backXFTM, which function actually enforced receipt of each leg, and whether newly printedXFTMcould still be sold into reserve-backed value before defenders reacted. -
Fantasm did not mainly fail because its collateral ratio moved. It failed because the contract treated a two-asset backing rule as satisfied when only one leg had really been paid.
-
Sources:
- https://medium.com/@fantasmfinance/fantasm-finance-post-mortem-exploit-09-march-2022-daf48ead016f
- https://www.certik.com/blog/fantasm-finance
- https://www.coinspect.com/learn-evm-attacks/cases/fantasm-finance/
- https://www.odaily.news/en/post/5177002
- https://github.com/JIAMING-LI/FantasmFinanceExploit
-
Last reviewed: 2026-08-05 UTC