
On March 28, 2024, Prisma Finance was exploited for over $11.6M through a vulnerability in its MigrateTroveZap contract, code designed to help users migrate troves during a system upgrade. The attack wasn’t complex. It was a straight-line path built on assumptions that should never exist in permissionless systems.
The exploiters bypassed the intended migration flow and directly invoked the flashloan entry point. Because the callback function onFlashLoan() blindly trusted the calldata and lacked origin validation, attackers were able to spoof migration logic, hijack troves, and extract collateral.
In total, three attacker addresses executed variations of the exploit, draining over 3,400 ETH worth of assets. The incident forced Prisma to pause protocol operations, cut total TVL nearly in half, and trigger an emergency response from the team and security partners.
Later, the lead exploiter sent an on-chain message claiming to be a whitehat and expressed intent to return funds. But the architecture mistake remains.
This wasn’t a coding error. It was a systems-level failure to treat trust boundaries seriously, and a prime case study in how “helper” contracts often become critical attack surfaces.
Exploited Contract
0xcC7218100da61441905e0c327749972e3CBee9EE
Exploit Transaction
0x00c503b595946bccaea3d58025b5f9b3726177bbdc9674e634244135282116c7
Prisma’s MigrateTroveZap contract was built to automate trove migration across manager contracts. It offered a one-click path for users to close their existing trove, take out a flashloan to cover debt, and reopen an equivalent position in a new manager — all without touching collateral manually.

