Malda Mendi→Malda Migrator Fake-Comptroller Synthetic-Position Drain (2025-05-30)
- Name: Malda Mendi→Malda migrator fake-comptroller synthetic-position drain
- Date: 2025-05-30
- Protocol: Malda (formerly Mendi Finance)
- Chain: Linea
- Category: migration trust-boundary failure / fake legacy-root substitution / synthetic-position mint / migrator-authorized borrow drain
- Loss: about
$285,000 - Summary: Malda’s Mendi→Malda migration helper treated legacy Mendi state as if the migration contract itself had already authenticated it. Malda’s postmortem says the deployed
Migrator.solaccepted the Mendi Comptroller address dynamically instead of pinning one canonical legacy source. The attacker supplied a fake Comptroller plus attacker-controlled market contracts, made the migrator believe they owned migratable Mendi positions, and used the migrator-onlymintOrBorrowMigration()path on Malda markets to create a synthetic position that could borrow real funds. Malda later said the protocol had to remain paused because the attacker still held a fake position onchain that could be used for further extraction. - Trigger / entry point:
- Malda exposed a self-service Mendi→Malda migration path on Linea through
Migrator.sol. - Per Malda’s postmortem, the vulnerable migrator let the Mendi Comptroller address be supplied dynamically rather than hardcoding the one legitimate Mendi Comptroller.
- The migration flow then imported balances from whatever Comptroller / market set it trusted and forwarded those balances into Malda’s privileged migration mint / borrow path.
- Malda’s host-market docs describe
mintOrBorrowMigration()as a migrator-only function that can mint mTokens or borrow during migration without requiring underlying transfer.
- Malda exposed a self-service Mendi→Malda migration path on Linea through
- Root cause:
- The migrator authenticated the wrong legacy truth object. Instead of pinning one immutable Mendi source-of-truth and deriving migrated balances only from that source, it let attacker-shaped legacy infrastructure answer
what Mendi positions does this user have? - Once that fake legacy root was admitted, the migration path became a mint / borrow primitive. The protocol no longer needed a real old-system withdrawal or consumed legacy entitlement before creating new Malda state.
- The privileged
mintOrBorrowMigration()lane widened the blast radius. That path existed specifically to bypass ordinary deposit-before-mint UX during migration, so once the migrator itself trusted counterfeit legacy data, Malda’s own helper authority converted the fake migration into live balance-sheet state.
- The migrator authenticated the wrong legacy truth object. Instead of pinning one immutable Mendi source-of-truth and deriving migrated balances only from that source, it let attacker-shaped legacy infrastructure answer
- Exploit path:
- The attacker deployed or controlled a fake Mendi Comptroller and fake Mendi-style market contracts.
- They invoked the Mendi→Malda migrator with that counterfeit legacy root, causing the migrator to query attacker-authored
getAssetsIn(),balanceOfUnderlying(), and related migration state. - The migrator mapped those purported legacy positions into live Malda markets whose underlyings and allowlist checks it considered acceptable for migration.
- Using migrator authority, the flow reached
mintOrBorrowMigration()on Malda host markets, creating a synthetic Malda position without first proving that canonical Mendi collateral had really been consumed. - The attacker extracted roughly
$285kbefore full pause. Malda’s timeline records an initial exploit transaction at2025-05-30 18:51 UTCand a second malicious transaction at2025-05-30 19:30 UTC. - Even after pause, Malda later said the attacker still held a fake position whose residual borrow power or liquidity effect prevented safe unpause until a recovery plan could be designed.
- What the attacker needed to be true:
- The migration path had to trust legacy Mendi provenance from a dynamically supplied or otherwise attacker-influenced Comptroller source.
- The migrator had to remain registered as an authorized caller on Malda host markets.
mintOrBorrowMigration()had to create Malda mint / borrow state on the strength of the migration helper alone instead of requiring normal deposit and borrow checks tied to canonical old-state consumption.- The attacker needed the fake legacy positions to resolve into allowed Malda markets by underlying identity and market allowlist.
- Incident response needed to be slower than the attacker’s ability to replay the migration path at least once; Malda’s own timeline shows two exploit transactions before the full protocol pause landed.
- Technical anatomy worth remembering:
- This was not a generic oracle incident or a core zk-verifier failure. Malda’s own postmortem says the exploit was isolated to the migrator contract.
- The sharper mistake is migration-authentication collapse:
legacy source exists,legacy source is canonical Mendi truth, andtherefore Malda may mint or borrow against itgot flattened into one helper success path. - Malda’s own docs make the helper boundary explicit:
mintOrBorrowMigration()is a special migrator-only lane designed to mint or borrow during migration without the usual underlying transfer precondition. That can be safe only if the migrator has already proven old-state authenticity. - The recovery note adds an operationally important detail: the pause persisted because the attacker still held a fake position on Malda. That is a strong clue the synthetic migration did not merely steal one withdrawal; it published continuing false solvency state inside the new system.
- Malda’s postmortem says the long-term fix is to hardcode both the Mendi Comptroller and Malda operator addresses. The current public
Migrator.solsource does exactly that, which is a clean artifact of how the trust boundary was tightened after the incident.
- Representative transactions and artifacts:
- First exploit tx:
0xd62a3d483b89e38b681777804b286dec682919891924c2b13c566dfaad666ed3 - Second exploit tx:
0x9f12f7b982ffbd90ac5944b3ab8520f7fb5a9882a0a9acf20d63f6922950e59a - Full protocol pause tx:
0xa9d5c00aee3be937ceada32181f0f02b9ac3d9be9b2b8c008213ca53114257fd - Attacker destination wallet called out by Malda:
0x370a8Db1F020CE70E8eAB2502c739844Ca2C2910 - Funding tx Malda highlighted from ChangeNow:
0xaccb4b73fed508958ec8b913e8c4dabd125c15d7800d5b8c63466910df8a5c77 - Addresses Malda later identified as still holding fake protocol positions:
0xb1e2c543035dc0ca845f91aec68f8a891ea5d74fand0x6af48ed89d4e2693c5ef6017413c6465009f8004 - Current public migration source showing hardened constants:
MENDI_COMPTROLLERand immutableMALDA_OPERATORinsrc/migration/Migrator.sol - Current public host-market docs describing
mintOrBorrowMigration():src/mToken/host/mErc20Host.sol
- First exploit tx:
- Response / mitigation:
- Hypernative detected the exploit and attempted automated pause actions, but Malda’s postmortem says two operations failed and the team initiated a manual pause.
- Malda fully paused the protocol on Linea on
2025-05-30. - Malda publicly stated the long-term remediation is to hardcode the Mendi Comptroller and Malda operator addresses rather than accepting migration trust anchors dynamically.
- The protocol remained paused because the attacker still retained a fake position that could be used to keep extracting funds if ordinary operations resumed.
- Malda later proposed a recovery plan and pro-rata distribution because the migration exploit had left both stolen funds and poisoned residual solvency state to unwind.
Pattern tags
- migration-trust-boundary-failure
- migration-cutover-authenticity-and-single-record-consumption-discipline
- helper-path-invariant-reuse-and-fail-closed-discipline
- The migrator let attacker-shaped legacy infrastructure answer
what exact Mendi positions exist for this user, so the new system’s first trust boundary was already counterfeit before any mint or borrow occurred. - Malda’s migration helper combined two dangerous privileges: authority to recognize legacy balances and authority to invoke a mint / borrow path that explicitly skips ordinary underlying-transfer assumptions. Once the first privilege was fooled, the second privilege monetized it.
- The lingering fake position mattered operationally as much as the initial theft. The exploit did not just pull funds out once; it left forged state inside the new lending system that still had to be quarantined before any safe reopen.
Why the exploit worked
- Malda’s postmortem preserves the decisive mistake cleanly: the migrator trusted a dynamically provided Mendi Comptroller instead of one hardcoded canonical legacy root. That means the contract asked the attacker to tell it where migration truth lived.
- The migration path then promoted counterfeit legacy reads into money-good Malda state. In a healthy cutover, old-state provenance, one-time legacy-right consumption, successor-position creation, and any helper-only mint / borrow privileges all stay fused. Here they split apart at the very first question.
- The helper boundary made the exploit more severe.
mintOrBorrowMigration()exists precisely because migration code needs a special path that does not look like an ordinary user deposit. But the moment migration authenticity fails, that convenience path becomes an issuance primitive. - Malda’s later statement that the attacker still held a fake position is the strongest downstream symptom: the protocol had not merely paid out against a bad migration story; it had accepted that bad story into its own continuing solvency state.
- This is why the incident belongs in the lending / migration branch rather than only the approvals or admin branch. The real damage came from counterfeit legacy-state admission turning into live collateral / borrow authority inside the successor lending system.
Detection and audit pivots
- Treat migration roots like oracle roots. Ask which exact old-system contract, registry, or state object is allowed to answer
what value may be imported, and whether that source is immutable, tightly allowlisted, or merely user-supplied. - Review every helper-only mint / borrow / claim path that skips ordinary deposit or transfer assumptions. Those lanes are only safe if upstream migration authenticity is stronger than normal, not weaker.
- Trace whether the successor system can name one consumed legacy record per imported position. If the answer is only
the helper said this user had some old position, the migration object is probably already counterfeit. - Ask what happens after partial incident response. If a fake migrated position can remain live after theft, the protocol needs a plan for residual solvency poisoning, not just stolen-asset accounting.
- Compare current hardened source against postmortem language. If remediation involves hardcoding trust anchors that used to be dynamic, the original exploit probably lived in legacy-root admission rather than in downstream math.
- Monitor migration-specific impossible states: new positions created without matching canonical old-state burn / withdraw events, migration-only debt appearing without canonical cutover records, or accounts whose solvency persists after migration should have been impossible.
Failure-prevention checklist
- Hardcode or tightly allowlist the exact legacy Comptroller, market registry, adapter, or verifier that may certify old-system balances; do not let user calldata or helper-controlled parameters decide where migration truth comes from.
- Bind every migration-created position to one canonical consumed legacy record and make the successor system able to prove that record really belonged to the migrating user.
- Keep helper-only mint / borrow lanes behind stronger evidence than ordinary user paths, not weaker evidence. If a function can skip
underlying transferbecause it trusts a migration helper, then that helper must already have proven canonical old-state consumption. - Reconcile successor solvency against legacy consumption in real time: no new collateral, debt, or claim should appear unless one canonical old-system right was simultaneously retired or proven unavailable for reuse.
- Add a kill path that does more than pause new calls. Migration incidents can leave synthetic positions live inside the system, so responders need a plan for freezing or quarantining residual successor-state authority too.
- Treat recovery from migration exploits as a balance-sheet problem, not just a treasury-loss problem. Fake positions, bad debt, and poisoned liquidity state may still exist after the first theft transaction ends.
- Review helper source after the fact to ensure postmortem remediations actually collapse dynamic trust anchors into immutable or tightly governed ones.
Audit prompts
- What exact contract or registry is allowed to answer
which old positions does this user have, and can the user influence that answer directly or indirectly? - If the migration path can mint or borrow without first receiving underlying, what exact proof replaces the missing transfer and why is it canonical?
- Can the successor protocol prove one old-system entitlement was actually consumed for every new-system position it creates?
- If a migration exploit lands, could the attacker still hold a fake live position after the first draining transactions, and what onchain mechanism would neutralize it?
- Are legacy-source identity, migrated amount, beneficiary, and successor market selection all derived from one authenticated old-state object, or can helper inputs quietly substitute one of those answers?
- Does pause logic stop only fresh entry, or can already-published fake successor positions still keep borrowing power or liquidity rights during the pause?
- Which helper-only roles or pathways bypass normal mint / borrow guardrails, and what upstream assumptions are those bypasses depending on?
Lessons that should already be standard
- Migration code is lending code when it can publish new collateral or debt state.
Helper-onlyis not a safety argument. A privileged migration lane is exactly where old-state authenticity has to be strongest.- Legacy-root identity is the first security object in a migration. If the protocol cannot prove which old-system contract is authoritative, every later accounting check is downstream of a lie.
- Pause plans for migration incidents must include residual fake-state containment, not just
stop the next call. - A postmortem that ends with
we will hardcode the trusted source nowis usually describing an auth bug, not a math bug. - Successor protocols should never have to guess whether a migrated position is real after the fact. The cutover should already have produced one canonical consumed-state proof per new position.
Why this incident matters
- It is a crisp 2025 example of migration-trust-boundary failure inside a lending protocol rather than a staking or vesting migration. The successor system did not merely recreate a UI entitlement; it published live solvency state.
- It shows how dangerous helper-only mint / borrow lanes are when they depend on imported legacy truth. The same convenience that makes migration smoother can also convert one fake legacy root into immediate protocol loss.
- It preserves an operational lesson many exploit writeups skip: false successor-state can survive the initial theft. Malda explicitly said the attacker still held a fake position weeks later, which kept the protocol paused and forced recovery planning around poisoned state, not just missing assets.
- It also pairs cleanly with STAX, Team Finance, and Prisma as a fourth migration archetype: counterfeit legacy root-of-truth substitution.
Internal linkages
- Core recurring pattern: migration-trust-boundary-failure.
- Positive-design cutover companion: migration-cutover-authenticity-and-single-record-consumption-discipline — the missing object was a single canonical legacy-consumption record that should have fused legacy source identity, migrated amount, beneficiary, successor market, and one-time use.
- Positive-design helper companion: helper-path-invariant-reuse-and-fail-closed-discipline — Malda’s special migration mint / borrow lane should have failed closed the moment legacy-source authenticity was uncertain rather than honoring helper-only privilege on attacker-shaped data.
- Positive-design entitlement companion: exit-right-consumed-state-binding — successor positions should have become cashable only after one canonical old-system right was actually consumed.
- Sibling migration variant: Prisma Finance migrateTroveZap — Prisma preserves the spoofed-callback migration cousin where the old system was real but the migration story was counterfeit.
- Sibling migration variant: Team Finance V2→V3 — Team Finance preserves the legacy-position identity mismatch cousin.
- Sibling migration variant: STAX — STAX preserves the arbitrary old-contract injection cousin.
Control surface
-
The decisive leverage sat in the migration helper’s root-of-truth choice: which Comptroller and market set it accepted as canonical Mendi state, how that state was translated into allowed Malda markets, and what privileged mint / borrow powers the migrator could exercise once the state looked valid.
-
More concretely, the control surface was not
can the migrator move tokens; it waswho is allowed to define migratable legacy solvency at all. Once the attacker answered that question, Malda’s own migration privileges did the rest. -
The follow-on risk sat in residual fake successor state. Even after theft, the protocol still had to reason about whether attacker-authored migration output remained live inside account-liquidity calculations.
-
Sources:
- https://paragraph.com/@malda-2/may-30th-incident-post-mortem
- https://paragraph.com/@malda-2/the-road-to-recovery
- https://raw.githubusercontent.com/malda-protocol/malda-lending/main/src/migration/Migrator.sol
- https://malda-protocol.github.io/malda-lending/src/mToken/host/mErc20Host.sol/contract.mErc20Host.html
- https://lineascan.build/tx/0xd62a3d483b89e38b681777804b286dec682919891924c2b13c566dfaad666ed3
- https://lineascan.build/tx/0x9f12f7b982ffbd90ac5944b3ab8520f7fb5a9882a0a9acf20d63f6922950e59a
- https://lineascan.build/tx/0xa9d5c00aee3be937ceada32181f0f02b9ac3d9be9b2b8c008213ca53114257fd
- https://lineascan.build/address/0x05bD298c0C3F34B541B42F867BAF6707911BE437#readProxyContract
-
Last reviewed: 2026-07-31 UTC