Prisma Finance MigrateTroveZap onFlashLoan() Spoofed-Migration Collateral Theft (2024-03-28)

  • Name: Prisma Finance MigrateTroveZap onFlashLoan() spoofed-migration collateral theft
  • Date: 2024-03-28
  • Protocol: Prisma Finance
  • Chain: Ethereum
  • Category: migration trust-boundary failure / spoofed migration context / unvalidated flash-loan callback data / delegated-position collateral theft
  • Loss: about 3,479.24 ETH total, roughly $12M at the time; the primary exploiter accounted for about 3,257 ETH
  • Summary: Prisma’s migration helper MigrateTroveZap (0xcc7218100da61441905e0c327749972e3cbee9ee) was supposed to atomically move a user’s trove from one TroveManager to another by taking an mkUSD flash loan, closing the old trove, and reopening an equivalent one in the new manager. The fatal mistake was that onFlashLoan() trusted the callback data as if it could only have been produced by Prisma’s own migrateTrove() flow. In reality, any user could call mkUSD.flashLoan() directly, set receiver = MigrateTroveZap, and feed attacker-chosen migration parameters into the callback. Because affected users had granted setDelegateApproval(MigrateTroveZap, true) on BorrowerOperations, the zap could close victim troves on their behalf, reopen them inside the same manager with much less collateral but almost the same debt, leave the collateral difference stranded inside the zap, and then reuse a second spoofed migration to move that leftover wstETH into an attacker-controlled trove and cash it out.
  • Trigger / entry point:
    • The exploiter did not call Prisma’s intended migrateTrove() entry point. Instead, they called mkUSD.flashLoan() directly and pointed the receiver at MigrateTroveZap.
    • That direct flash-loan path triggered MigrateTroveZap.onFlashLoan() with attacker-supplied data.
    • onFlashLoan() did not authenticate that the callback came from a legitimate zap-initiated migration nor that the encoded migration plan matched the victim trove’s real collateral state.
    • The exploit only worked against trove owners who had previously approved the zap as a delegate on BorrowerOperations.
  • Root cause:
    • Prisma treated the flash-loan callback context as trusted migration state instead of hostile user input.
    • MigrateTroveZap assumed onFlashLoan() would only ever be reached from the protocol’s own migration flow, even though the underlying debt-token flash loan was publicly callable.
    • The helper allowed a mismatch between the collateral removed from the old trove and the collateral used to reopen the new one, so excess collateral could remain inside the zap contract.
    • Because delegated trove permissions were already in place, spoofed migration instructions became a direct way to reconfigure and strip victim positions.
  • Exploit path:
    1. The main exploiter funded EOA 0x7e39e3b3ff7adef2613d5cc49558eab74b9a4202 and deployed exploit contract 0xd996073019c74b2fb94ead236e32032405bc027c.
    2. In the first exploit transaction 0x00c503b595946bccaea3d58025b5f9b3726177bbdc9674e634244135282116c7, the attacker called mkUSD.flashLoan() for roughly 1,442,100 mkUSD and set MigrateTroveZap as the receiver.
    3. The crafted callback data targeted victim trove owner 0x56a201b872b50bbdee0021ed4d1bb36359d291ed in wstEthTroveManagerV2 (0x1cc79f3f47bfc060b6f761fcd1afc6d399a968b6). That trove held about 1,745.08 wstETH collateral against about 1,442,100 mkUSD debt.
    4. Using the delegate approval the victim had previously granted, MigrateTroveZap closed the victim trove and reopened it inside the same trove manager with only about 463.18 wstETH collateral and roughly the same debt.
    5. The missing collateral — about 1,281.90 wstETH in the first example — remained in MigrateTroveZap rather than being returned to the victim.
    6. The attacker then opened a tiny trove of their own, sourced 1 wstETH of additional flash liquidity, and executed a second spoofed migration so the zap reopened the attacker’s trove using the leftover wstETH now sitting inside the contract.
    7. The attacker closed that newly collateralized trove, withdrew the captured wstETH, and repaid the flash loans.
    8. The same pattern was repeated across multiple approved victim troves until Prisma paused the protocol. Public postmortem data lists 11 addresses hit by the main exploiter, 11 more by a copycat exploiter, and 3 by a third exploiter.
  • What the attacker needed to be true:
    • mkUSD.flashLoan() had to be callable by arbitrary users with an arbitrary receiver and arbitrary callback data.
    • MigrateTroveZap.onFlashLoan() had to trust that callback data as an authentic migration plan.
    • Victims had to have live delegate approval set for MigrateTroveZap on Prisma’s BorrowerOperations contract.
    • The migration helper had to allow the old trove’s full collateral to be withdrawn while reopening the replacement trove with less collateral, leaving a stealable remainder.
    • The attacker needed a way to convert collateral marooned in the zap into an attacker-owned trove position before the protocol paused.
  • Technical anatomy worth remembering:
    • This was not just missing access control on a helper. The sharper failure is that migration-context authenticity lived in a public flash-loan callback with no proof that the callback represented a zap-authored migration.
    • The user approval surface mattered as much as the helper bug. setDelegateApproval() turned a migration convenience contract into a standing authority over live troves, so once callback authenticity failed the attacker inherited real position-management rights.
    • The theft did not require inventing debt or breaking an oracle. It repurposed legitimate trove-management permissions to move a victim from healthy collateralized position to same debt, much thinner collateral, then captured the difference.
    • Prisma is a strong reminder that migration code can fail even when the legacy source contracts are correct. If the helper does not authenticate who authored the migration plan, the migration wrapper itself becomes the exploit surface.
  • Representative transactions and artifacts:
    • Main exploiter EOA: 0x7e39e3b3ff7adef2613d5cc49558eab74b9a4202
    • Main exploiter contract: 0xd996073019c74b2fb94ead236e32032405bc027c
    • Copycat exploiter EOAs: 0x7Fe83f45e0f53651b3ED9650d2a2C67D8855e385, 0x7C9FC6E2B908e858F30c5c71a20273315Efd5cf8
    • Vulnerable MigrateTroveZap (mkUSD): 0xcc7218100da61441905e0c327749972e3cbee9ee
    • Vulnerable MigrateTroveZap (ULTRA): 0xC3eAf094e2586965244aB6534f6Dc69c6C16b5D5
    • BorrowerOperations (LST): 0x72c590349535AD52e6953744cb2A36B409542719
    • wstEthTroveManagerV2: 0x1cc79f3f47bfc060b6f761fcd1afc6d399a968b6
    • Main exploit tx: 0x00c503b595946bccaea3d58025b5f9b3726177bbdc9674e634244135282116c7
    • Exploit contract deployment tx: 0x1319a80286b9c229a2d7566e21d7476b792956e4b59ee00d26449b6b77f2bcd0
    • Funding tx highlighted in Prisma’s timeline: 0xe55bd15d2bd17c0a97356f612480887df91bc483f6affae8edd06a6d08b60f53
    • Emergency pause tx: 0x1a35836eec322195046387fd0aa43f945aa484c062b8b1442166db6860d733ae
  • Response / mitigation:
    • Prisma publicly disclosed the incident, paused protocol operations, and told vault owners to revoke delegate approval for both zap contracts.
    • The immediate operational fix was to shut down the zap-authority surface by revoking approvals and disabling new zap activity.
    • The design-level fix is stricter: a migration helper must authenticate callback provenance and bind the migration plan to an internally initiated state transition, not to calldata echoed back from a public flash-loan primitive.
    • The healthier long-term shape is one single-use migration-closeout object that binds who initiated this migration, which trove may be closed, which destination manager may receive the position, how much debt and collateral may move, and where any leftover collateral must go before a callback is allowed to exercise delegated trove rights.
    • Prisma’s postmortem also made clear that the risk surface extended to both mkUSD and ULTRA versions of the helper, even though only mkUSD wstETH troves were exploited in practice.

