Abracadabra GMX V2 Cauldron RouterOrder Phantom-Collateral Self-Liquidation Drain (2025-03-25)
-
Name: Abracadabra GMX V2 Cauldron RouterOrder phantom-collateral self-liquidation drain
-
Date: 2025-03-25
-
Protocol: Abracadabra Money / GMX V2 gmCauldrons
-
Chain: Arbitrum
-
Category: lending-protocol solvency check bypass / asynchronous-order collateral desynchronization / phantom collateral / self-liquidation
-
Loss: about 13.4M, roughly
6,260-6,262 ETH -
Summary: Abracadabra’s GMX V2 cauldrons let users count in-flight GMX orders as collateral through
orders[user].orderValueInCollateral(). That design failed when a GMX deposit order was forced to fail and real short-token inventory stayed parked inside the per-userRouterOrder. During liquidation,GmxV2CauldronV4.liquidate()could callorders[user].sendValueInCollateral()to pull real short tokens out of that order and hand them to the liquidator, butGmxV2CauldronRouterOrder.sendValueInCollateral()did not decrement the order’s accounting fieldsinputAmount,minOut, orminOutLong. The same order therefore kept reporting the original pending-collateral value throughorderValueInCollateral()even after real assets had already been extracted. The attacker used that stale pending-collateral mark to borrow MIM, self-liquidate to pull real assets out of the RouterOrder, leave the ghost collateral visible, then borrow again against the same already-spent order state until about6,260 ETHhad left the gmCauldrons. -
Trigger / entry point:
- Abracadabra’s GMX V2 integration extended
_isSolvent()so user solvency included not only deposited collateral shares but also any activeorders[user].orderValueInCollateral()from an asynchronous GMX order. - The GMX V2
RouterOrderproxy tracked pending order value through local fields likeinputAmount,minOut, andminOutLonginstead of recomputing strictly from remaining extractable balances. liquidate()could cancel an active order, then if more collateral still needed to be seized than the user’s on-cauldron collateral shares, callorders[user].sendValueInCollateral(to, collateralShare - userCollateralShare[user])to extract real short-token value from the RouterOrder.- The critical bug was that
sendValueInCollateral()transferred real short tokens to BentoBox for the liquidator but did not reduce the order-accounting fields later reused byorderValueInCollateral(), so the same order could keep serving as apparent collateral after partial or full value extraction.
- Abracadabra’s GMX V2 integration extended
-
Root cause:
- Abracadabra split one collateral object into two inconsistent truths:
what value can still be extracted from the RouterOrder right nowandwhat value orderValueInCollateral() still reports for solvency. _isSolvent()trusted the reported pending-order value fromorderValueInCollateral(), while the liquidation path could remove real value throughsendValueInCollateral()without updating that reported value.- Because
closeOrder()only zeroedorders[user]when called by the order contract itself, the broader liquidation / failed-order lifecycle could leave a RouterOrder whose economically meaningful assets had already been pulled while the cauldron still treated the order as live pending collateral. - In short: Abracadabra let liquidation consume real order inventory without consuming the order’s solvency value, creating phantom collateral that could be borrowed against repeatedly.
- Abracadabra split one collateral object into two inconsistent truths:
-
Exploit path:
- The attacker prepared multiple wallets and gm markets on Arbitrum, then deployed exploit contract
0xf29120acd274a0c60a181a37b1ae9119fe0f1c9c. - They created GMX V2 deposit orders through Abracadabra gmCauldrons in a way designed to fail on execution, leaving the input short token stranded in the user-specific
RouterOrderinstead of converting cleanly into settled GM collateral. - Abracadabra still counted that failed-order state as pending collateral through
_isSolvent() -> orders[user].orderValueInCollateral(). - The attacker borrowed MIM against that pending collateral and then pushed the position into liquidation under attacker-controlled conditions.
- During liquidation,
GmxV2CauldronV4.liquidate()calledorders[user].sendValueInCollateral(...), which moved real short-token value from the RouterOrder into BentoBox for the liquidator. - Because
sendValueInCollateral()did not decrementinputAmount,minOut, orminOutLong, the same RouterOrder continued to report the old pending collateral value throughorderValueInCollateral(). - With the ghost order value still counted as collateral, the attacker could borrow again against what was now mostly or entirely already-extracted order inventory.
- The attacker repeated the failed-order / liquidation / reborrow sequence across several gmCauldrons over roughly 56 transactions, then bridged the stolen ETH out to Ethereum.
- The attacker prepared multiple wallets and gm markets on Arbitrum, then deployed exploit contract
-
What the attacker needed to be true:
- The gmCauldron had to include asynchronous order value in solvency through
_isSolvent(). - A failed GMX order had to leave real extractable assets in the RouterOrder instead of cleanly settling to final collateral or fully disappearing from the solvency picture.
- The liquidation path had to be able to pull real value out of the RouterOrder through
sendValueInCollateral(). sendValueInCollateral()had to leave the accounting inputs fororderValueInCollateral()untouched so the same pending-order value survived after extraction.- The attacker needed enough control over liquidation flow to self-liquidate or otherwise internalize the value being pulled from the RouterOrder before outside liquidators consumed it.
- The affected gmCauldrons needed enough remaining MIM liquidity for the phantom collateral to be monetized repeatedly.
- The gmCauldron had to include asynchronous order value in solvency through
-
Technical anatomy worth remembering:
- This was not a GMX core exploit. GMX supplied the asynchronous order model; Abracadabra’s integration failed to keep
pending order collateralandremaining extractable order assetsas one security object. - The decisive code seam is easy to state and worth preserving.
_isSolvent()addedorders[user].orderValueInCollateral()to user collateral, whileliquidate()could separately callorders[user].sendValueInCollateral()to extract real assets from that same order. - In the vulnerable
RouterOrder,orderValueInCollateral()depended on static order fields such asinputAmount,minOut, andminOutLong;sendValueInCollateral()transferred real short tokens but did not burn or decrement those fields. - That made the exploit a strong lending-side
phantom collateralcase: the same asynchronous order state could first justify borrowing power and later pay liquidation value without those two consumptions ever being reconciled. - The self-liquidation angle matters. This was not just stale accounting on paper; attacker-controlled liquidation was the rail that turned stale pending collateral into extracted real assets while leaving the ghost mark alive for the next borrow.
- The sharper design failure is therefore not only
phantom collateral existed, butAbracadabra treated the liquidator as if it were an independent rescuer. In practice the borrower could control the liquidation side too, soliquidation consumes unhealthy collateralbecamethe attacker chooses when the unhealthy order state pays them real assets. - The incident is also a sharp composability lesson: once a lending protocol admits
in-flight external order stateas collateral, partial cancellations, failed orders, refunds, and liquidation-side extraction all have to consume the same backing object or the protocol starts lending against order metadata instead of value.
- This was not a GMX core exploit. GMX supplied the asynchronous order model; Abracadabra’s integration failed to keep
-
Representative transactions and artifacts:
- Main exploit transaction:
0xed17089aa6c57b7d5461209e853bdb56bc3460a91805e20d2590609a515ef0b0 - Exploit contract:
0xf29120acd274a0c60a181a37b1ae9119fe0f1c9c - Example exploited gmCauldron:
0x625Fe79547828b1B54467E5Ed822a9A8a074bD61 - Publicly identified Ethereum recipient wallets for bridged proceeds included
0xa8f822E937C982e65b0437Ac81792a3AdA76A1ff,0x047C2a3dd1Ab4105B365685d4804fE5c440B5729, and0x018182FD7B856AeE1606D7E0AA8bca10F1Cb0b5d.
- Main exploit transaction:
-
Response / mitigation:
- Abracadabra paused borrowing on the affected gmCauldrons and publicly acknowledged that the GMX-linked cauldrons, not GMX core itself, were the compromised surface.
- The obvious fix direction was to make RouterOrder extraction paths consume or reduce the exact same order-state variables later trusted by
orderValueInCollateral(), or to stop counting unsettled / failed external-order state as ordinary borrowable collateral. - More generally, asynchronous collateral adapters need one shared consumed-state boundary for
pending collateral still counts,liquidation may seize it,refund may withdraw it, andorder is closed.
Pattern tags
- Solvency check bypass
- Self-liquidation monetization
- asynchronous external-order collateral desynchronization
- phantom collateral / consumed-state omission
Why the exploit worked
- Abracadabra treated a GMX
RouterOrderas collateral truth before proving that the order’s reported value and its remaining extractable backing were still the same object. GmxV2CauldronV4.liquidate()could spend real short-token inventory out of the order throughsendValueInCollateral(), but the fields later trusted byorderValueInCollateral()stayed unchanged, so liquidation consumed backing without consuming borrow authority.- The attacker’s control of the liquidation side mattered. Self-liquidation turned what should have been a defensive unwind path into the mechanism that paid real assets out while leaving the phantom collateral mark alive for the next borrow.
- Failed or stranded asynchronous order state therefore sat in the worst possible middle state: too broken to be safe collateral, but still live enough for
_isSolvent()to honor as ordinary borrow power.
Detection and audit pivots
- For every async-collateral adapter, compare
reported pending collateralagainstcurrently extractable or claimable backingafter liquidation, cancellation, refund, rescue, and partial-close paths — not just after happy-path settlement. - Diff the state consumed by solvency checks against the state consumed by liquidation-side extraction. If a path can move value out of an order without decrementing the variables later read by
_isSolvent(), the protocol is already carrying phantom-collateral risk. - Replay borrower-controlled liquidation explicitly. If the same actor can
borrow -> self-liquidate -> keep the seized value -> borrow againagainst the surviving adapter object, the liquidation path is part of the exploit surface rather than a safety rail. - Treat failed or limbo external orders as a named distrust state.
Order still existsshould never be enough to preserve borrow authority once execution failed, backing became partial, or one payout path already consumed value.
Why this incident matters
- It preserves a clean major example of a lending market treating asynchronous external-order state as collateral truth, then forgetting to consume that truth when liquidation extracted the real backing assets.
- It adds a useful self-liquidation cousin to the corpus where the core bug is not a false oracle or a simple reserve donation, but stale collateral published by an order adapter that liquidation itself can partially empty.
- It also shows why failed or partially unwindable integration state is often more dangerous than obviously successful state: a
pendingobject that survives errors, refunds, and partial seizures has to be audited like a live balance sheet, not like temporary middleware.
Failure-prevention checklist
- Treat asynchronous external-order state as a weaker collateral object than settled inventory. A pending GMX order, bridge message, withdrawal ticket, or other adapter-side receipt should not inherit ordinary borrow authority until the protocol can prove what executable value still remains after failure, cancellation, partial refund, or forced unwind.
- Keep
reported pending collateralandremaining extractable backingas one consumed-state object. Any path that transfers value out of the order — liquidation, refund, cancel, rescue, or partial close — must atomically reduce or extinguish the exact same variables later trusted by solvency checks. - Recompute pending-order collateral from current extractable balances or current claimable rights rather than from stale setup-era fields like
inputAmount,minOut, orminOutLongonce those fields can diverge from the assets still inside the adapter. - Make failed, cancelled, or partially filled asynchronous orders fail closed for borrowing unless the protocol can prove the residual order state still deserves live collateral recognition.
Order still existsis not enough. - Treat liquidation of adapter-side collateral as a solvency-changing state transition, not as harmless cleanup. After any liquidation-side extraction, the protocol should re-evaluate solvency from the post-seizure state before allowing further borrowing, rollover, or repeated liquidation against the same object.
- Encode the invariant Phylax highlighted in simpler economic form: reported pending collateral must never exceed actual extractable collateral value after every call that can move funds or mutate order state.
- Make borrower-liquidator collusion part of the default threat model for async collateral paths. If the borrower can self-liquidate or cheaply control the liquidator side, liquidation discounts and seize rights become attacker-owned extraction rails rather than independent recovery tools.
- Keep asset-local containment ready for external-order collateral adapters: pause borrowing, zero collateral factors, isolate the market, or disable liquidation-side order extraction before one desynchronized adapter state can drain deeper shared reserves.
Audit prompts
- Which exact functions can move value out of an asynchronous order or adapter object — liquidation, cancel, refund, claim, rescue, partial close — and do all of them consume the same state later used by
_isSolvent()or other borrow-power checks? - Does solvency trust setup-era metadata (
inputAmount,minOut, cached order fields, stored preview values) when the economically real question iswhat can still be extracted or claimed right now? - Can an order fail, partially fill, or sit in a limbo state where assets remain stranded in an adapter while the lending market still treats the old order quote as money-good collateral?
- If
sendValueInCollateral()or its equivalent transfers real assets to a liquidator, what exact post-state proves the same order cannot still support fresh borrowing or further liquidation payouts? - Would the core invariant
reported pending collateral <= actual extractable valuehold after every state transition, including failed execution, partial seizure, repeated liquidation, and order closure? - Is liquidation assuming an independent rescuer when the borrower can cheaply self-liquidate through a sister wallet, helper contract, or same-bundle execution path?
- What happens if the external system’s order lifecycle and the lending market’s collateral lifecycle disagree for one block, one callback, or one failed execution? Which side wins borrow authority during that disagreement?
- Could a market-wide pause, collateral-factor zeroing, or liquidation gate on async-order collateral have cut off monetization even if the adapter bug itself still existed?
Lessons that should already be standard
- A pending external order is not ordinary settled collateral. If its value can still disappear, shrink, or be seized through failure handling and liquidation paths, the protocol should not lend against it as though it were already final inventory.
We can still point to the order objectis not the same thing asthe order still contains borrow-supporting value. Adapter existence is not backing proof.- Solvency and extraction paths must consume the same object. If liquidation can spend a pending order without spending its borrow authority, the protocol is lending against metadata rather than assets.
- Failed-state middleware is often balance-sheet state. The most dangerous adapter objects are frequently the ones stuck between
successandfully closed, because every recovery path becomes another chance for accounting drift. - Liquidation against asynchronous collateral is not automatically defensive. Once the borrower can influence or own the liquidator side, liquidation-side extraction can become the mechanism that turns stale accounting into real stolen assets.
- Invariant testing does not need to understand every external-order nuance to catch this family. A blunt check that reported collateral never exceeds extractable backing would already have been stronger than trusting parallel accounting tracks to stay aligned by convention.
- The healthy-side design question is not only
can we price pending external state?It iswhen does that state deserve borrow authority, and what exact event consumes that authority forever?
Internal linkages
- Closest recurring pattern: solvency-check-bypass.
- Monetization lens: self-liquidation-monetization — the attacker did not just create phantom collateral; they used attacker-controlled liquidation to pull real RouterOrder assets out while leaving the phantom mark alive.
- Positive-design companion: collateral-liveness-and-right-consumption-discipline — the sharpest healthy-side follow-up when the question becomes why
sendValueInCollateral()should have consumed the same RouterOrder meaning later trusted byorderValueInCollateral(). - Positive-design companion: collateral-admission-lag-and-borrow-side-quarantine — the adjacent healthy-side question is why a failed or partially unwindable order adapter ever deserved immediate ordinary borrow authority before its residual backing had proven itself.
- Positive-design companion: borrower-separation discipline — the deeper missed assumption was that a
different liquidator addresswould behave like an external rescuer rather than the same actor consuming the stale order state from the other side. - Adjacent control-surface pattern: entrypoint-only-policy-off-path-state-mutation — the protocol’s economic truth lived in the RouterOrder’s remaining extractable assets, but solvency continued to trust broader stale order fields instead.
Control surface
-
The decisive leverage sat in
GmxV2CauldronV4._isSolvent(),GmxV2CauldronV4.liquidate(),GmxV2CauldronRouterOrder.orderValueInCollateral(), andGmxV2CauldronRouterOrder.sendValueInCollateral(). -
Abracadabra did not fail because asynchronous orders exist. It failed because order-state accounting, liquidation extraction, and collateral recognition were allowed to talk about different residual value after the first extraction.
-
Sources:
- https://rekt.news/abracadabra-rekt2
- https://docs.phylax.systems/assertions-book/previous-hacks/abracadabra-gmx-v2-exploit
- https://threesigma.xyz/blog/exploit/abracadabra-gmx-defi-exploit-explained
- https://arbiscan.io/tx/0xed17089aa6c57b7d5461209e853bdb56bc3460a91805e20d2590609a515ef0b0
- https://github.com/Abracadabra-money/abracadabra-money-contracts/blob/dff69a19a219bbff90ab7b752c9f9c0ab5e8fe6f/src/cauldrons/GmxV2CauldronV4.sol#L47-L67
- https://github.com/Abracadabra-money/abracadabra-money-contracts/blob/dff69a19a219bbff90ab7b752c9f9c0ab5e8fe6f/src/cauldrons/GmxV2CauldronV4.sol#L114-L154
- https://github.com/Abracadabra-money/abracadabra-money-contracts/blob/dff69a19a219bbff90ab7b752c9f9c0ab5e8fe6f/src/cauldrons/GmxV2CauldronV4.sol#L197-L203
- https://github.com/Abracadabra-money/abracadabra-money-contracts/blob/dff69a19a219bbff90ab7b752c9f9c0ab5e8fe6f/src/periphery/GmxV2CauldronOrderAgent.sol#L241-L280
-
Last reviewed: 2026-07-29 UTC