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 ETH bridged out by the attacker, roughly $2.62M at the time; Fantasm later said it whitehacked another 935,415 FTM from the remaining collateral pool for user compensation
  • Summary: Fantasm’s synthetic-FTM mint path was supposed to require both native FTM and protocol token FSM as backing for new XFTM, with the exact split determined by the live collateral ratio. But the pool’s public mint(uint256 _fantasmIn, uint256 _minXftmOut) path only enforced the FSM side. It set _ftmIn = msg.value, passed that into calcMint(_ftmIn, _fantasmIn), and later wrapped whatever _ftmIn happened 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 of FSM could legitimately justify a large XFTM mint only if the caller also supplied the dominant FTM leg. The attacker simply omitted that FTM leg, minted outsized XFTM, called collect() to receive it, sold the synthetic XFTM into real FTM, bought more FSM, 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 0x944b58c9b3b49487005cead0ac5d71c857749e3e to automate repeated mint / collect / dump cycles.
    • Public postmortem and third-party reproductions agree that the exploit path hinged on mint() accepting only the algorithmic FSM component while failing to enforce the matching native-FTM contribution.
  • Root cause:
    • Fantasm modeled XFTM issuance as a two-legged backing problem: some value had to arrive as native FTM, and the remainder as burnable FSM, 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 into calcMint(), and verified only that enough FSM had been provided for the computed minFantasmIn requirement.
    • In the live system, calcMint() used _fantasmIn, the FSM price, and the term (COLLATERAL_RATIO_MAX - collateralRatio) to determine _xftmOut, so when the collateral ratio was high the caller could mint a large amount of XFTM with a small FSM input if the dominant FTM side was also supplied.
    • Because mint() never rejected the FTM == 0 case 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 XFTM could still be sold into real FTM, so the broken issuance side and still-honest exit side created a direct drain loop.
  • Exploit path:
    1. The attacker funded on BNB Chain via Tornado Cash, swapped into USDC, and bridged that capital to Fantom through Celer.
    2. On Fantom they swapped into FTM, then into enough FSM to start the exploit loop.
    3. Using attack contract 0x944b58c9b3b49487005cead0ac5d71c857749e3e, they called Fantasm’s mint() with meaningful FSM input but without supplying the matching native FTM collateral the economic model assumed.
    4. mint() set _ftmIn = msg.value, called calcMint(_ftmIn, _fantasmIn), required only that _minFantasmIn <= _fantasmIn, wrapped the zero or trivial _ftmIn, and credited the caller with unclaimed XFTM.
    5. The attacker then called collect() to receive the minted XFTM.
    6. They sold that XFTM for real FTM, bought more FSM, and repeated the cycle at a larger size.
    7. 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.
    8. Fantasm and CertiK both described the attacker as ultimately minting and dumping roughly 2.8M XFTM, converting the extracted FTM into ETH, bridging that ETH to Ethereum through Celer, and then laundering through Tornado Cash.
  • What the attacker needed to be true:
    • Fantasm had to keep the XFTM issuance formula live even when the required native FTM leg was not actually enforced.
    • The collateral ratio had to be high enough that a relatively small FSM payment could justify a very large XFTM amount if the missing FTM side went unchecked.
    • The protocol needed a working collect() path so minted-but-unclaimed XFTM could be turned into transferable inventory.
    • There had to still be usable onchain liquidity or protocol exit paths that valued XFTM against real FTM.
    • No reserve-outflow cap, mint cap keyed to actual collateral intake, or same-block circuit breaker could interrupt the recursive mint -> collect -> dump -> rebuy FSM loop.
  • 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 FSM side 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. When collateralRatio is high, the denominator becomes small, so small FSM input can imply large XFTM output — but only if the corresponding FTM collateral 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 leg and burn leg assumptions spread across helper logic.
    • It also shows how recursive looping can turn a seemingly small missing require into complete reserve depletion once each cycle buys the attacker more of the scarce input token needed for the next mint.
  • 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
  • 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 FTM from the remaining pool balances and planned user compensation using that recovered collateral plus accumulated protocol fees.
    • Fantasm also halted FSM farm emissions, stopped further XFTM minting, 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 bring FSM to Fantasm’s payable mint() entrypoint even when the matching native-FTM leg that the quote economically assumed had not been supplied.
  • That partial payment was over-promoted into full synthetic-backing truth. Once mint() accepted the FSM side and threaded msg.value through calcMint(), the protocol behaved as though the whole FTM + FSM collateral package required by the live collateral ratio had arrived, rather than preserving only one leg is proven as a blocking state.
  • That accepted backing truth still inherited live claim-creation and reserve-exit authority. The protocol then let the attacker collect() real transferable XFTM, and downstream markets still honored that synthetic against real FTM, 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() computed XFTM from a two-legged FTM + FSM backing model but only enforced the FSM leg 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.value was 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 dangerous FTM == 0 shape.

Why the exploit worked

  • Fantasm’s mint formula already treated XFTM as one reserve-backed claim object with two funding legs: native FTM supplied immediately and FSM burned for the algorithmic remainder. But the public mint() path only enforced one side of that object. Once _minFantasmIn <= _fantasmIn passed, 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 collateralRatio sitting in the denominator of _xftmOut; when the ratio is high, a relatively small FSM amount can imply a much larger XFTM mint, but only if the paired FTM contribution 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.value and later wrapped _ftmIn, but it never rejected the shape where the calculated mint still made sense only under a nonzero FTM contribution. In practice, the protocol confused caller invoked the payable mint surface with the 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 printed XFTM into transferable inventory, and downstream liquidity still treated that synthetic as redeemable against real FTM. 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 real FTM, which bought more FSM, 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 while mint() only enforced the FSM side 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.value is 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 issuance in 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 XFTM as 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.value should be checked against the economically required collateral amount, not merely passed through as one convenient variable in later math.
  • Keep one explicit total backing arrived invariant per issuance path. A protocol should not let helper logic separately assume the FSM side was paid and the 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 -> repeat loops 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 dominate and 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 == 0 or msg.value is 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 observed with economically sufficient payment received, especially on native-asset paths where there is no ERC-20 transferFrom() to make the receipt boundary obvious?
  • Have reviewers simulated recursive mint -> collect -> dump -> rebuy scarce input loops, 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.
  • payable is not validation. Reading msg.value into 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? but can 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 inspect is 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 value in 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

Control surface