package arbitrage import ( "context" "fmt" "math/big" "sync" "time" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/ethclient" "github.com/fraktal/mev-beta/internal/config" "github.com/fraktal/mev-beta/internal/logger" "github.com/fraktal/mev-beta/pkg/security" ) // ArbitrageService orchestrates arbitrage detection and execution type ArbitrageService struct { client *ethclient.Client logger *logger.Logger config *config.ArbitrageConfig // Core components marketManager *market.MarketManager multiHopScanner *MultiHopScanner executor *ArbitrageExecutor eventMonitor *monitor.Monitor poolDiscovery *pools.PoolDiscovery // Channels for communication eventsChan chan *events.SwapEvent arbitrageChan chan *ArbitrageOpportunity resultsChan chan *ExecutionResult // State management isRunning bool runMutex sync.RWMutex ctx context.Context cancel context.CancelFunc // Metrics and monitoring stats *ArbitrageStats statsMutex sync.RWMutex // Risk management maxConcurrentExecutions int executionSemaphore chan struct{} // Database integration database ArbitrageDatabase } // ArbitrageOpportunity represents a detected arbitrage opportunity type ArbitrageOpportunity struct { ID string Path *ArbitragePath TriggerEvent *events.SwapEvent DetectedAt time.Time EstimatedProfit *big.Int RequiredAmount *big.Int Urgency int // 1-10 priority level ExpiresAt time.Time } // ArbitrageStats contains service statistics type ArbitrageStats struct { TotalOpportunitiesDetected int64 TotalOpportunitiesExecuted int64 TotalSuccessfulExecutions int64 TotalProfitRealized *big.Int TotalGasSpent *big.Int AverageExecutionTime time.Duration LastExecutionTime time.Time } // ArbitrageDatabase interface for persistence type ArbitrageDatabase interface { SaveOpportunity(ctx context.Context, opportunity *ArbitrageOpportunity) error SaveExecution(ctx context.Context, result *ExecutionResult) error GetExecutionHistory(ctx context.Context, limit int) ([]*ExecutionResult, error) SavePoolData(ctx context.Context, poolData *pools.PoolData) error GetPoolData(ctx context.Context, poolAddress common.Address) (*pools.PoolData, error) } // NewArbitrageService creates a new arbitrage service func NewArbitrageService( client *ethclient.Client, logger *logger.Logger, config *config.ArbitrageConfig, keyManager *security.KeyManager, database ArbitrageDatabase, ) (*ArbitrageService, error) { ctx, cancel := context.WithCancel(context.Background()) // Create market manager marketManager := market.NewMarketManager(logger, client) // Create multi-hop scanner multiHopScanner := NewMultiHopScanner(logger, marketManager) // Create arbitrage executor executor, err := NewArbitrageExecutor( client, logger, keyManager, common.HexToAddress(config.ArbitrageContractAddress), common.HexToAddress(config.FlashSwapContractAddress), ) if err != nil { cancel() return nil, fmt.Errorf("failed to create arbitrage executor: %w", err) } // Create event monitor eventMonitor := monitor.NewMonitor(logger, client) // Create pool discovery service poolDiscovery, err := pools.NewPoolDiscovery(client, logger, config.PoolDiscoveryConfig) if err != nil { cancel() return nil, fmt.Errorf("failed to create pool discovery: %w", err) } // Initialize stats stats := &ArbitrageStats{ TotalProfitRealized: big.NewInt(0), TotalGasSpent: big.NewInt(0), } service := &ArbitrageService{ client: client, logger: logger, config: config, marketManager: marketManager, multiHopScanner: multiHopScanner, executor: executor, eventMonitor: eventMonitor, poolDiscovery: poolDiscovery, eventsChan: make(chan *events.SwapEvent, 1000), arbitrageChan: make(chan *ArbitrageOpportunity, 100), resultsChan: make(chan *ExecutionResult, 100), ctx: ctx, cancel: cancel, stats: stats, maxConcurrentExecutions: config.MaxConcurrentExecutions, executionSemaphore: make(chan struct{}, config.MaxConcurrentExecutions), database: database, } return service, nil } // Start begins the arbitrage service func (as *ArbitrageService) Start() error { as.runMutex.Lock() defer as.runMutex.Unlock() if as.isRunning { return fmt.Errorf("arbitrage service is already running") } as.logger.Info("Starting arbitrage service...") // Start pool discovery if err := as.poolDiscovery.Start(as.ctx); err != nil { return fmt.Errorf("failed to start pool discovery: %w", err) } // Start market manager if err := as.marketManager.Start(as.ctx); err != nil { return fmt.Errorf("failed to start market manager: %w", err) } // Start event monitor if err := as.eventMonitor.Start(as.ctx); err != nil { return fmt.Errorf("failed to start event monitor: %w", err) } // Start worker goroutines go as.eventProcessor() go as.arbitrageDetector() go as.arbitrageExecutor() go as.resultProcessor() go as.poolDataProcessor() go as.statsUpdater() as.isRunning = true as.logger.Info("Arbitrage service started successfully") return nil } // Stop stops the arbitrage service func (as *ArbitrageService) Stop() error { as.runMutex.Lock() defer as.runMutex.Unlock() if !as.isRunning { return nil } as.logger.Info("Stopping arbitrage service...") // Cancel context to stop all workers as.cancel() // Stop components as.eventMonitor.Stop() as.marketManager.Stop() as.poolDiscovery.Stop() // Close channels close(as.eventsChan) close(as.arbitrageChan) close(as.resultsChan) as.isRunning = false as.logger.Info("Arbitrage service stopped") return nil } // eventProcessor processes incoming swap events func (as *ArbitrageService) eventProcessor() { defer as.logger.Info("Event processor stopped") // Subscribe to swap events from the event monitor swapEvents := as.eventMonitor.SubscribeToSwapEvents() for { select { case <-as.ctx.Done(): return case event := <-swapEvents: if event != nil { as.processSwapEvent(event) } } } } // processSwapEvent processes a single swap event for arbitrage opportunities func (as *ArbitrageService) processSwapEvent(event *events.SwapEvent) { as.logger.Debug(fmt.Sprintf("Processing swap event: token0=%s, token1=%s, amount0=%s, amount1=%s", event.Token0.Hex(), event.Token1.Hex(), event.Amount0.String(), event.Amount1.String())) // Check if this swap is large enough to potentially move prices if !as.isSignificantSwap(event) { return } // Forward to arbitrage detection select { case as.eventsChan <- event: case <-as.ctx.Done(): return default: as.logger.Warn("Event channel full, dropping swap event") } } // isSignificantSwap checks if a swap is large enough to create arbitrage opportunities func (as *ArbitrageService) isSignificantSwap(event *events.SwapEvent) bool { // Convert amounts to absolute values for comparison amount0Abs := new(big.Int).Abs(event.Amount0) amount1Abs := new(big.Int).Abs(event.Amount1) // Check if either amount is above our threshold minSwapSize := big.NewInt(as.config.MinSignificantSwapSize) return amount0Abs.Cmp(minSwapSize) > 0 || amount1Abs.Cmp(minSwapSize) > 0 } // arbitrageDetector detects arbitrage opportunities from swap events func (as *ArbitrageService) arbitrageDetector() { defer as.logger.Info("Arbitrage detector stopped") for { select { case <-as.ctx.Done(): return case event := <-as.eventsChan: if event != nil { as.detectArbitrageOpportunities(event) } } } } // detectArbitrageOpportunities scans for arbitrage opportunities triggered by an event func (as *ArbitrageService) detectArbitrageOpportunities(event *events.SwapEvent) { start := time.Now() // Determine the tokens involved in potential arbitrage tokens := []common.Address{event.Token0, event.Token1} var allOpportunities []*ArbitrageOpportunity // Scan for opportunities starting with each token for _, token := range tokens { // Determine appropriate amount to use for scanning scanAmount := as.calculateScanAmount(event, token) // Use multi-hop scanner to find arbitrage paths paths, err := as.multiHopScanner.ScanForArbitrage(as.ctx, token, scanAmount) if err != nil { as.logger.Debug(fmt.Sprintf("Arbitrage scan failed for token %s: %v", token.Hex(), err)) continue } // Convert paths to opportunities for _, path := range paths { if as.isValidOpportunity(path) { opportunity := &ArbitrageOpportunity{ ID: as.generateOpportunityID(path, event), Path: path, TriggerEvent: event, DetectedAt: time.Now(), EstimatedProfit: path.NetProfit, RequiredAmount: scanAmount, Urgency: as.calculateUrgency(path), ExpiresAt: time.Now().Add(as.config.OpportunityTTL), } allOpportunities = append(allOpportunities, opportunity) } } } // Sort opportunities by urgency and profit as.rankOpportunities(allOpportunities) // Send top opportunities for execution maxOpportunities := as.config.MaxOpportunitiesPerEvent for i, opportunity := range allOpportunities { if i >= maxOpportunities { break } // Update stats as.statsMutex.Lock() as.stats.TotalOpportunitiesDetected++ as.statsMutex.Unlock() // Save to database if err := as.database.SaveOpportunity(as.ctx, opportunity); err != nil { as.logger.Warn(fmt.Sprintf("Failed to save opportunity to database: %v", err)) } // Send for execution select { case as.arbitrageChan <- opportunity: case <-as.ctx.Done(): return default: as.logger.Warn("Arbitrage channel full, dropping opportunity") } } elapsed := time.Since(start) as.logger.Debug(fmt.Sprintf("Arbitrage detection completed in %v: found %d opportunities", elapsed, len(allOpportunities))) } // isValidOpportunity validates an arbitrage opportunity func (as *ArbitrageService) isValidOpportunity(path *ArbitragePath) bool { // Check minimum profit threshold minProfit := big.NewInt(as.config.MinProfitWei) if path.NetProfit.Cmp(minProfit) < 0 { return false } // Check minimum ROI if path.ROI < as.config.MinROIPercent { return false } // Check path freshness if time.Since(path.LastUpdated) > as.config.MaxPathAge { return false } // Check gas profitability currentGasPrice, err := as.client.SuggestGasPrice(as.ctx) if err != nil { // Assume worst case if we can't get gas price currentGasPrice = big.NewInt(as.config.MaxGasPriceWei) } return as.executor.IsProfitableAfterGas(path, currentGasPrice) } // calculateScanAmount determines the optimal amount to use for arbitrage scanning func (as *ArbitrageService) calculateScanAmount(event *events.SwapEvent, token common.Address) *big.Int { // Base amount on the swap size, but cap it for risk management var swapAmount *big.Int if token == event.Token0 { swapAmount = new(big.Int).Abs(event.Amount0) } else { swapAmount = new(big.Int).Abs(event.Amount1) } // Use a fraction of the swap amount (default 10%) scanAmount := new(big.Int).Div(swapAmount, big.NewInt(10)) // Ensure minimum scan amount minAmount := big.NewInt(as.config.MinScanAmountWei) if scanAmount.Cmp(minAmount) < 0 { scanAmount = minAmount } // Cap maximum scan amount maxAmount := big.NewInt(as.config.MaxScanAmountWei) if scanAmount.Cmp(maxAmount) > 0 { scanAmount = maxAmount } return scanAmount } // calculateUrgency calculates the urgency level of an opportunity func (as *ArbitrageService) calculateUrgency(path *ArbitragePath) int { // Base urgency on ROI urgency := int(path.ROI / 2) // 2% ROI = urgency level 1 // Boost urgency for higher profits profitETH := new(big.Float).SetInt(path.NetProfit) profitETH.Quo(profitETH, big.NewFloat(1e18)) profitFloat, _ := profitETH.Float64() if profitFloat > 1.0 { // > 1 ETH profit urgency += 5 } else if profitFloat > 0.1 { // > 0.1 ETH profit urgency += 2 } // Cap urgency between 1 and 10 if urgency < 1 { urgency = 1 } if urgency > 10 { urgency = 10 } return urgency } // rankOpportunities sorts opportunities by priority func (as *ArbitrageService) rankOpportunities(opportunities []*ArbitrageOpportunity) { // Sort by urgency (descending) then by profit (descending) for i := 0; i < len(opportunities); i++ { for j := i + 1; j < len(opportunities); j++ { iOpp := opportunities[i] jOpp := opportunities[j] if jOpp.Urgency > iOpp.Urgency { opportunities[i], opportunities[j] = opportunities[j], opportunities[i] } else if jOpp.Urgency == iOpp.Urgency { if jOpp.EstimatedProfit.Cmp(iOpp.EstimatedProfit) > 0 { opportunities[i], opportunities[j] = opportunities[j], opportunities[i] } } } } } // arbitrageExecutor executes arbitrage opportunities func (as *ArbitrageService) arbitrageExecutor() { defer as.logger.Info("Arbitrage executor stopped") for { select { case <-as.ctx.Done(): return case opportunity := <-as.arbitrageChan: if opportunity != nil { // Rate limit concurrent executions select { case as.executionSemaphore <- struct{}{}: go as.executeOpportunity(opportunity) case <-as.ctx.Done(): return default: as.logger.Warn("Too many concurrent executions, dropping opportunity") } } } } } // executeOpportunity executes a single arbitrage opportunity func (as *ArbitrageService) executeOpportunity(opportunity *ArbitrageOpportunity) { defer func() { <-as.executionSemaphore // Release semaphore }() // Check if opportunity is still valid if time.Now().After(opportunity.ExpiresAt) { as.logger.Debug(fmt.Sprintf("Opportunity %s expired", opportunity.ID)) return } // Update stats as.statsMutex.Lock() as.stats.TotalOpportunitiesExecuted++ as.statsMutex.Unlock() // Prepare execution parameters params := &ArbitrageParams{ Path: opportunity.Path, InputAmount: opportunity.RequiredAmount, MinOutputAmount: as.calculateMinOutput(opportunity), Deadline: big.NewInt(time.Now().Add(5 * time.Minute).Unix()), FlashSwapData: []byte{}, // Additional data if needed } as.logger.Info(fmt.Sprintf("Executing arbitrage opportunity %s with estimated profit %s ETH", opportunity.ID, formatEther(opportunity.EstimatedProfit))) // Execute the arbitrage result, err := as.executor.ExecuteArbitrage(as.ctx, params) if err != nil { as.logger.Error(fmt.Sprintf("Arbitrage execution failed for opportunity %s: %v", opportunity.ID, err)) } // Send result for processing select { case as.resultsChan <- result: case <-as.ctx.Done(): return default: as.logger.Warn("Results channel full, dropping execution result") } } // calculateMinOutput calculates minimum acceptable output for slippage protection func (as *ArbitrageService) calculateMinOutput(opportunity *ArbitrageOpportunity) *big.Int { // Calculate expected output expectedOutput := new(big.Int).Add(opportunity.RequiredAmount, opportunity.EstimatedProfit) // Apply slippage tolerance slippageTolerance := as.config.SlippageTolerance slippageMultiplier := big.NewFloat(1.0 - slippageTolerance) expectedFloat := new(big.Float).SetInt(expectedOutput) minOutputFloat := new(big.Float).Mul(expectedFloat, slippageMultiplier) minOutput := new(big.Int) minOutputFloat.Int(minOutput) return minOutput } // resultProcessor processes execution results func (as *ArbitrageService) resultProcessor() { defer as.logger.Info("Result processor stopped") for { select { case <-as.ctx.Done(): return case result := <-as.resultsChan: if result != nil { as.processExecutionResult(result) } } } } // processExecutionResult processes a single execution result func (as *ArbitrageService) processExecutionResult(result *ExecutionResult) { // Update statistics as.statsMutex.Lock() if result.Success { as.stats.TotalSuccessfulExecutions++ as.stats.TotalProfitRealized.Add(as.stats.TotalProfitRealized, result.ProfitRealized) } gasCost := new(big.Int).Mul(result.GasPrice, big.NewInt(int64(result.GasUsed))) as.stats.TotalGasSpent.Add(as.stats.TotalGasSpent, gasCost) as.stats.LastExecutionTime = time.Now() as.statsMutex.Unlock() // Save to database if err := as.database.SaveExecution(as.ctx, result); err != nil { as.logger.Warn(fmt.Sprintf("Failed to save execution result to database: %v", err)) } // Log results if result.Success { as.logger.Info(fmt.Sprintf("Arbitrage execution successful: TX %s, Profit: %s ETH, Gas: %d", result.TransactionHash.Hex(), formatEther(result.ProfitRealized), result.GasUsed)) } else { as.logger.Error(fmt.Sprintf("Arbitrage execution failed: TX %s, Error: %v", result.TransactionHash.Hex(), result.Error)) } } // poolDataProcessor handles pool data updates and saves them to database func (as *ArbitrageService) poolDataProcessor() { defer as.logger.Info("Pool data processor stopped") // Subscribe to pool discovery events poolEvents := as.poolDiscovery.SubscribeToPoolEvents() for { select { case <-as.ctx.Done(): return case poolData := <-poolEvents: if poolData != nil { // Save pool data to database if err := as.database.SavePoolData(as.ctx, poolData); err != nil { as.logger.Warn(fmt.Sprintf("Failed to save pool data: %v", err)) } // Update market manager with new pool as.marketManager.AddPool(poolData) as.logger.Debug(fmt.Sprintf("Added new pool: %s (%s/%s)", poolData.Address.Hex(), poolData.Token0.Hex(), poolData.Token1.Hex())) } } } } // statsUpdater periodically logs service statistics func (as *ArbitrageService) statsUpdater() { defer as.logger.Info("Stats updater stopped") ticker := time.NewTicker(as.config.StatsUpdateInterval) defer ticker.Stop() for { select { case <-as.ctx.Done(): return case <-ticker.C: as.logStats() } } } // logStats logs current service statistics func (as *ArbitrageService) logStats() { as.statsMutex.RLock() stats := *as.stats as.statsMutex.RUnlock() successRate := 0.0 if stats.TotalOpportunitiesExecuted > 0 { successRate = float64(stats.TotalSuccessfulExecutions) / float64(stats.TotalOpportunitiesExecuted) * 100 } as.logger.Info(fmt.Sprintf("Arbitrage Service Stats - Detected: %d, Executed: %d, Success Rate: %.2f%%, "+ "Total Profit: %s ETH, Total Gas: %s ETH", stats.TotalOpportunitiesDetected, stats.TotalOpportunitiesExecuted, successRate, formatEther(stats.TotalProfitRealized), formatEther(stats.TotalGasSpent))) } // generateOpportunityID generates a unique ID for an arbitrage opportunity func (as *ArbitrageService) generateOpportunityID(path *ArbitragePath, event *events.SwapEvent) string { return fmt.Sprintf("%s_%s_%d", event.TxHash.Hex()[:10], path.Tokens[0].Hex()[:8], time.Now().UnixNano()) } // GetStats returns current service statistics func (as *ArbitrageService) GetStats() *ArbitrageStats { as.statsMutex.RLock() defer as.statsMutex.RUnlock() // Return a copy to avoid race conditions statsCopy := *as.stats return &statsCopy } // IsRunning returns whether the service is currently running func (as *ArbitrageService) IsRunning() bool { as.runMutex.RLock() defer as.runMutex.RUnlock() return as.isRunning }