Pattern tags

  • migration-trust-boundary-failure
  • migration-cutover-authenticity-and-single-record-consumption-discipline
  • helper-path-invariant-reuse-and-fail-closed-discipline
  • Prisma let a public flash-loan callback answer what migration is happening right now while delegated trove approval separately answered who may touch this user's position, so migration-context authenticity and position authority quietly split apart.
  • The theft rail was the leftover collateral sitting inside MigrateTroveZap, not just the victim-trove close itself. Once the helper reopened the replacement trove with less collateral than it had withdrawn, the marooned wstETH became a second migration-authentication problem: who was allowed to claim that residual balance.
  • This is the callback-authenticity branch of migration-trust-boundary failure. The old trove and the delegate approval were both real; the forged object was the migration story that said this callback represented a legitimate close-and-reopen flow for that victim.

Three migration-closeout promotions this incident collapsed

  1. A callback-capable migration helper and a delegated trove authority path both existed

    • MigrateTroveZap had real delegated closeout power over approved users, and mkUSD.flashLoan() could really call onFlashLoan().
    • By itself this only meant the ingredients for migration existed; it did not yet prove that any specific callback encoded a legitimate protocol-authored cutover.
  2. Prisma promoted attacker-authored callback data into canonical migration truth for one victim trove

    • onFlashLoan() treated the calldata as if it already proved this owner, this source trove, this destination manager, and this close-and-reopen plan were the intended migration object.
    • That was the decisive authenticity break: the protocol accepted a public flash-loan callback as the authoritative answer to what migration is happening right now?
  3. That accepted migration story still retained live collateral-reallocation and residual-claim authority

    • Once the forged callback was accepted, the zap could close the victim trove, reopen a thinner replacement, strand real wstETH inside the helper, and later route that leftover collateral into an attacker-owned trove.
    • callback accepted and accepted callback may now move and cash out live collateral were separate promotions, and Prisma collapsed them together.

