package main import ( "context" "flag" "fmt" "log/slog" "os" "os/signal" "syscall" "time" "github.com/ethereum/go-ethereum/ethclient" "coppertone.tech/fraktal/mev-bot/pkg/arbitrage" "coppertone.tech/fraktal/mev-bot/pkg/cache" "coppertone.tech/fraktal/mev-bot/pkg/discovery" "coppertone.tech/fraktal/mev-bot/pkg/execution" "coppertone.tech/fraktal/mev-bot/pkg/observability" ) func main() { // Command line flags rpcURL := flag.String("rpc", os.Getenv("ARBITRUM_RPC_URL"), "Arbitrum RPC URL") scanInterval := flag.Duration("interval", 30*time.Second, "Scan interval") minProfitBPS := flag.Int64("min-profit", 10, "Minimum profit in basis points (10 = 0.1%)") flag.Parse() if *rpcURL == "" { fmt.Println("Error: ARBITRUM_RPC_URL environment variable not set") fmt.Println("Usage: export ARBITRUM_RPC_URL=https://arb1.arbitrum.io/rpc") os.Exit(1) } // Setup logger logger := observability.NewLogger(slog.LevelInfo) logger.Info("MEV Flash Loan Bot starting", "rpcURL", *rpcURL, "scanInterval", scanInterval.String(), "minProfitBPS", *minProfitBPS, ) // Connect to Arbitrum client, err := ethclient.Dial(*rpcURL) if err != nil { logger.Error("failed to connect to Arbitrum", "error", err) os.Exit(1) } defer client.Close() ctx, cancel := context.WithCancel(context.Background()) defer cancel() // Verify connection chainID, err := client.ChainID(ctx) if err != nil { logger.Error("failed to get chain ID", "error", err) os.Exit(1) } logger.Info("connected to Arbitrum", "chainID", chainID.String()) // Initialize components poolCache := cache.NewPoolCache() logger.Info("pool cache initialized") // Discover pools logger.Info("discovering UniswapV2 pools on Arbitrum...") poolDiscovery, err := discovery.NewUniswapV2PoolDiscovery(client, poolCache) if err != nil { logger.Error("failed to create pool discovery", "error", err) os.Exit(1) } poolCount, err := poolDiscovery.DiscoverMajorPools(ctx) if err != nil { logger.Error("failed to discover pools", "error", err) os.Exit(1) } logger.Info("pool discovery complete", "poolsFound", poolCount) // Initialize arbitrage detector arbConfig := arbitrage.Config{ MinProfitBPS: *minProfitBPS, MaxGasCostWei: 1e16, // 0.01 ETH SlippageBPS: 50, // 0.5% MinLiquidityUSD: 10000, } detector, err := arbitrage.NewSimpleDetector(poolCache, logger, arbConfig) if err != nil { logger.Error("failed to create detector", "error", err) os.Exit(1) } logger.Info("arbitrage detector initialized", "minProfitBPS", arbConfig.MinProfitBPS) // Initialize flash loan executor (simulation mode until contract deployed) execConfig := execution.DefaultConfig() executor, err := execution.NewFlashLoanExecutor( client, nil, // No signer yet - will simulate only logger, execConfig, ) if err != nil { // This will fail without a signer, which is expected for now logger.Warn("flash loan executor in simulation mode", "reason", "no signer configured") } // Setup graceful shutdown sigChan := make(chan os.Signal, 1) signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) // Main loop ticker := time.NewTicker(*scanInterval) defer ticker.Stop() blockNumber := uint64(0) logger.Info("starting arbitrage scanner", "interval", scanInterval.String()) logger.Info("") logger.Info("=== BOT READY ===") logger.Info("Scanning for profitable arbitrage opportunities...") logger.Info("Press Ctrl+C to stop") logger.Info("") for { select { case <-ticker.C: blockNumber++ // Scan for opportunities opportunities, err := detector.ScanForOpportunities(ctx, blockNumber) if err != nil { logger.Error("scan failed", "error", err) continue } if len(opportunities) > 0 { logger.Info("opportunities found!", "count", len(opportunities)) for i, opp := range opportunities { logger.Info(fmt.Sprintf("Opportunity #%d", i+1), "inputToken", opp.InputToken.Hex(), "bridgeToken", opp.BridgeToken.Hex(), "inputAmount", opp.InputAmount.String(), "outputAmount", opp.OutputAmount.String(), "profitAmount", opp.ProfitAmount.String(), "profitBPS", opp.ProfitBPS.String(), "pool1", opp.FirstPool.Address.Hex(), "pool2", opp.SecondPool.Address.Hex(), ) // Simulate execution if executor != nil { result, err := executor.SimulateExecution(opp) if err != nil { logger.Error("simulation failed", "error", err) continue } logger.Info("simulation result", "grossProfit", result.GrossProfit.String(), "flashLoanFee", result.FlashLoanFee.String(), "netProfit", result.NetProfit.String(), "estimatedGas", result.EstimatedGas.String(), "finalProfit", result.FinalProfit.String(), "profitable", result.IsProfitable, ) if result.IsProfitable { logger.Info("✅ PROFITABLE OPPORTUNITY FOUND!") logger.Info("Deploy flash loan contract to execute") logger.Info("See contracts/DEPLOY.md for instructions") } } } } else { logger.Debug("no opportunities found", "block", blockNumber) } // Show stats oppsFound, lastBlock := detector.GetStats() logger.Info("scan complete", "block", blockNumber, "totalOpportunities", oppsFound, "lastScanBlock", lastBlock, ) case <-sigChan: logger.Info("shutdown signal received") logger.Info("shutting down gracefully...") // Print final stats oppsFound, _ := detector.GetStats() logger.Info("final statistics", "totalOpportunitiesFound", oppsFound, ) if executor != nil { execCount, totalProfit := executor.GetStats() logger.Info("execution statistics", "executedCount", execCount, "totalProfit", totalProfit.String(), ) } logger.Info("shutdown complete") return } } }