package arbitrum import ( "context" "fmt" "math/big" "sort" "sync" "time" "github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/ethclient" "github.com/fraktal/mev-beta/internal/logger" ) // GasStrategy represents different gas pricing strategies type GasStrategy int const ( Conservative GasStrategy = iota // 0.7x percentile multiplier Standard // 1.0x percentile multiplier Aggressive // 1.5x percentile multiplier ) // DynamicGasEstimator provides network-aware dynamic gas estimation type DynamicGasEstimator struct { logger *logger.Logger client *ethclient.Client mu sync.RWMutex // Historical gas price tracking (last 50 blocks) recentGasPrices []uint64 recentBaseFees []uint64 maxHistorySize int // Current network stats currentBaseFee uint64 currentPriorityFee uint64 networkPercentile50 uint64 // Median gas price networkPercentile75 uint64 // 75th percentile networkPercentile90 uint64 // 90th percentile // L1 data fee tracking l1DataFeeScalar float64 l1BaseFee uint64 lastL1Update time.Time // Update control updateTicker *time.Ticker stopChan chan struct{} } // NewDynamicGasEstimator creates a new dynamic gas estimator func NewDynamicGasEstimator(logger *logger.Logger, client *ethclient.Client) *DynamicGasEstimator { estimator := &DynamicGasEstimator{ logger: logger, client: client, maxHistorySize: 50, recentGasPrices: make([]uint64, 0, 50), recentBaseFees: make([]uint64, 0, 50), stopChan: make(chan struct{}), l1DataFeeScalar: 1.3, // Default scalar } return estimator } // Start begins tracking gas prices func (dge *DynamicGasEstimator) Start() { // Initial update ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) dge.updateGasStats(ctx) cancel() // Start periodic updates every 5 blocks (~10 seconds on Arbitrum) dge.updateTicker = time.NewTicker(10 * time.Second) go dge.updateLoop() dge.logger.Info("✅ Dynamic gas estimator started") } // Stop stops the gas estimator func (dge *DynamicGasEstimator) Stop() { close(dge.stopChan) if dge.updateTicker != nil { dge.updateTicker.Stop() } dge.logger.Info("✅ Dynamic gas estimator stopped") } // updateLoop continuously updates gas statistics func (dge *DynamicGasEstimator) updateLoop() { for { select { case <-dge.updateTicker.C: ctx, cancel := context.WithTimeout(context.Background(), 8*time.Second) dge.updateGasStats(ctx) cancel() case <-dge.stopChan: return } } } // updateGasStats updates current gas price statistics func (dge *DynamicGasEstimator) updateGasStats(ctx context.Context) { // Get latest block latestBlock, err := dge.client.BlockByNumber(ctx, nil) if err != nil { dge.logger.Debug(fmt.Sprintf("Failed to get latest block for gas stats: %v", err)) return } dge.mu.Lock() defer dge.mu.Unlock() // Update base fee if latestBlock.BaseFee() != nil { dge.currentBaseFee = latestBlock.BaseFee().Uint64() dge.addBaseFeeToHistory(dge.currentBaseFee) } // Calculate priority fee from recent transactions priorityFeeSum := uint64(0) txCount := 0 for _, tx := range latestBlock.Transactions() { if tx.Type() == types.DynamicFeeTxType { if gasTipCap := tx.GasTipCap(); gasTipCap != nil { priorityFeeSum += gasTipCap.Uint64() txCount++ } } } if txCount > 0 { dge.currentPriorityFee = priorityFeeSum / uint64(txCount) } else { // Default to 0.1 gwei if no dynamic fee transactions dge.currentPriorityFee = 100000000 // 0.1 gwei in wei } // Add to history effectiveGasPrice := dge.currentBaseFee + dge.currentPriorityFee dge.addGasPriceToHistory(effectiveGasPrice) // Calculate percentiles dge.calculatePercentiles() // Update L1 data fee if needed (every 5 minutes) if time.Since(dge.lastL1Update) > 5*time.Minute { go dge.updateL1DataFee(ctx) } dge.logger.Debug(fmt.Sprintf("Gas stats updated - Base: %d wei, Priority: %d wei, P50: %d, P75: %d, P90: %d", dge.currentBaseFee, dge.currentPriorityFee, dge.networkPercentile50, dge.networkPercentile75, dge.networkPercentile90)) } // addGasPriceToHistory adds a gas price to history func (dge *DynamicGasEstimator) addGasPriceToHistory(gasPrice uint64) { dge.recentGasPrices = append(dge.recentGasPrices, gasPrice) if len(dge.recentGasPrices) > dge.maxHistorySize { dge.recentGasPrices = dge.recentGasPrices[1:] } } // addBaseFeeToHistory adds a base fee to history func (dge *DynamicGasEstimator) addBaseFeeToHistory(baseFee uint64) { dge.recentBaseFees = append(dge.recentBaseFees, baseFee) if len(dge.recentBaseFees) > dge.maxHistorySize { dge.recentBaseFees = dge.recentBaseFees[1:] } } // calculatePercentiles calculates gas price percentiles func (dge *DynamicGasEstimator) calculatePercentiles() { if len(dge.recentGasPrices) == 0 { return } // Create sorted copy sorted := make([]uint64, len(dge.recentGasPrices)) copy(sorted, dge.recentGasPrices) sort.Slice(sorted, func(i, j int) bool { return sorted[i] < sorted[j] }) // Calculate percentiles p50Index := len(sorted) * 50 / 100 p75Index := len(sorted) * 75 / 100 p90Index := len(sorted) * 90 / 100 dge.networkPercentile50 = sorted[p50Index] dge.networkPercentile75 = sorted[p75Index] dge.networkPercentile90 = sorted[p90Index] } // EstimateGasWithStrategy estimates gas parameters using the specified strategy func (dge *DynamicGasEstimator) EstimateGasWithStrategy(ctx context.Context, msg ethereum.CallMsg, strategy GasStrategy) (*DynamicGasEstimate, error) { dge.mu.RLock() baseFee := dge.currentBaseFee priorityFee := dge.currentPriorityFee p50 := dge.networkPercentile50 p75 := dge.networkPercentile75 p90 := dge.networkPercentile90 l1Scalar := dge.l1DataFeeScalar l1BaseFee := dge.l1BaseFee dge.mu.RUnlock() // Estimate gas limit gasLimit, err := dge.client.EstimateGas(ctx, msg) if err != nil { // Use default if estimation fails gasLimit = 500000 dge.logger.Debug(fmt.Sprintf("Gas estimation failed, using default: %v", err)) } // Add 20% buffer to gas limit gasLimit = gasLimit * 12 / 10 // Calculate gas price based on strategy var targetGasPrice uint64 var multiplier float64 switch strategy { case Conservative: // Use median (P50) with 0.7x multiplier targetGasPrice = p50 multiplier = 0.7 case Standard: // Use P75 with 1.0x multiplier targetGasPrice = p75 multiplier = 1.0 case Aggressive: // Use P90 with 1.5x multiplier targetGasPrice = p90 multiplier = 1.5 default: targetGasPrice = p75 multiplier = 1.0 } // Apply multiplier targetGasPrice = uint64(float64(targetGasPrice) * multiplier) // Ensure minimum gas price (base fee + 0.1 gwei priority) minGasPrice := baseFee + 100000000 // 0.1 gwei if targetGasPrice < minGasPrice { targetGasPrice = minGasPrice } // Calculate EIP-1559 parameters maxPriorityFeePerGas := uint64(float64(priorityFee) * multiplier) if maxPriorityFeePerGas < 100000000 { // Minimum 0.1 gwei maxPriorityFeePerGas = 100000000 } maxFeePerGas := baseFee*2 + maxPriorityFeePerGas // 2x base fee for buffer // Estimate L1 data fee callDataSize := uint64(len(msg.Data)) l1DataFee := dge.estimateL1DataFee(callDataSize, l1BaseFee, l1Scalar) estimate := &DynamicGasEstimate{ GasLimit: gasLimit, MaxFeePerGas: maxFeePerGas, MaxPriorityFeePerGas: maxPriorityFeePerGas, L1DataFee: l1DataFee, TotalGasCost: (gasLimit * maxFeePerGas) + l1DataFee, Strategy: strategy, BaseFee: baseFee, NetworkPercentile: targetGasPrice, } return estimate, nil } // estimateL1DataFee estimates the L1 data fee for Arbitrum func (dge *DynamicGasEstimator) estimateL1DataFee(callDataSize uint64, l1BaseFee uint64, scalar float64) uint64 { if callDataSize == 0 { return 0 } // Arbitrum L1 data fee formula: // L1 fee = calldata_size * L1_base_fee * scalar l1Fee := float64(callDataSize) * float64(l1BaseFee) * scalar return uint64(l1Fee) } // updateL1DataFee updates L1 data fee parameters from ArbGasInfo func (dge *DynamicGasEstimator) updateL1DataFee(ctx context.Context) { // ArbGasInfo precompile address arbGasInfoAddr := common.HexToAddress("0x000000000000000000000000000000000000006C") // Call getPricesInWei() function // Function signature: getPricesInWei() returns (uint256, uint256, uint256, uint256, uint256, uint256) callData := common.Hex2Bytes("02199f34") // getPricesInWei function selector msg := ethereum.CallMsg{ To: &arbGasInfoAddr, Data: callData, } result, err := dge.client.CallContract(ctx, msg, nil) if err != nil { dge.logger.Debug(fmt.Sprintf("Failed to get L1 base fee from ArbGasInfo: %v", err)) return } if len(result) < 32 { dge.logger.Debug("Invalid result from ArbGasInfo.getPricesInWei") return } // Parse L1 base fee (first return value) l1BaseFee := new(big.Int).SetBytes(result[0:32]) dge.mu.Lock() dge.l1BaseFee = l1BaseFee.Uint64() dge.lastL1Update = time.Now() dge.mu.Unlock() dge.logger.Debug(fmt.Sprintf("Updated L1 base fee from ArbGasInfo: %d wei", dge.l1BaseFee)) } // GetCurrentStats returns current gas statistics func (dge *DynamicGasEstimator) GetCurrentStats() GasStats { dge.mu.RLock() defer dge.mu.RUnlock() return GasStats{ BaseFee: dge.currentBaseFee, PriorityFee: dge.currentPriorityFee, Percentile50: dge.networkPercentile50, Percentile75: dge.networkPercentile75, Percentile90: dge.networkPercentile90, L1DataFeeScalar: dge.l1DataFeeScalar, L1BaseFee: dge.l1BaseFee, HistorySize: len(dge.recentGasPrices), } } // DynamicGasEstimate contains dynamic gas estimation details with strategy type DynamicGasEstimate struct { GasLimit uint64 MaxFeePerGas uint64 MaxPriorityFeePerGas uint64 L1DataFee uint64 TotalGasCost uint64 Strategy GasStrategy BaseFee uint64 NetworkPercentile uint64 } // GasStats contains current gas statistics type GasStats struct { BaseFee uint64 PriorityFee uint64 Percentile50 uint64 Percentile75 uint64 Percentile90 uint64 L1DataFeeScalar float64 L1BaseFee uint64 HistorySize int } // String returns strategy name func (gs GasStrategy) String() string { switch gs { case Conservative: return "Conservative" case Standard: return "Standard" case Aggressive: return "Aggressive" default: return "Unknown" } }