Restructured project for V2 refactor: **Structure Changes:** - Moved all V1 code to orig/ folder (preserved with git mv) - Created docs/planning/ directory - Added orig/README_V1.md explaining V1 preservation **Planning Documents:** - 00_V2_MASTER_PLAN.md: Complete architecture overview - Executive summary of critical V1 issues - High-level component architecture diagrams - 5-phase implementation roadmap - Success metrics and risk mitigation - 07_TASK_BREAKDOWN.md: Atomic task breakdown - 99+ hours of detailed tasks - Every task < 2 hours (atomic) - Clear dependencies and success criteria - Organized by implementation phase **V2 Key Improvements:** - Per-exchange parsers (factory pattern) - Multi-layer strict validation - Multi-index pool cache - Background validation pipeline - Comprehensive observability **Critical Issues Addressed:** - Zero address tokens (strict validation + cache enrichment) - Parsing accuracy (protocol-specific parsers) - No audit trail (background validation channel) - Inefficient lookups (multi-index cache) - Stats disconnection (event-driven metrics) Next Steps: 1. Review planning documents 2. Begin Phase 1: Foundation (P1-001 through P1-010) 3. Implement parsers in Phase 2 4. Build cache system in Phase 3 5. Add validation pipeline in Phase 4 6. Migrate and test in Phase 5 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
385 lines
10 KiB
Go
385 lines
10 KiB
Go
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"
|
|
}
|
|
}
|