package execution import ( "context" "fmt" "math/big" "time" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/ethclient" "github.com/fraktal/mev-beta/internal/logger" "github.com/fraktal/mev-beta/pkg/types" ) // ExecutionMode defines how opportunities should be executed type ExecutionMode int const ( // SimulationMode only simulates execution without sending transactions SimulationMode ExecutionMode = iota // DryRunMode validates transactions but doesn't send DryRunMode // LiveMode executes real transactions on-chain LiveMode ) // ExecutionResult represents the result of an arbitrage execution type ExecutionResult struct { OpportunityID string Success bool TxHash common.Hash GasUsed uint64 ActualProfit *big.Int EstimatedProfit *big.Int SlippagePercent float64 ExecutionTime time.Duration Error error Timestamp time.Time } // ExecutionConfig holds configuration for the executor type ExecutionConfig struct { Mode ExecutionMode MaxGasPrice *big.Int // Maximum gas price willing to pay (wei) MaxSlippage float64 // Maximum slippage tolerance (0.05 = 5%) MinProfitThreshold *big.Int // Minimum profit to execute (wei) SimulationRPCURL string // RPC URL for simulation/fork testing FlashLoanProvider string // "aave", "uniswap", "balancer" MaxRetries int // Maximum execution retries RetryDelay time.Duration EnableParallelExec bool // Execute multiple opportunities in parallel DryRun bool // If true, don't send transactions } // ArbitrageExecutor handles execution of arbitrage opportunities type ArbitrageExecutor struct { config *ExecutionConfig client *ethclient.Client logger *logger.Logger flashLoan FlashLoanProvider slippage *SlippageProtector simulator *ExecutionSimulator resultsChan chan *ExecutionResult stopChan chan struct{} } // FlashLoanProvider interface for different flash loan protocols type FlashLoanProvider interface { // ExecuteFlashLoan executes an arbitrage opportunity using flash loans ExecuteFlashLoan(ctx context.Context, opportunity *types.ArbitrageOpportunity, config *ExecutionConfig) (*ExecutionResult, error) // GetMaxLoanAmount returns maximum loan amount available for a token GetMaxLoanAmount(ctx context.Context, token common.Address) (*big.Int, error) // GetFee returns the flash loan fee for a given amount GetFee(ctx context.Context, amount *big.Int) (*big.Int, error) // SupportsToken checks if the provider supports a given token SupportsToken(token common.Address) bool } // SlippageProtector handles slippage protection and validation type SlippageProtector struct { maxSlippage float64 logger *logger.Logger } // ExecutionSimulator simulates trades on a fork before real execution type ExecutionSimulator struct { forkClient *ethclient.Client logger *logger.Logger } // NewArbitrageExecutor creates a new arbitrage executor func NewArbitrageExecutor( config *ExecutionConfig, client *ethclient.Client, logger *logger.Logger, ) (*ArbitrageExecutor, error) { if config == nil { return nil, fmt.Errorf("execution config cannot be nil") } executor := &ArbitrageExecutor{ config: config, client: client, logger: logger, resultsChan: make(chan *ExecutionResult, 100), stopChan: make(chan struct{}), } // Initialize slippage protector executor.slippage = &SlippageProtector{ maxSlippage: config.MaxSlippage, logger: logger, } // Initialize simulator if simulation RPC is provided if config.SimulationRPCURL != "" { forkClient, err := ethclient.Dial(config.SimulationRPCURL) if err != nil { logger.Warn(fmt.Sprintf("Failed to connect to simulation RPC: %v", err)) } else { executor.simulator = &ExecutionSimulator{ forkClient: forkClient, logger: logger, } logger.Info("Execution simulator initialized") } } // Initialize flash loan provider switch config.FlashLoanProvider { case "aave": executor.flashLoan = NewAaveFlashLoanProvider(client, logger) logger.Info("Using Aave flash loans") case "uniswap": executor.flashLoan = NewUniswapFlashLoanProvider(client, logger) logger.Info("Using Uniswap flash swaps") case "balancer": executor.flashLoan = NewBalancerFlashLoanProvider(client, logger) logger.Info("Using Balancer flash loans") default: logger.Warn(fmt.Sprintf("Unknown flash loan provider: %s, using Aave", config.FlashLoanProvider)) executor.flashLoan = NewAaveFlashLoanProvider(client, logger) } return executor, nil } // ExecuteOpportunity executes an arbitrage opportunity func (ae *ArbitrageExecutor) ExecuteOpportunity(ctx context.Context, opportunity *types.ArbitrageOpportunity) (*ExecutionResult, error) { startTime := time.Now() ae.logger.Info(fmt.Sprintf("๐ŸŽฏ Executing arbitrage opportunity: %s", opportunity.ID)) // Step 1: Validate opportunity is still profitable if !ae.validateOpportunity(opportunity) { return &ExecutionResult{ OpportunityID: opportunity.ID, Success: false, Error: fmt.Errorf("opportunity validation failed"), Timestamp: time.Now(), }, nil } // Step 2: Check slippage limits if err := ae.slippage.ValidateSlippage(opportunity); err != nil { ae.logger.Warn(fmt.Sprintf("Slippage validation failed: %v", err)) return &ExecutionResult{ OpportunityID: opportunity.ID, Success: false, Error: fmt.Errorf("slippage too high: %w", err), Timestamp: time.Now(), }, nil } // Step 3: Simulate execution if simulator available if ae.simulator != nil && ae.config.Mode != LiveMode { simulationResult, err := ae.simulator.Simulate(ctx, opportunity, ae.config) if err != nil { ae.logger.Error(fmt.Sprintf("Simulation failed: %v", err)) return &ExecutionResult{ OpportunityID: opportunity.ID, Success: false, Error: fmt.Errorf("simulation failed: %w", err), Timestamp: time.Now(), }, nil } // If in simulation mode, return simulation result if ae.config.Mode == SimulationMode { simulationResult.ExecutionTime = time.Since(startTime) return simulationResult, nil } ae.logger.Info(fmt.Sprintf("Simulation succeeded: profit=%s ETH", simulationResult.ActualProfit.String())) } // Step 4: Execute via flash loan (if not in dry-run mode) if ae.config.DryRun || ae.config.Mode == DryRunMode { ae.logger.Info("Dry-run mode: skipping real execution") return &ExecutionResult{ OpportunityID: opportunity.ID, Success: true, EstimatedProfit: opportunity.NetProfit, Error: nil, ExecutionTime: time.Since(startTime), Timestamp: time.Now(), }, nil } // Step 5: Real execution result, err := ae.flashLoan.ExecuteFlashLoan(ctx, opportunity, ae.config) if err != nil { ae.logger.Error(fmt.Sprintf("Flash loan execution failed: %v", err)) return &ExecutionResult{ OpportunityID: opportunity.ID, Success: false, Error: err, ExecutionTime: time.Since(startTime), Timestamp: time.Now(), }, err } result.ExecutionTime = time.Since(startTime) ae.logger.Info(fmt.Sprintf("โœ… Arbitrage executed successfully: profit=%s ETH, gas=%d", result.ActualProfit.String(), result.GasUsed)) // Send result to channel for monitoring select { case ae.resultsChan <- result: default: ae.logger.Warn("Results channel full, dropping result") } return result, nil } // validateOpportunity validates that an opportunity is still valid func (ae *ArbitrageExecutor) validateOpportunity(opp *types.ArbitrageOpportunity) bool { // Check minimum profit threshold if opp.NetProfit.Cmp(ae.config.MinProfitThreshold) < 0 { ae.logger.Debug(fmt.Sprintf("Opportunity below profit threshold: %s < %s", opp.NetProfit.String(), ae.config.MinProfitThreshold.String())) return false } // Check opportunity hasn't expired if time.Now().After(opp.ExpiresAt) { ae.logger.Debug("Opportunity has expired") return false } // Additional validation checks can be added here // - Re-fetch pool states // - Verify liquidity still available // - Check gas prices haven't spiked return true } // ValidateSlippage checks if slippage is within acceptable limits func (sp *SlippageProtector) ValidateSlippage(opp *types.ArbitrageOpportunity) error { // Calculate expected slippage based on pool liquidity // This is a simplified version - production would need more sophisticated calculation if opp.PriceImpact > sp.maxSlippage { return fmt.Errorf("slippage %.2f%% exceeds maximum %.2f%%", opp.PriceImpact*100, sp.maxSlippage*100) } return nil } // Simulate simulates execution on a fork func (es *ExecutionSimulator) Simulate( ctx context.Context, opportunity *types.ArbitrageOpportunity, config *ExecutionConfig, ) (*ExecutionResult, error) { es.logger.Info(fmt.Sprintf("๐Ÿงช Simulating arbitrage: %s", opportunity.ID)) // In a real implementation, this would: // 1. Fork the current blockchain state // 2. Execute the arbitrage path on the fork // 3. Validate results match expectations // 4. Return simulated result // For now, return a simulated success return &ExecutionResult{ OpportunityID: opportunity.ID, Success: true, ActualProfit: opportunity.NetProfit, EstimatedProfit: opportunity.NetProfit, SlippagePercent: 0.01, // 1% simulated slippage Timestamp: time.Now(), }, nil } // GetResultsChannel returns the channel for execution results func (ae *ArbitrageExecutor) GetResultsChannel() <-chan *ExecutionResult { return ae.resultsChan } // Stop stops the executor func (ae *ArbitrageExecutor) Stop() { close(ae.stopChan) ae.logger.Info("Arbitrage executor stopped") }