Why the exploit worked

  • Prisma authenticated the wrong thing. BorrowerOperations delegate approval proved only that MigrateTroveZap could act for a user, not that this specific flash-loan callback encoded a legitimate protocol-authored migration for that user’s trove.
  • onFlashLoan() treated attacker-supplied calldata as if it were continuation state from migrateTrove(). Because mkUSD.flashLoan() was publicly callable, the attacker could originate that supposedly trusted migration context directly.
  • The helper also failed to keep collateral removed from the old trove, collateral assigned to the replacement trove, and residual collateral recipient bound to one canonical consumed migration record. That let real victim collateral settle temporarily inside the zap and then get recycled into an attacker-owned trove.
  • The exploit did not require fake debt repayment or oracle corruption. Prisma already exposed real delegated trove-management authority; the bug was that a public callback could impersonate the missing this migration was internally initiated and this residual belongs to the same user proof.
  • This is why the incident is sharper than generic callback reentrancy or delegate approval risk: the protocol let a hostile flash-loan callback originate migration meaning, then let ordinary trove-close / trove-open machinery honor that fabricated meaning.

Detection and audit pivots

  • Treat every migration callback as an auth surface. Ask what exact nonce, session record, or internally stored migration object proves this callback came from a protocol-initiated cutover rather than a user calling the flash-loan primitive directly.
  • Trace one migration record end to end: which trove is being closed, which manager may receive the replacement, how much debt is repaid, how much collateral must be re-posted, and where any residual collateral is allowed to settle. If any one answer stays attacker-shaped in callback data, the migration object is already split.
  • Fuzz direct callback reachability with attacker-crafted flash-loan calldata, especially when the helper already has standing delegate approval over user positions. If direct callback entry can close a victim trove or strand residual collateral in the helper, the cutover surface is live custody logic.
  • Compare collateral withdrawn from victim, collateral assigned to replacement trove, and collateral left in helper per successful migration. Any gap must either revert or be deterministically returned to the same consumed legacy record’s owner.
  • Review leftover-balance handling as first-class custody code. Prisma shows that temporary helper inventory is not harmless transit state when the helper can later spend it into a second attacker-controlled migration.
  • Ask whether delegated approvals outlive the migration window. A zap with standing closeout authority plus a publicly reachable callback is already a high-risk composite surface even before a bug is found.

Failure-prevention checklist

  • Bind every flash-loan or helper callback to an internally created single-use migration record that stores owner, source trove manager, destination trove manager, debt amount, collateral amount, residual-recipient rules, and expiry.
  • Reject any callback that was not initiated by the helper itself in the same protocol-defined migration flow; public flash-loan reachability must not be enough to originate migration meaning.
  • Derive replacement-position collateral and beneficiary from the consumed migration record, not from callback calldata that can quietly downsize the replacement trove or redirect residual balances.
  • Require post-migration reconciliation such that collateral removed from old trove = collateral assigned to replacement + explicitly derived residual returned to the same owner.
  • Keep residual collateral routing deterministic and provenance-bound. Leftover balances should return to the owner of the consumed trove or revert, never sit in the helper as reusable ambient inventory.
  • Scope delegate approvals narrowly in time and function, or replace standing approval with per-migration signed authorization / permit-style consent that expires after one cutover.
  • Add emergency disablement for migration helpers and fast user guidance to revoke helper approvals whenever callback authenticity or residual-routing assumptions become uncertain.

