critical: Bug #91 - Use SDK closePosition() for exact BN (no Math.floor truncation)

Root Cause: Math.floor(sizeToClose * 1e9) truncated position sizes, leaving tiny fractional remnants (e.g., 0.00000008 SOL) that prevented full position closure.

Discovery: Drift UI 'Close All Positions' failed with 'not enough collateral' but clicking 'Market' order worked - because Market uses exact position size.

Solution: SDK's driftClient.closePosition() uses exact BN arithmetic internally (baseAssetAmount.abs()), avoiding any floating point truncation.

Changes:
- lib/drift/orders.ts lines 647-690
- For 100% closes: Now uses driftClient.closePosition(marketIndex)
- For partial closes: Continues using placeAndTakePerpOrder

Expected Impact: Flip operations will now fully close positions without leaving fractional remnants that cause 'position still open' failures.

Financial Impact: Prevents flip failures that caused user 000+ losses from multiple bugs in position closing logic.
This commit is contained in:
mindesbunister
2026-01-01 13:50:45 +01:00
parent 465f6bdb82
commit 4bf5761ec0

View File

@@ -644,35 +644,48 @@ export async function closePosition(
}
}
// Prepare close order (opposite direction)
const orderParams = {
orderType: OrderType.MARKET,
marketIndex: marketConfig.driftMarketIndex,
direction: position.side === 'long'
? PositionDirection.SHORT
: PositionDirection.LONG,
baseAssetAmount: new BN(Math.floor(sizeToClose * 1e9)), // 9 decimals
reduceOnly: true, // Important: only close existing position
}
// CRITICAL FIX (Jan 1, 2026 - Bug #91): Use SDK's closePosition() for 100% closes
//
// ROOT CAUSE: Our baseAssetAmount calculation uses:
// baseAssetAmount: new BN(Math.floor(sizeToClose * 1e9))
//
// The Math.floor() TRUNCATES, leaving fractional remnants that never close!
// Example: Drift has 23.02145678 SOL, we try to close 23.02145670 SOL
// Remaining 0.00000008 SOL stays open forever!
//
// SOLUTION: For 100% closes, use SDK's driftClient.closePosition() which:
// 1. Gets EXACT position size from Drift as BN (no floating point)
// 2. Uses baseAssetAmount.abs() internally (no truncation)
// 3. Calls placeAndTakePerpOrder with exact amount
//
// This matches what the Drift UI "Market" button does vs "Close All" which
// may use a similar size calculation that leaves remnants.
// CRITICAL FIX (Dec 31, 2025 - Bug #89): Use placeAndTakePerpOrder instead of placePerpOrder
//
// ROOT CAUSE: placePerpOrder only PLACES an order on the order book.
// For MARKET orders, the transaction confirms when the order is placed, NOT when it's filled.
// This caused the flip operation to fail - close tx confirmed but position never closed.
//
// SOLUTION: placeAndTakePerpOrder places the order AND fills it against makers atomically.
// This guarantees the position is closed when the transaction confirms.
//
// Evidence: Solscan showed tx 5E713pD6... as "Place long 24.72 SOL perp order at price 0"
// - The order was placed on the book but never filled
// - Position remained open despite "confirmed" transaction
//
// See: https://github.com/drift-labs/drift-vaults/blob/main/tests/testHelpers.ts#L1542
console.log('🚀 Placing market close order with IMMEDIATE FILL (placeAndTakePerpOrder)...')
const txSig = await retryWithBackoff(async () => {
return await driftClient.placeAndTakePerpOrder(orderParams)
}, 3, 8000) // 8s base delay, 3 max retries
let txSig: string
if (params.percentToClose === 100) {
// USE SDK'S BUILT-IN CLOSE - Gets EXACT position size from Drift internally
console.log('🚀 Using SDK closePosition() for 100% close (exact BN, no truncation)...')
txSig = await retryWithBackoff(async () => {
return await driftClient.closePosition(marketConfig.driftMarketIndex)
}, 3, 8000) // 8s base delay, 3 max retries
} else {
// Partial close - use our calculation (partial closes are less critical)
const orderParams = {
orderType: OrderType.MARKET,
marketIndex: marketConfig.driftMarketIndex,
direction: position.side === 'long'
? PositionDirection.SHORT
: PositionDirection.LONG,
baseAssetAmount: new BN(Math.floor(sizeToClose * 1e9)), // 9 decimals
reduceOnly: true,
}
console.log('🚀 Placing partial close order with placeAndTakePerpOrder...')
txSig = await retryWithBackoff(async () => {
return await driftClient.placeAndTakePerpOrder(orderParams)
}, 3, 8000)
}
console.log(`✅ Close order executed! Transaction: ${txSig}`)