critical: Bug #93 - Three-layer entry price validation with oracle fallback

Root Cause: quoteAssetAmount/baseAssetAmount division producing garbage entry prices
- Found 6 autosync records with impossible prices (.18, 6.30, 7.11, 116.24 for SOL)
- Drift SDK values can be corrupted during state transitions

Fix Layer 1 (lib/drift/client.ts):
- Added per-asset price range validation (SOL: 0-000, BTC: 0k-00k, ETH: 00-0k)
- Returns null for invalid prices

Fix Layer 2 (lib/trading/sync-helper.ts):
- Added validatedEntryPrice calculation with oracle fallback
- Falls back to Pyth oracle when calculated price is garbage

Fix Layer 3 (lib/trading/sync-helper.ts):
- Trade creation uses validatedEntryPrice in all 4 price fields
- entryPrice, peakPrice, maxFavorablePrice, maxAdversePrice

Documentation:
- Full Bug #93 added to COMMON_PITFALLS.md with code examples
- Quick Reference table updated

Cleaned: 6 garbage autosync records deleted from database
This commit is contained in:
mindesbunister
2026-01-08 19:29:58 +01:00
parent bb2432f3bf
commit f57aa925b8
3 changed files with 235 additions and 13 deletions

View File

@@ -107,7 +107,31 @@ export async function syncSinglePosition(driftPos: any, positionManager: any): P
const direction = driftPos.side
const entryPrice = driftPos.entryPrice
// Calculate TP/SL prices
// BUG #89 FIX: Validate entry price is realistic before creating sync record
// Prevents garbage autosync records with impossible entry prices
const priceValidation: { [symbol: string]: { min: number; max: number } } = {
'SOL-PERP': { min: 50, max: 1000 }, // SOL: $50-$1000 range
'BTC-PERP': { min: 10000, max: 500000 }, // BTC: $10k-$500k range
'ETH-PERP': { min: 500, max: 20000 }, // ETH: $500-$20k range
}
const validation = priceValidation[driftPos.symbol]
if (validation && (entryPrice < validation.min || entryPrice > validation.max)) {
console.error(`❌ AUTOSYNC REJECTED: Invalid entry price $${entryPrice.toFixed(2)} for ${driftPos.symbol}`)
console.error(` Expected range: $${validation.min}-$${validation.max}`)
console.error(` Current oracle price: $${currentPrice.toFixed(2)}`)
console.error(` Position size: ${driftPos.size} tokens`)
console.error(` Using oracle price as fallback entry price`)
// Use oracle price as fallback instead of garbage entry price
// This is acceptable for syncing since we're just trying to track the position
}
// Use validated entry price (fallback to oracle if garbage)
const validatedEntryPrice = (validation && (entryPrice < validation.min || entryPrice > validation.max))
? currentPrice
: entryPrice
// Calculate TP/SL prices using validated entry
const calculatePrice = (entry: number, percent: number, dir: 'long' | 'short') => {
if (dir === 'long') {
return entry * (1 + percent / 100)
@@ -116,10 +140,10 @@ export async function syncSinglePosition(driftPos: any, positionManager: any): P
}
}
const stopLossPrice = calculatePrice(entryPrice, config.stopLossPercent, direction)
const tp1Price = calculatePrice(entryPrice, config.takeProfit1Percent, direction)
const tp2Price = calculatePrice(entryPrice, config.takeProfit2Percent, direction)
const emergencyStopPrice = calculatePrice(entryPrice, config.emergencyStopPercent, direction)
const stopLossPrice = calculatePrice(validatedEntryPrice, config.stopLossPercent, direction)
const tp1Price = calculatePrice(validatedEntryPrice, config.takeProfit1Percent, direction)
const tp2Price = calculatePrice(validatedEntryPrice, config.takeProfit2Percent, direction)
const emergencyStopPrice = calculatePrice(validatedEntryPrice, config.emergencyStopPercent, direction)
// Calculate position size in USD (Drift size is tokens)
const positionSizeUSD = Math.abs(driftPos.size) * currentPrice
@@ -286,7 +310,7 @@ export async function syncSinglePosition(driftPos: any, positionManager: any): P
positionId: syntheticPositionId,
symbol: driftPos.symbol,
direction,
entryPrice,
entryPrice: validatedEntryPrice, // BUG #89 FIX: Use validated entry price
entryTime: now,
positionSizeUSD,
collateralUSD: positionSizeUSD / config.leverage,
@@ -323,12 +347,12 @@ export async function syncSinglePosition(driftPos: any, positionManager: any): P
realizedPnL: 0,
unrealizedPnL: driftPos.unrealizedPnL ?? 0,
peakPnL: driftPos.unrealizedPnL ?? 0,
peakPrice: entryPrice,
peakPrice: validatedEntryPrice, // BUG #89 FIX: Use validated entry price
lastPrice: currentPrice,
maxFavorableExcursion: 0,
maxAdverseExcursion: 0,
maxFavorablePrice: entryPrice,
maxAdversePrice: entryPrice,
maxFavorablePrice: validatedEntryPrice, // BUG #89 FIX: Use validated entry price
maxAdversePrice: validatedEntryPrice, // BUG #89 FIX: Use validated entry price
lastUpdate: now.toISOString(),
// TP2 runner configuration (CRITICAL FIX - Dec 12, 2025)
tp1SizePercent: config.takeProfit1SizePercent,