Audit prompts

  • What exact stored object proves that this callback corresponds to a migration the protocol itself initiated, rather than a user calling the flash-loan lender directly?
  • Can delegate approved helper and authenticated migration plan ever become two different facts, with the former true and the latter attacker-authored?
  • If the replacement trove is reopened with less collateral than the old trove surrendered, where is the difference forced to go and who is allowed to claim it?
  • Can a successful callback ever leave economically meaningful balances stranded inside the helper, and if so what prevents a second call from reusing them for another user’s or the attacker’s position?
  • Are source manager, destination manager, trove owner, debt amount, collateral amount, and residual recipient all derived from one canonical migration record, or can callback data silently reshape one of those answers?
  • During incident response, can the protocol disable the helper or invalidate its migration sessions without depending on every user immediately revoking standing delegate approval?
  • If the flash-loan primitive is permissionless, what onchain proof distinguishes a legitimate helper-initiated callback from a counterfeit one?

Lessons that should already be standard

  • A public callback is part of the auth layer if it can carry migration meaning.
  • Standing delegate approval is not proof that any particular migration story is legitimate.
  • Residual balances inside a migration helper are part of the theft surface, not harmless transit dust.
  • We only call this callback from our helper flow is not a control unless the contract proves that claim onchain.
  • Migration helpers should spend one canonical closeout record, not improvise trove owner, replacement collateral, and leftover routing from fresh callback data.
  • Temporary convenience approvals age into live custody authority unless they are tightly scoped and aggressively sunset.

Why this incident matters

  • It is a strong modern example of migration code failing through context spoofing rather than through arbitrary old-contract injection. The protocol did not ask the attacker to supply a fake legacy contract; it let the attacker supply a fake migration story to a callback that was supposed to represent trusted protocol flow.
  • It broadens the migration corpus beyond staking upgrades like STAX into CDP / trove systems where delegate approvals and helper contracts create their own standing authority boundary.
  • It also preserves an important design lesson for any zap or migration helper: if a public primitive like a flash loan can call your settlement callback, then the callback itself is part of your auth layer and must prove why this particular migration is legitimate.
  • The sharper healthy-side boundary is that delegated closeout, replacement-position creation, and leftover-collateral routing all needed to collapse onto one canonical migration-closeout record. Prisma instead let attacker-authored callback data define that record on the fly while live delegate approval supplied the authority.

Internal linkages

  • Core recurring pattern: migration-trust-boundary-failure.
  • Positive-design cutover companion: migration-cutover-authenticity-and-single-record-consumption-discipline — the clean healthy-side answer when the sharper question is how one consumed trove migration record should have kept callback authenticity, collateral identity, replacement ownership, and residual routing fused through the zap.
  • Positive-design helper companion: helper-path-invariant-reuse-and-fail-closed-discipline — the zap should have reused a migration-only state machine that could be entered only from internally initiated flow, not treated a public flash-loan callback as if it were already trusted context.
  • Positive-design entitlement companion: exit-right-consumed-state-binding — migration helpers should spend one canonical closeout / replacement entitlement so close victim trove, reopen replacement trove, and route any residual collateral cannot drift into separate attacker-shaped accounting branches.
  • Adjacent pattern: privileged-message-dispatcher-abuse — both families turn a generic execution path into unauthorized state change, but Prisma’s decisive bug was migration-context authentication rather than a general arbitrary-call executor.
  • Adjacent pattern: approval-by-address-mutable-code-execution — both incidents monetize a standing authority grant, but here the dangerous thing was not code mutability at an approved address; it was the zap’s ability to act on attacker-forged migration instructions once users had delegated it.
  • Sibling migration variant: templedao-stax-migratestake-unvalidated-oldstaking-drain-2022-10-11 — STAX preserves the arbitrary-legacy-source cousin, where the attacker got to choose which contract vouched for migrated stake at all.
  • Sibling migration variant: team-finance-v2-to-v3-migration-lock-identity-mismatch-refund-drain-2022-10-27 — Team Finance preserves the burn-and-recreate identity-mismatch cousin, where authorization, consumed LP position, and refund destination failed to collapse onto one canonical record.

Control surface