That convenience came with a critical flaw. The system assumed onFlashLoan() would only ever be called by its own internal flow. But Ethereum doesn’t enforce call context. Anyone could call flashLoan() directly and inject data that the Zap contract would trust blindly.
Here’s how the attack unfolded.
The attacker calls mkUSD.flashLoan() with a large borrow amount corresponding to an existing user’s trove debt. They set the receiver address to MigrateTroveZap and attach spoofed calldata mimicking a legitimate migration.
The flashLoan() function sends the loaned amount and triggers the onFlashLoan() function on the receiver. Prisma’s Zap contract receives the call and executes it without checking whether this came from its own migrateTrove() logic.
The attacker crafts calldata that instructs MigrateTroveZap to:
Victim’s new trove is reopened with:
With the excess collateral sitting in the Zap contract and unclaimed, the attacker creates a dummy trove under their own address. They then initiate a second fake migration, this time assigning themselves the full leftover wstETH as the collateral for the new trove.
After creating the new position, the attacker immediately closes the trove and walks away with the 1,282.79 wstETH.
The attacker repays the flashloan and routes stolen funds to secondary addresses. Some of the funds were funneled through Tornado Cash. Others were redistributed to wallets that later claimed whitehat intent.
The Prisma exploit was not the result of a novel bug. It was the result of classic architectural negligence: trusting calldata in a high-privilege context without enforcing call integrity.
The MigrateTroveZap contract’s onFlashLoan() function decoded calldata and executed privileged operations — closeTrove() and openTrove() — with no guardrails. It assumed the flashloan could only be initiated by the contract’s internal migrateTrove() function. That assumption was false.
Here’s where Prisma went wrong:
flashLoan() is a public function. Anyone could call it and set the receiver to MigrateTroveZap. The contract accepted this call without confirming whether it came from a trusted internal flow.
The onFlashLoan() function directly decoded attacker-supplied bytes into actionable parameters: the user address, trove managers, fee slippage, collateral amount, and hint addresses. There were no checks on whether the account owned the trove, whether the migration was internally initiated, or whether the collateral amount made sense.
There were no require statements to assert that:
As a result, the function was effectively a backdoor. Any attacker who could call flashLoan() and spoof calldata could perform arbitrary trove operations on any user that had approved the Zap contract.
The Prisma hack was not a missed require() or a typo in arithmetic. It was a failure in system design. Every line executed as written. That’s the problem.
The architecture trusted that onFlashLoan() would only be called via a specific flow — initiated by migrateTrove(), triggered through flashLoan(), and executed in a controlled path. But Ethereum doesn’t enforce internal vs external origin. If a function can be called, it will be.
This is where most “audited” contracts still fail: they assume control flow instead of enforcing it.
Prisma’s developers assumed the only path to onFlashLoan() would be via the intended helper call. But the flashLoan() function was callable by anyone. That meant any attacker could push arbitrary bytes into the Zap contract and trigger collateral-moving logic.
The bug wasn’t that validation was missing. It’s that the entire design hinged on implicit trust in how the system would be used.
This wasn’t a buried logic bug. It was cleanly written, clearly documented functionality. Which is exactly why it passed audits. The auditors reviewed code paths under assumed intent, not under malicious conditions. This is a failure of threat modeling, not of code review.
If your contract:
Then you have built an attacker interface.
That is exactly what happened here.
At the heart of this exploit was a security model that delegated control without constraining execution. Prisma’s contracts combined delegated approvals, flashloan logic, and calldata execution into a system that implicitly trusted users and ignored enforcement.
This created multiple privilege escalation paths.
Users gave MigrateTroveZap blanket approval via setDelegateApproval() to operate on their Troves. That approval was:
This meant the Zap contract could perform closeTrove() and openTrove() on behalf of any user who opted into the migration flow — at any time, for any reason.
The Zap contract did not read trove state from storage. Instead, it relied on externally passed calldata to dictate:
This gave attackers full control over both who was targeted and what value was stolen.
The flashloan flow allowed the attacker to:
At no point did the system enforce that the initiator of the migration was also the owner of the trove being acted on. That’s a complete breakdown of access control.
The Zap contract temporarily held unclaimed collateral during migration. But there was no mechanism to reconcile or return this value if the migration was spoofed. This made the Zap contract a honeypot for leftover collateral — available to whoever claimed it next.
That is not an implementation bug. It is a design failure to define custody, authorization, and cleanup in a multi-step, high-value flow.
The Prisma exploit did not require a novel fix. The safeguards needed were standard practice in any system handling delegated authority and external callbacks. Here’s what would have stopped it cold.
The Zap contract should have enforced that onFlashLoan() could only be executed as part of an internally initiated migrateTrove() flow. That means checking:
No trusted call path should exist without explicit checks. Never assume intent from a function call alone.
The Zap contract should have validated:
Without this, the attacker was able to pass arbitrarily forged values into a function that directly moved funds.
The contract should have used a nonReentrant modifier on any function that handles flashloan callbacks. Even if not directly reentered, this would have added a layer of defense against unexpected execution paths.
Stateful execution context should have been set during the migrateTrove() call, then checked during the callback. For example:
This pattern enforces that internal callbacks only happen during valid flows.
The contract should have never left unclaimed collateral sitting in its own balance. At minimum:
The residual wstETH was not protected, which made the second half of the exploit trivial.
These are not expensive solutions. They are table stakes for any system that handles user assets through intermediated logic.
This exploit type sits in a blind spot between logic correctness and access control. Most audit tooling wouldn’t catch it because the code paths were valid. The logic was consistent. The failure was contextual. But it is exactly the kind of issue dynamic mutation testing is built to expose.
Here’s how we would detect this at Olympix.
A mutation test that directly calls flashLoan() with the Zap contract as receiver would reveal:
These are not unit test assertions. These are behavioral mutations that mimic real-world adversarial input.
Our Insufficient Assertion Detector (in development) flags test functions that:
In Prisma’s case, test coverage likely validated successful migrations but never tested invalid initiators or spoofed calldata. That’s an assertion gap.
Static analyzers often trace call graphs. But they don’t enforce context constraints. We are working on context-sensitive fuzzing that mutates:
These patterns are how real exploits emerge. They need to be baked into automated test frameworks.
This exploit wasn’t just about a single contract mistake. It exposed systemic design patterns in DeFi that quietly expand attack surfaces. Prisma wasn’t alone in making this mistake — it was just next in line.
When users approve a contract to act on their behalf without granular control or expiration, they inherit the contract’s full risk profile. If that contract has a logic flaw, every user is exposed until they manually revoke.
DeFi protocols still overuse delegate approvals without providing dashboards, revocation tools, or scoped permissions. That’s not just a UX issue — it’s a security failure.
Contracts that rely on internal call flows or implicit trust boundaries break under flashloan pressure. Flashloans let attackers simulate any internal execution flow with precision timing and custom data.
If your contract assumes any function will only be called “the right way,” a flashloan will prove you wrong.
Migration helpers, vault zaps, and position routers handle privileged flows but rarely receive the same scrutiny as core contracts. That’s a blind spot. These contracts often:
Which makes them ideal for both users and attackers.
Prisma’s pause mechanism kicked in quickly — but the funds were already gone. Relying on emergency breaks is not a replacement for enforcing trusted execution upfront.
Security can’t be reactive. It has to be compositional and preventative.
Security isn’t just about code correctness. It’s about execution integrity. Prisma’s exploit delivered a clear set of lessons for teams building composable, delegated, or flashloan-exposed systems.
If your function:
Then it must validate the initiator, the data, and the call path. If it doesn’t, you’ve built a permissionless exploit interface.
The EVM doesn’t care how you meant your contract to be used. Every public or external function can be called by anyone, in any order, with any data. Design for abuse, not just use.
If your system requires delegation, enforce:
Global approvals with no controls are attacker gold.
Your test suite should:
Mutation testing is not optional anymore. If your test suite doesn’t mutate actor, data, and order, it is incomplete.
Every contract that handles user assets should:
Residual value is one of the most overlooked risk surfaces in DeFi.
The Prisma exploit wasn’t sophisticated. It was clean, obvious, and devastating — because the system trusted the happy path.
In DeFi, every public function is an attack surface. Every migration helper is a vault. Every approval is a blank check. If your architecture doesn’t account for adversarial usage from the start, then you haven’t built a secure system — you’ve built a honeypot with good intentions.
Smart contract security is no longer about catching bugs. It’s about killing assumptions.
Olympix provides advanced Solidity analysis tools to help developers identify and fix vulnerabilities before they become critical exploits.
Get started today to fortify your smart contracts and proactively shield them from exploits in the evolving Web3 security landscape.
Connect with us on:
Twitter | LinkedIn | Discord | Medium | Instagram | Telegram
Prisma Finance Incident — March 28, 2024 — Post-Mortem by HackMD