Migration Trust-Boundary Failure / Unvalidated Legacy-Contract Migration
- Name: migration trust-boundary failure / unvalidated legacy-contract migration
- Category: exploit pattern / migration-path failure / legacy-balance authentication failure / transition-state trust failure
- Summary: Protocol migrations often try to make upgrades feel seamless: point the new contract at the old one, let users self-migrate, and carry balances or entitlements forward automatically. The danger is that migration code is authenticating value, not merely moving UX state. If the new system lets users choose the old contract address, dynamically supply the legacy root contract that defines old-state truth, declare the amount being migrated, or otherwise provide the proof of legacy entitlement without a hard allowlist and balance check, the migration path becomes a mint primitive. The same family also appears when a helper settles migration state from an attacker-reachable callback and assumes that callback could only have come from the protocol’s intended flow, or when a helper-only mint / borrow lane skips ordinary
underlying transferchecks because it believes migration authenticity was already proven upstream. The protocol is not drained because the old contract actually released value; it is drained because the new contract credits value after trusting an attacker-shaped story about what the old contract, helper, callback, or legacy root supposedly proved. - What it is:
- A new contract needs to import balances, stakes, vesting rights, or some other claim from a retiring contract.
- The migration path reaches out to an
oldcontract, helper, or adapter to withdraw or attest to the legacy position. - The new contract then credits internal balance, shares, stake, or claimable assets in the replacement system.
- The dangerous mistake is treating a caller-supplied migration source or amount as if it were canonical protocol truth.
- Once that happens, the new system can create redeemable value even when no real legacy balance was burned, withdrawn, or proven.
- Typical preconditions:
- The protocol is running a live migration, wrapper replacement, or staking-contract upgrade.
- The new contract exposes a self-service migration function callable by ordinary users.
- The migration function accepts an
oldContract, adapter, or claimed amount from user input rather than deriving it from immutable configuration. - The new contract credits value after an external migration callback without independently proving that the legacy side actually decreased by the same amount.
- The new contract already custodies real assets, or its internal accounting can later be redeemed for real assets.
- Common vulnerable surfaces:
- staking migrations that let users pass an arbitrary
oldStakingcontract - liquidity-lock or LP-migration helpers that check one lock ID or beneficiary record but burn and refund a different underlying position
- vault or receipt-token upgrades that trust caller-supplied legacy share amounts
- vesting / rewards migrations that recreate entitlements before consumed-state is proven on the old contract
- wrapper upgrades that assume a helper contract returning successfully means real assets moved
- transition helpers where
old system says yesis modeled as a low-level call rather than a hard-authenticated accounting event - migration zaps or callback-based helpers that settle user balances from flash-loan or adapter callback data without proving that the callback encodes a protocol-authored migration
- staking migrations that let users pass an arbitrary
- Exploit sequence:
- The attacker identifies a migration entry point in the new contract that accepts a legacy source address, amount, or proof helper from user input.
- They deploy or control a fake legacy contract, or otherwise supply migration parameters untethered from canonical old-state accounting.
- The new contract calls into that fake source and receives a success path or harmless no-op.
- The migration logic then credits stake, shares, debt capacity, or withdrawal rights in the new system as if real legacy value had arrived.
- The attacker immediately redeems or withdraws against the newly created balance.
- The protocol loses real assets because the migration path minted internal entitlement before authenticating that old entitlement had actually been consumed.
- Variants:
- Arbitrary old-contract injection — the user chooses which legacy contract the new system calls.
- Counterfeit legacy-root substitution — the user or helper can point the migration flow at an attacker-controlled comptroller, registry, or other contract that defines what old positions supposedly exist, and downstream migrator-only privilege converts that fake legacy state into live successor solvency, as in Malda.
- Claimed-amount trust — the user supplies the migrated amount and the new system does not independently verify old-state burn / withdrawal.
- Legacy-position identity mismatch — the migration auth checks are satisfied by one user-controlled position, lock, or NFT while the assets actually burned, unlocked, or refunded come from another position already held in protocol custody.
- No-op migration callback — success of an external call is treated as economic proof even though the callee moved no assets.
- Spoofed migration context — a public callback path such as a flash-loan receiver or adapter hook can be invoked with attacker-crafted migration data, and the helper mistakes that callback for trusted internal flow.
- Transition-helper overreach — a migration adapter intended to ease UX quietly becomes the root authority for creating balances in the new system.
- Runbook-only safety — the protocol assumes users will migrate only from the one intended contract because the frontend or docs say so, but the contract itself never enforces that assumption.
Three migration-authority promotions reviewers should not collapse
-
Some legacy-looking source, helper, or callback object exists at all
- A retiring staking contract, a trove-migration zap, a lock-migration helper, or a flash-loan callback can be reached.
- This is only a reachability claim. It does not yet mean the object is the one canonical certifier of migrated value.
-
The new system promotes that object into canonical migration truth for this exact cutover
- The successor contract decides that this legacy address, this callback payload, this consumed lock, or this helper-returned amount is the authoritative answer to
what old value is being retired and recreated right now? - This is the decisive authentication step, and it is stronger than mere callback reachability or legacy-contract existence.
- The successor contract decides that this legacy address, this callback payload, this consumed lock, or this helper-returned amount is the authoritative answer to
-
That accepted migration truth still deserves live replacement-credit, refund, or withdrawal authority
- Once the protocol has accepted one migration story, it may still mint a replacement position, pay a residual refund, reopen debt capacity, or let the result cash out through ordinary exits.
- This is a separate promotion again:
migration object acceptedis not yet the same thing asthat accepted object deserves immediate balance-sheet authority over live assets.
Healthy review keeps these three questions separate instead of flattening them into one vague migration helper was callable story.
- Detection heuristics:
- Inventory every migration and upgrade helper as custody-critical code, not operational glue.
- Ask whether the new contract hardcodes or immutably stores the only acceptable legacy source, or whether callers choose it.
- Compare when the new system credits value versus when the old system demonstrably burns, transfers, or invalidates the legacy claim.
- Treat any migration callback that can be satisfied by a user-controlled contract as a red alert.
- Test with a dummy old contract whose migration function does nothing but return cleanly; if the new system still credits value, the migration path is broken.
- For burn-and-recreate migrations, trace whether
legacy record consulted,asset actually burned,beneficiary credited, andrefund recipient paidall collapse to one canonical stored record. If any one of those pivots stays caller-shaped, the migration path is still authenticating a story rather than a fact.
- Defenses:
- Make the legacy source immutable or tightly allowlisted in the new contract.
- Derive migrated value from verified old-state reads or one-time burn / transfer proofs rather than from caller-supplied amounts.
- Credit value only after the old system has irreversibly transferred assets or consumed the entitlement being migrated.
- Treat migration helpers as part of the balance-sheet boundary; they deserve the same review standard as withdraw and mint logic.
- Add transition invariants asserting that total redeemable value after migration cannot exceed legacy value actually withdrawn or burned.
- Bind migration authorization to the exact legacy asset identity being consumed: lock ID, LP token address, beneficiary, reserve pair, and refund recipient should all resolve to one canonical record rather than partially user-supplied context.
- In burn-and-recreate flows, derive the replacement position owner, replacement asset identity, and any residual refund destination from that same canonical legacy record rather than from adjacent calldata fields.
- Treat leftover-token refund logic as part of the solvency boundary whenever a migration burns one representation and mints another;
residualshould never become a free payout rail for attacker-shaped migrations.
- Cutover invariants worth testing:
- The specific legacy record that authorizes migration is the same record whose assets are withdrawn, burned, unlocked, or closed.
new position value created + residual value refunded <= value actually consumed from that exact legacy record.- Replacement-position beneficiary and refund recipient are both derived from the stored owner / beneficiary of the consumed legacy record, not from attacker-supplied migration context.
- A successful callback is never enough by itself; the cutover must also prove one-time consumption of the legacy entitlement.
- High-signal incidents:
- [[../../../incidents/lending-liquidations/templedao-stax-migratestake-unvalidated-oldstaking-drain-2022-10-11|TempleDAO / STAX
migrateStake()unvalidated oldStaking drain (2022-10-11)]] — the clean arbitrary-source exemplar: STAX let users pointmigrateStake()at an arbitraryoldStakingcontract and choose anyamount, then credited that amount in the new staking contract after a no-op callback and allowed immediate withdrawal of the real LP tokens already sitting in custody. - [[../../../incidents/lending-liquidations/prisma-finance-migratetrovezap-onflashloan-spoofed-migration-collateral-theft-2024-03-28|Prisma Finance
MigrateTroveZaponFlashLoan()spoofed-migration collateral theft (2024-03-28)]] — the callback-authenticity variant: a publicmkUSD.flashLoan()could invoke Prisma’s migration zap with attacker-crafted calldata, and because users had pre-delegated trove control to the zap, spoofed migration instructions could strip collateral from victim troves and recycle the remainder into attacker-owned positions. - Malda Mendi→Malda migrator fake-Comptroller synthetic-position drain (2025-05-30) — the counterfeit legacy-root variant: the migrator accepted a dynamically supplied Mendi Comptroller, queried attacker-authored legacy markets, and then used a migrator-only
mintOrBorrowMigration()lane to publish synthetic successor solvency without canonical old-state consumption. - refund drain (2022-10-27) — the legacy-position identity mismatch variant: attacker-created dummy locks satisfied Team Finance’s migration checks while the contract actually burned and re-homed unrelated victim LP positions, then paid the leftover migration refunds to the attacker-controlled withdraw address.
- [[../../../incidents/lending-liquidations/templedao-stax-migratestake-unvalidated-oldstaking-drain-2022-10-11|TempleDAO / STAX
What sound design looks like
- A migration should consume one canonical legacy entitlement and create at most one bounded replacement entitlement plus explicitly derived residuals. The old lock, trove, stake record, or receipt should be the same object that authorizes the migration, defines the migrated amount, determines the replacement beneficiary, and controls any refund destination. Migration cutover authenticity and single-record consumption discipline is the cleaner positive-design note when the sharper question is how to keep one legacy record authoritative all the way through the cutover.
- Healthy systems pin legacy sources and helper identities in code rather than in frontend assumptions, docs, or operator runbooks. Users should never get to choose which contract, callback, or adapter certifies old-state truth.
- The retiring system should lose authority as the successor gains it. A legacy staking contract, vesting rail, trove manager, or lock registry should not remain a generally accepted certifier of money-good balance once the new system is live; migration compatibility should narrow toward one pinned, expiring, monitorable bridge instead of leaving the old surface ambiently authoritative. Decommissioning authority-extinguishment discipline is the lifecycle-side companion when the sharper question is how to make
legacystop meaningstill trusted if calldata points there. - New-system credit should appear only after the old-system right has been irreversibly consumed, withdrawn, burned, or otherwise proven unavailable for reuse. Exit right consumed-state binding is the positive-design companion for preserving that
one consumed right, one replacement payout pathboundary. - Migration helpers should reuse the same internal balance-sheet invariants as ordinary withdraw / redeem / lock-consumption paths, then fail closed when architecture changes make old assumptions uncertain. Helper-path invariant reuse and fail-closed discipline is the positive-design companion for that
migration helper is not exempt from core accounting truthrule. - Ordinary exits should still demand provenance from a legitimately consumed legacy right rather than cashing out whatever balance a migration helper happened to write. no ambient internal balance trust is the downstream companion for that
migration-created balance must still prove why it is money-goodboundary. - Public callbacks, flash-loan hooks, router hooks, and adapter callbacks should carry migration data only as authenticated continuations of protocol-authored state, not as fresh authority origination points.
Failure-prevention checklist
- Pin the only acceptable legacy contracts, helper modules, adapters, and callback senders in immutable or tightly governed storage; do not accept them from user calldata.
- Decommission legacy authority on purpose. Once migration starts, the old system should lose any broader ability to certify fresh value except through the one pinned cutover path that the successor still explicitly defends and monitors.
- Derive migrated amount, asset identity, and beneficiary from one canonical legacy record or one-time consumption proof, not from caller-supplied amounts or adjacent calldata fields.
- Make the old entitlement unavailable before the new entitlement becomes cashable: burn first, mark consumed first, or prove one-time withdrawal first.
- Bind authorization, asset consumption, replacement-position creation, and refund routing to the same stored legacy record. If those four questions can resolve to different identities, the migration path is already split.
- Treat public callbacks as hostile by default. Require nonce- or session-bound evidence that the callback corresponds to a migration the protocol itself initiated.
- Test dummy legacy contracts, direct flash-loan callback entry, mismatched lock IDs, forged beneficiary fields, forged refund recipients, and helper returns that succeed without moving value.
- Make residual refund logic prove where leftovers came from and who is entitled to them.
Unused amountsare part of custody logic, not harmless convenience plumbing. - Scope or expire delegated approvals, migration permissions, and one-off helper authorities aggressively once the cutover completes. Migration convenience should not remain live balance-sheet authority indefinitely.
- Add reconciliation invariants that total value created in the new system never exceeds value consumed from the old system for the same migration cohort and for each individual migrated record.
- Put easy kill switches around migration helpers during rollout. Cutovers are temporary high-risk windows; the safest helper is one that can be disabled quickly once migration demand falls.
Audit prompts
- What exact legacy record authorizes this migration, and is it the same record whose value is actually withdrawn, burned, unlocked, or closed?
- Can the user choose any part of the certifying authority surface: old contract address, adapter, callback sender, helper module, claimed amount, or refund recipient?
- What exact event, storage mutation, or proof shows the old entitlement became unavailable before the new entitlement became redeemable?
- If a dummy legacy contract returns success without moving assets, what line of code prevents the new system from crediting value anyway?
- If a public flash loan, router callback, or generic hook can reach the migration helper directly, what authenticates that the encoded migration plan came from an internally initiated flow rather than the attacker?
- Are replacement beneficiary, migrated asset identity, and refund destination all derived from the consumed legacy record, or can adjacent calldata quietly redirect one of them?
- Can one user-controlled legacy record satisfy the authorization checks while a different victim-owned position is the thing actually burned, migrated, or refunded?
- After migration, can the same legacy entitlement still be exercised through an ordinary exit, duplicate migration, cancel path, or other sibling branch?
- Which standing approvals, delegate rights, or pre-approved helpers become dangerous once migration context authenticity fails?
- If the system were paused immediately after
new credit createdbut beforeresidual refund paid, would accounting still reconcile cleanly per migrated record?
Lessons that should already be standard
- Migration code is not UX glue. It is mint code, withdraw code, and custody code wearing operational language.
The old and new systems are both oursis not an authentication argument. If the contract lets users pick the certifying source or forge the callback context, ownership of both systems does not matter.- The legacy position identity has to stay singular all the way through the cutover. Authorization record, burned record, replacement beneficiary, and refund recipient cannot be allowed to drift into separate attacker-shaped answers.
- Public callbacks are authority surfaces when they can carry migration meaning. A flash-loan or adapter hook that can reach settlement logic directly is part of the auth layer whether designers intended that or not.
- Residual refunds are part of the theft path in migration exploits more often than teams expect.
Leftoveris a balance-sheet category, not a miscellaneous output bucket. - Legacy-root identity is the first security object in a migration. If the successor cannot prove which old comptroller, staking contract, lock registry, or receipt system is authoritative, every later balance check is downstream of a counterfeit source of truth.
- Standing migration approvals and delegated helper permissions deserve sunset plans. Temporary convenience authority ages into latent exploit leverage.
- A migration that cannot prove
old right consumedbeforenew right creditedis not a migration yet; it is an issuance path with sentimental branding.
Where this pattern stops
- Use unprotected-initializer-reinitialization when the decisive failure is transition-time setup or reinitialization of authority/state without any imported legacy-value authentication story.
- Use solvency-check-bypass or entrypoint-only-policy-off-path-state-mutation when the sharper issue is a helper mutating lending state off the canonical path but not specifically authenticating value from a retiring legacy position.
- Use exit-right consumed-state binding when the imported right was legitimate, but two sibling payout branches later disagreed about whether that right had already been spent.
- Use exit-right provenance binding when the main question is why an internal balance, note, or entitlement was cashable at all, rather than how a migration helper initially authenticated the imported value.
- In short: this pattern begins when cutover code decides what legacy value is real and transferable into the new system, and it stops once the sharper story is instead about generic helper-path mutation, transition-time authority setup, or later replay of an already legitimate right.
Internal linkages
- Positive-design cutover companion: migration-cutover-authenticity-and-single-record-consumption-discipline — the cleanest healthy-side answer when the sharper question is how one consumed legacy record should keep source identity, amount, beneficiary, callback authenticity, and residual routing fused through the successor cutover.
- Lifecycle / decommissioning companion: decommissioning-authority-extinguishment-discipline — useful when the sharper question is how a retiring legacy staking, vesting, or lock surface should stop being ambiently trusted once migration support begins.
- Positive-design helper companion: helper-path-invariant-reuse-and-fail-closed-discipline — migration wrappers should route through the same economic-finality core as ordinary consume / withdraw paths or fail closed until requalified.
- Positive-design payout companion: exit-right-consumed-state-binding — the safest migration shape keeps one legacy entitlement bound to one consumed-state object before any replacement value or residual refund exists.
- Downstream cashability companion: exit-right-provenance-binding — even after migration, ordinary exits should still require provenance from a genuinely consumed legacy right.
- Adjacent pattern: unprotected-initializer-reinitialization — both families live in transition code that developers often treat as operational setup rather than as attacker-facing balance-sheet logic.
- Adjacent pattern: fresh-market-initialization-failure — both can be introduced by launch or cutover runbooks, but this family is specifically about authenticating migrated legacy value, not about seed-liquidity and collateral quarantine.
- Best arbitrary-source case study: templedao-stax-migratestake-unvalidated-oldstaking-drain-2022-10-11.
- Best callback-authenticity case study: prisma-finance-migratetrovezap-onflashloan-spoofed-migration-collateral-theft-2024-03-28.
- Best counterfeit legacy-root case study: malda-mendi-to-malda-migrator-fake-comptroller-synthetic-position-drain-2025-05-30.
- Best legacy-position identity mismatch case study: team-finance-v2-to-v3-migration-lock-identity-mismatch-refund-drain-2022-10-27.
Control surface
-
The leverage sits in migration entry points, legacy-source allowlists, claimed-amount semantics, callback-authentication boundaries, delegated helper authority, residual-refund routing, and the exact moment the new system decides
old entitlement has been consumed, so new entitlement may exist. -
The safe mental model is harsh but useful: migration code is mint code wearing ops clothing.
-
Sources:
- https://sourcify.dev/server/repository/contracts/full_match/1/0xd2869042e12a3506100af1d192b5b04d65137941/sources/contracts/StaxLPStaking.sol
- https://faraz.faith/2022-11-15-templedao-exploit-remastered/
- https://blog.solidityscan.com/temple-dao-hack-analysis-c96db856322c/
- https://hackmd.io/@PrismaRisk/PostMortem0328
- https://www.certik.com/blog/prisma-finance-incident-analysis
- https://paragraph.com/@malda-2/may-30th-incident-post-mortem
- https://malda-protocol.github.io/malda-lending/src/mToken/host/mErc20Host.sol/contract.mErc20Host.html
- https://rekt.news/teamfinance-rekt
- https://slowmist.medium.com/analysis-review-of-team-finance-exploit-f439c2f63e2
-
Last reviewed: 2026-08-02 UTC