docs: Add Bug #91 to COMMON_PITFALLS.md and copilot-instructions.md
Bug #91: Math.floor truncation leaves fractional position remnants - Root cause: new BN(Math.floor(sizeToClose * 1e9)) truncates position sizes - Solution: SDK's closePosition() uses exact BN arithmetic (baseAssetAmount.abs()) - Added to Quick Reference Table in COMMON_PITFALLS.md - Added full documentation entry (~70 lines) - Updated Top 10 Critical Pitfalls in copilot-instructions.md - Replaced #75 (Wrong Year) with #91 (more impactful to trading operations)
This commit is contained in:
2
.github/copilot-instructions.md
vendored
2
.github/copilot-instructions.md
vendored
@@ -879,7 +879,7 @@ docs/COMMON_PITFALLS.md
|
|||||||
1. **Position Manager Never Monitors (#77)** - Logs say "added" but isMonitoring=false = $1,000+ losses
|
1. **Position Manager Never Monitors (#77)** - Logs say "added" but isMonitoring=false = $1,000+ losses
|
||||||
2. **Silent SL Placement Failure (#76)** - placeExitOrders() returns SUCCESS with 2/3 orders, no SL protection
|
2. **Silent SL Placement Failure (#76)** - placeExitOrders() returns SUCCESS with 2/3 orders, no SL protection
|
||||||
3. **Orphan Cleanup Removes Active Orders (#78)** - cancelAllOrders() affects ALL positions on symbol
|
3. **Orphan Cleanup Removes Active Orders (#78)** - cancelAllOrders() affects ALL positions on symbol
|
||||||
4. **Wrong Year in SQL Queries (#75)** - Query 2024 dates when current is 2025 = 12× inflated results
|
4. **Math.floor Truncation in Position Close (#91)** - Leaves fractional remnants → Use SDK closePosition()
|
||||||
5. **Drift SDK Memory Leak (#1)** - JS heap OOM after 10+ hours → Smart health monitoring
|
5. **Drift SDK Memory Leak (#1)** - JS heap OOM after 10+ hours → Smart health monitoring
|
||||||
6. **Wrong RPC Provider (#2)** - Alchemy breaks Drift SDK → Use Helius only
|
6. **Wrong RPC Provider (#2)** - Alchemy breaks Drift SDK → Use Helius only
|
||||||
7. **P&L Compounding Race Condition (#48, #49, #61)** - Multiple closures → Atomic Map.delete()
|
7. **P&L Compounding Race Condition (#48, #49, #61)** - Multiple closures → Atomic Map.delete()
|
||||||
|
|||||||
@@ -99,6 +99,8 @@ This document is the **comprehensive reference** for all documented pitfalls, bu
|
|||||||
| 71 | 🔴 CRITICAL | Revenge System | Dec 3, 2025 | Revenge system missing external closure integration |
|
| 71 | 🔴 CRITICAL | Revenge System | Dec 3, 2025 | Revenge system missing external closure integration |
|
||||||
| 72 | 🔴 CRITICAL | Telegram | Dec 4, 2025 | Telegram webhook conflicts with polling bot |
|
| 72 | 🔴 CRITICAL | Telegram | Dec 4, 2025 | Telegram webhook conflicts with polling bot |
|
||||||
| 89 | 🔴 CRITICAL | Drift Protocol | Dec 16, 2025 | Drift fractional position remnants after SL execution |
|
| 89 | 🔴 CRITICAL | Drift Protocol | Dec 16, 2025 | Drift fractional position remnants after SL execution |
|
||||||
|
| 90 | 🔴 CRITICAL | Drift Protocol | Dec 31, 2025 | placePerpOrder vs placeAndTakePerpOrder - MARKET orders not filling |
|
||||||
|
| 91 | 🔴 CRITICAL | Drift Protocol | Jan 1, 2026 | Math.floor truncation leaves fractional position remnants |
|
||||||
| 90 | 🔴 CRITICAL | Drift Protocol | Dec 31, 2025 | placePerpOrder only places orders, doesn't fill - use placeAndTakePerpOrder |
|
| 90 | 🔴 CRITICAL | Drift Protocol | Dec 31, 2025 | placePerpOrder only places orders, doesn't fill - use placeAndTakePerpOrder |
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -1749,6 +1751,85 @@ docker logs -f trading-bot-v4 | grep "placeAndTakePerpOrder\|IMMEDIATE FILL"
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
### 91. Math.floor Truncation Leaves Fractional Position Remnants (CRITICAL - Jan 1, 2026)
|
||||||
|
|
||||||
|
**Severity:** 🔴 CRITICAL
|
||||||
|
**Category:** Drift Protocol / Floating Point Math
|
||||||
|
**Date Discovered:** January 1, 2026
|
||||||
|
**Financial Impact:** Flip failures causing positions to not close, leading to $1,000+ losses
|
||||||
|
|
||||||
|
**Symptom:** Position close transactions confirm but position still exists on Drift with tiny fractional remnant (e.g., 0.00000008 SOL)
|
||||||
|
|
||||||
|
**User Report:** "Drift UI 'Close All Positions' fails with 'not enough collateral' but clicking 'Market' order directly works"
|
||||||
|
|
||||||
|
**Root Cause:**
|
||||||
|
```typescript
|
||||||
|
// BROKEN CODE (lib/drift/orders.ts line ~680):
|
||||||
|
const sizeToClose = (trade.currentSize / currentPrice) * (percentToClose / 100)
|
||||||
|
const sizeToCloseNormalized = new BN(Math.floor(sizeToClose * 1e9)) // ❌ TRUNCATES!
|
||||||
|
|
||||||
|
// Example:
|
||||||
|
// Original position: 21.83456789012 SOL
|
||||||
|
// Math.floor(21.83456789012 * 1e9) = 21834567890 (loses 0.12 precision)
|
||||||
|
// Actual close: 21.834567890 SOL
|
||||||
|
// Remnant: 0.00000008 SOL still open on Drift
|
||||||
|
```
|
||||||
|
|
||||||
|
**Discovery Path:**
|
||||||
|
1. Bug #90 fix deployed (placeAndTakePerpOrder) - flip still failed
|
||||||
|
2. User tested Drift UI: "Close All Positions" failed, but "Market" order worked
|
||||||
|
3. Key insight: Market order uses EXACT position size from Drift state
|
||||||
|
4. Investigation revealed Math.floor truncation causes fractional remnants
|
||||||
|
|
||||||
|
**THE FIX (Jan 1, 2026):**
|
||||||
|
```typescript
|
||||||
|
// FIXED CODE (lib/drift/orders.ts lines 647-690):
|
||||||
|
if (percentToClose === 100) {
|
||||||
|
// Use SDK's closePosition() which handles exact BN internally
|
||||||
|
// SDK gets exact baseAssetAmount from position state (no truncation)
|
||||||
|
console.log(`🚀 Using SDK closePosition() for 100% close (exact BN, no truncation)...`)
|
||||||
|
|
||||||
|
txSig = await driftClient.closePosition(marketConfig.driftMarketIndex)
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// Partial close: Continue using placeAndTakePerpOrder with calculated size
|
||||||
|
// Truncation is acceptable for partial closes
|
||||||
|
const sizeToClose = (trade.currentSize / currentPrice) * (percentToClose / 100)
|
||||||
|
const sizeToCloseNormalized = new BN(Math.floor(sizeToClose * 1e9))
|
||||||
|
// ... placeAndTakePerpOrder code
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Why SDK closePosition() Works:**
|
||||||
|
- Drift SDK's `closePosition()` internally uses `position.baseAssetAmount.abs()`
|
||||||
|
- This is the EXACT BN value stored in Drift's state - no floating point conversion
|
||||||
|
- No truncation, no remnants, complete position closure guaranteed
|
||||||
|
|
||||||
|
**Files Changed:**
|
||||||
|
- lib/drift/orders.ts: Lines 647-690 (conditional closePosition vs placeAndTakePerpOrder)
|
||||||
|
|
||||||
|
**Prevention Rules:**
|
||||||
|
1. NEVER use Math.floor for 100% position closes - use SDK's closePosition()
|
||||||
|
2. Floating point arithmetic introduces truncation errors in BN conversion
|
||||||
|
3. For exact values, always let SDK read from on-chain state directly
|
||||||
|
4. Partial closes can tolerate truncation (tiny remnant acceptable)
|
||||||
|
|
||||||
|
**Red Flags Indicating This Bug:**
|
||||||
|
- Transaction confirms but position still shows in Drift UI
|
||||||
|
- Position size shows tiny remnant (< 0.001 tokens)
|
||||||
|
- "Close All Positions" button fails with collateral error
|
||||||
|
- Manual Market order works but automated close fails
|
||||||
|
|
||||||
|
**Git Commit:** 4bf5761 "critical: Bug #91 - Use SDK closePosition() for exact BN (no Math.floor truncation)"
|
||||||
|
|
||||||
|
**Deployment:** Jan 1, 2026 13:42 UTC (container trading-bot-v4)
|
||||||
|
|
||||||
|
**Status:** ✅ FIXED AND DEPLOYED
|
||||||
|
|
||||||
|
**Lesson Learned:** Never use floating point math to calculate position sizes for 100% closes. The Drift SDK provides `closePosition()` specifically for this purpose - it reads the exact BN value directly from on-chain state. Math.floor() truncation is invisible but catastrophic for exact position operations.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Appendix: Pattern Recognition
|
## Appendix: Pattern Recognition
|
||||||
|
|
||||||
### Common Root Causes
|
### Common Root Causes
|
||||||
|
|||||||
Reference in New Issue
Block a user