package main import ( "context" "flag" "fmt" "log/slog" "math/big" "os" "os/signal" "syscall" "time" "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" gethtypes "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" "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%)") privateKeyHex := flag.String("private-key", os.Getenv("MEV_EXECUTOR_PK"), "Hex-encoded private key for execution (optional, simulation-only if empty)") refreshReserves := flag.Bool("refresh-reserves", true, "Refresh pool reserves from chain before each scan (requires RPC)") uniswapV4PoolManager := flag.String("uniswap-v4-poolmanager", "", "Optional: Uniswap v4 PoolManager address on Arbitrum (validated for bytecode presence)") 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 V2 pools", "error", err) os.Exit(1) } logger.Info("UniswapV2 pool discovery complete", "poolsFound", poolCount) // Discover UniswapV3 pools for cross-protocol arbitrage logger.Info("discovering UniswapV3 pools on Arbitrum...") v3Discovery, err := discovery.NewUniswapV3PoolDiscovery(client, poolCache) if err != nil { logger.Error("failed to create V3 pool discovery", "error", err) os.Exit(1) } v3PoolCount, err := v3Discovery.DiscoverMajorPools(ctx) if err != nil { logger.Error("failed to discover V3 pools", "error", err) os.Exit(1) } logger.Info("UniswapV3 pool discovery complete", "poolsFound", v3PoolCount) logger.Info("total pools discovered", "v2", poolCount, "v3", v3PoolCount, "total", poolCount+v3PoolCount) // 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) } // Wire RPC to detector for live gas price detector.WithRPC(client) logger.Info("arbitrage detector initialized", "minProfitBPS", arbConfig.MinProfitBPS) // Initialize flash loan executor (simulation mode until contract deployed) execConfig := execution.DefaultConfig() var txSigner *bind.TransactOpts if *privateKeyHex != "" { txSigner, err = buildTransactor(*privateKeyHex, chainID) if err != nil { logger.Error("invalid executor private key", "error", err) os.Exit(1) } logger.Info("execution signer configured", "from", txSigner.From.Hex()) } else { logger.Warn("flash loan executor running in simulation-only mode (no signer configured)") } executor, err := execution.NewFlashLoanExecutor(client, txSigner, logger, execConfig) if err != nil { logger.Error("failed to initialize flash loan executor", "error", err) os.Exit(1) } // Optional: validate Uniswap v4 PoolManager address if provided if *uniswapV4PoolManager != "" { pm := common.HexToAddress(*uniswapV4PoolManager) if err := discovery.VerifyContractDeployed(ctx, client, pm); err != nil { logger.Warn("uniswap v4 poolmanager validation failed", "address", pm.Hex(), "error", err) } else { logger.Info("uniswap v4 poolmanager validated", "address", pm.Hex()) } } // Setup graceful shutdown sigChan := make(chan os.Signal, 1) signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) // Shared scan function runScan := func(blockNumber uint64) { // Optional: refresh reserves for cached pools to avoid stale pricing if *refreshReserves { if err := discovery.RefreshUniswapV2Reserves(ctx, client, poolCache, logger); err != nil { logger.Warn("v2 reserve refresh failed", "error", err) } if err := discovery.RefreshUniswapV3Reserves(ctx, client, poolCache, logger); err != nil { logger.Warn("v3 reserve refresh failed", "error", err) } } // Scan for opportunities opportunities, err := detector.ScanForOpportunities(ctx, blockNumber) if err != nil { logger.Error("scan failed", "error", err, "block", blockNumber) return } if len(opportunities) > 0 { logger.Info("opportunities found!", "count", len(opportunities), "block", blockNumber) 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, ) } 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("") // Try real-time header subscription first headers := make(chan *gethtypes.Header, 16) sub, err := client.SubscribeNewHead(ctx, headers) if err != nil { logger.Warn("failed to subscribe to new heads, falling back to polling", "error", err) sub = nil } // Polling fallback ticker (also used if subscription drops) ticker := time.NewTicker(*scanInterval) defer ticker.Stop() for { if sub != nil { select { case head := <-headers: if head == nil { continue } runScan(head.Number.Uint64()) case err := <-sub.Err(): logger.Warn("header subscription ended, switching to polling", "error", err) sub.Unsubscribe() sub = nil case <-sigChan: logger.Info("shutdown signal received") logger.Info("shutting down gracefully...") goto shutdown } } else { select { case <-ticker.C: blockNumber, err := client.BlockNumber(ctx) if err != nil { logger.Error("failed to fetch block number", "error", err) continue } runScan(blockNumber) case <-sigChan: logger.Info("shutdown signal received") logger.Info("shutting down gracefully...") goto shutdown } } } shutdown: // 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") } // buildTransactor constructs a keyed transactor from a hex private key. func buildTransactor(hexKey string, chainID *big.Int) (*bind.TransactOpts, error) { key, err := crypto.HexToECDSA(trimHexPrefix(hexKey)) if err != nil { return nil, err } return bind.NewKeyedTransactorWithChainID(key, chainID) } func trimHexPrefix(s string) string { if len(s) >= 2 && (s[:2] == "0x" || s[:2] == "0X") { return s[2:] } return s }