package arbitrum import ( "context" "fmt" "math/big" "github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" "github.com/fraktal/mev-beta/internal/logger" "github.com/fraktal/mev-beta/pkg/math" ) // L2GasEstimator provides Arbitrum-specific gas estimation and optimization type L2GasEstimator struct { client *ArbitrumClient logger *logger.Logger // L2 gas price configuration baseFeeMultiplier float64 priorityFeeMin *big.Int priorityFeeMax *big.Int gasLimitMultiplier float64 } // GasEstimate represents an L2 gas estimate with detailed breakdown type GasEstimate struct { GasLimit uint64 MaxFeePerGas *big.Int MaxPriorityFee *big.Int L1DataFee *big.Int L2ComputeFee *big.Int TotalFee *big.Int Confidence float64 // 0-1 scale } // NewL2GasEstimator creates a new L2 gas estimator func NewL2GasEstimator(client *ArbitrumClient, logger *logger.Logger) *L2GasEstimator { return &L2GasEstimator{ client: client, logger: logger, baseFeeMultiplier: 1.1, // 10% buffer on base fee priorityFeeMin: big.NewInt(100000000), // 0.1 gwei minimum priorityFeeMax: big.NewInt(2000000000), // 2 gwei maximum gasLimitMultiplier: 1.2, // 20% buffer on gas limit } } // EstimateL2Gas provides comprehensive gas estimation for L2 transactions func (g *L2GasEstimator) EstimateL2Gas(ctx context.Context, tx *types.Transaction) (*GasEstimate, error) { // Get current gas price data gasPrice, err := g.client.SuggestGasPrice(ctx) if err != nil { return nil, fmt.Errorf("failed to get gas price: %v", err) } // Estimate gas limit gasLimit, err := g.estimateGasLimit(ctx, tx) if err != nil { return nil, fmt.Errorf("failed to estimate gas limit: %v", err) } // Get L1 data fee (Arbitrum-specific) l1DataFee, err := g.estimateL1DataFee(ctx, tx) if err != nil { g.logger.Warn(fmt.Sprintf("Failed to estimate L1 data fee: %v", err)) l1DataFee = big.NewInt(0) } // Calculate L2 compute fee l2ComputeFee := new(big.Int).Mul(gasPrice, big.NewInt(int64(gasLimit))) // Calculate priority fee priorityFee := g.calculateOptimalPriorityFee(ctx, gasPrice) // Calculate max fee per gas maxFeePerGas := new(big.Int).Add(gasPrice, priorityFee) // Total fee includes both L1 and L2 components totalFee := new(big.Int).Add(l1DataFee, l2ComputeFee) // Apply gas limit buffer bufferedGasLimit := uint64(float64(gasLimit) * g.gasLimitMultiplier) estimate := &GasEstimate{ GasLimit: bufferedGasLimit, MaxFeePerGas: maxFeePerGas, MaxPriorityFee: priorityFee, L1DataFee: l1DataFee, L2ComputeFee: l2ComputeFee, TotalFee: totalFee, Confidence: g.calculateConfidence(gasPrice, priorityFee), } return estimate, nil } // estimateGasLimit estimates the gas limit for an L2 transaction func (g *L2GasEstimator) estimateGasLimit(ctx context.Context, tx *types.Transaction) (uint64, error) { // Create a call message for gas estimation msg := ethereum.CallMsg{ From: common.Address{}, // Will be overridden To: tx.To(), Value: tx.Value(), Data: tx.Data(), GasPrice: tx.GasPrice(), } // Estimate gas using the client gasLimit, err := g.client.EstimateGas(ctx, msg) if err != nil { // Fallback to default gas limits based on transaction type return g.getDefaultGasLimit(tx), nil } return gasLimit, nil } // estimateL1DataFee calculates the L1 data fee component (Arbitrum-specific) func (g *L2GasEstimator) estimateL1DataFee(ctx context.Context, tx *types.Transaction) (*big.Int, error) { // Get current L1 gas price from Arbitrum's ArbGasInfo precompile _, err := g.getL1GasPrice(ctx) if err != nil { g.logger.Debug(fmt.Sprintf("Failed to get L1 gas price, using fallback: %v", err)) // Fallback to estimated L1 gas price with historical average _ = g.getEstimatedL1GasPrice(ctx) } // Get L1 data fee multiplier from ArbGasInfo l1PricePerUnit, err := g.getL1PricePerUnit(ctx) if err != nil { g.logger.Debug(fmt.Sprintf("Failed to get L1 price per unit, using default: %v", err)) l1PricePerUnit = big.NewInt(1000000000) // 1 gwei default } // Serialize the transaction to get the exact L1 calldata txData, err := g.serializeTransactionForL1(tx) if err != nil { return nil, fmt.Errorf("failed to serialize transaction: %w", err) } // Count zero and non-zero bytes (EIP-2028 pricing) zeroBytes := 0 nonZeroBytes := 0 for _, b := range txData { if b == 0 { zeroBytes++ } else { nonZeroBytes++ } } // Calculate L1 gas used based on EIP-2028 formula // 4 gas per zero byte, 16 gas per non-zero byte l1GasUsed := int64(zeroBytes*4 + nonZeroBytes*16) // Add base transaction overhead (21000 gas) l1GasUsed += 21000 // Add signature verification cost (additional cost for ECDSA signature) l1GasUsed += 2000 // Apply Arbitrum's L1 data fee calculation // L1 data fee = l1GasUsed * l1PricePerUnit * baseFeeScalar baseFeeScalar, err := g.getBaseFeeScalar(ctx) if err != nil { g.logger.Debug(fmt.Sprintf("Failed to get base fee scalar, using default: %v", err)) baseFeeScalar = big.NewInt(1300000) // Default scalar of 1.3 } // Calculate the L1 data fee l1GasCost := new(big.Int).Mul(big.NewInt(l1GasUsed), l1PricePerUnit) l1DataFee := new(big.Int).Mul(l1GasCost, baseFeeScalar) l1DataFee = new(big.Int).Div(l1DataFee, big.NewInt(1000000)) // Scale down by 10^6 g.logger.Debug(fmt.Sprintf("L1 data fee calculation: gasUsed=%d, pricePerUnit=%s, scalar=%s, fee=%s", l1GasUsed, l1PricePerUnit.String(), baseFeeScalar.String(), l1DataFee.String())) return l1DataFee, nil } // calculateOptimalPriorityFee calculates an optimal priority fee for fast inclusion func (g *L2GasEstimator) calculateOptimalPriorityFee(ctx context.Context, baseFee *big.Int) *big.Int { // Try to get recent priority fees from the network priorityFee, err := g.getSuggestedPriorityFee(ctx) if err != nil { // Fallback to base fee percentage priorityFee = new(big.Int).Div(baseFee, big.NewInt(10)) // 10% of base fee } // Ensure within bounds if priorityFee.Cmp(g.priorityFeeMin) < 0 { priorityFee = new(big.Int).Set(g.priorityFeeMin) } if priorityFee.Cmp(g.priorityFeeMax) > 0 { priorityFee = new(big.Int).Set(g.priorityFeeMax) } return priorityFee } // getSuggestedPriorityFee gets suggested priority fee from the network func (g *L2GasEstimator) getSuggestedPriorityFee(ctx context.Context) (*big.Int, error) { // Use eth_maxPriorityFeePerGas if available var result string err := g.client.rpcClient.CallContext(ctx, &result, "eth_maxPriorityFeePerGas") if err != nil { return nil, err } priorityFee := new(big.Int) if _, success := priorityFee.SetString(result[2:], 16); !success { return nil, fmt.Errorf("invalid priority fee response") } return priorityFee, nil } // calculateConfidence calculates confidence level for the gas estimate func (g *L2GasEstimator) calculateConfidence(gasPrice, priorityFee *big.Int) float64 { // Higher priority fee relative to gas price = higher confidence ratio := new(big.Float).Quo(new(big.Float).SetInt(priorityFee), new(big.Float).SetInt(gasPrice)) ratioFloat, _ := ratio.Float64() // Confidence scale: 0.1 ratio = 0.5 confidence, 0.5 ratio = 0.9 confidence confidence := 0.3 + (ratioFloat * 1.2) if confidence > 1.0 { confidence = 1.0 } if confidence < 0.1 { confidence = 0.1 } return confidence } // getDefaultGasLimit returns default gas limits based on transaction type func (g *L2GasEstimator) getDefaultGasLimit(tx *types.Transaction) uint64 { dataSize := len(tx.Data()) switch { case dataSize == 0: // Simple transfer return 21000 case dataSize < 100: // Simple contract interaction return 50000 case dataSize < 1000: // Complex contract interaction return 150000 case dataSize < 5000: // Very complex interaction (e.g., DEX swap) return 300000 default: // Extremely complex interaction return 500000 } } // OptimizeForSpeed adjusts gas parameters for fastest execution func (g *L2GasEstimator) OptimizeForSpeed(estimate *GasEstimate) *GasEstimate { optimized := *estimate // Increase priority fee by 50% speedPriorityFee := new(big.Int).Mul(estimate.MaxPriorityFee, big.NewInt(150)) optimized.MaxPriorityFee = new(big.Int).Div(speedPriorityFee, big.NewInt(100)) // Increase max fee per gas accordingly optimized.MaxFeePerGas = new(big.Int).Add( new(big.Int).Sub(estimate.MaxFeePerGas, estimate.MaxPriorityFee), optimized.MaxPriorityFee, ) // Increase gas limit by 10% more optimized.GasLimit = uint64(float64(estimate.GasLimit) * 1.1) // Recalculate total fee l2Fee := new(big.Int).Mul(optimized.MaxFeePerGas, big.NewInt(int64(optimized.GasLimit))) optimized.TotalFee = new(big.Int).Add(estimate.L1DataFee, l2Fee) // Higher confidence due to aggressive pricing optimized.Confidence = estimate.Confidence * 1.2 if optimized.Confidence > 1.0 { optimized.Confidence = 1.0 } return &optimized } // OptimizeForCost adjusts gas parameters for lowest cost func (g *L2GasEstimator) OptimizeForCost(estimate *GasEstimate) *GasEstimate { optimized := *estimate // Use minimum priority fee optimized.MaxPriorityFee = new(big.Int).Set(g.priorityFeeMin) // Reduce max fee per gas optimized.MaxFeePerGas = new(big.Int).Add( new(big.Int).Sub(estimate.MaxFeePerGas, estimate.MaxPriorityFee), optimized.MaxPriorityFee, ) // Use exact gas limit (no buffer) optimized.GasLimit = uint64(float64(estimate.GasLimit) / g.gasLimitMultiplier) // Recalculate total fee l2Fee := new(big.Int).Mul(optimized.MaxFeePerGas, big.NewInt(int64(optimized.GasLimit))) optimized.TotalFee = new(big.Int).Add(estimate.L1DataFee, l2Fee) // Lower confidence due to minimal gas pricing optimized.Confidence = estimate.Confidence * 0.7 return &optimized } // IsL2TransactionViable checks if an L2 transaction is economically viable func (g *L2GasEstimator) IsL2TransactionViable(estimate *GasEstimate, expectedProfit *big.Int) bool { // Compare total fee to expected profit return estimate.TotalFee.Cmp(expectedProfit) < 0 } // getL1GasPrice fetches the current L1 gas price from Arbitrum's ArbGasInfo precompile func (g *L2GasEstimator) getL1GasPrice(ctx context.Context) (*big.Int, error) { // ArbGasInfo precompile address on Arbitrum arbGasInfoAddr := common.HexToAddress("0x000000000000000000000000000000000000006C") // Call getL1BaseFeeEstimate() function (function selector: 0xf5d6ded7) data := common.Hex2Bytes("f5d6ded7") msg := ethereum.CallMsg{ To: &arbGasInfoAddr, Data: data, } result, err := g.client.CallContract(ctx, msg, nil) if err != nil { return nil, fmt.Errorf("failed to call ArbGasInfo.getL1BaseFeeEstimate: %w", err) } if len(result) < 32 { return nil, fmt.Errorf("invalid response length from ArbGasInfo") } l1GasPrice := new(big.Int).SetBytes(result[:32]) g.logger.Debug(fmt.Sprintf("Retrieved L1 gas price from ArbGasInfo: %s wei", l1GasPrice.String())) return l1GasPrice, nil } // getEstimatedL1GasPrice provides a fallback L1 gas price estimate using historical data func (g *L2GasEstimator) getEstimatedL1GasPrice(ctx context.Context) *big.Int { // Try to get recent blocks to estimate average L1 gas price latestBlock, err := g.client.BlockByNumber(ctx, nil) if err != nil { g.logger.Debug(fmt.Sprintf("Failed to get latest block for gas estimation: %v", err)) return big.NewInt(20000000000) // 20 gwei fallback } // Analyze last 10 blocks for gas price trend blockCount := int64(10) totalGasPrice := big.NewInt(0) validBlocks := int64(0) for i := int64(0); i < blockCount; i++ { blockNum := new(big.Int).Sub(latestBlock.Number(), big.NewInt(i)) if blockNum.Sign() <= 0 { break } block, err := g.client.BlockByNumber(ctx, blockNum) if err != nil { continue } // Use base fee as proxy for gas price trend if block.BaseFee() != nil { totalGasPrice.Add(totalGasPrice, block.BaseFee()) validBlocks++ } } if validBlocks > 0 { avgGasPrice := new(big.Int).Div(totalGasPrice, big.NewInt(validBlocks)) // Scale up for L1 (L1 typically 5-10x higher than L2) l1Estimate := new(big.Int).Mul(avgGasPrice, big.NewInt(7)) g.logger.Debug(fmt.Sprintf("Estimated L1 gas price from %d blocks: %s wei", validBlocks, l1Estimate.String())) return l1Estimate } // Final fallback return big.NewInt(25000000000) // 25 gwei } // getL1PricePerUnit fetches the L1 price per unit from ArbGasInfo func (g *L2GasEstimator) getL1PricePerUnit(ctx context.Context) (*big.Int, error) { // ArbGasInfo precompile address arbGasInfoAddr := common.HexToAddress("0x000000000000000000000000000000000000006C") // Call getPerBatchGasCharge() function (function selector: 0x6eca253a) data := common.Hex2Bytes("6eca253a") msg := ethereum.CallMsg{ To: &arbGasInfoAddr, Data: data, } result, err := g.client.CallContract(ctx, msg, nil) if err != nil { return nil, fmt.Errorf("failed to call ArbGasInfo.getPerBatchGasCharge: %w", err) } if len(result) < 32 { return nil, fmt.Errorf("invalid response length from ArbGasInfo") } pricePerUnit := new(big.Int).SetBytes(result[:32]) g.logger.Debug(fmt.Sprintf("Retrieved L1 price per unit: %s", pricePerUnit.String())) return pricePerUnit, nil } // getBaseFeeScalar fetches the base fee scalar from ArbGasInfo func (g *L2GasEstimator) getBaseFeeScalar(ctx context.Context) (*big.Int, error) { // ArbGasInfo precompile address arbGasInfoAddr := common.HexToAddress("0x000000000000000000000000000000000000006C") // Call getL1FeesAvailable() function (function selector: 0x5ca5a4d7) to get pricing info data := common.Hex2Bytes("5ca5a4d7") msg := ethereum.CallMsg{ To: &arbGasInfoAddr, Data: data, } result, err := g.client.CallContract(ctx, msg, nil) if err != nil { return nil, fmt.Errorf("failed to call ArbGasInfo.getL1FeesAvailable: %w", err) } if len(result) < 32 { return nil, fmt.Errorf("invalid response length from ArbGasInfo") } // Extract the scalar from the response (typically in the first 32 bytes) scalar := new(big.Int).SetBytes(result[:32]) // Ensure scalar is reasonable (between 1.0 and 2.0, scaled by 10^6) minScalar := big.NewInt(1000000) // 1.0 maxScalar := big.NewInt(2000000) // 2.0 if scalar.Cmp(minScalar) < 0 { scalar = minScalar } if scalar.Cmp(maxScalar) > 0 { scalar = maxScalar } g.logger.Debug(fmt.Sprintf("Retrieved base fee scalar: %s", scalar.String())) return scalar, nil } // serializeTransactionForL1 serializes the transaction as it would appear on L1 func (g *L2GasEstimator) serializeTransactionForL1(tx *types.Transaction) ([]byte, error) { // For L1 data fee calculation, we need the transaction as it would be serialized on L1 // This includes the complete transaction data including signature // Get the transaction data txData := tx.Data() // Create a basic serialization that includes: // - nonce (8 bytes) // - gas price (32 bytes) // - gas limit (8 bytes) // - to address (20 bytes) // - value (32 bytes) // - data (variable) // - v, r, s signature (65 bytes total) serialized := make([]byte, 0, 165+len(txData)) // Add transaction fields (simplified encoding) nonce := tx.Nonce() serialized = append(serialized, big.NewInt(int64(nonce)).Bytes()...) if tx.GasPrice() != nil { gasPrice := tx.GasPrice().Bytes() serialized = append(serialized, gasPrice...) } gasLimit := tx.Gas() serialized = append(serialized, big.NewInt(int64(gasLimit)).Bytes()...) if tx.To() != nil { serialized = append(serialized, tx.To().Bytes()...) } else { // Contract creation - add 20 zero bytes serialized = append(serialized, make([]byte, 20)...) } if tx.Value() != nil { value := tx.Value().Bytes() serialized = append(serialized, value...) } // Add the transaction data serialized = append(serialized, txData...) // Add signature components (v, r, s) - 65 bytes total // For estimation purposes, we'll add placeholder signature bytes v, r, s := tx.RawSignatureValues() if v != nil && r != nil && s != nil { serialized = append(serialized, v.Bytes()...) serialized = append(serialized, r.Bytes()...) serialized = append(serialized, s.Bytes()...) } else { // Add placeholder signature (65 bytes) serialized = append(serialized, make([]byte, 65)...) } g.logger.Debug(fmt.Sprintf("Serialized transaction for L1 fee calculation: %d bytes", len(serialized))) return serialized, nil } // EstimateSwapGas implements math.GasEstimator interface func (g *L2GasEstimator) EstimateSwapGas(exchange math.ExchangeType, poolData *math.PoolData) (uint64, error) { // Base gas for different exchange types on Arbitrum L2 baseGas := map[math.ExchangeType]uint64{ math.ExchangeUniswapV2: 120000, // Uniswap V2 swap math.ExchangeUniswapV3: 150000, // Uniswap V3 swap (more complex) math.ExchangeSushiSwap: 125000, // SushiSwap swap math.ExchangeCamelot: 140000, // Camelot swap math.ExchangeBalancer: 180000, // Balancer swap (complex) math.ExchangeCurve: 160000, // Curve swap math.ExchangeTraderJoe: 130000, // TraderJoe swap math.ExchangeRamses: 135000, // Ramses swap } gas, exists := baseGas[exchange] if !exists { gas = 150000 // Default fallback } // Apply L2 gas limit multiplier return uint64(float64(gas) * g.gasLimitMultiplier), nil } // EstimateFlashSwapGas implements math.GasEstimator interface func (g *L2GasEstimator) EstimateFlashSwapGas(route []*math.PoolData) (uint64, error) { // Base flash swap overhead on Arbitrum L2 baseGas := uint64(200000) // Add gas for each hop in the route hopGas := uint64(len(route)) * 50000 // Add complexity gas based on different exchanges complexityGas := uint64(0) for _, pool := range route { switch pool.ExchangeType { case math.ExchangeUniswapV3: complexityGas += 30000 // V3 concentrated liquidity complexity case math.ExchangeBalancer: complexityGas += 50000 // Weighted pool complexity case math.ExchangeCurve: complexityGas += 40000 // Stable swap complexity case math.ExchangeTraderJoe: complexityGas += 25000 // TraderJoe complexity case math.ExchangeRamses: complexityGas += 35000 // Ramses complexity default: complexityGas += 20000 // Standard AMM } } totalGas := baseGas + hopGas + complexityGas // Apply L2 gas limit multiplier with safety margin for flash swaps return uint64(float64(totalGas) * g.gasLimitMultiplier * 1.5), nil } // GetCurrentGasPrice implements math.GasEstimator interface func (g *L2GasEstimator) GetCurrentGasPrice() (*math.UniversalDecimal, error) { ctx := context.Background() // Get current gas price from the network gasPrice, err := g.client.Client.SuggestGasPrice(ctx) if err != nil { // Fallback to typical Arbitrum L2 gas price gasPrice = big.NewInt(100000000) // 0.1 gwei g.logger.Warn(fmt.Sprintf("Failed to get gas price, using fallback: %v", err)) } // Apply base fee multiplier adjustedGasPrice := new(big.Int).Mul(gasPrice, big.NewInt(int64(g.baseFeeMultiplier*100))) adjustedGasPrice = new(big.Int).Div(adjustedGasPrice, big.NewInt(100)) // Convert to UniversalDecimal (gas price is in wei, so 18 decimals) gasPriceDecimal, err := math.NewUniversalDecimal(adjustedGasPrice, 18, "GWEI") if err != nil { return nil, fmt.Errorf("failed to convert gas price to decimal: %w", err) } return gasPriceDecimal, nil }