Predy Finance PredyPool Callback-Locker Cross-Pair Liquidity Migration Drain (2024-05-14)
- Name: Predy Finance PredyPool callback-locker cross-pair liquidity migration drain
- Date: 2024-05-14
- Protocol: Predy Finance / PredyPool
- Chain: Arbitrum
- Category: callback-frame authority reuse / cross-pair liquidity migration / balance-provenance failure
- Loss: about
83.9 WETHplus219,585.737814 USDC, commonly reported as roughly$464k - Summary: Predy’s pool let anyone create a new trading pair and then call
trade()on that pair, which opened a callback window where the caller became the currentlockerand could use privileged pool functions liketake()andsupply(). The attacker created a freshUSDC/WETHpair they themselves were allowed to trade, enteredtrade()on that new pair, then insidepredyTradeAfterCallback()usedtake()to pull the realUSDCandWETHbacking existing pair2out of PredyPool and immediatelysupply()those same assets into attacker-owned pair3. Predy’s final settlement check only cared that the pool’s aggregate token balances matched before and after the callback, not that liquidity had stayed attached to the same pair. Once the callback finished, pair3held the assets, the attacker held the supply claims, and a normalwithdraw()drained the pool. - Trigger / entry point:
- On
2024-05-14 07:41:28 UTC, the attacker targeted PredyPool on Arbitrum via exploit transaction0xbe163f651d23f0c9e4d4a443c0cc163134a31a1c2761b60188adcfd33178f50f. - Predy’s own postmortem and third-party reconstructions identify the vulnerable PredyPool contract as
0x9215748657319b17fecb2b5d086a3147bfbc8613, the primary attacker EOA as0x76b02ab483482740248e2ab38b5a879a31c6d008, and the exploit contract used in the callback flow as0x7b8b944ab2f24c829504a7a6d70fce5298f2147c. - Predy already had two live pairs:
pair id 1for bridgedUSDC/WETHandpair id 2for nativeUSDC/WETH; the attacker first registeredpair id 3with the same nativeUSDC/WETHassets so they could become the authorized trader for that new pair. - The decisive entry point was the
trade()→TradeLogic.callTradeAfterCallback()flow, which intentionally let the activelockercall privileged liquidity-moving functions during the callback beforeglobalData.finalizeLock()closed the frame.
- On
- Root cause:
- Predy treated
some callback for the active locker is in progressas if it provedthis locker may only settle liquidity movements consistent with this specific pair and this specific trade. - The temporary callback authority was pool-global rather than pair-scoped: once the attacker became the
lockerfor pair3,take()could pull token balances funded by pair2. - Predy’s post-callback validation focused on aggregate pool token deltas, so moving assets out of one pair and back into another pair could still look balanced at the pool level.
- In effect, Predy authenticated the callback frame too loosely and then accepted
same total balances returnedas proof that no economic theft had happened, even though pair ownership and withdrawal rights had been rewritten underneath that aggregate balance.
- Predy treated
- Exploit path:
- The attacker deployed an attack contract ahead of time and registered a new pair (
pair id 3) using the same nativeUSDC/WETHmarket already used by Predy’s existingpair id 2. - They called
trade()onpair id 3, which set the attack contract as the activelockerand invoked the caller’spredyTradeAfterCallback()hook. - Inside that callback, the attacker called
PredyPool.take()to withdraw the fullUSDCandWETHliquidity then backing the older live pair. - Still inside the same callback window, they called
PredyPool.supply()to deposit those tokens into attacker-controlledpair id 3, minting themselves the corresponding supply position there. - Predy’s callback finalization accepted the maneuver because the pool’s aggregate token balances were back where they started by the end of the callback, even though the funds had been reassigned from pair
2to pair3. - After the callback closed, the attacker called
withdraw()onpair id 3and removed219,585.737814 USDCplus about83.9 WETHas the now-legitimate supplier to that attacker-created pair.
- The attacker deployed an attack contract ahead of time and registered a new pair (
- What the attacker needed to be true:
- They had to be able to register a new pair and become the contract allowed to trade through that pair’s callback flow.
- The callback
lockerprivilege had to be broad enough to calltake()against pool-wide liquidity rather than liquidity strictly namespaced to the in-flight pair. - The protocol had to allow
take()andsupply()during the callback because legitimate leverage flows depended on that temporary authority. - Final callback settlement had to validate only token balance differences at the whole-pool level, not provenance such as
which pair lost liquidityandwhich pair gained the withdrawal right. - The attacker-created pair had to remain withdrawable under ordinary supply accounting after the callback finished.
- Technical anatomy worth remembering:
- This was not a classic
reenter and double-spend before a balance updateexploit. The sharper failure was that a real callback frame exposed a temporary capability that was too broad for the pair and stage it was supposed to represent. - The concrete authority leak was
onlyByLocker: onceTradeLogic.callTradeAfterCallback()initialized the lock for the in-flight trader, the callback could calltake()andsupply()against pool-global liquidity rather than liquidity namespaced to the active pair. - The theft did not require falsifying balances. Predy’s pool ended the callback with the same token totals it started with; what changed was which pair those assets backed and therefore who could withdraw them afterward.
- That makes this a good companion to pure balance-delta bugs: aggregate conservation is not enough when the protocol also needs provenance about which sub-ledger or market owns the conserved balance. Put more compactly, balances stayed real but pair-level provenance did not.
- The attacker did not need a fake Uniswap pool, oracle manipulation, or stolen key. They only needed the right to open one callback on a self-created pair and then use that temporary authority against liquidity attributed to another pair.
- This was not a classic
- Representative transactions and addresses:
- Main exploit tx:
0xbe163f651d23f0c9e4d4a443c0cc163134a31a1c2761b60188adcfd33178f50f - Vulnerable PredyPool:
0x9215748657319b17fecb2b5d086a3147bfbc8613 - Exploit contract:
0x7b8b944ab2f24c829504a7a6d70fce5298f2147c - Primary attacker EOA:
0x76b02ab483482740248e2ab38b5a879a31c6d008 - Ethereum bridge-out destination later highlighted in public analysis:
0xeDe4E01347C012BD57302ea606095FB1eC5c848E
- Main exploit tx:
- Response / mitigation:
- Predy publicly acknowledged the incident, halted the affected contracts, and said no further damage could occur because the exploited path had been stopped.
- The team later published a postmortem and said the public-facing functions used in the attack had been disabled while they considered a secure relaunch after further audit work.
- Predy also sent an onchain message to the exploiter offering a
10%bounty in exchange for returning funds, and told users to revoke related Permit2 approvals as an additional precaution even though the drained assets came from the pool itself rather than from direct user-wallet pulls.
Pattern tags
legitimate callback frame, wrong pair-scoped authority objectpool-global locker privilege was treated as if it authenticated pair-local liquidity rightsaggregate token-balance conservation was mistaken for conservation of pair-level supplier ownershippermissionless pair creation plus callback-time supply rights created a fresh namespace that could inherit withdraw claims over legacy liquidity
Why the exploit worked
- Predy authenticated the callback too coarsely.
onlyByLockerproved only that some authorized locker was active, not that this locker could touch only the liquidity economically owned by the in-flight pair. - During
predyTradeAfterCallback(), the attacker could call bothtake()andsupply()while the lock remained live. That made the callback frame powerful enough not just to borrow execution inventory, but to rewrite which pair held the supplier-side claim over that inventory. - Predy’s end-of-callback check looked only at aggregate pool token balances. Because the attacker moved
USDCandWETHout of pair2and back into the pool under pair3, the pool-level diff still looked clean even though pair-level ownership had changed. - Permissionless pair creation completed the monetization path. The attacker did not need to fake balances or forge a pool; they only needed a fresh pair namespace they controlled so the migrated liquidity would later be withdrawable through ordinary accounting.
- The final
withdraw()succeeded because Predy treatedpair 3 now has supplied liquidityas sufficient proof that the same party deserved the redemption right, even though the liquidity had been sourced from pair2during a broader callback frame.
Detection and audit pivots
- Trace the pair id, market id, or sub-ledger id through every callback-callable helper. If
take()orsupply()can move value without proving it belongs to the same in-flight pair, the callback frame is already too broad. - Compare
pool stayed balancedchecks againstpair-level ownership stayed conservedchecks. Any review that stops at aggregate token deltas will miss cross-ledger custody migration bugs like Predy. - Model the full adversarial sequence
register pair -> trade on new pair -> callback -> take old-pair assets -> supply into new pair -> withdraw. If that path works while all final pool balances net to zero, settlement provenance is under-specified. - Alert on sudden liquidity migration from mature pairs into brand-new pairs, especially when the new pair received no prior third-party funding and the transfer happened inside a single trade callback.
- Treat permissionless market creation as part of the same security review as callback privileges. A harmless-looking
anyone can create a pairfeature becomes custody-relevant once callback-time helpers can retitle pooled inventory. - Review temporary lock / mutex variables as execution-frame credentials, not mere reentrancy guards. Ask what exact actor, pair, stage, and inventory bucket they authenticate before any privileged helper trusts them.
Failure-prevention checklist
- Bind callback authority to one explicit frame object before external control leaves: pair id, active locker, allowed selector set, and the exact liquidity buckets the callback may touch.
- Make
take()andsupply()consume pair-local inventory rather than pool-global balances. If a callback is entered for pair3, it should not be able to move reserves economically owned by pair2. - Reconcile settlement on provenance, not just totals:
same aggregate token balances returnedis weaker thanthe same pair kept ownership of the balances it started with. - Separate temporary trade-time liquidity access from supplier-right minting. A callback that borrows inventory for execution should not be able to rewrite who owns the withdraw claim for that inventory unless that exact rewrite was pre-committed.
- Treat public pair creation plus callback-time privileged helper access as a combined authority surface. Permissionless market creation is only safe if newly created markets cannot inherit pooled custody rights over existing markets.
- Add end-to-end tests for
create new pair -> open callback -> move assets from old pair -> re-supply into new pair -> withdraw, and fail the build if aggregate-conservation checks still pass. - Keep emergency pause coverage on every path that can reassign supplier claims or move pooled liquidity across internal ledgers during callbacks.
Audit prompts
- What exact fields does
onlyByLockeror equivalent callback auth prove: merelysome locker is active, or the specific pair, actor, stage, and inventory bucket this callback may mutate? - Can a callback opened for one pair call helper functions that read or write liquidity attributed to a different pair sharing the same tokens?
- Does final settlement prove pair-level ownership conservation, or only that the pool’s raw token balances ended unchanged?
- Which functions callable during callbacks can mint, burn, or transfer supplier rights, and are those rights bound to the same market whose liquidity was actually touched?
- If a new pair is permissionlessly creatable, what stops it from becoming a fresh namespace that inherits temporary authority over legacy pooled assets?
- Could a malicious callback leave the pool globally solvent in token-count terms while silently migrating withdrawable claims from one internal ledger to another?
- What telemetry would distinguish
legitimate in-callback liquidity movementfromcross-pair custody reassignmentbefore a laterwithdraw()cashes it out? - If the callback reverted after
take()but before final settlement, which invariants guarantee that partially migrated provenance cannot survive?
Lessons that should already be standard
- Aggregate balance conservation is not enough. If internal ownership moved from one pair to another, the theft already happened even when the pool still holds the same number of tokens.
- A real callback from a legitimate in-flight trade can still be the wrong authority object. The question is not only
was a locker active?butwhich pair and which inventory was that locker allowed to touch?. - Pair identity is a custody surface, not just product metadata. When supplier claims and withdrawal rights are pair-scoped, pair provenance has to survive every callback intact.
- Permissionless market creation gets much more dangerous once temporary privileged callbacks can touch pooled inventory.
Anyone can list a pairandcallbacks can move shared liquiditycompose into a custody problem, not just a UX feature. Funds came back to the poolis a dangerously weak settlement story. Protocols need to know whether the same economic object came back to the same owner ledger.- Callback-time helper functions deserve the same review intensity as withdraw paths. In Predy,
take()andsupply()during a legitimate callback were already enough to rewrite who could later cash out real reserves.
Why this incident matters
- It is a clean example of callback authority reuse where the exploit was not
call back from the wrong senderbutuse a legitimate callback to exercise rights that were broader than the in-flight pair deserved. - It also preserves a subtle accounting lesson: a pool can remain globally solvent in raw token-count terms while still losing economically if pair-level entitlement is reassigned under a coarse aggregate balance check.
- The case broadens the corpus beyond the more familiar fake-pool callback drains by showing that even honest callback senders are dangerous when temporary settlement authority is not scoped tightly enough.
Internal linkages
- Core recurring pattern: callback-frame-authority-reuse.
- Sharper positive-design companion: callback-frame-truth-security-object — Predy needed one callback frame binding pair id, active locker, allowed liquidity-movement surface, and the pair-level payout / ownership ledger that final settlement was supposed to preserve.
- Adjacent accounting lens: balance-delta-provenance-failure — Predy is the cross-pair provenance-under-aggregate-conservation cousin: callback finalization treated
pool balances ended unchangedas proof that the intended settlement happened, even though balances stayed real while pair-level provenance was silently migrated from one ledger to another. - Adjacent broader family: reentrancy — there is callback-driven control transfer here, but the preserved lesson is less
state updated too latethanthe temporary callback capability was authenticated too coarsely.
Control surface
-
The decisive leverage sat in public pair creation, pair-specific trade authorization, the pool-global
lockerprivilege duringpredyTradeAfterCallback(), and a final balance-difference check that reasoned over pooled assets without preserving which pair still owned them. -
The practical lesson is that
assets came back to the poolis not a sufficient callback invariant when the same callback can also rewrite which internal market or sub-ledger those assets now belong to; aggregate conservation without pair-ledger provenance is still theft. -
Sources:
- https://predyfinance.medium.com/postmortem-report-on-the-details-of-the-events-of-may-14-2024-8690508c820b
- https://blog.verichains.io/p/predy-finance-attack-how-a-liquidity
- https://olympix.security/biweekly-exploits/305m-vanishes-dmm-predy-tsuru-and-osn-wrecked-by-wallet-compromise-and-access-control-failures
- https://medium.com/neptune-mutual/breaking-down-the-predy-finance-hack-3f27bd82da52
- https://x.com/predyfinance/status/1791354505956565147
-
Last reviewed: 2026-07-26 UTC