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:
@@ -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}`)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user