Description
This exploit drained $13.4M from Abracadabra’s GMX V2 Cauldron through an accounting bug that created “phantom collateral” - allowing the same tokens to be borrowed against multiple times. The Core Bug: TheGmxV2CauldronRouterOrder contract had two critical functions:
sendValueInCollateral()- Extracted real tokens during liquidationsorderValueInCollateral()- Reported collateral value for borrowing calculations
sendValueInCollateral() removed tokens, it failed to update internal accounting variables (inputAmount, minOut, minOutLong). This meant orderValueInCollateral() continued reporting the original collateral value even after tokens were extracted.
Exploitation Steps:
- Setup: Attacker creates a failed GMX deposit, leaving tokens in the RouterOrder contract
- Exploit Loop:
- Borrow MIM against the reported collateral value
- Self-liquidate to extract real tokens via
sendValueInCollateral() - Internal accounting remains unchanged - same collateral value still reported
- Borrow again against the “phantom” collateral
- Repeat until all real tokens are drained
Proposed Solution
The core issue was a fundamental violation of a basic invariant: reported collateral values should never exceed actual extractable assets. A simple phantom collateral assertion could have prevented this exploit:How This Assertion Prevents the Exploit
This assertion implements a fundamental economic sanity check that would have caught the accounting manipulation: What it does:- Captures reported collateral from the buggy
orderValueInCollateral()function - Calculates actual extractable value by checking real token balances
- Enforces the invariant that reported values cannot exceed actual extractable amounts
- Before exploit: RouterOrder has 1000 USDC,
orderValueInCollateral()returns 1000 USDC equivalent, actual balance = 1000 USDC → Assertion passes ✅ - During exploit:
sendValueInCollateral()extracts 500 USDC, butorderValueInCollateral()still returns 1000 USDC equivalent, actual balance = 500 USDC → Assertion fails ❌ - After multiple extractions: All tokens extracted, but
orderValueInCollateral()still returns 1000 USDC equivalent, actual balance = 0 USDC → Assertion fails ❌

