package arbitrage import ( "bytes" "context" "encoding/hex" "errors" "fmt" stdmath "math" "math/big" "strings" "time" "github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/ethclient" "github.com/fraktal/mev-beta/bindings/contracts" "github.com/fraktal/mev-beta/bindings/flashswap" "github.com/fraktal/mev-beta/internal/logger" "github.com/fraktal/mev-beta/pkg/arbitrum" "github.com/fraktal/mev-beta/pkg/math" "github.com/fraktal/mev-beta/pkg/security" pkgtypes "github.com/fraktal/mev-beta/pkg/types" ) // FlashSwapExecutor executes arbitrage using flash swaps for capital efficiency type FlashSwapExecutor struct { client *ethclient.Client logger *logger.Logger keyManager *security.KeyManager gasEstimator *arbitrum.L2GasEstimator decimalConverter *math.DecimalConverter // Contract addresses flashSwapContract common.Address arbitrageContract common.Address // Contract bindings flashSwapBinding *flashswap.BaseFlashSwapper arbitrageBinding arbitrageLogParser // Configuration config ExecutionConfig // State tracking pendingExecutions map[common.Hash]*ExecutionState executionHistory []*ExecutionResult totalProfit *math.UniversalDecimal totalGasCost *math.UniversalDecimal // Token metadata helpers tokenRegistry map[common.Address]tokenDescriptor ethReferenceToken common.Address } // ExecutionConfig configures the flash swap executor type ExecutionConfig struct { // Risk management MaxSlippage *math.UniversalDecimal MinProfitThreshold *math.UniversalDecimal MaxPositionSize *math.UniversalDecimal MaxDailyVolume *math.UniversalDecimal // Gas settings GasLimitMultiplier float64 MaxGasPrice *math.UniversalDecimal PriorityFeeStrategy string // "conservative", "aggressive", "competitive" // Execution settings ExecutionTimeout time.Duration ConfirmationBlocks uint64 RetryAttempts int RetryDelay time.Duration // MEV protection EnableMEVProtection bool PrivateMempool bool FlashbotsRelay string // Monitoring EnableDetailedLogs bool TrackPerformance bool } // ExecutionState tracks the state of an ongoing execution type ExecutionState struct { Opportunity *pkgtypes.ArbitrageOpportunity TransactionHash common.Hash Status ExecutionStatus StartTime time.Time SubmissionTime time.Time ConfirmationTime time.Time GasUsed uint64 EffectiveGasPrice *big.Int ActualProfit *math.UniversalDecimal Error error } // ExecutionStatus represents the current status of an execution type ExecutionStatus string const ( StatusPending ExecutionStatus = "pending" StatusSubmitted ExecutionStatus = "submitted" StatusConfirmed ExecutionStatus = "confirmed" StatusFailed ExecutionStatus = "failed" StatusReverted ExecutionStatus = "reverted" ) // FlashSwapCalldata represents the data needed for a flash swap execution type FlashSwapCalldata struct { InitiatorPool common.Address TokenPath []common.Address Pools []common.Address AmountIn *big.Int MinAmountOut *big.Int Recipient common.Address Data []byte } type arbitrageLogParser interface { ParseArbitrageExecuted(types.Log) (*contracts.ArbitrageExecutorArbitrageExecuted, error) } type tokenDescriptor struct { Symbol string Decimals uint8 PriceUSD *big.Rat } var ( revertErrorSelector = []byte{0x08, 0xc3, 0x79, 0xa0} // keccak256("Error(string)")[:4] revertPanicSelector = []byte{0x4e, 0x48, 0x7b, 0x71} // keccak256("Panic(uint256)")[:4] ) // NewFlashSwapExecutor creates a new flash swap executor func NewFlashSwapExecutor( client *ethclient.Client, logger *logger.Logger, keyManager *security.KeyManager, gasEstimator *arbitrum.L2GasEstimator, flashSwapContract, arbitrageContract common.Address, config ExecutionConfig, ) *FlashSwapExecutor { executor := &FlashSwapExecutor{ client: client, logger: logger, keyManager: keyManager, gasEstimator: gasEstimator, decimalConverter: math.NewDecimalConverter(), flashSwapContract: flashSwapContract, arbitrageContract: arbitrageContract, config: config, pendingExecutions: make(map[common.Hash]*ExecutionState), executionHistory: make([]*ExecutionResult, 0), tokenRegistry: defaultTokenRegistry(), ethReferenceToken: common.HexToAddress("0x82aF49447D8a07e3bd95BD0d56f35241523fBab1"), } if client != nil && flashSwapContract != (common.Address{}) { if binding, err := flashswap.NewBaseFlashSwapper(flashSwapContract, client); err != nil { if logger != nil { logger.Warn(fmt.Sprintf("Failed to initialise flash swap contract binding: %v", err)) } } else { executor.flashSwapBinding = binding } } if client != nil && arbitrageContract != (common.Address{}) { if binding, err := contracts.NewArbitrageExecutor(arbitrageContract, client); err != nil { if logger != nil { logger.Warn(fmt.Sprintf("Failed to initialise arbitrage contract binding: %v", err)) } } else { executor.arbitrageBinding = binding } } // Initialize counters executor.totalProfit, _ = executor.decimalConverter.FromString("0", 18, "ETH") executor.totalGasCost, _ = executor.decimalConverter.FromString("0", 18, "ETH") // Set default configuration executor.setDefaultConfig() return executor } // setDefaultConfig sets default configuration values func (executor *FlashSwapExecutor) setDefaultConfig() { if executor.config.MaxSlippage == nil { executor.config.MaxSlippage, _ = executor.decimalConverter.FromString("1", 4, "PERCENT") // 1% } if executor.config.MinProfitThreshold == nil { executor.config.MinProfitThreshold, _ = executor.decimalConverter.FromString("0.01", 18, "ETH") } if executor.config.MaxPositionSize == nil { executor.config.MaxPositionSize, _ = executor.decimalConverter.FromString("10", 18, "ETH") } if executor.config.GasLimitMultiplier == 0 { executor.config.GasLimitMultiplier = 1.2 // 20% buffer } if executor.config.ExecutionTimeout == 0 { executor.config.ExecutionTimeout = 30 * time.Second } if executor.config.ConfirmationBlocks == 0 { executor.config.ConfirmationBlocks = 1 // Arbitrum has fast finality } if executor.config.RetryAttempts == 0 { executor.config.RetryAttempts = 3 } if executor.config.RetryDelay == 0 { executor.config.RetryDelay = 2 * time.Second } if executor.config.PriorityFeeStrategy == "" { executor.config.PriorityFeeStrategy = "competitive" } } // ExecuteArbitrage executes an arbitrage opportunity using flash swaps func (executor *FlashSwapExecutor) ExecuteArbitrage(ctx context.Context, opportunity *pkgtypes.ArbitrageOpportunity) (*ExecutionResult, error) { // Validate opportunity before execution if err := executor.validateOpportunity(opportunity); err != nil { return nil, fmt.Errorf("opportunity validation failed: %w", err) } profitEstimate, err := executor.profitEstimateWei(opportunity) if err != nil { return nil, fmt.Errorf("unable to determine profit estimate: %w", err) } profitDisplay := ethAmountString(executor.decimalConverter, nil, profitEstimate) executor.logger.Info(fmt.Sprintf("🚀 Executing arbitrage opportunity: %s ETH profit expected", profitDisplay)) // Create execution state executionState := &ExecutionState{ Opportunity: opportunity, Status: StatusPending, StartTime: time.Now(), } // Prepare flash swap transaction flashSwapData, err := executor.prepareFlashSwap(opportunity) if err != nil { result := executor.createFailedResult(executionState, fmt.Errorf("failed to prepare flash swap: %w", err)) return result, nil } // Get transaction options with dynamic gas pricing transactOpts, err := executor.getTransactionOptions(ctx, flashSwapData) if err != nil { return executor.createFailedResult(executionState, fmt.Errorf("failed to get transaction options: %w", err)), nil } // Execute with retry logic var result *ExecutionResult for attempt := 0; attempt <= executor.config.RetryAttempts; attempt++ { if attempt > 0 { executor.logger.Info(fmt.Sprintf("Retrying execution attempt %d/%d", attempt, executor.config.RetryAttempts)) time.Sleep(executor.config.RetryDelay) } result = executor.executeWithTimeout(ctx, executionState, flashSwapData, transactOpts) // If successful or non-retryable error, break errorMsg := "" if result.Error != nil { errorMsg = result.Error.Error() } if result.Success || !executor.isRetryableError(errorMsg) { break } // Update gas price for retry if attempt < executor.config.RetryAttempts { transactOpts, err = executor.updateGasPriceForRetry(ctx, transactOpts, attempt) if err != nil { executor.logger.Warn(fmt.Sprintf("Failed to update gas price for retry: %v", err)) } } } // Update statistics executor.updateExecutionStats(result) status := "Unknown" if result.Success { status = "Success" } else if result.Error != nil { status = "Failed" } else { status = "Incomplete" } executor.logger.Info(fmt.Sprintf("✅ Arbitrage execution completed: %s", status)) if result.Success && result.ProfitRealized != nil { // Note: opportunity.NetProfit is not directly accessible through ExecutionResult structure // So we just log that execution was successful with actual profit profitDisplay := ethAmountString(executor.decimalConverter, nil, result.ProfitRealized) executor.logger.Info(fmt.Sprintf("💰 Actual profit: %s ETH", profitDisplay)) } return result, nil } // validateOpportunity validates an opportunity before execution func (executor *FlashSwapExecutor) validateOpportunity(opportunity *pkgtypes.ArbitrageOpportunity) error { if opportunity == nil { return fmt.Errorf("opportunity cannot be nil") } if opportunity.AmountIn == nil || opportunity.AmountIn.Sign() <= 0 { return fmt.Errorf("invalid amount in for opportunity") } netProfit, err := executor.profitEstimateWei(opportunity) if err != nil { return fmt.Errorf("profit estimate invalid: %w", err) } // Check minimum profit threshold // Set to 0.001 ETH to ensure profitability after gas costs // Arbitrum gas cost: ~100k-200k gas @ 0.1-0.2 gwei = ~0.00002-0.00004 ETH // 0.001 ETH provides ~25-50x gas cost safety margin minProfitWei := big.NewInt(1000000000000000) // 0.001 ETH in wei if netProfit.Cmp(minProfitWei) < 0 { return fmt.Errorf("profit %s below minimum threshold %s", netProfit.String(), minProfitWei.String()) } // Check maximum position size maxPositionWei := big.NewInt(1000000000000000000) // 1 ETH in wei if opportunity.AmountIn.Cmp(maxPositionWei) > 0 { return fmt.Errorf("position size %s exceeds maximum %s", opportunity.AmountIn.String(), maxPositionWei.String()) } // Check price impact maxPriceImpact := 5.0 // 5% max if opportunity.PriceImpact > maxPriceImpact { return fmt.Errorf("price impact %.2f%% too high", opportunity.PriceImpact) } // Check confidence level if opportunity.Confidence < 0.7 { return fmt.Errorf("confidence level %.1f%% too low", opportunity.Confidence*100) } // Check execution path if len(opportunity.Path) < 2 { return fmt.Errorf("empty execution path") } // Basic validation for path if len(opportunity.Path) < 2 { return fmt.Errorf("path must have at least 2 tokens") } return nil } // prepareFlashSwap prepares the flash swap transaction data func (executor *FlashSwapExecutor) prepareFlashSwap(opportunity *pkgtypes.ArbitrageOpportunity) (*FlashSwapCalldata, error) { if opportunity == nil { return nil, fmt.Errorf("opportunity cannot be nil") } if len(opportunity.Path) < 2 { return nil, fmt.Errorf("path must have at least 2 tokens") } if opportunity.AmountIn == nil || opportunity.AmountIn.Sign() <= 0 { return nil, fmt.Errorf("opportunity amount in must be positive") } // Convert path strings to token addresses tokenPath := make([]common.Address, len(opportunity.Path)) for i, tokenAddr := range opportunity.Path { tokenPath[i] = common.HexToAddress(tokenAddr) } // Use pool addresses from opportunity if available poolAddresses := make([]common.Address, len(opportunity.Pools)) for i, poolAddr := range opportunity.Pools { poolAddresses[i] = common.HexToAddress(poolAddr) } if len(poolAddresses) == 0 { return nil, fmt.Errorf("opportunity missing pool data") } profitEstimate, err := executor.profitEstimateWei(opportunity) if err != nil { return nil, err } expectedOutput := new(big.Int).Add(opportunity.AmountIn, profitEstimate) if expectedOutput.Sign() <= 0 { return nil, fmt.Errorf("expected output amount must be positive") } slippageFraction := executor.resolveSlippage(opportunity) if slippageFraction < 0 { slippageFraction = 0 } if slippageFraction >= 1 { slippageFraction = 0.99 } minAmountOut := new(big.Int).Set(expectedOutput) if slippageFraction > 0 { minOutputFloat := new(big.Float).Mul(new(big.Float).SetInt(expectedOutput), big.NewFloat(1-slippageFraction)) minAmountOut = new(big.Int) minOutputFloat.Int(minAmountOut) } if minAmountOut.Sign() <= 0 { return nil, fmt.Errorf("minimum amount out must be positive") } calldata := &FlashSwapCalldata{ InitiatorPool: poolAddresses[0], // First pool initiates the flash swap TokenPath: tokenPath, Pools: poolAddresses, AmountIn: new(big.Int).Set(opportunity.AmountIn), MinAmountOut: minAmountOut, Recipient: executor.arbitrageContract, // Arbitrage contract receives callback Data: executor.encodeArbitrageData(tokenPath, poolAddresses, nil, minAmountOut), } return calldata, nil } // encodeArbitrageData encodes the arbitrage execution data func (executor *FlashSwapExecutor) encodeArbitrageData(tokenPath, poolPath []common.Address, fees []*big.Int, minAmountOut *big.Int) []byte { payload, err := encodeFlashSwapCallback(tokenPath, poolPath, fees, minAmountOut) if err != nil { if executor.logger != nil { executor.logger.Warn(fmt.Sprintf("Failed to encode flash swap callback data: %v", err)) } // Provide a structured fallback payload for debugging purposes fallback := []string{"arbitrage"} for _, token := range tokenPath { fallback = append(fallback, token.Hex()) } return []byte(strings.Join(fallback, ":")) } return payload } func (executor *FlashSwapExecutor) profitEstimateWei(opportunity *pkgtypes.ArbitrageOpportunity) (*big.Int, error) { if opportunity == nil { return nil, fmt.Errorf("opportunity cannot be nil") } candidates := []*big.Int{ opportunity.NetProfit, opportunity.Profit, opportunity.EstimatedProfit, } for _, candidate := range candidates { if candidate != nil && candidate.Sign() > 0 { return new(big.Int).Set(candidate), nil } } return nil, fmt.Errorf("opportunity profit estimate unavailable or non-positive") } func (executor *FlashSwapExecutor) resolveSlippage(opportunity *pkgtypes.ArbitrageOpportunity) float64 { if opportunity != nil && opportunity.MaxSlippage > 0 { if opportunity.MaxSlippage > 1 { return opportunity.MaxSlippage / 100.0 } return opportunity.MaxSlippage } if executor.config.MaxSlippage != nil && executor.config.MaxSlippage.Value != nil { numerator := new(big.Float).SetInt(executor.config.MaxSlippage.Value) denominator := big.NewFloat(stdmath.Pow10(int(executor.config.MaxSlippage.Decimals))) if denominator.Sign() != 0 { percent, _ := new(big.Float).Quo(numerator, denominator).Float64() if percent > 0 { return percent / 100.0 } } } return 0.01 // Default to 1% if nothing provided } func (executor *FlashSwapExecutor) gasEstimateWei(opportunity *pkgtypes.ArbitrageOpportunity) *big.Int { if opportunity == nil { return nil } if opportunity.GasEstimate != nil { return new(big.Int).Set(opportunity.GasEstimate) } if opportunity.GasCost != nil { return new(big.Int).Set(opportunity.GasCost) } return nil } // getTransactionOptions prepares transaction options with dynamic gas pricing func (executor *FlashSwapExecutor) getTransactionOptions(ctx context.Context, flashSwapData *FlashSwapCalldata) (*bind.TransactOpts, error) { if executor.client == nil { return nil, fmt.Errorf("ethereum client not configured") } if executor.keyManager == nil { return nil, fmt.Errorf("key manager not configured") } if flashSwapData == nil { return nil, fmt.Errorf("flash swap data cannot be nil") } privateKey, err := executor.keyManager.GetActivePrivateKey() if err != nil { return nil, fmt.Errorf("failed to get private key: %w", err) } chainID, err := executor.client.ChainID(ctx) if err != nil { return nil, fmt.Errorf("failed to get chain ID: %w", err) } transactOpts, err := bind.NewKeyedTransactorWithChainID(privateKey, chainID) if err != nil { return nil, fmt.Errorf("failed to create transactor: %w", err) } nonce, err := executor.client.PendingNonceAt(ctx, transactOpts.From) if err != nil { return nil, fmt.Errorf("failed to fetch account nonce: %w", err) } transactOpts.Nonce = new(big.Int).SetUint64(nonce) params, err := executor.buildFlashSwapParams(flashSwapData) if err != nil { return nil, err } callData, err := executor.encodeFlashSwapCall(flashSwapData.Pools[0], params) if err != nil { return nil, fmt.Errorf("failed to encode flash swap calldata: %w", err) } gasLimit := uint64(800000) // Sensible default for complex flash swaps var candidateFeeCap *big.Int var candidateTip *big.Int if executor.flashSwapContract == (common.Address{}) { executor.logger.Warn("Flash swap contract address not configured; using default gas limit") } else { callMsg := ethereum.CallMsg{ From: transactOpts.From, To: &executor.flashSwapContract, Data: callData, Value: func() *big.Int { if transactOpts.Value == nil { return big.NewInt(0) } return transactOpts.Value }(), } if estimatedGas, gasErr := executor.client.EstimateGas(ctx, callMsg); gasErr == nil && estimatedGas > 0 { gasLimit = estimatedGas } else { if gasErr != nil { executor.logger.Warn(fmt.Sprintf("Gas estimation via RPC failed: %v", gasErr)) } if executor.gasEstimator != nil { dummyTx := types.NewTx(&types.DynamicFeeTx{ ChainID: chainID, Nonce: nonce, To: &executor.flashSwapContract, Value: callMsg.Value, Data: callData, }) if estimate, estErr := executor.gasEstimator.EstimateL2Gas(ctx, dummyTx); estErr == nil { if estimate.GasLimit > 0 { gasLimit = estimate.GasLimit } candidateFeeCap = estimate.MaxFeePerGas candidateTip = estimate.MaxPriorityFee } else { executor.logger.Warn(fmt.Sprintf("Arbitrum gas estimator fallback failed: %v", estErr)) } } } } multiplier := executor.config.GasLimitMultiplier if multiplier <= 0 { multiplier = 1.2 } adjustedGasLimit := gasLimit if multiplier != 1.0 { adjusted := uint64(stdmath.Ceil(float64(gasLimit) * multiplier)) if adjusted < gasLimit { adjusted = gasLimit } if adjusted == 0 { adjusted = gasLimit } adjustedGasLimit = adjusted } if adjustedGasLimit == 0 { adjustedGasLimit = gasLimit } transactOpts.GasLimit = adjustedGasLimit if candidateTip == nil { if suggestedTip, tipErr := executor.client.SuggestGasTipCap(ctx); tipErr == nil { candidateTip = suggestedTip } else { candidateTip = big.NewInt(100000000) // 0.1 gwei fallback executor.logger.Debug(fmt.Sprintf("Using fallback priority fee: %s", candidateTip.String())) } } if candidateFeeCap == nil { if header, headerErr := executor.client.HeaderByNumber(ctx, nil); headerErr == nil && header != nil && header.BaseFee != nil { candidateFeeCap = new(big.Int).Add(new(big.Int).Mul(header.BaseFee, big.NewInt(2)), candidateTip) } else { defaultBase := big.NewInt(1000000000) // 1 gwei candidateFeeCap = new(big.Int).Add(defaultBase, candidateTip) } } transactOpts.GasTipCap = new(big.Int).Set(candidateTip) transactOpts.GasFeeCap = new(big.Int).Set(candidateFeeCap) baseStr := fmt.Sprintf("%d", gasLimit) maxFeeStr := "" tipStr := "" if transactOpts.GasFeeCap != nil { maxFeeStr = transactOpts.GasFeeCap.String() } if transactOpts.GasTipCap != nil { tipStr = transactOpts.GasTipCap.String() } executor.logger.Debug(fmt.Sprintf("Gas estimate - Base: %s, Adjusted: %d, MaxFee: %s, Priority: %s", baseStr, transactOpts.GasLimit, maxFeeStr, tipStr)) // Apply priority fee strategy and enforce configured limits executor.applyPriorityFeeStrategy(transactOpts) executor.enforceGasBounds(transactOpts) return transactOpts, nil } func (executor *FlashSwapExecutor) buildFlashSwapParams(flashSwapData *FlashSwapCalldata) (flashswap.FlashSwapParams, error) { if flashSwapData == nil { return flashswap.FlashSwapParams{}, fmt.Errorf("flash swap data cannot be nil") } if len(flashSwapData.TokenPath) < 2 { return flashswap.FlashSwapParams{}, fmt.Errorf("token path must include at least two tokens") } if len(flashSwapData.Pools) == 0 { return flashswap.FlashSwapParams{}, fmt.Errorf("pool path cannot be empty") } if flashSwapData.AmountIn == nil || flashSwapData.AmountIn.Sign() <= 0 { return flashswap.FlashSwapParams{}, fmt.Errorf("amount in must be positive") } if flashSwapData.MinAmountOut == nil || flashSwapData.MinAmountOut.Sign() <= 0 { return flashswap.FlashSwapParams{}, fmt.Errorf("minimum amount out must be positive") } params := flashswap.FlashSwapParams{ Token0: flashSwapData.TokenPath[0], Token1: flashSwapData.TokenPath[1], Amount0: flashSwapData.AmountIn, Amount1: big.NewInt(0), To: executor.arbitrageContract, Data: flashSwapData.Data, } return params, nil } func (executor *FlashSwapExecutor) encodeFlashSwapCall(pool common.Address, params flashswap.FlashSwapParams) ([]byte, error) { if pool == (common.Address{}) { return nil, fmt.Errorf("pool address cannot be zero") } flashSwapABI, err := flashswap.BaseFlashSwapperMetaData.GetAbi() if err != nil { return nil, fmt.Errorf("failed to load BaseFlashSwapper ABI: %w", err) } return flashSwapABI.Pack("executeFlashSwap", pool, params) } func (executor *FlashSwapExecutor) maxGasPriceWei() *big.Int { if executor.config.MaxGasPrice == nil || executor.config.MaxGasPrice.Value == nil { return nil } if executor.decimalConverter == nil { return new(big.Int).Set(executor.config.MaxGasPrice.Value) } weiDecimal := executor.decimalConverter.ToWei(executor.config.MaxGasPrice) if weiDecimal == nil || weiDecimal.Value == nil { return new(big.Int).Set(executor.config.MaxGasPrice.Value) } return new(big.Int).Set(weiDecimal.Value) } func (executor *FlashSwapExecutor) enforceGasBounds(transactOpts *bind.TransactOpts) { if transactOpts == nil { return } maxGas := executor.maxGasPriceWei() if maxGas == nil || maxGas.Sign() <= 0 { return } if transactOpts.GasFeeCap != nil && transactOpts.GasFeeCap.Cmp(maxGas) > 0 { transactOpts.GasFeeCap = new(big.Int).Set(maxGas) executor.logger.Debug(fmt.Sprintf("Clamped gas fee cap to configured maximum %s", maxGas.String())) } if transactOpts.GasTipCap != nil && transactOpts.GasTipCap.Cmp(maxGas) > 0 { transactOpts.GasTipCap = new(big.Int).Set(maxGas) executor.logger.Debug(fmt.Sprintf("Clamped priority fee to configured maximum %s", maxGas.String())) } if transactOpts.GasFeeCap != nil && transactOpts.GasTipCap != nil && transactOpts.GasFeeCap.Cmp(transactOpts.GasTipCap) < 0 { transactOpts.GasFeeCap = new(big.Int).Set(transactOpts.GasTipCap) } } // applyPriorityFeeStrategy adjusts gas pricing based on strategy func (executor *FlashSwapExecutor) applyPriorityFeeStrategy(transactOpts *bind.TransactOpts) { switch executor.config.PriorityFeeStrategy { case "aggressive": // Increase priority fee by 50% if transactOpts.GasTipCap != nil { newTip := new(big.Int).Mul(transactOpts.GasTipCap, big.NewInt(150)) transactOpts.GasTipCap = new(big.Int).Div(newTip, big.NewInt(100)) } case "competitive": // Increase priority fee by 25% if transactOpts.GasTipCap != nil { newTip := new(big.Int).Mul(transactOpts.GasTipCap, big.NewInt(125)) transactOpts.GasTipCap = new(big.Int).Div(newTip, big.NewInt(100)) } case "conservative": // Use default priority fee (no change) } } // executeWithTimeout executes the flash swap with timeout protection func (executor *FlashSwapExecutor) executeWithTimeout( ctx context.Context, executionState *ExecutionState, flashSwapData *FlashSwapCalldata, transactOpts *bind.TransactOpts, ) *ExecutionResult { // Create timeout context timeoutCtx, cancel := context.WithTimeout(ctx, executor.config.ExecutionTimeout) defer cancel() // Submit transaction tx, err := executor.submitTransaction(timeoutCtx, flashSwapData, transactOpts) if err != nil { return executor.createFailedResult(executionState, fmt.Errorf("transaction submission failed: %w", err)) } executionState.TransactionHash = tx.Hash() executionState.Status = StatusSubmitted executionState.SubmissionTime = time.Now() executor.pendingExecutions[tx.Hash()] = executionState executor.logger.Info(fmt.Sprintf("📤 Transaction submitted: %s", tx.Hash().Hex())) // Wait for confirmation receipt, err := executor.waitForConfirmation(timeoutCtx, tx.Hash()) if err != nil { return executor.createFailedResult(executionState, fmt.Errorf("confirmation failed: %w", err)) } executionState.ConfirmationTime = time.Now() executionState.GasUsed = receipt.GasUsed executionState.EffectiveGasPrice = receipt.EffectiveGasPrice // Check transaction status if receipt.Status == types.ReceiptStatusFailed { executionState.Status = StatusReverted revertReason := executor.fetchRevertReason(timeoutCtx, tx.Hash(), receipt) if revertReason != "" { return executor.createFailedResult(executionState, fmt.Errorf("transaction reverted: %s", revertReason)) } return executor.createFailedResult(executionState, fmt.Errorf("transaction reverted")) } executionState.Status = StatusConfirmed // Calculate actual results actualProfit, err := executor.calculateActualProfit(receipt, executionState.Opportunity) if err != nil { executor.logger.Warn(fmt.Sprintf("Failed to calculate actual profit: %v", err)) if estimate, estimateErr := executor.profitEstimateWei(executionState.Opportunity); estimateErr == nil { actualProfit = universalFromWei(executor.decimalConverter, estimate, "ETH") } else { actualProfit, _ = math.NewUniversalDecimal(big.NewInt(0), 18, "ETH") } } executionState.ActualProfit = actualProfit // Create successful result return executor.createSuccessfulResult(executionState, receipt) } // submitTransaction submits the flash swap transaction func (executor *FlashSwapExecutor) submitTransaction( ctx context.Context, flashSwapData *FlashSwapCalldata, transactOpts *bind.TransactOpts, ) (*types.Transaction, error) { if executor.flashSwapBinding == nil { return nil, fmt.Errorf("flash swap contract binding not initialised") } if executor.arbitrageContract == (common.Address{}) { return nil, fmt.Errorf("arbitrage contract address not configured") } params, err := executor.buildFlashSwapParams(flashSwapData) if err != nil { return nil, err } executor.logger.Debug("Submitting flash swap transaction...") executor.logger.Debug(fmt.Sprintf(" Initiator Pool: %s", flashSwapData.InitiatorPool.Hex())) executor.logger.Debug(fmt.Sprintf(" Amount In: %s", flashSwapData.AmountIn.String())) executor.logger.Debug(fmt.Sprintf(" Min Amount Out: %s", flashSwapData.MinAmountOut.String())) executor.logger.Debug(fmt.Sprintf(" Token Path: %d tokens", len(flashSwapData.TokenPath))) executor.logger.Debug(fmt.Sprintf(" Pool Path: %d pools", len(flashSwapData.Pools))) opts := *transactOpts opts.Context = ctx tx, err := executor.flashSwapBinding.ExecuteFlashSwap(&opts, flashSwapData.Pools[0], params) if err != nil { return nil, err } return tx, nil } // waitForConfirmation waits for transaction confirmation func (executor *FlashSwapExecutor) waitForConfirmation(ctx context.Context, txHash common.Hash) (*types.Receipt, error) { executor.logger.Debug(fmt.Sprintf("Waiting for confirmation of transaction: %s", txHash.Hex())) ticker := time.NewTicker(2 * time.Second) defer ticker.Stop() for { select { case <-ctx.Done(): return nil, fmt.Errorf("timeout waiting for confirmation: %w", ctx.Err()) case <-ticker.C: receipt, err := executor.client.TransactionReceipt(ctx, txHash) if err != nil { if errors.Is(err, ethereum.NotFound) { continue } return nil, fmt.Errorf("failed to fetch transaction receipt: %w", err) } if receipt == nil { continue } if receipt.BlockNumber == nil || executor.config.ConfirmationBlocks <= 1 { return receipt, nil } targetBlock := new(big.Int).Add(receipt.BlockNumber, big.NewInt(int64(executor.config.ConfirmationBlocks-1))) for { select { case <-ctx.Done(): return nil, fmt.Errorf("timeout waiting for confirmations: %w", ctx.Err()) case <-ticker.C: header, headerErr := executor.client.HeaderByNumber(ctx, nil) if headerErr != nil { if errors.Is(headerErr, ethereum.NotFound) { continue } return nil, fmt.Errorf("failed to fetch latest block header: %w", headerErr) } if header != nil && header.Number.Cmp(targetBlock) >= 0 { return receipt, nil } } } } } } func (executor *FlashSwapExecutor) fetchRevertReason(ctx context.Context, txHash common.Hash, receipt *types.Receipt) string { if executor.client == nil || receipt == nil { return "" } callCtx := ctx if callCtx == nil || callCtx.Err() != nil { var cancel context.CancelFunc callCtx, cancel = context.WithTimeout(context.Background(), 5*time.Second) defer cancel() } tx, _, err := executor.client.TransactionByHash(callCtx, txHash) if err != nil || tx == nil { if err != nil && executor.logger != nil { executor.logger.Debug(fmt.Sprintf("Failed to fetch transaction for revert reason: %v", err)) } return "" } if tx.To() == nil { return "" } msg := ethereum.CallMsg{ To: tx.To(), Data: tx.Data(), Gas: tx.Gas(), Value: tx.Value(), } switch tx.Type() { case types.DynamicFeeTxType: msg.GasFeeCap = tx.GasFeeCap() msg.GasTipCap = tx.GasTipCap() default: msg.GasPrice = tx.GasPrice() } revertData, callErr := executor.client.CallContract(callCtx, msg, receipt.BlockNumber) if callErr != nil || len(revertData) == 0 { if callErr != nil && executor.logger != nil { executor.logger.Debug(fmt.Sprintf("Failed to retrieve revert data: %v", callErr)) } return "" } reason := parseRevertReason(revertData) if reason == "" { return fmt.Sprintf("0x%s", hex.EncodeToString(revertData)) } return reason } func parseRevertReason(data []byte) string { if len(data) < 4 { return "" } selector := data[:4] payload := data[4:] switch { case bytes.Equal(selector, revertErrorSelector): strType, err := abi.NewType("string", "", nil) if err != nil { return "" } args := abi.Arguments{{Type: strType}} values, err := args.Unpack(payload) if err != nil || len(values) == 0 { return "" } if reason, ok := values[0].(string); ok { return reason } case bytes.Equal(selector, revertPanicSelector): uintType, err := abi.NewType("uint256", "", nil) if err != nil { return "" } args := abi.Arguments{{Type: uintType}} values, err := args.Unpack(payload) if err != nil || len(values) == 0 { return "" } if code, ok := values[0].(*big.Int); ok { return fmt.Sprintf("panic code 0x%s", strings.ToLower(code.Text(16))) } default: // Some contracts return raw reason strings without selector if utf8String := extractUTF8String(data); utf8String != "" { return utf8String } } return "" } func extractUTF8String(data []byte) string { if len(data) == 0 { return "" } trimmed := bytes.Trim(data, "\x00") if len(trimmed) == 0 { return "" } for _, b := range trimmed { // Allow printable ASCII range plus common whitespace if (b < 0x20 || b > 0x7E) && b != 0x0a && b != 0x0d && b != 0x09 { return "" } } return string(trimmed) } // calculateActualProfit calculates the actual profit from the transaction func (executor *FlashSwapExecutor) calculateActualProfit(receipt *types.Receipt, opportunity *pkgtypes.ArbitrageOpportunity) (*math.UniversalDecimal, error) { if receipt == nil { return nil, fmt.Errorf("transaction receipt cannot be nil") } gasCostWei := executor.calculateGasCostWei(receipt) var parsedEvent *contracts.ArbitrageExecutorArbitrageExecuted if executor.arbitrageBinding != nil { for _, log := range receipt.Logs { if log.Address != executor.arbitrageContract { continue } event, err := executor.arbitrageBinding.ParseArbitrageExecuted(*log) if err != nil { if executor.logger != nil { executor.logger.Debug("Failed to parse arbitrage execution log", "error", err) } continue } parsedEvent = event break } } profitAmount := executor.extractProfitAmount(parsedEvent, opportunity) if profitAmount == nil { return nil, fmt.Errorf("unable to determine profit from receipt or opportunity") } profitToken, descriptor := executor.resolveProfitDescriptor(parsedEvent, opportunity) gasCostInToken := executor.convertGasCostToTokenUnits(gasCostWei, profitToken, descriptor) netProfit := new(big.Int).Set(profitAmount) if gasCostInToken != nil { netProfit.Sub(netProfit, gasCostInToken) } actualProfitDecimal, err := math.NewUniversalDecimal(netProfit, descriptor.Decimals, descriptor.Symbol) if err != nil { return nil, err } return actualProfitDecimal, nil } func (executor *FlashSwapExecutor) calculateGasCostWei(receipt *types.Receipt) *big.Int { if receipt == nil { return big.NewInt(0) } gasUsedBigInt := new(big.Int).SetUint64(receipt.GasUsed) gasPrice := receipt.EffectiveGasPrice if gasPrice == nil { gasPrice = big.NewInt(0) } return new(big.Int).Mul(gasUsedBigInt, gasPrice) } func (executor *FlashSwapExecutor) extractProfitAmount(event *contracts.ArbitrageExecutorArbitrageExecuted, opportunity *pkgtypes.ArbitrageOpportunity) *big.Int { if event != nil && event.Profit != nil { return new(big.Int).Set(event.Profit) } if opportunity == nil { return nil } if opportunity.NetProfit != nil && opportunity.NetProfit.Sign() > 0 { return new(big.Int).Set(opportunity.NetProfit) } if estimate, err := executor.profitEstimateWei(opportunity); err == nil { return estimate } return nil } func (executor *FlashSwapExecutor) resolveProfitDescriptor(event *contracts.ArbitrageExecutorArbitrageExecuted, opportunity *pkgtypes.ArbitrageOpportunity) (common.Address, tokenDescriptor) { var tokenAddr common.Address if event != nil && len(event.Tokens) > 0 { tokenAddr = event.Tokens[len(event.Tokens)-1] } else if opportunity != nil && opportunity.TokenOut != (common.Address{}) { tokenAddr = opportunity.TokenOut } descriptor, ok := executor.tokenRegistry[tokenAddr] if !ok { if opportunity != nil && opportunity.Quantities != nil { descriptor.Symbol = opportunity.Quantities.NetProfit.Symbol descriptor.Decimals = opportunity.Quantities.NetProfit.Decimals } if descriptor.Symbol == "" { descriptor.Symbol = "ETH" } if descriptor.Decimals == 0 { descriptor.Decimals = 18 } } else { // Copy to avoid mutating registry entry descriptor = tokenDescriptor{ Symbol: descriptor.Symbol, Decimals: descriptor.Decimals, PriceUSD: descriptor.PriceUSD, } } return tokenAddr, descriptor } func (executor *FlashSwapExecutor) convertGasCostToTokenUnits(gasCostWei *big.Int, tokenAddr common.Address, descriptor tokenDescriptor) *big.Int { if gasCostWei == nil || gasCostWei.Sign() == 0 { return big.NewInt(0) } if tokenAddr == (common.Address{}) || tokenAddr == executor.ethReferenceToken || strings.EqualFold(descriptor.Symbol, "ETH") || strings.EqualFold(descriptor.Symbol, "WETH") { return new(big.Int).Set(gasCostWei) } if descriptor.PriceUSD == nil { if executor.logger != nil { executor.logger.Debug("Gas cost conversion skipped due to missing price data", "token", descriptor.Symbol) } return nil } ethDescriptor, ok := executor.tokenRegistry[executor.ethReferenceToken] if !ok || ethDescriptor.PriceUSD == nil { if executor.logger != nil { executor.logger.Debug("Gas cost conversion skipped due to missing ETH pricing") } return nil } numerator := new(big.Rat).SetInt(gasCostWei) numerator.Mul(numerator, ethDescriptor.PriceUSD) denominator := new(big.Rat).Mul(new(big.Rat).SetInt(powerOfTenInt(18)), descriptor.PriceUSD) if denominator.Sign() == 0 { return nil } tokenAmount := new(big.Rat).Quo(numerator, denominator) tokenAmount.Mul(tokenAmount, new(big.Rat).SetInt(powerOfTenUint(descriptor.Decimals))) // Floor conversion to avoid overstating deductions result := new(big.Int).Quo(tokenAmount.Num(), tokenAmount.Denom()) if result.Sign() == 0 && tokenAmount.Sign() > 0 { // Ensure non-zero deduction when value exists to avoid under-accounting result = big.NewInt(1) } return result } func defaultTokenRegistry() map[common.Address]tokenDescriptor { registry := map[common.Address]tokenDescriptor{ common.Address{}: newTokenDescriptor("ETH", 18, 2000.0), common.HexToAddress("0x82aF49447D8a07e3bd95BD0d56f35241523fBab1"): newTokenDescriptor("WETH", 18, 2000.0), common.HexToAddress("0xaF88d065e77c8cC2239327C5EDb3A432268e5831"): newTokenDescriptor("USDC", 6, 1.0), common.HexToAddress("0xff970a61a04b1ca14834a43f5de4533ebddb5cc8"): newTokenDescriptor("USDC.e", 6, 1.0), common.HexToAddress("0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9"): newTokenDescriptor("USDT", 6, 1.0), common.HexToAddress("0x2f2a2543b76a4166549f7aab2e75bef0aefc5b0f"): newTokenDescriptor("WBTC", 8, 43000.0), common.HexToAddress("0x912CE59144191C1204E64559FE8253a0e49E6548"): newTokenDescriptor("ARB", 18, 0.75), common.HexToAddress("0xfc5a1a6eb076a2c7ad06ed22c90d7e710e35ad0a"): newTokenDescriptor("GMX", 18, 45.0), common.HexToAddress("0xf97f4df75117a78c1a5a0dbb814af92458539fb4"): newTokenDescriptor("LINK", 18, 12.0), common.HexToAddress("0xfa7f8980b0f1e64a2062791cc3b0871572f1f7f0"): newTokenDescriptor("UNI", 18, 8.0), common.HexToAddress("0xba5ddd1f9d7f570dc94a51479a000e3bce967196"): newTokenDescriptor("AAVE", 18, 85.0), } return registry } func newTokenDescriptor(symbol string, decimals uint8, price float64) tokenDescriptor { desc := tokenDescriptor{ Symbol: symbol, Decimals: decimals, } if price > 0 { desc.PriceUSD = new(big.Rat).SetFloat64(price) } return desc } func powerOfTenInt(exp int) *big.Int { return new(big.Int).Exp(big.NewInt(10), big.NewInt(int64(exp)), nil) } func powerOfTenUint(exp uint8) *big.Int { return new(big.Int).Exp(big.NewInt(10), big.NewInt(int64(exp)), nil) } // createSuccessfulResult creates a successful execution result func (executor *FlashSwapExecutor) createSuccessfulResult(state *ExecutionState, receipt *types.Receipt) *ExecutionResult { // Convert UniversalDecimal to big.Int for ProfitRealized profitRealized := big.NewInt(0) if state.ActualProfit != nil { profitRealized = new(big.Int).Set(state.ActualProfit.Value) } else if state.Opportunity != nil && state.Opportunity.NetProfit != nil { profitRealized = new(big.Int).Set(state.Opportunity.NetProfit) } else if estimate, err := executor.profitEstimateWei(state.Opportunity); err == nil { profitRealized = estimate } // Create a minimal ArbitragePath based on the opportunity path := &ArbitragePath{ Tokens: []common.Address{state.Opportunity.TokenIn, state.Opportunity.TokenOut}, // Basic 2-token path Pools: []*PoolInfo{}, // Empty for now Protocols: []string{}, // Empty for now Fees: []int64{}, // Empty for now EstimatedGas: big.NewInt(0), // To be calculated NetProfit: profitRealized, ROI: 0, // To be calculated LastUpdated: time.Now(), } gasUsedBigInt := new(big.Int).SetUint64(receipt.GasUsed) gasCost := new(big.Int).Mul(gasUsedBigInt, receipt.EffectiveGasPrice) gasCostDecimal, _ := math.NewUniversalDecimal(gasCost, 18, "ETH") return &ExecutionResult{ TransactionHash: state.TransactionHash, GasUsed: receipt.GasUsed, GasPrice: receipt.EffectiveGasPrice, GasCost: gasCostDecimal, ProfitRealized: profitRealized, Success: true, Error: nil, ErrorMessage: "", Status: "Success", ExecutionTime: time.Since(state.StartTime), Path: path, } } // createFailedResult creates a failed execution result func (executor *FlashSwapExecutor) createFailedResult(state *ExecutionState, err error) *ExecutionResult { // Create a minimal ArbitragePath based on the opportunity path := &ArbitragePath{ Tokens: []common.Address{state.Opportunity.TokenIn, state.Opportunity.TokenOut}, // Basic 2-token path Pools: []*PoolInfo{}, // Empty for now Protocols: []string{}, // Empty for now Fees: []int64{}, // Empty for now EstimatedGas: big.NewInt(0), // To be calculated NetProfit: big.NewInt(0), ROI: 0, // To be calculated LastUpdated: time.Now(), } gasCostDecimal, _ := math.NewUniversalDecimal(big.NewInt(0), 18, "ETH") return &ExecutionResult{ TransactionHash: state.TransactionHash, GasUsed: 0, GasPrice: big.NewInt(0), GasCost: gasCostDecimal, ProfitRealized: big.NewInt(0), Success: false, Error: err, ErrorMessage: err.Error(), Status: "Failed", ExecutionTime: time.Since(state.StartTime), Path: path, } } // isRetryableError determines if an error is retryable func (executor *FlashSwapExecutor) isRetryableError(errorMsg string) bool { retryableErrors := []string{ "gas price too low", "nonce too low", "timeout", "network error", "connection refused", "transaction underpriced", "replacement transaction underpriced", "known transaction", } for _, retryable := range retryableErrors { if strings.Contains(strings.ToLower(errorMsg), strings.ToLower(retryable)) { return true } } return false } // updateGasPriceForRetry updates gas price for retry attempts func (executor *FlashSwapExecutor) updateGasPriceForRetry( ctx context.Context, transactOpts *bind.TransactOpts, attempt int, ) (*bind.TransactOpts, error) { // Increase gas price by 20% for each retry multiplier := 1.0 + float64(attempt)*0.2 if transactOpts.GasFeeCap != nil { newGasFeeCap := new(big.Float).Mul( new(big.Float).SetInt(transactOpts.GasFeeCap), big.NewFloat(multiplier), ) newGasFeeCapInt, _ := newGasFeeCap.Int(nil) transactOpts.GasFeeCap = newGasFeeCapInt } if transactOpts.GasTipCap != nil { newGasTipCap := new(big.Float).Mul( new(big.Float).SetInt(transactOpts.GasTipCap), big.NewFloat(multiplier), ) newGasTipCapInt, _ := newGasTipCap.Int(nil) transactOpts.GasTipCap = newGasTipCapInt } executor.logger.Debug(fmt.Sprintf("Updated gas prices for retry %d: MaxFee=%s, Priority=%s", attempt, transactOpts.GasFeeCap.String(), transactOpts.GasTipCap.String())) return transactOpts, nil } // updateExecutionStats updates execution statistics func (executor *FlashSwapExecutor) updateExecutionStats(result *ExecutionResult) { executor.executionHistory = append(executor.executionHistory, result) if result.Success && result.ProfitRealized != nil { profitDecimal, _ := math.NewUniversalDecimal(result.ProfitRealized, 18, "ETH") executor.totalProfit, _ = executor.decimalConverter.Add(executor.totalProfit, profitDecimal) } if result.GasCost != nil { executor.totalGasCost, _ = executor.decimalConverter.Add(executor.totalGasCost, result.GasCost) } // Clean up pending executions delete(executor.pendingExecutions, result.TransactionHash) // Keep only last 100 execution results if len(executor.executionHistory) > 100 { executor.executionHistory = executor.executionHistory[len(executor.executionHistory)-100:] } } // GetExecutionStats returns execution statistics func (executor *FlashSwapExecutor) GetExecutionStats() ExecutionStats { successCount := 0 totalExecutions := len(executor.executionHistory) for _, result := range executor.executionHistory { if result.Success { successCount++ } } successRate := 0.0 if totalExecutions > 0 { successRate = float64(successCount) / float64(totalExecutions) * 100 } return ExecutionStats{ TotalExecutions: totalExecutions, SuccessfulExecutions: successCount, SuccessRate: successRate, TotalProfit: executor.totalProfit, TotalGasCost: executor.totalGasCost, PendingExecutions: len(executor.pendingExecutions), } } // ExecutionStats contains execution statistics type ExecutionStats struct { TotalExecutions int SuccessfulExecutions int SuccessRate float64 TotalProfit *math.UniversalDecimal TotalGasCost *math.UniversalDecimal PendingExecutions int } // GetPendingExecutions returns currently pending executions func (executor *FlashSwapExecutor) GetPendingExecutions() map[common.Hash]*ExecutionState { return executor.pendingExecutions } // GetExecutionHistory returns recent execution history func (executor *FlashSwapExecutor) GetExecutionHistory(limit int) []*ExecutionResult { if limit <= 0 || limit > len(executor.executionHistory) { limit = len(executor.executionHistory) } start := len(executor.executionHistory) - limit return executor.executionHistory[start:] }