Files
mev-beta/pkg/arbitrum/gas.go
Krypto Kajun 850223a953 fix(multicall): resolve critical multicall parsing corruption issues
- Added comprehensive bounds checking to prevent buffer overruns in multicall parsing
- Implemented graduated validation system (Strict/Moderate/Permissive) to reduce false positives
- Added LRU caching system for address validation with 10-minute TTL
- Enhanced ABI decoder with missing Universal Router and Arbitrum-specific DEX signatures
- Fixed duplicate function declarations and import conflicts across multiple files
- Added error recovery mechanisms with multiple fallback strategies
- Updated tests to handle new validation behavior for suspicious addresses
- Fixed parser test expectations for improved validation system
- Applied gofmt formatting fixes to ensure code style compliance
- Fixed mutex copying issues in monitoring package by introducing MetricsSnapshot
- Resolved critical security vulnerabilities in heuristic address extraction
- Progress: Updated TODO audit from 10% to 35% complete

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-17 00:12:55 -05:00

613 lines
19 KiB
Go

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
gasLimitBigInt := new(big.Int).SetUint64(gasLimit)
l2ComputeFee := new(big.Int).Mul(gasPrice, gasLimitBigInt)
// 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
gasLimitBigInt := new(big.Int).SetUint64(optimized.GasLimit)
l2Fee := new(big.Int).Mul(optimized.MaxFeePerGas, gasLimitBigInt)
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
gasLimitBigInt := new(big.Int).SetUint64(optimized.GasLimit)
l2Fee := new(big.Int).Mul(optimized.MaxFeePerGas, gasLimitBigInt)
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()
nonceBigInt := new(big.Int).SetUint64(nonce)
serialized = append(serialized, nonceBigInt.Bytes()...)
if tx.GasPrice() != nil {
gasPrice := tx.GasPrice().Bytes()
serialized = append(serialized, gasPrice...)
}
gasLimit := tx.Gas()
gasLimitBigInt := new(big.Int).SetUint64(gasLimit)
serialized = append(serialized, gasLimitBigInt.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
}