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 ❌