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