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>
This commit is contained in:
Krypto Kajun
2025-10-17 00:12:55 -05:00
parent f358f49aa9
commit 850223a953
8621 changed files with 79808 additions and 7340 deletions

View File

@@ -9,9 +9,10 @@ import (
"time"
"github.com/ethereum/go-ethereum/common"
_ "github.com/mattn/go-sqlite3"
"github.com/fraktal/mev-beta/internal/logger"
pkgtypes "github.com/fraktal/mev-beta/pkg/types"
_ "github.com/mattn/go-sqlite3"
)
// SQLiteDatabase implements ArbitrageDatabase using SQLite

View File

@@ -0,0 +1,100 @@
package arbitrage
import (
"fmt"
"math/big"
"github.com/fraktal/mev-beta/pkg/math"
)
var sharedDecimalConverter = math.NewDecimalConverter()
// universalFromWei converts a wei-denominated big.Int into a UniversalDecimal with 18 decimals.
// It gracefully handles nil values by returning a zero amount.
func universalFromWei(dec *math.DecimalConverter, value *big.Int, symbol string) *math.UniversalDecimal {
if dec == nil {
dec = sharedDecimalConverter
}
if value == nil {
zero, _ := math.NewUniversalDecimal(big.NewInt(0), 18, symbol)
return zero
}
return dec.FromWei(value, 18, symbol)
}
func universalOrFromWei(dec *math.DecimalConverter, decimal *math.UniversalDecimal, fallback *big.Int, decimals uint8, symbol string) *math.UniversalDecimal {
if decimal != nil {
return decimal
}
if fallback == nil {
zero, _ := math.NewUniversalDecimal(big.NewInt(0), decimals, symbol)
return zero
}
if dec == nil {
dc := sharedDecimalConverter
return dc.FromWei(fallback, decimals, symbol)
}
return dec.FromWei(fallback, decimals, symbol)
}
func floatStringFromDecimal(ud *math.UniversalDecimal, precision int) string {
if precision < 0 {
precision = int(ud.Decimals)
}
if ud == nil {
if precision <= 0 {
return "0"
}
return fmt.Sprintf("0.%0*d", precision, 0)
}
if ud.Decimals == 0 {
return ud.Value.String()
}
numerator := new(big.Int).Set(ud.Value)
denominator := new(big.Int).Exp(big.NewInt(10), big.NewInt(int64(ud.Decimals)), nil)
rat := new(big.Rat).SetFrac(numerator, denominator)
return rat.FloatString(precision)
}
func ethAmountString(dec *math.DecimalConverter, decimal *math.UniversalDecimal, wei *big.Int) string {
if dec == nil {
dec = sharedDecimalConverter
}
ud := universalOrFromWei(dec, decimal, wei, 18, "ETH")
return floatStringFromDecimal(ud, 6)
}
func gweiAmountString(dec *math.DecimalConverter, decimal *math.UniversalDecimal, wei *big.Int) string {
if dec == nil {
dec = sharedDecimalConverter
}
if decimal != nil {
return floatStringFromDecimal(decimal, 2)
}
if wei == nil {
return "0.00"
}
numerator := new(big.Rat).SetInt(wei)
denominator := new(big.Rat).SetInt(big.NewInt(1_000_000_000))
return new(big.Rat).Quo(numerator, denominator).FloatString(2)
}
func formatEther(wei *big.Int) string {
return ethAmountString(sharedDecimalConverter, nil, wei)
}
func formatEtherFromWei(wei *big.Int) string {
return formatEther(wei)
}
func formatGweiFromWei(wei *big.Int) string {
return gweiAmountString(sharedDecimalConverter, nil, wei)
}

View File

@@ -0,0 +1,38 @@
package arbitrage
import (
"math/big"
"testing"
"github.com/fraktal/mev-beta/pkg/math"
)
func TestEthAmountStringPrefersDecimalSnapshot(t *testing.T) {
ud, err := math.NewUniversalDecimal(big.NewInt(1500000000000000000), 18, "ETH")
if err != nil {
t.Fatalf("unexpected error creating decimal: %v", err)
}
got := ethAmountString(nil, ud, nil)
if got != "1.500000" {
t.Fatalf("expected 1.500000, got %s", got)
}
}
func TestEthAmountStringFallsBackToWei(t *testing.T) {
wei := big.NewInt(123450000000000000)
got := ethAmountString(nil, nil, wei)
if got != "0.123450" {
t.Fatalf("expected 0.123450, got %s", got)
}
}
func TestGweiAmountStringFormatsTwoDecimals(t *testing.T) {
wei := big.NewInt(987654321)
got := gweiAmountString(nil, nil, wei)
if got != "0.99" {
t.Fatalf("expected 0.99, got %s", got)
}
}

View File

@@ -1,74 +1,102 @@
// Package arbitrage provides the core arbitrage detection and analysis engine
// for the MEV bot. This package is responsible for identifying profitable
// arbitrage opportunities across multiple DEX protocols on Arbitrum.
//
// The detection engine continuously scans for price discrepancies between
// exchanges and calculates potential profits after accounting for gas costs,
// slippage, and other factors. It uses advanced mathematical models to
// optimize trade sizing and minimize risks.
package arbitrage
import (
"context"
"fmt"
"math/big"
"sync"
"time"
"github.com/ethereum/go-ethereum/common"
"github.com/fraktal/mev-beta/internal/logger"
"github.com/fraktal/mev-beta/pkg/exchanges"
"github.com/fraktal/mev-beta/pkg/math"
"github.com/fraktal/mev-beta/pkg/types"
)
// ArbitrageDetectionEngine discovers profitable arbitrage opportunities in real-time
// ArbitrageDetectionEngine is the core component responsible for discovering
// profitable arbitrage opportunities in real-time across multiple DEX protocols.
// The engine uses sophisticated algorithms to:
//
// 1. Continuously scan token pairs across supported exchanges
// 2. Identify price discrepancies that exceed minimum profit thresholds
// 3. Calculate optimal trade sizes considering slippage and gas costs
// 4. Filter opportunities based on risk and confidence metrics
// 5. Provide real-time opportunity feeds to execution systems
//
// The engine operates with configurable parameters for different trading strategies
// and risk profiles, making it suitable for both conservative and aggressive MEV extraction.
type ArbitrageDetectionEngine struct {
registry *exchanges.ExchangeRegistry
calculator *math.ArbitrageCalculator
gasEstimator math.GasEstimator
logger *logger.Logger
decimalConverter *math.DecimalConverter
// Core dependencies for arbitrage detection
registry *exchanges.ExchangeRegistry // Registry of supported exchanges and their configurations
calculator *math.ArbitrageCalculator // Mathematical engine for profit calculations
gasEstimator math.GasEstimator // Gas cost estimation for transaction profitability
logger *logger.Logger // Structured logging for monitoring and debugging
decimalConverter *math.DecimalConverter // Handles precision math for token amounts
// Configuration
// Callback function for handling discovered opportunities
// This is typically connected to an execution engine
opportunityHandler func(*types.ArbitrageOpportunity)
// Configuration parameters that control detection behavior
config DetectionConfig
// State management
runningMutex sync.RWMutex
isRunning bool
stopChan chan struct{}
opportunityChan chan *types.ArbitrageOpportunity
// State management for concurrent operation
runningMutex sync.RWMutex // Protects running state from race conditions
isRunning bool // Indicates if the engine is currently active
stopChan chan struct{} // Channel for graceful shutdown signaling
opportunityChan chan *types.ArbitrageOpportunity // Buffered channel for opportunity distribution
// Performance tracking
scanCount uint64
opportunityCount uint64
lastScanTime time.Time
// Performance tracking and metrics
scanCount uint64 // Total number of scans performed since startup
opportunityCount uint64 // Total number of opportunities discovered
lastScanTime time.Time // Timestamp of the most recent scan completion
// Worker pools
scanWorkers *WorkerPool
pathWorkers *WorkerPool
// Concurrent processing infrastructure
scanWorkers *WorkerPool // Pool of workers for parallel opportunity scanning
pathWorkers *WorkerPool // Pool of workers for complex path analysis
}
// DetectionConfig configures the arbitrage detection engine
// DetectionConfig contains all configuration parameters for the arbitrage detection engine.
// These parameters control scanning behavior, opportunity filtering, and performance characteristics.
// The configuration allows fine-tuning for different trading strategies and risk profiles.
type DetectionConfig struct {
// Scanning parameters
ScanInterval time.Duration
MaxConcurrentScans int
MaxConcurrentPaths int
// Scanning timing and concurrency parameters
ScanInterval time.Duration // How frequently to scan for new opportunities
MaxConcurrentScans int // Maximum number of simultaneous scanning operations
MaxConcurrentPaths int // Maximum number of paths to analyze in parallel
// Opportunity criteria
MinProfitThreshold *math.UniversalDecimal
MaxPriceImpact *math.UniversalDecimal
MaxHops int
// Opportunity filtering criteria - these determine what qualifies as actionable
MinProfitThreshold *math.UniversalDecimal // Minimum profit required to consider an opportunity
MaxPriceImpact *math.UniversalDecimal // Maximum acceptable price impact (slippage)
MaxHops int // Maximum number of hops in a trading path
// Token filtering
HighPriorityTokens []common.Address
TokenWhitelist []common.Address
TokenBlacklist []common.Address
// Token filtering and prioritization
HighPriorityTokens []common.Address // Tokens to scan more frequently (e.g., WETH, USDC)
TokenWhitelist []common.Address // Only scan these tokens if specified
TokenBlacklist []common.Address // Never scan these tokens (e.g., known scam tokens)
// Exchange filtering
EnabledExchanges []math.ExchangeType
ExchangeWeights map[math.ExchangeType]float64
// Exchange selection and weighting
EnabledExchanges []math.ExchangeType // Which exchanges to include in scanning
ExchangeWeights map[math.ExchangeType]float64 // Relative weights for exchange prioritization
// Performance settings
CachePoolData bool
CacheTTL time.Duration
BatchSize int
// Performance optimization settings
CachePoolData bool // Whether to cache pool data between scans
CacheTTL time.Duration // How long to keep cached data valid
BatchSize int // Number of opportunities to process in each batch
// Risk management
MaxPositionSize *math.UniversalDecimal
RequiredConfidence float64
// Risk management constraints
MaxPositionSize *math.UniversalDecimal // Maximum size for any single arbitrage
RequiredConfidence float64 // Minimum confidence score (0.0-1.0) for execution
}
// WorkerPool manages concurrent workers for scanning
@@ -95,7 +123,18 @@ type ScanResult struct {
ScanTime time.Duration
}
// NewArbitrageDetectionEngine creates a new arbitrage detection engine
// NewArbitrageDetectionEngine creates and initializes a new arbitrage detection engine
// with the specified dependencies and configuration. The engine is created in a stopped
// state and must be started explicitly using the Start() method.
//
// Parameters:
// - registry: Exchange registry containing supported DEX configurations
// - gasEstimator: Component for estimating transaction gas costs
// - logger: Structured logger for monitoring and debugging
// - config: Configuration parameters for detection behavior
//
// Returns:
// - *ArbitrageDetectionEngine: Configured but not yet started detection engine
func NewArbitrageDetectionEngine(
registry *exchanges.ExchangeRegistry,
gasEstimator math.GasEstimator,
@@ -169,8 +208,23 @@ func (engine *ArbitrageDetectionEngine) setDefaultConfig() {
}
}
// Start begins the arbitrage detection process
// Start begins the arbitrage detection process and initializes all background workers.
// This method starts the main detection loop, worker pools, and opportunity processing.
// The engine will continue running until Stop() is called or the context is cancelled.
//
// The startup process includes:
// 1. Validating that the engine is not already running
// 2. Initializing worker pools for concurrent processing
// 3. Starting the main detection loop
// 4. Starting the opportunity processing pipeline
//
// Parameters:
// - ctx: Context for controlling the engine lifecycle
//
// Returns:
// - error: Any startup errors
func (engine *ArbitrageDetectionEngine) Start(ctx context.Context) error {
// Ensure thread-safe state management during startup
engine.runningMutex.Lock()
defer engine.runningMutex.Unlock()
@@ -263,10 +317,24 @@ func (engine *ArbitrageDetectionEngine) detectionLoop(ctx context.Context) {
}
}
// performScan executes a complete arbitrage scan
// performScan executes a complete arbitrage scanning cycle across all configured
// token pairs and exchanges. This is the core scanning logic that runs periodically
// to discover new arbitrage opportunities.
//
// The scanning process includes:
// 1. Identifying token pairs to analyze
// 2. Determining input amounts to test
// 3. Creating scanning tasks for worker pools
// 4. Processing results and filtering opportunities
//
// Each scan is tracked for performance monitoring and optimization.
//
// Parameters:
// - ctx: Context for cancellation and timeout control
func (engine *ArbitrageDetectionEngine) performScan(ctx context.Context) {
// Track scan performance for monitoring
scanStart := time.Now()
engine.scanCount++
engine.scanCount++ // Increment total scan counter
engine.logger.Debug(fmt.Sprintf("Starting arbitrage scan #%d", engine.scanCount))
@@ -431,8 +499,9 @@ func (engine *ArbitrageDetectionEngine) processScanBatch(ctx context.Context, ba
// Send opportunity to processing channel
select {
case engine.opportunityChan <- result.Opportunity:
engine.logger.Info(fmt.Sprintf("🎯 Found profitable arbitrage: %s profit, %0.1f%% confidence",
result.Opportunity.NetProfit.String(),
profitDisplay := ethAmountString(engine.decimalConverter, nil, result.Opportunity.NetProfit)
engine.logger.Info(fmt.Sprintf("🎯 Found profitable arbitrage: %s ETH profit, %0.1f%% confidence",
profitDisplay,
result.Opportunity.Confidence*100))
default:
engine.logger.Warn("Opportunity channel full, dropping opportunity")
@@ -548,14 +617,37 @@ func (engine *ArbitrageDetectionEngine) findBestPool(exchange *exchanges.Exchang
// isOpportunityBetter compares two opportunities and returns true if the first is better
func (engine *ArbitrageDetectionEngine) isOpportunityBetter(opp1, opp2 *types.ArbitrageOpportunity) bool {
// Compare net profit first
if opp1.NetProfit.Cmp(opp2.NetProfit) > 0 {
return true
} else if opp1.NetProfit.Cmp(opp2.NetProfit) < 0 {
if opp1 == nil {
return false
}
if opp2 == nil {
return true
}
if opp1.Quantities != nil && opp2.Quantities != nil {
net1, err1 := engine.decimalAmountToUniversal(opp1.Quantities.NetProfit)
net2, err2 := engine.decimalAmountToUniversal(opp2.Quantities.NetProfit)
if err1 == nil && err2 == nil {
cmp, err := engine.decimalConverter.Compare(net1, net2)
if err == nil {
if cmp > 0 {
return true
} else if cmp < 0 {
return false
}
}
}
}
// Fallback to canonical big.Int comparison
if opp1.NetProfit != nil && opp2.NetProfit != nil {
if opp1.NetProfit.Cmp(opp2.NetProfit) > 0 {
return true
} else if opp1.NetProfit.Cmp(opp2.NetProfit) < 0 {
return false
}
}
// If profits are equal, compare confidence
return opp1.Confidence > opp2.Confidence
}
@@ -566,6 +658,17 @@ func (engine *ArbitrageDetectionEngine) processPathTask(task ScanTask) {
engine.processScanTask(task)
}
func (engine *ArbitrageDetectionEngine) decimalAmountToUniversal(dec types.DecimalAmount) (*math.UniversalDecimal, error) {
if dec.Value == "" {
return nil, fmt.Errorf("decimal amount empty")
}
value, ok := new(big.Int).SetString(dec.Value, 10)
if !ok {
return nil, fmt.Errorf("invalid decimal amount %s", dec.Value)
}
return math.NewUniversalDecimal(value, dec.Decimals, dec.Symbol)
}
// opportunityProcessor processes discovered opportunities
func (engine *ArbitrageDetectionEngine) opportunityProcessor(ctx context.Context) {
for {
@@ -580,25 +683,58 @@ func (engine *ArbitrageDetectionEngine) opportunityProcessor(ctx context.Context
}
}
// processOpportunity processes a discovered arbitrage opportunity
// processOpportunity handles the detailed processing of a discovered arbitrage opportunity.
// This includes logging detailed information about the opportunity, validating its parameters,
// and potentially forwarding it to execution systems.
//
// The processing includes:
// 1. Detailed logging of opportunity parameters
// 2. Validation of profit calculations
// 3. Risk assessment
// 4. Forwarding to registered opportunity handlers
//
// Parameters:
// - opportunity: The arbitrage opportunity to process
func (engine *ArbitrageDetectionEngine) processOpportunity(opportunity *types.ArbitrageOpportunity) {
// Log the opportunity discovery with truncated addresses for readability
engine.logger.Info(fmt.Sprintf("Processing arbitrage opportunity: %s -> %s",
opportunity.TokenIn.Hex()[:8],
opportunity.TokenOut.Hex()[:8]))
// Log detailed opportunity information
engine.logger.Info(fmt.Sprintf(" Input Amount: %s",
opportunity.AmountIn.String()))
if opportunity.Quantities != nil {
if amt, err := engine.decimalAmountToUniversal(opportunity.Quantities.AmountIn); err == nil {
engine.logger.Info(fmt.Sprintf(" Input Amount: %s %s",
engine.decimalConverter.ToHumanReadable(amt), amt.Symbol))
}
} else if opportunity.AmountIn != nil {
amountDisplay := ethAmountString(engine.decimalConverter, nil, opportunity.AmountIn)
engine.logger.Info(fmt.Sprintf(" Input Amount: %s", amountDisplay))
}
engine.logger.Info(fmt.Sprintf(" Input Token: %s",
opportunity.TokenIn.Hex()))
engine.logger.Info(fmt.Sprintf(" Net Profit: %s ETH",
opportunity.NetProfit.String()))
if opportunity.Quantities != nil {
if net, err := engine.decimalAmountToUniversal(opportunity.Quantities.NetProfit); err == nil {
engine.logger.Info(fmt.Sprintf(" Net Profit: %s %s",
engine.decimalConverter.ToHumanReadable(net), net.Symbol))
}
} else if opportunity.NetProfit != nil {
netProfitDisplay := ethAmountString(engine.decimalConverter, nil, opportunity.NetProfit)
engine.logger.Info(fmt.Sprintf(" Net Profit: %s ETH",
netProfitDisplay))
}
engine.logger.Info(fmt.Sprintf(" ROI: %.2f%%", opportunity.ROI))
engine.logger.Info(fmt.Sprintf(" Price Impact: %.2f%%", opportunity.PriceImpact))
if opportunity.Quantities != nil {
if impact, err := engine.decimalAmountToUniversal(opportunity.Quantities.PriceImpact); err == nil {
engine.logger.Info(fmt.Sprintf(" Price Impact: %s %s",
engine.decimalConverter.ToHumanReadable(impact), impact.Symbol))
}
} else {
engine.logger.Info(fmt.Sprintf(" Price Impact: %.2f%%", opportunity.PriceImpact))
}
engine.logger.Info(fmt.Sprintf(" Confidence: %.1f%%", opportunity.Confidence*100))
@@ -607,8 +743,28 @@ func (engine *ArbitrageDetectionEngine) processOpportunity(opportunity *types.Ar
engine.logger.Info(fmt.Sprintf(" Protocol: %s", opportunity.Protocol))
engine.logger.Info(fmt.Sprintf(" Path length: %d", len(opportunity.Path)))
// TODO: Send to execution engine for actual execution
// This would integrate with Component 4: Flash Swap Execution System
if engine.opportunityHandler != nil {
// Do not block detection loop while execution occurs
go engine.opportunityHandler(opportunity)
}
}
// SetOpportunityHandler registers a callback function that will be invoked when a profitable
// arbitrage opportunity is discovered. This is the primary integration point between the
// detection engine and execution systems.
//
// The handler function should:
// 1. Perform additional validation if needed
// 2. Make final go/no-go decisions based on current market conditions
// 3. Trigger transaction execution if appropriate
// 4. Handle any execution errors or failures
//
// Note: The handler is called asynchronously to avoid blocking the detection loop.
//
// Parameters:
// - handler: Function to call when opportunities are discovered
func (engine *ArbitrageDetectionEngine) SetOpportunityHandler(handler func(*types.ArbitrageOpportunity)) {
engine.opportunityHandler = handler
}
// GetOpportunityChannel returns the channel for receiving opportunities
@@ -641,81 +797,111 @@ func (engine *ArbitrageDetectionEngine) ScanOpportunities(ctx context.Context, p
// Process each detection parameter
for _, param := range params {
// Create token info using simplified approach for now
// In production, this would query contract metadata
token0Info := exchanges.TokenInfo{
Address: param.TokenA.Hex(),
Symbol: param.TokenA.Hex()[:8], // Use first 8 chars of address as symbol
Name: "Unknown Token",
Decimals: 18, // Standard ERC-20 decimals
}
token1Info := exchanges.TokenInfo{
Address: param.TokenB.Hex(),
Symbol: param.TokenB.Hex()[:8], // Use first 8 chars of address as symbol
Name: "Unknown Token",
Decimals: 18, // Standard ERC-20 decimals
}
tokenPair := exchanges.TokenPair{
Token0: token0Info,
Token1: token1Info,
}
// Get exchange configurations for this token pair
exchangeConfigs := engine.registry.GetExchangesForPair(common.HexToAddress(tokenPair.Token0.Address), common.HexToAddress(tokenPair.Token1.Address))
if len(exchangeConfigs) < 2 {
continue // Need at least 2 exchanges for arbitrage
}
// Find all possible arbitrage paths between the tokens
paths := engine.findArbitragePaths(tokenPair, exchangeConfigs)
// Calculate profitability for each path
for _, path := range paths {
if len(path) == 0 {
continue
}
// Get token info for the first and last pools in the path
tokenA := path[0].Token0
tokenZ := path[len(path)-1].Token1
if path[len(path)-1].Token0.Address == tokenA.Address {
tokenZ = path[len(path)-1].Token0
}
// Test various input amounts to find the most profitable one
inputAmounts := engine.getInputAmountsToTest()
for _, inputAmount := range inputAmounts {
// Calculate arbitrage opportunity using the calculator
opportunity, err := engine.calculator.CalculateArbitrageOpportunity(path, inputAmount, tokenA, tokenZ)
if err != nil {
engine.logger.Debug(fmt.Sprintf("Failed to calculate opportunity for path: %v", err))
continue
}
// Apply filters based on the parameters
if opportunity.NetProfit.Cmp(param.MinProfit) < 0 {
continue // Below minimum profit threshold
}
// Check slippage threshold
if opportunity.PriceImpact > param.MaxSlippage {
continue // Above maximum slippage tolerance
}
// Add to opportunities if it passes all checks
opportunities = append(opportunities, opportunity)
// For now, break after finding one good opportunity per path
// to avoid too many similar results (can be made configurable)
break
}
}
paramOpportunities := engine.scanForSingleParam(param)
opportunities = append(opportunities, paramOpportunities...)
}
return opportunities, nil
}
// scanForSingleParam handles scanning for a single detection parameter
func (engine *ArbitrageDetectionEngine) scanForSingleParam(param *DetectionParams) []*types.ArbitrageOpportunity {
tokenPair := engine.createTokenPair(param)
// Get exchange configurations for this token pair
exchangeConfigs := engine.registry.GetExchangesForPair(common.HexToAddress(tokenPair.Token0.Address), common.HexToAddress(tokenPair.Token1.Address))
if len(exchangeConfigs) < 2 {
return nil // Need at least 2 exchanges for arbitrage
}
// Find all possible arbitrage paths between the tokens
paths := engine.findArbitragePaths(tokenPair, exchangeConfigs)
return engine.processPathsForOpportunities(paths, param)
}
// createTokenPair creates a token pair from detection parameters
func (engine *ArbitrageDetectionEngine) createTokenPair(param *DetectionParams) exchanges.TokenPair {
// Create token info using simplified approach for now
// In production, this would query contract metadata
token0Info := exchanges.TokenInfo{
Address: param.TokenA.Hex(),
Symbol: param.TokenA.Hex()[:8], // Use first 8 chars of address as symbol
Name: "Unknown Token",
Decimals: 18, // Standard ERC-20 decimals
}
token1Info := exchanges.TokenInfo{
Address: param.TokenB.Hex(),
Symbol: param.TokenB.Hex()[:8], // Use first 8 chars of address as symbol
Name: "Unknown Token",
Decimals: 18, // Standard ERC-20 decimals
}
return exchanges.TokenPair{
Token0: token0Info,
Token1: token1Info,
}
}
// processPathsForOpportunities processes paths to find profitable opportunities
func (engine *ArbitrageDetectionEngine) processPathsForOpportunities(paths [][]*math.PoolData, param *DetectionParams) []*types.ArbitrageOpportunity {
var opportunities []*types.ArbitrageOpportunity
for _, path := range paths {
if len(path) == 0 {
continue
}
pathOpportunities := engine.evaluatePath(path, param)
opportunities = append(opportunities, pathOpportunities...)
}
return opportunities
}
// evaluatePath evaluates an arbitrage path for opportunities
func (engine *ArbitrageDetectionEngine) evaluatePath(path []*math.PoolData, param *DetectionParams) []*types.ArbitrageOpportunity {
var opportunities []*types.ArbitrageOpportunity
// Get token info for the first and last pools in the path
tokenA := path[0].Token0
tokenZ := path[len(path)-1].Token1
if path[len(path)-1].Token0.Address == tokenA.Address {
tokenZ = path[len(path)-1].Token0
}
// Test various input amounts to find the most profitable one
inputAmounts := engine.getInputAmountsToTest()
for _, inputAmount := range inputAmounts {
// Calculate arbitrage opportunity using the calculator
opportunity, err := engine.calculator.CalculateArbitrageOpportunity(path, inputAmount, tokenA, tokenZ)
if err != nil {
engine.logger.Debug(fmt.Sprintf("Failed to calculate opportunity for path: %v", err))
continue
}
// Apply filters based on the parameters
if opportunity.NetProfit.Cmp(param.MinProfit) < 0 {
continue // Below minimum profit threshold
}
// Check slippage threshold
if opportunity.PriceImpact > param.MaxSlippage {
continue // Above maximum slippage tolerance
}
// Add to opportunities if it passes all checks
opportunities = append(opportunities, opportunity)
// For now, break after finding one good opportunity per path
// to avoid too many similar results (can be made configurable)
break
}
return opportunities
}
// DetectionStats contains statistics about the detection engine
type DetectionStats struct {
IsRunning bool

View File

@@ -7,10 +7,12 @@ import (
"math/big"
"time"
"github.com/ethereum/go-ethereum"
"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/arbitrage"
"github.com/fraktal/mev-beta/bindings/flashswap"
"github.com/fraktal/mev-beta/bindings/tokens"
@@ -53,10 +55,11 @@ type ArbitrageExecutor struct {
flashSwapAddress common.Address
// Configuration
maxGasPrice *big.Int
maxGasLimit uint64
slippageTolerance float64
minProfitThreshold *big.Int
maxGasPrice *big.Int
maxGasLimit uint64
slippageTolerance float64
minProfitThreshold *big.Int
minProfitThresholdDecimal *math.UniversalDecimal
// Transaction options
transactOpts *bind.TransactOpts
@@ -311,47 +314,49 @@ func NewArbitrageExecutor(
logger.Warn("⚠️ Could not get private key, will run in monitoring mode only")
// For now, just continue without transaction capabilities
return &ArbitrageExecutor{
client: client,
logger: logger,
keyManager: keyManager,
competitionAnalyzer: competitionAnalyzer,
gasEstimator: gasEstimator,
exchangeRegistry: exchangeRegistry,
arbitrageCalculator: arbitrageCalculator,
detectionEngine: detectionEngine,
flashExecutor: flashExecutor,
liveFramework: liveFramework,
contractValidator: contractValidator,
arbitrageAddress: arbitrageAddr,
flashSwapAddress: flashSwapAddr,
maxGasPrice: big.NewInt(500000000), // 0.5 gwei (Arbitrum L2 typical)
maxGasLimit: 2000000, // 2M gas (Arbitrum allows higher)
slippageTolerance: 0.01, // 1%
minProfitThreshold: big.NewInt(10000000000000000), // 0.01 ETH
client: client,
logger: logger,
keyManager: keyManager,
competitionAnalyzer: competitionAnalyzer,
gasEstimator: gasEstimator,
exchangeRegistry: exchangeRegistry,
arbitrageCalculator: arbitrageCalculator,
detectionEngine: detectionEngine,
flashExecutor: flashExecutor,
liveFramework: liveFramework,
contractValidator: contractValidator,
arbitrageAddress: arbitrageAddr,
flashSwapAddress: flashSwapAddr,
maxGasPrice: big.NewInt(500000000), // 0.5 gwei (Arbitrum L2 typical)
maxGasLimit: 2000000, // 2M gas (Arbitrum allows higher)
slippageTolerance: 0.01, // 1%
minProfitThreshold: big.NewInt(10000000000000000), // 0.01 ETH
minProfitThresholdDecimal: minProfitThreshold,
}, nil
}
privateKey = result.key
case <-time.After(5 * time.Second):
logger.Warn("⚠️ Key retrieval timed out, will run in monitoring mode only")
return &ArbitrageExecutor{
client: client,
logger: logger,
keyManager: keyManager,
competitionAnalyzer: competitionAnalyzer,
gasEstimator: gasEstimator,
exchangeRegistry: exchangeRegistry,
arbitrageCalculator: arbitrageCalculator,
detectionEngine: detectionEngine,
flashExecutor: flashExecutor,
liveFramework: liveFramework,
decimalConverter: decimalConverter,
contractValidator: contractValidator,
arbitrageAddress: arbitrageAddr,
flashSwapAddress: flashSwapAddr,
maxGasPrice: big.NewInt(500000000), // 0.5 gwei (Arbitrum L2 typical)
maxGasLimit: 2000000, // 2M gas (Arbitrum allows higher)
slippageTolerance: 0.01, // 1%
minProfitThreshold: big.NewInt(10000000000000000), // 0.01 ETH
client: client,
logger: logger,
keyManager: keyManager,
competitionAnalyzer: competitionAnalyzer,
gasEstimator: gasEstimator,
exchangeRegistry: exchangeRegistry,
arbitrageCalculator: arbitrageCalculator,
detectionEngine: detectionEngine,
flashExecutor: flashExecutor,
liveFramework: liveFramework,
decimalConverter: decimalConverter,
contractValidator: contractValidator,
arbitrageAddress: arbitrageAddr,
flashSwapAddress: flashSwapAddr,
maxGasPrice: big.NewInt(500000000), // 0.5 gwei (Arbitrum L2 typical)
maxGasLimit: 2000000, // 2M gas (Arbitrum allows higher)
slippageTolerance: 0.01, // 1%
minProfitThreshold: big.NewInt(10000000000000000), // 0.01 ETH
minProfitThresholdDecimal: minProfitThreshold,
}, nil
}
logger.Info("Active private key retrieved successfully")
@@ -375,28 +380,29 @@ func NewArbitrageExecutor(
// Gas price will be dynamically calculated using L2GasEstimator per transaction
return &ArbitrageExecutor{
client: client,
logger: logger,
keyManager: keyManager,
competitionAnalyzer: competitionAnalyzer, // CRITICAL: MEV competition analysis
gasEstimator: gasEstimator, // L2 gas estimation for Arbitrum
exchangeRegistry: exchangeRegistry,
arbitrageCalculator: arbitrageCalculator,
detectionEngine: detectionEngine,
flashExecutor: flashExecutor,
liveFramework: liveFramework,
decimalConverter: decimalConverter,
contractValidator: contractValidator, // Security: Contract validation
arbitrageContract: arbitrageContract,
flashSwapContract: flashSwapContract,
arbitrageAddress: arbitrageAddr,
flashSwapAddress: flashSwapAddr,
maxGasPrice: big.NewInt(500000000), // 0.5 gwei max (Arbitrum optimized)
maxGasLimit: 3000000, // 3M gas max (complex arbitrage on Arbitrum)
slippageTolerance: 0.003, // 0.3% slippage tolerance (tight for profit)
minProfitThreshold: big.NewInt(5000000000000000), // 0.005 ETH minimum profit ($5 at $1000/ETH)
transactOpts: transactOpts,
callOpts: &bind.CallOpts{},
client: client,
logger: logger,
keyManager: keyManager,
competitionAnalyzer: competitionAnalyzer, // CRITICAL: MEV competition analysis
gasEstimator: gasEstimator, // L2 gas estimation for Arbitrum
exchangeRegistry: exchangeRegistry,
arbitrageCalculator: arbitrageCalculator,
detectionEngine: detectionEngine,
flashExecutor: flashExecutor,
liveFramework: liveFramework,
decimalConverter: decimalConverter,
contractValidator: contractValidator, // Security: Contract validation
arbitrageContract: arbitrageContract,
flashSwapContract: flashSwapContract,
arbitrageAddress: arbitrageAddr,
flashSwapAddress: flashSwapAddr,
maxGasPrice: big.NewInt(500000000), // 0.5 gwei max (Arbitrum optimized)
maxGasLimit: 3000000, // 3M gas max (complex arbitrage on Arbitrum)
slippageTolerance: 0.003, // 0.3% slippage tolerance (tight for profit)
minProfitThreshold: new(big.Int).Set(minProfitThreshold.Value),
minProfitThresholdDecimal: minProfitThreshold,
transactOpts: transactOpts,
callOpts: &bind.CallOpts{},
}, nil
}
@@ -404,8 +410,9 @@ func NewArbitrageExecutor(
func (ae *ArbitrageExecutor) SimulateArbitrage(ctx context.Context, params *ArbitrageParams) (*SimulationResult, error) {
start := time.Now()
expectedProfit := ethAmountString(ae.decimalConverter, params.Path.NetProfitDecimal, params.Path.NetProfit)
ae.logger.Info(fmt.Sprintf("🔬 Simulating arbitrage execution for path with %d hops, expected profit: %s ETH",
len(params.Path.Pools), formatEther(params.Path.NetProfit)))
len(params.Path.Pools), expectedProfit))
result := &SimulationResult{
Path: params.Path,
@@ -448,8 +455,9 @@ func (ae *ArbitrageExecutor) SimulateArbitrage(ctx context.Context, params *Arbi
result.ErrorDetails = simulation.Error
result.ExecutionSteps = simulation.Steps
profitRealized := ethAmountString(ae.decimalConverter, nil, result.ProfitRealized)
ae.logger.Info(fmt.Sprintf("🧪 Arbitrage simulation successful! Estimated gas: %d, Profit: %s ETH",
result.GasEstimate, formatEther(result.ProfitRealized)))
result.GasEstimate, profitRealized))
} else {
result.Error = fmt.Errorf("simulation failed: %s", simulation.Error)
ae.logger.Error(fmt.Sprintf("🧪 Arbitrage simulation failed! Error: %s", simulation.Error))
@@ -553,9 +561,10 @@ func (ae *ArbitrageExecutor) simulateFlashSwapArbitrage(ctx context.Context, par
Status: "Completed",
})
inputAmountStr := ethAmountString(ae.decimalConverter, nil, params.AmountIn)
simulation.Steps = append(simulation.Steps, SimulationStep{
Name: "Profit Calculation",
Description: fmt.Sprintf("Calculated profit: %s ETH", formatEther(params.AmountIn)),
Description: fmt.Sprintf("Calculated profit: %s ETH", inputAmountStr),
Duration: time.Millisecond * 8,
Status: "Completed",
})
@@ -575,9 +584,10 @@ func (ae *ArbitrageExecutor) simulateFlashSwapArbitrage(ctx context.Context, par
})
} else {
simulation.Profit = realProfit
realProfitStr := ethAmountString(ae.decimalConverter, nil, realProfit)
simulation.Steps = append(simulation.Steps, SimulationStep{
Name: "Real Profit Calculated",
Description: fmt.Sprintf("Calculated real profit: %s ETH", formatEther(realProfit)),
Description: fmt.Sprintf("Calculated real profit: %s ETH", realProfitStr),
Duration: time.Millisecond * 8,
Status: "Completed",
})
@@ -708,15 +718,19 @@ func (ae *ArbitrageExecutor) ExecuteArbitrage(ctx context.Context, params *Arbit
ae.transactOpts.GasPrice = biddingStrategy.PriorityFee
ae.transactOpts.GasLimit = biddingStrategy.GasLimit
netAfterCosts := new(big.Int).Sub(opportunity.EstimatedProfit, biddingStrategy.TotalCost)
netAfterCostsStr := ethAmountString(ae.decimalConverter, nil, netAfterCosts)
priorityFeeStr := gweiAmountString(ae.decimalConverter, nil, biddingStrategy.PriorityFee)
ae.logger.Info(fmt.Sprintf("MEV Strategy: Priority fee: %s gwei, Success rate: %.1f%%, Net profit expected: %s ETH",
formatGweiFromWei(biddingStrategy.PriorityFee),
priorityFeeStr,
biddingStrategy.SuccessProbability*100,
formatEtherFromWei(new(big.Int).Sub(opportunity.EstimatedProfit, biddingStrategy.TotalCost))))
netAfterCostsStr))
}
start := time.Now()
pathProfit := ethAmountString(ae.decimalConverter, params.Path.NetProfitDecimal, params.Path.NetProfit)
ae.logger.Info(fmt.Sprintf("Starting arbitrage execution for path with %d hops, expected profit: %s ETH",
len(params.Path.Pools), formatEther(params.Path.NetProfit)))
len(params.Path.Pools), pathProfit))
result := &ExecutionResult{
Path: params.Path,
@@ -772,8 +786,9 @@ func (ae *ArbitrageExecutor) ExecuteArbitrage(ctx context.Context, params *Arbit
}
result.ProfitRealized = actualProfit
profitRealizedStr := ethAmountString(ae.decimalConverter, nil, result.ProfitRealized)
ae.logger.Info(fmt.Sprintf("Arbitrage execution successful! TX: %s, Gas used: %d, Profit: %s ETH",
result.TransactionHash.Hex(), result.GasUsed, formatEther(result.ProfitRealized)))
result.TransactionHash.Hex(), result.GasUsed, profitRealizedStr))
} else {
result.Error = fmt.Errorf("transaction failed with status %d", receipt.Status)
ae.logger.Error(fmt.Sprintf("Arbitrage execution failed! TX: %s, Gas used: %d",
@@ -787,9 +802,16 @@ func (ae *ArbitrageExecutor) ExecuteArbitrage(ctx context.Context, params *Arbit
// validateExecution validates the arbitrage execution parameters
func (ae *ArbitrageExecutor) validateExecution(ctx context.Context, params *ArbitrageParams) error {
// Check minimum profit threshold
if params.Path.NetProfit.Cmp(ae.minProfitThreshold) < 0 {
return fmt.Errorf("profit %s below minimum threshold %s",
formatEther(params.Path.NetProfit), formatEther(ae.minProfitThreshold))
if params.Path.NetProfitDecimal != nil && ae.minProfitThresholdDecimal != nil {
if cmp, err := ae.decimalConverter.Compare(params.Path.NetProfitDecimal, ae.minProfitThresholdDecimal); err == nil && cmp < 0 {
return fmt.Errorf("profit %s below minimum threshold %s",
ae.decimalConverter.ToHumanReadable(params.Path.NetProfitDecimal),
ae.decimalConverter.ToHumanReadable(ae.minProfitThresholdDecimal))
}
} else if params.Path.NetProfit != nil && params.Path.NetProfit.Cmp(ae.minProfitThreshold) < 0 {
profitStr := ethAmountString(ae.decimalConverter, params.Path.NetProfitDecimal, params.Path.NetProfit)
thresholdStr := ethAmountString(ae.decimalConverter, ae.minProfitThresholdDecimal, ae.minProfitThreshold)
return fmt.Errorf("profit %s below minimum threshold %s", profitStr, thresholdStr)
}
// Validate path has at least 2 hops
@@ -905,7 +927,11 @@ func (ae *ArbitrageExecutor) executeFlashSwapArbitrage(ctx context.Context, para
params.Deadline = big.NewInt(time.Now().Add(5 * time.Minute).Unix())
}
// Estimate gas limit
poolAddress, flashSwapParams, err := ae.buildFlashSwapExecution(params)
if err != nil {
return nil, err
}
gasLimit, err := ae.estimateGasForArbitrage(ctx, params)
if err != nil {
ae.logger.Warn(fmt.Sprintf("Gas estimation failed, using default: %v", err))
@@ -914,45 +940,89 @@ func (ae *ArbitrageExecutor) executeFlashSwapArbitrage(ctx context.Context, para
ae.transactOpts.GasLimit = gasLimit
ae.transactOpts.Context = ctx
ae.transactOpts.Nonce = nil
ae.logger.Debug(fmt.Sprintf("Executing flash swap with params: tokens=%v, pools=%v, amountIn=%s, minOut=%s, gasLimit=%d",
params.TokenPath, params.PoolPath, params.AmountIn.String(), params.MinAmountOut.String(), gasLimit))
ae.logger.Debug(fmt.Sprintf("Executing flash swap via aggregator: pool=%s amount=%s minOut=%s gas=%d",
poolAddress.Hex(), params.AmountIn.String(), params.MinAmountOut.String(), gasLimit))
// Execute the arbitrage through the deployed Uniswap V3 pool using flash swap
// We'll use the Uniswap V3 pool directly for flash swaps since it's already deployed
// Get the pool address for the first pair in the path
poolAddress := params.PoolPath[0]
// Create flash swap parameters
flashSwapParams := flashswap.IFlashSwapperFlashSwapParams{
Token0: params.TokenPath[0],
Token1: params.TokenPath[1],
Amount0: params.AmountIn,
Amount1: big.NewInt(0), // We only need one token for flash swap
To: ae.transactOpts.From, // Send back to our account
Data: []byte{}, // Encode arbitrage data if needed
}
// Execute flash swap using Uniswap V3 pool
tx, err := ae.executeUniswapV3FlashSwap(ctx, poolAddress, flashSwapParams)
tx, err := ae.flashSwapContract.ExecuteFlashSwap(ae.transactOpts, poolAddress, flashSwapParams)
if err != nil {
return nil, fmt.Errorf("failed to execute Uniswap V3 flash swap: %w", err)
return nil, fmt.Errorf("failed to execute flash swap: %w", err)
}
ae.logger.Info(fmt.Sprintf("Flash swap transaction submitted: %s", tx.Hash().Hex()))
return tx, nil
}
func (ae *ArbitrageExecutor) buildFlashSwapExecution(params *FlashSwapParams) (common.Address, flashswap.IFlashSwapperFlashSwapParams, error) {
if params == nil {
return common.Address{}, flashswap.IFlashSwapperFlashSwapParams{}, fmt.Errorf("flash swap params cannot be nil")
}
if len(params.TokenPath) < 2 {
return common.Address{}, flashswap.IFlashSwapperFlashSwapParams{}, fmt.Errorf("token path must include at least two tokens")
}
if len(params.PoolPath) == 0 {
return common.Address{}, flashswap.IFlashSwapperFlashSwapParams{}, fmt.Errorf("pool path cannot be empty")
}
if params.AmountIn == nil || params.AmountIn.Sign() <= 0 {
return common.Address{}, flashswap.IFlashSwapperFlashSwapParams{}, fmt.Errorf("amount in must be positive")
}
fees := params.Fees
if fees == nil {
fees = make([]*big.Int, 0)
}
callbackData, err := encodeFlashSwapCallback(params.TokenPath, params.PoolPath, fees, params.MinAmountOut)
if err != nil {
return common.Address{}, flashswap.IFlashSwapperFlashSwapParams{}, err
}
flashParams := flashswap.IFlashSwapperFlashSwapParams{
Token0: params.TokenPath[0],
Token1: params.TokenPath[1],
Amount0: params.AmountIn,
Amount1: big.NewInt(0),
To: ae.arbitrageAddress,
Data: callbackData,
}
return params.PoolPath[0], flashParams, nil
}
// estimateGasForArbitrage estimates gas needed for the arbitrage transaction
func (ae *ArbitrageExecutor) estimateGasForArbitrage(ctx context.Context, params *FlashSwapParams) (uint64, error) {
// For now, return a conservative estimate
// In production, this would call the contract's estimateGas method
estimatedGas := uint64(500000) // 500k gas conservative estimate
poolAddress, flashSwapParams, err := ae.buildFlashSwapExecution(params)
if err != nil {
return 0, err
}
// Add 20% buffer to estimated gas
gasWithBuffer := estimatedGas + (estimatedGas * 20 / 100)
abiDefinition, err := flashswap.BaseFlashSwapperMetaData.GetAbi()
if err != nil {
return 0, err
}
callData, err := abiDefinition.Pack("executeFlashSwap", poolAddress, flashSwapParams)
if err != nil {
return 0, err
}
msg := ethereum.CallMsg{
From: ae.transactOpts.From,
To: &ae.flashSwapAddress,
Gas: 0,
Data: callData,
}
estimatedGas, err := ae.client.EstimateGas(ctx, msg)
if err != nil {
return 0, err
}
gasWithBuffer := uint64(float64(estimatedGas) * 1.2)
if gasWithBuffer > ae.maxGasLimit {
gasWithBuffer = ae.maxGasLimit
}
@@ -962,21 +1032,33 @@ func (ae *ArbitrageExecutor) estimateGasForArbitrage(ctx context.Context, params
// updateGasPrice updates gas price based on network conditions
func (ae *ArbitrageExecutor) updateGasPrice(ctx context.Context) error {
gasPrice, err := ae.client.SuggestGasPrice(ctx)
tipCap, err := ae.client.SuggestGasTipCap(ctx)
if err != nil {
return err
tipCap = big.NewInt(100000000) // 0.1 gwei fallback
}
// Apply 10% premium for faster execution
premiumGasPrice := new(big.Int).Add(gasPrice, new(big.Int).Div(gasPrice, big.NewInt(10)))
if premiumGasPrice.Cmp(ae.maxGasPrice) > 0 {
premiumGasPrice = ae.maxGasPrice
header, err := ae.client.HeaderByNumber(ctx, nil)
var feeCap *big.Int
if err != nil || header.BaseFee == nil {
baseFee, baseErr := ae.client.SuggestGasPrice(ctx)
if baseErr != nil {
baseFee = big.NewInt(1000000000) // 1 gwei fallback
}
feeCap = new(big.Int).Add(baseFee, tipCap)
} else {
feeCap = new(big.Int).Mul(header.BaseFee, big.NewInt(2))
feeCap.Add(feeCap, tipCap)
}
ae.transactOpts.GasPrice = premiumGasPrice
ae.logger.Debug(fmt.Sprintf("Updated gas price to %s wei", premiumGasPrice.String()))
if ae.maxGasPrice != nil && feeCap.Cmp(ae.maxGasPrice) > 0 {
feeCap = new(big.Int).Set(ae.maxGasPrice)
}
ae.transactOpts.GasTipCap = tipCap
ae.transactOpts.GasFeeCap = feeCap
ae.transactOpts.GasPrice = nil
ae.logger.Debug(fmt.Sprintf("Updated gas parameters - tip: %s wei, max fee: %s wei", tipCap.String(), feeCap.String()))
return nil
}
@@ -1043,12 +1125,15 @@ func (ae *ArbitrageExecutor) calculateProfitFromBalanceChange(ctx context.Contex
netProfit := new(big.Int).Sub(profit, gasCost)
if netProfit.Sign() > 0 {
netProfitStr := ethAmountString(ae.decimalConverter, nil, netProfit)
ae.logger.Info(fmt.Sprintf("PROFITABLE ARBITRAGE - Net profit: %s ETH",
formatEther(netProfit)))
netProfitStr))
return netProfit, nil
} else {
loss := new(big.Int).Neg(netProfit)
lossStr := ethAmountString(ae.decimalConverter, nil, loss)
ae.logger.Warn(fmt.Sprintf("UNPROFITABLE ARBITRAGE - Loss: %s ETH",
formatEther(new(big.Int).Neg(netProfit))))
lossStr))
return big.NewInt(0), nil
}
}
@@ -1060,31 +1145,6 @@ func (ae *ArbitrageExecutor) calculateProfitFromBalanceChange(ctx context.Contex
return big.NewInt(0), fmt.Errorf("no arbitrage execution detected in transaction")
}
// formatEther converts wei to ETH string with 6 decimal places
func formatEther(wei *big.Int) string {
if wei == nil {
return "0.000000"
}
eth := new(big.Float).SetInt(wei)
eth.Quo(eth, big.NewFloat(1e18))
return fmt.Sprintf("%.6f", eth)
}
// formatEtherFromWei is an alias for formatEther for consistency
func formatEtherFromWei(wei *big.Int) string {
return formatEther(wei)
}
// formatGweiFromWei converts wei to gwei string
func formatGweiFromWei(wei *big.Int) string {
if wei == nil {
return "0.0"
}
gwei := new(big.Float).SetInt(wei)
gwei.Quo(gwei, big.NewFloat(1e9))
return fmt.Sprintf("%.2f", gwei)
}
// GetArbitrageHistory retrieves historical arbitrage executions by parsing contract events
func (ae *ArbitrageExecutor) GetArbitrageHistory(ctx context.Context, fromBlock, toBlock *big.Int) ([]*ArbitrageEvent, error) {
ae.logger.Info(fmt.Sprintf("Fetching arbitrage history from block %s to %s", fromBlock.String(), toBlock.String()))
@@ -1170,7 +1230,7 @@ type ArbitrageEvent struct {
// Helper method to check if execution is profitable after gas costs
func (ae *ArbitrageExecutor) IsProfitableAfterGas(path *ArbitragePath, gasPrice *big.Int) bool {
gasCost := new(big.Int).Mul(gasPrice, big.NewInt(int64(path.EstimatedGas.Uint64())))
gasCost := new(big.Int).Mul(gasPrice, path.EstimatedGas)
netProfit := new(big.Int).Sub(path.NetProfit, gasCost)
return netProfit.Cmp(ae.minProfitThreshold) > 0
}
@@ -1403,10 +1463,16 @@ func addTrustedContractsToValidator(validator *security.ContractValidator, arbit
// Add arbitrage contract
arbitrageInfo := &security.ContractInfo{
Address: arbitrageAddr,
BytecodeHash: "placeholder_arbitrage_hash", // TODO: Get actual bytecode hash
Name: "MEV Arbitrage Contract",
Version: "1.0.0",
IsWhitelisted: true,
RiskLevel: security.RiskLevelLow,
Permissions: security.ContractPermissions{
CanInteract: true,
CanSendValue: true,
RequireConfirm: false,
},
}
if err := validator.AddTrustedContract(arbitrageInfo); err != nil {
return fmt.Errorf("failed to add arbitrage contract: %w", err)
@@ -1415,10 +1481,16 @@ func addTrustedContractsToValidator(validator *security.ContractValidator, arbit
// Add flash swap contract
flashSwapInfo := &security.ContractInfo{
Address: flashSwapAddr,
BytecodeHash: "placeholder_flashswap_hash", // TODO: Get actual bytecode hash
Name: "Flash Swap Contract",
Version: "1.0.0",
IsWhitelisted: true,
RiskLevel: security.RiskLevelLow,
Permissions: security.ContractPermissions{
CanInteract: true,
CanSendValue: true,
RequireConfirm: false,
},
}
if err := validator.AddTrustedContract(flashSwapInfo); err != nil {
return fmt.Errorf("failed to add flash swap contract: %w", err)

View File

@@ -11,6 +11,7 @@ import (
"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"
"github.com/fraktal/mev-beta/pkg/arbitrum"
"github.com/fraktal/mev-beta/pkg/math"
@@ -252,8 +253,9 @@ func (executor *FlashSwapExecutor) ExecuteArbitrage(ctx context.Context, opportu
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",
formatEther(result.ProfitRealized)))
profitDisplay))
}
return result, nil
@@ -357,17 +359,19 @@ func (executor *FlashSwapExecutor) encodeArbitrageData(opportunity *pkgtypes.Arb
// This is a simplified approach - real implementation would use proper ABI encoding
// with go-ethereum's abi package
data := []byte(fmt.Sprintf("arbitrage:%s:%s:%s:%s",
opportunity.TokenIn,
opportunity.TokenOut,
opportunity.AmountIn.String(),
opportunity.Profit.String()))
token0 := opportunity.TokenIn
token1 := opportunity.TokenOut
if len(data) > 1024 { // Limit the size
data = data[:1024]
payload, err := encodeFlashSwapCallback([]common.Address{token0, token1}, nil, nil, opportunity.NetProfit)
if err != nil {
executor.logger.Warn(fmt.Sprintf("Failed to encode flash swap callback data: %v", err))
return []byte(fmt.Sprintf("arbitrage:%s:%s:%s:%s",
opportunity.TokenIn,
opportunity.TokenOut,
opportunity.AmountIn.String(),
opportunity.Profit.String()))
}
return data
return payload
}
// getTransactionOptions prepares transaction options with dynamic gas pricing
@@ -508,7 +512,7 @@ func (executor *FlashSwapExecutor) executeWithTimeout(
actualProfit, err := executor.calculateActualProfit(receipt, executionState.Opportunity)
if err != nil {
executor.logger.Warn(fmt.Sprintf("Failed to calculate actual profit: %v", err))
actualProfit = executionState.Opportunity.NetProfit // Use expected as fallback
actualProfit = universalFromWei(executor.decimalConverter, executionState.Opportunity.NetProfit, "ETH") // Use expected as fallback
}
executionState.ActualProfit = actualProfit
@@ -588,7 +592,8 @@ func (executor *FlashSwapExecutor) waitForConfirmation(ctx context.Context, txHa
// calculateActualProfit calculates the actual profit from the transaction
func (executor *FlashSwapExecutor) calculateActualProfit(receipt *types.Receipt, opportunity *pkgtypes.ArbitrageOpportunity) (*math.UniversalDecimal, error) {
// Calculate actual gas cost
gasCost := new(big.Int).Mul(big.NewInt(int64(receipt.GasUsed)), receipt.EffectiveGasPrice)
gasUsedBigInt := new(big.Int).SetUint64(receipt.GasUsed)
gasCost := new(big.Int).Mul(gasUsedBigInt, receipt.EffectiveGasPrice)
gasCostDecimal, err := math.NewUniversalDecimal(gasCost, 18, "ETH")
if err != nil {
return nil, err
@@ -596,12 +601,13 @@ func (executor *FlashSwapExecutor) calculateActualProfit(receipt *types.Receipt,
// For demonstration, assume we got the expected output
// Production would parse the transaction logs to get actual amounts
expectedOutput := opportunity.Profit
expectedOutput := universalFromWei(executor.decimalConverter, opportunity.Profit, "ETH")
amountIn := universalFromWei(executor.decimalConverter, opportunity.AmountIn, "ETH")
// Use the decimal converter to convert to ETH equivalent
// For simplicity, assume both input and output are already in compatible formats
// In real implementation, you'd need actual price data
netProfit, err := executor.decimalConverter.Subtract(expectedOutput, opportunity.AmountIn)
netProfit, err := executor.decimalConverter.Subtract(expectedOutput, amountIn)
if err != nil {
return nil, err
}
@@ -620,7 +626,9 @@ func (executor *FlashSwapExecutor) createSuccessfulResult(state *ExecutionState,
// Convert UniversalDecimal to big.Int for ProfitRealized
profitRealized := big.NewInt(0)
if state.ActualProfit != nil {
profitRealized = state.ActualProfit
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)
}
// Create a minimal ArbitragePath based on the opportunity
@@ -635,7 +643,8 @@ func (executor *FlashSwapExecutor) createSuccessfulResult(state *ExecutionState,
LastUpdated: time.Now(),
}
gasCost := new(big.Int).Mul(big.NewInt(int64(receipt.GasUsed)), receipt.EffectiveGasPrice)
gasUsedBigInt := new(big.Int).SetUint64(receipt.GasUsed)
gasCost := new(big.Int).Mul(gasUsedBigInt, receipt.EffectiveGasPrice)
gasCostDecimal, _ := math.NewUniversalDecimal(gasCost, 18, "ETH")
return &ExecutionResult{

View File

@@ -0,0 +1,38 @@
package arbitrage
import (
"math/big"
"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/common"
)
type flashSwapCallback struct {
TokenPath []common.Address
PoolPath []common.Address
Fees []*big.Int
MinAmountOut *big.Int
}
func encodeFlashSwapCallback(tokenPath []common.Address, poolPath []common.Address, fees []*big.Int, minAmountOut *big.Int) ([]byte, error) {
tupleType, err := abi.NewType("tuple", "flashSwapCallback", []abi.ArgumentMarshaling{
{Name: "tokenPath", Type: "address[]"},
{Name: "poolPath", Type: "address[]"},
{Name: "fees", Type: "uint256[]"},
{Name: "minAmountOut", Type: "uint256"},
})
if err != nil {
return nil, err
}
arguments := abi.Arguments{{Type: tupleType}}
callback := flashSwapCallback{
TokenPath: tokenPath,
PoolPath: poolPath,
Fees: fees,
MinAmountOut: minAmountOut,
}
return arguments.Pack(callback)
}

View File

@@ -3,12 +3,14 @@ package arbitrage
import (
"context"
"fmt"
stdmath "math"
"math/big"
"sync"
"time"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/fraktal/mev-beta/internal/logger"
"github.com/fraktal/mev-beta/pkg/arbitrum"
"github.com/fraktal/mev-beta/pkg/exchanges"
@@ -18,6 +20,14 @@ import (
pkgtypes "github.com/fraktal/mev-beta/pkg/types"
)
// safeConvertUint64ToInt64 safely converts a uint64 to int64, capping at MaxInt64 if overflow would occur
func safeConvertUint64ToInt64(v uint64) int64 {
if v > stdmath.MaxInt64 {
return stdmath.MaxInt64
}
return int64(v)
}
// LiveExecutionFramework orchestrates the complete MEV bot pipeline
type LiveExecutionFramework struct {
// Core components
@@ -56,6 +66,7 @@ type LiveExecutionFramework struct {
}
// FrameworkConfig configures the live execution framework
type FrameworkConfig struct {
// Detection settings
DetectionConfig DetectionConfig
@@ -371,8 +382,8 @@ func (framework *LiveExecutionFramework) opportunityProcessor(ctx context.Contex
func (framework *LiveExecutionFramework) convertArbitrageOpportunityToMEVOpportunity(opportunity *pkgtypes.ArbitrageOpportunity) *mev.MEVOpportunity {
// Convert the arbitrage opportunity to MEV opportunity format for competition analysis
estimatedProfit := big.NewInt(0)
if opportunity.NetProfit != nil && opportunity.NetProfit.Value != nil {
estimatedProfit = opportunity.NetProfit.Value
if opportunity.NetProfit != nil {
estimatedProfit = new(big.Int).Set(opportunity.NetProfit)
}
// Calculate required gas estimate (placeholder - would be more precise in production)
@@ -399,7 +410,7 @@ func (framework *LiveExecutionFramework) processOpportunity(ctx context.Context,
framework.statsMutex.Unlock()
framework.logger.Debug(fmt.Sprintf("Processing opportunity: %s profit",
framework.decimalConverter.ToHumanReadable(opportunity.NetProfit)))
framework.decimalConverter.ToHumanReadable(universalFromWei(framework.decimalConverter, opportunity.NetProfit, "ETH"))))
// Perform risk checks
if !framework.performRiskChecks(opportunity) {
@@ -443,7 +454,7 @@ func (framework *LiveExecutionFramework) processOpportunity(ctx context.Context,
framework.statsMutex.Unlock()
framework.logger.Info(fmt.Sprintf("🎯 Queued opportunity for execution: %s profit, Priority: %d",
framework.decimalConverter.ToHumanReadable(opportunity.NetProfit), priority))
framework.decimalConverter.ToHumanReadable(universalFromWei(framework.decimalConverter, opportunity.NetProfit, "ETH")), priority))
default:
framework.logger.Warn("Execution queue full, dropping opportunity")
}
@@ -513,7 +524,7 @@ func (framework *LiveExecutionFramework) executeOpportunity(ctx context.Context,
framework.statsMutex.Unlock()
framework.logger.Info(fmt.Sprintf("🚀 Executing arbitrage: %s expected profit",
framework.decimalConverter.ToHumanReadable(task.Opportunity.NetProfit)))
opportunityAmountString(framework.decimalConverter, task.Opportunity)))
startTime := time.Now()
@@ -535,9 +546,9 @@ func (framework *LiveExecutionFramework) executeOpportunity(ctx context.Context,
// Log result
if result.Success {
profitDecimal, _ := math.NewUniversalDecimal(result.ProfitRealized, 18, "ETH")
realized := executionProfitToString(framework.decimalConverter, result)
framework.logger.Info(fmt.Sprintf("✅ Execution successful: %s profit realized in %v",
framework.decimalConverter.ToHumanReadable(profitDecimal),
realized,
executionTime))
} else {
framework.logger.Warn(fmt.Sprintf("❌ Execution failed: %s", result.ErrorMessage))
@@ -554,22 +565,63 @@ func (framework *LiveExecutionFramework) executeOpportunity(ctx context.Context,
func (framework *LiveExecutionFramework) performRiskChecks(opportunity *pkgtypes.ArbitrageOpportunity) bool {
// Check position size
if comp, _ := framework.decimalConverter.Compare(opportunity.InputAmount, framework.config.MaxPositionSize); comp > 0 {
if !framework.checkPositionSize(opportunity) {
return false
}
// Check daily loss limit
if !framework.checkDailyLossLimit() {
return false
}
// Check daily profit target
if !framework.checkDailyProfitTarget(opportunity) {
return false
}
return true
}
// checkPositionSize validates that the position size is within limits
func (framework *LiveExecutionFramework) checkPositionSize(opportunity *pkgtypes.ArbitrageOpportunity) bool {
amountWei := opportunity.AmountIn
if amountWei == nil {
amountWei = opportunity.RequiredAmount
}
if framework.config.MaxPositionSize != nil {
positionSize := universalFromWei(framework.decimalConverter, amountWei, "ETH")
if comp, _ := framework.decimalConverter.Compare(positionSize, framework.config.MaxPositionSize); comp > 0 {
return false
}
}
return true
}
// checkDailyLossLimit validates that we haven't exceeded daily loss limits
func (framework *LiveExecutionFramework) checkDailyLossLimit() bool {
today := time.Now().Format("2006-01-02")
if dailyStats, exists := framework.stats.DailyStats[today]; exists {
if dailyStats.NetProfit.IsNegative() {
if comp, _ := framework.decimalConverter.Compare(dailyStats.NetProfit, framework.config.DailyLossLimit); comp < 0 {
framework.logger.Warn("Daily loss limit reached, skipping opportunity")
return false
if framework.config.DailyLossLimit != nil {
if dailyStats, exists := framework.stats.DailyStats[today]; exists && dailyStats.NetProfit != nil {
if dailyStats.NetProfit.IsNegative() {
loss := dailyStats.NetProfit.Copy()
loss.Value.Abs(loss.Value)
if comp, err := framework.decimalConverter.Compare(loss, framework.config.DailyLossLimit); err == nil {
if comp > 0 {
framework.logger.Warn("Daily loss limit reached, skipping opportunity")
return false
}
} else {
framework.logger.Warn(fmt.Sprintf("Failed to compare daily loss to limit: %v", err))
}
}
}
}
return true
}
// Check if we've hit daily profit target
// checkDailyProfitTarget validates that we haven't exceeded daily profit target
func (framework *LiveExecutionFramework) checkDailyProfitTarget(opportunity *pkgtypes.ArbitrageOpportunity) bool {
today := time.Now().Format("2006-01-02")
if dailyStats, exists := framework.stats.DailyStats[today]; exists {
if comp, _ := framework.decimalConverter.Compare(dailyStats.ProfitRealized, framework.config.DailyProfitTarget); comp >= 0 {
framework.logger.Info("Daily profit target reached, being conservative")
@@ -579,7 +631,6 @@ func (framework *LiveExecutionFramework) performRiskChecks(opportunity *pkgtypes
}
}
}
return true
}
@@ -591,7 +642,6 @@ func (framework *LiveExecutionFramework) shouldExecuteBasedOnCompetition(analysi
}
// Skip if profit after gas is negative
// NetProfit in CompetitionData represents profit after gas
if analysis.NetProfit.Sign() < 0 {
return false
}
@@ -610,9 +660,11 @@ func (framework *LiveExecutionFramework) calculatePriority(
var basePriority TaskPriority
if comp, _ := framework.decimalConverter.Compare(opportunity.NetProfit, largeProfit); comp > 0 {
netProfitDecimal := opportunityNetProfitDecimal(framework.decimalConverter, opportunity)
if comp, _ := framework.decimalConverter.Compare(netProfitDecimal, largeProfit); comp > 0 {
basePriority = PriorityHigh
} else if comp, _ := framework.decimalConverter.Compare(opportunity.NetProfit, mediumProfit); comp > 0 {
} else if comp, _ := framework.decimalConverter.Compare(netProfitDecimal, mediumProfit); comp > 0 {
basePriority = PriorityMedium
} else {
basePriority = PriorityLow
@@ -807,9 +859,9 @@ func (framework *LiveExecutionFramework) GetMetrics() *LiveExecutionMetrics {
stats := framework.GetStats()
return &LiveExecutionMetrics{
OpportunitiesDetected: int64(stats.TotalOpportunitiesDetected),
SuccessfulExecutions: int64(stats.TotalExecutionsSuccessful),
FailedExecutions: int64(stats.TotalExecutionsAttempted - stats.TotalExecutionsSuccessful), // Failed = Attempted - Successful
OpportunitiesDetected: safeConvertUint64ToInt64(stats.TotalOpportunitiesDetected),
SuccessfulExecutions: safeConvertUint64ToInt64(stats.TotalExecutionsSuccessful),
FailedExecutions: safeConvertUint64ToInt64(stats.TotalExecutionsAttempted - stats.TotalExecutionsSuccessful), // Failed = Attempted - Successful
TotalProfit: stats.TotalProfitRealized,
AverageExecutionTime: stats.AverageExecutionTime,
CurrentWorkers: int(framework.config.WorkerPoolSize), // Using configured worker pool size as a proxy for active workers
@@ -883,6 +935,40 @@ func (pool *ExecutionWorkerPool) worker() {
}
}
func opportunityNetProfitDecimal(dc *math.DecimalConverter, opportunity *pkgtypes.ArbitrageOpportunity) *math.UniversalDecimal {
if opportunity == nil {
zero, _ := math.NewUniversalDecimal(big.NewInt(0), 18, "ETH")
return zero
}
if opportunity.Quantities != nil {
if val, ok := new(big.Int).SetString(opportunity.Quantities.NetProfit.Value, 10); ok {
if ud, err := math.NewUniversalDecimal(val, opportunity.Quantities.NetProfit.Decimals, opportunity.Quantities.NetProfit.Symbol); err == nil {
return ud
}
}
}
return universalFromWei(dc, opportunity.NetProfit, "ETH")
}
func opportunityAmountString(dc *math.DecimalConverter, opportunity *pkgtypes.ArbitrageOpportunity) string {
if opportunity == nil {
return "n/a"
}
ud := opportunityNetProfitDecimal(dc, opportunity)
return floatStringFromDecimal(ud, 6)
}
func executionProfitToString(dc *math.DecimalConverter, result *ExecutionResult) string {
if result != nil && result.ProfitRealized != nil {
ud := universalOrFromWei(dc, nil, result.ProfitRealized, 18, "ETH")
return floatStringFromDecimal(ud, 6)
}
return "0.000000"
}
// GasEstimatorWrapper wraps the Arbitrum gas estimator to implement the math.GasEstimator interface
type GasEstimatorWrapper struct {
gasEstimator *arbitrum.L2GasEstimator

View File

@@ -0,0 +1,108 @@
package arbitrage
import (
"math/big"
"testing"
"time"
"github.com/fraktal/mev-beta/internal/logger"
"github.com/fraktal/mev-beta/pkg/math"
pkgtypes "github.com/fraktal/mev-beta/pkg/types"
)
func TestPerformRiskChecksRespectsDailyLossLimitDecimals(t *testing.T) {
log := logger.New("debug", "text", "")
dc := math.NewDecimalConverter()
maxPos, _ := dc.FromString("10", 18, "ETH")
dailyLimit, _ := dc.FromString("0.5", 18, "ETH")
profitTarget, _ := dc.FromString("1", 18, "ETH")
zero, _ := dc.FromString("0", 18, "ETH")
framework := &LiveExecutionFramework{
logger: log,
decimalConverter: dc,
config: FrameworkConfig{
MaxPositionSize: maxPos,
DailyLossLimit: dailyLimit,
DailyProfitTarget: profitTarget,
},
stats: &FrameworkStats{
DailyStats: make(map[string]*DailyStats),
},
}
today := time.Now().Format("2006-01-02")
framework.stats.DailyStats[today] = &DailyStats{
Date: today,
ProfitRealized: zero.Copy(),
GasCostPaid: zero.Copy(),
}
loss, _ := math.NewUniversalDecimal(big.NewInt(-600000000000000000), 18, "ETH")
framework.stats.DailyStats[today].NetProfit = loss
opportunity := &pkgtypes.ArbitrageOpportunity{
AmountIn: big.NewInt(1000000000000000000),
Confidence: 0.95,
}
if framework.performRiskChecks(opportunity) {
t.Fatalf("expected opportunity to be rejected when loss exceeds limit")
}
acceptableLoss, _ := math.NewUniversalDecimal(big.NewInt(-200000000000000000), 18, "ETH")
framework.stats.DailyStats[today].NetProfit = acceptableLoss
if !framework.performRiskChecks(opportunity) {
t.Fatalf("expected opportunity to pass when loss within limit")
}
}
func TestOpportunityHelpersHandleMissingQuantities(t *testing.T) {
dc := math.NewDecimalConverter()
profit := big.NewInt(2100000000000000)
opportunity := &pkgtypes.ArbitrageOpportunity{
NetProfit: profit,
}
expectedDecimal := universalFromWei(dc, profit, "ETH")
resultDecimal := opportunityNetProfitDecimal(dc, opportunity)
if cmp, err := dc.Compare(resultDecimal, expectedDecimal); err != nil || cmp != 0 {
t.Fatalf("expected fallback decimal to match wei amount, got %s", dc.ToHumanReadable(resultDecimal))
}
expectedString := ethAmountString(dc, expectedDecimal, nil)
resultString := opportunityAmountString(dc, opportunity)
if resultString != expectedString {
t.Fatalf("expected fallback string %s, got %s", expectedString, resultString)
}
}
func TestOpportunityHelpersPreferDecimalSnapshots(t *testing.T) {
dc := math.NewDecimalConverter()
declared, _ := math.NewUniversalDecimal(big.NewInt(1500000000000000000), 18, "ETH")
opportunity := &pkgtypes.ArbitrageOpportunity{
NetProfit: new(big.Int).Set(declared.Value),
Quantities: &pkgtypes.OpportunityQuantities{
NetProfit: pkgtypes.DecimalAmount{
Value: declared.Value.String(),
Decimals: declared.Decimals,
Symbol: declared.Symbol,
},
},
}
resultDecimal := opportunityNetProfitDecimal(dc, opportunity)
if cmp, err := dc.Compare(resultDecimal, declared); err != nil || cmp != 0 {
t.Fatalf("expected decimal snapshot to be used, got %s", dc.ToHumanReadable(resultDecimal))
}
expectedString := ethAmountString(dc, declared, nil)
resultString := opportunityAmountString(dc, opportunity)
if resultString != expectedString {
t.Fatalf("expected human-readable snapshot %s, got %s", expectedString, resultString)
}
}

View File

@@ -10,9 +10,11 @@ import (
"time"
"github.com/ethereum/go-ethereum/common"
"github.com/fraktal/mev-beta/internal/logger"
"github.com/fraktal/mev-beta/pkg/uniswap"
"github.com/holiman/uint256"
"github.com/fraktal/mev-beta/internal/logger"
mmath "github.com/fraktal/mev-beta/pkg/math"
"github.com/fraktal/mev-beta/pkg/uniswap"
)
// MultiHopScanner implements advanced multi-hop arbitrage detection
@@ -41,14 +43,17 @@ type MultiHopScanner struct {
// ArbitragePath represents a complete arbitrage path
type ArbitragePath struct {
Tokens []common.Address // Token path (A -> B -> C -> A)
Pools []*PoolInfo // Pools used in each hop
Protocols []string // Protocol for each hop
Fees []int64 // Fee for each hop
EstimatedGas *big.Int // Estimated gas cost
NetProfit *big.Int // Net profit after gas
ROI float64 // Return on investment percentage
LastUpdated time.Time // When this path was calculated
Tokens []common.Address // Token path (A -> B -> C -> A)
Pools []*PoolInfo // Pools used in each hop
Protocols []string // Protocol for each hop
Fees []int64 // Fee for each hop
EstimatedGas *big.Int // Estimated gas cost
NetProfit *big.Int // Net profit after gas
ROI float64 // Return on investment percentage
LastUpdated time.Time // When this path was calculated
EstimatedGasDecimal *mmath.UniversalDecimal
NetProfitDecimal *mmath.UniversalDecimal
InputAmountDecimal *mmath.UniversalDecimal
}
// PoolInfo contains information about a trading pool
@@ -258,7 +263,7 @@ func (mhs *MultiHopScanner) createArbitragePath(tokens []common.Address, pools [
roi *= 100 // Convert to percentage
}
return &ArbitragePath{
path := &ArbitragePath{
Tokens: tokens,
Pools: pools,
Protocols: protocols,
@@ -268,6 +273,24 @@ func (mhs *MultiHopScanner) createArbitragePath(tokens []common.Address, pools [
ROI: roi,
LastUpdated: time.Now(),
}
if initialAmount != nil {
if ud, err := mmath.NewUniversalDecimal(new(big.Int).Set(initialAmount), 18, "INPUT"); err == nil {
path.InputAmountDecimal = ud
}
}
if totalGasCost != nil {
if ud, err := mmath.NewUniversalDecimal(new(big.Int).Set(totalGasCost), 18, "ETH"); err == nil {
path.EstimatedGasDecimal = ud
}
}
if netProfit != nil {
if ud, err := mmath.NewUniversalDecimal(new(big.Int).Set(netProfit), 18, "ETH"); err == nil {
path.NetProfitDecimal = ud
}
}
return path
}
// calculateSwapOutput calculates the output amount using sophisticated AMM mathematics

View File

@@ -7,11 +7,12 @@ import (
"time"
"github.com/ethereum/go-ethereum/common"
"github.com/fraktal/mev-beta/internal/logger"
"github.com/fraktal/mev-beta/pkg/market"
"github.com/holiman/uint256"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/fraktal/mev-beta/internal/logger"
"github.com/fraktal/mev-beta/pkg/market"
)
// MockMarketManager is a mock implementation of MarketManager for testing

View File

@@ -2,7 +2,9 @@ package arbitrage
import (
"context"
"encoding/binary"
"fmt"
stdmath "math"
"math/big"
"os"
"sync"
@@ -13,6 +15,8 @@ import (
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/holiman/uint256"
"github.com/fraktal/mev-beta/internal/config"
"github.com/fraktal/mev-beta/internal/logger"
"github.com/fraktal/mev-beta/internal/ratelimit"
@@ -26,9 +30,16 @@ import (
"github.com/fraktal/mev-beta/pkg/scanner"
"github.com/fraktal/mev-beta/pkg/security"
pkgtypes "github.com/fraktal/mev-beta/pkg/types"
"github.com/holiman/uint256"
)
// safeConvertUint32ToInt32 safely converts a uint32 to int32, capping at MaxInt32 if overflow would occur
func safeConvertUint32ToInt32(v uint32) int32 {
if v > stdmath.MaxInt32 {
return stdmath.MaxInt32
}
return int32(v)
}
// TokenPair is defined in executor.go to avoid duplication
// Use the canonical ArbitrageOpportunity from types package
@@ -85,6 +96,10 @@ type ArbitrageService struct {
tokenCache map[common.Address]TokenPair
tokenCacheMutex sync.RWMutex
// Opportunity path cache for execution
opportunityPathCache map[string]*ArbitragePath
opportunityPathMutex sync.RWMutex
// State management
isRunning bool
liveMode bool // NEW: Whether to use comprehensive live framework
@@ -271,28 +286,31 @@ func NewArbitrageService(
}
service := &ArbitrageService{
client: client,
logger: logger,
config: config,
keyManager: keyManager,
multiHopScanner: multiHopScanner,
executor: executor,
exchangeRegistry: exchangeRegistry,
arbitrageCalculator: arbitrageCalculator,
detectionEngine: detectionEngine,
flashExecutor: flashExecutor,
liveFramework: liveFramework,
marketManager: marketManager,
marketDataManager: marketDataManager,
ctx: serviceCtx,
cancel: cancel,
stats: stats,
database: database,
tokenCache: make(map[common.Address]TokenPair),
liveMode: liveFramework != nil,
monitoringOnly: false,
client: client,
logger: logger,
config: config,
keyManager: keyManager,
multiHopScanner: multiHopScanner,
executor: executor,
exchangeRegistry: exchangeRegistry,
arbitrageCalculator: arbitrageCalculator,
detectionEngine: detectionEngine,
flashExecutor: flashExecutor,
liveFramework: liveFramework,
marketManager: marketManager,
marketDataManager: marketDataManager,
ctx: serviceCtx,
cancel: cancel,
stats: stats,
database: database,
tokenCache: make(map[common.Address]TokenPair),
opportunityPathCache: make(map[string]*ArbitragePath),
liveMode: liveFramework != nil,
monitoringOnly: false,
}
detectionEngine.SetOpportunityHandler(service.handleDetectedOpportunity)
return service, nil
}
@@ -501,7 +519,7 @@ func (sas *ArbitrageService) detectArbitrageOpportunities(event *SimpleSwapEvent
// Determine the tokens involved in potential arbitrage
tokens := []common.Address{event.Token0, event.Token1}
var allOpportunities []*ArbitrageOpportunity
var allOpportunities []*pkgtypes.ArbitrageOpportunity
// Scan for opportunities starting with each token
for _, token := range tokens {
@@ -517,18 +535,50 @@ func (sas *ArbitrageService) detectArbitrageOpportunities(event *SimpleSwapEvent
// Convert paths to opportunities
for _, path := range paths {
if sas.isValidOpportunity(path) {
opportunity := &pkgtypes.ArbitrageOpportunity{
ID: sas.generateOpportunityID(path, event),
Path: path,
DetectedAt: time.Now(),
EstimatedProfit: path.NetProfit,
RequiredAmount: scanAmount,
Urgency: sas.calculateUrgency(path),
ExpiresAt: time.Now().Add(sas.config.OpportunityTTL),
}
allOpportunities = append(allOpportunities, opportunity)
if !sas.isValidOpportunity(path) {
continue
}
if len(path.Tokens) == 0 {
continue
}
pathTokens := make([]string, len(path.Tokens))
for i, tokenAddr := range path.Tokens {
pathTokens[i] = tokenAddr.Hex()
}
poolAddresses := make([]string, len(path.Pools))
for i, poolInfo := range path.Pools {
poolAddresses[i] = poolInfo.Address.Hex()
}
opportunityID := sas.generateOpportunityID(path, event)
amountCopy := new(big.Int).Set(scanAmount)
estimatedProfit := new(big.Int).Set(path.NetProfit)
opportunity := &pkgtypes.ArbitrageOpportunity{
ID: opportunityID,
Path: pathTokens,
Pools: poolAddresses,
AmountIn: amountCopy,
RequiredAmount: amountCopy,
Profit: estimatedProfit,
NetProfit: new(big.Int).Set(path.NetProfit),
EstimatedProfit: estimatedProfit,
DetectedAt: time.Now(),
ExpiresAt: time.Now().Add(sas.config.OpportunityTTL),
Timestamp: time.Now().Unix(),
Urgency: sas.calculateUrgency(path),
ROI: path.ROI,
TokenIn: path.Tokens[0],
TokenOut: path.Tokens[len(path.Tokens)-1],
Confidence: 0.7,
}
sas.storeOpportunityPath(opportunityID, path)
allOpportunities = append(allOpportunities, opportunity)
}
}
@@ -565,6 +615,13 @@ func (sas *ArbitrageService) detectArbitrageOpportunities(event *SimpleSwapEvent
}
// executeOpportunity executes a single arbitrage opportunity
func (sas *ArbitrageService) handleDetectedOpportunity(opportunity *pkgtypes.ArbitrageOpportunity) {
if opportunity == nil {
return
}
go sas.executeOpportunity(opportunity)
}
func (sas *ArbitrageService) executeOpportunity(opportunity *pkgtypes.ArbitrageOpportunity) {
// Check if opportunity is still valid
if time.Now().After(opportunity.ExpiresAt) {
@@ -576,17 +633,41 @@ func (sas *ArbitrageService) executeOpportunity(opportunity *pkgtypes.ArbitrageO
// Atomic increment for thread safety - no lock needed
atomic.AddInt64(&sas.stats.TotalOpportunitiesExecuted, 1)
// Resolve the execution path associated with this opportunity
executionPath := sas.getOpportunityPath(opportunity.ID)
if executionPath == nil {
executionPath = sas.fallbackPathFromOpportunity(opportunity)
}
if executionPath == nil {
sas.logger.Warn(fmt.Sprintf("No execution path available for opportunity %s", opportunity.ID))
return
}
inputAmount := opportunity.RequiredAmount
sas.deleteOpportunityPath(opportunity.ID)
if inputAmount != nil {
inputAmount = new(big.Int).Set(inputAmount)
}
// Prepare execution parameters
params := &ArbitrageParams{
Path: opportunity.Path,
InputAmount: opportunity.RequiredAmount,
Path: executionPath,
InputAmount: inputAmount,
MinOutputAmount: sas.calculateMinOutput(opportunity),
Deadline: big.NewInt(time.Now().Add(5 * time.Minute).Unix()),
FlashSwapData: []byte{}, // Additional data if needed
}
var estimatedProfitDecimal *math.UniversalDecimal
if opportunity.Quantities != nil {
if gross, err := decimalAmountToUniversal(opportunity.Quantities.GrossProfit); err == nil {
estimatedProfitDecimal = gross
}
}
profitDisplay := ethAmountString(nil, estimatedProfitDecimal, opportunity.EstimatedProfit)
sas.logger.Info(fmt.Sprintf("Executing arbitrage opportunity %s with estimated profit %s ETH",
opportunity.ID, formatEther(opportunity.EstimatedProfit)))
opportunity.ID, profitDisplay))
// Execute the arbitrage
result, err := sas.executor.ExecuteArbitrage(sas.ctx, params)
@@ -670,6 +751,85 @@ func (sas *ArbitrageService) calculateUrgency(path *ArbitragePath) int {
return urgency
}
func (sas *ArbitrageService) storeOpportunityPath(id string, path *ArbitragePath) {
if id == "" || path == nil {
return
}
sas.opportunityPathMutex.Lock()
sas.opportunityPathCache[id] = path
sas.opportunityPathMutex.Unlock()
}
func (sas *ArbitrageService) getOpportunityPath(id string) *ArbitragePath {
sas.opportunityPathMutex.RLock()
defer sas.opportunityPathMutex.RUnlock()
return sas.opportunityPathCache[id]
}
func (sas *ArbitrageService) deleteOpportunityPath(id string) {
if id == "" {
return
}
sas.opportunityPathMutex.Lock()
delete(sas.opportunityPathCache, id)
sas.opportunityPathMutex.Unlock()
}
func decimalAmountToUniversal(dec pkgtypes.DecimalAmount) (*math.UniversalDecimal, error) {
if dec.Value == "" {
return nil, fmt.Errorf("decimal amount empty")
}
value, ok := new(big.Int).SetString(dec.Value, 10)
if !ok {
return nil, fmt.Errorf("invalid decimal amount %s", dec.Value)
}
return math.NewUniversalDecimal(value, dec.Decimals, dec.Symbol)
}
func (sas *ArbitrageService) fallbackPathFromOpportunity(opportunity *pkgtypes.ArbitrageOpportunity) *ArbitragePath {
if opportunity == nil {
return nil
}
path := &ArbitragePath{
Tokens: make([]common.Address, len(opportunity.Path)),
Pools: make([]*PoolInfo, 0, len(opportunity.Pools)),
Protocols: make([]string, 0),
Fees: make([]int64, 0),
EstimatedGas: big.NewInt(0),
NetProfit: big.NewInt(0),
ROI: opportunity.ROI,
LastUpdated: time.Now(),
}
if opportunity.NetProfit != nil {
path.NetProfit = new(big.Int).Set(opportunity.NetProfit)
}
if opportunity.Quantities != nil {
if net, err := decimalAmountToUniversal(opportunity.Quantities.NetProfit); err == nil {
path.NetProfitDecimal = net
}
if gas, err := decimalAmountToUniversal(opportunity.Quantities.GasCost); err == nil {
path.EstimatedGasDecimal = gas
}
if amt, err := decimalAmountToUniversal(opportunity.Quantities.AmountIn); err == nil {
path.InputAmountDecimal = amt
}
}
for i, tokenStr := range opportunity.Path {
path.Tokens[i] = common.HexToAddress(tokenStr)
}
for _, poolStr := range opportunity.Pools {
path.Pools = append(path.Pools, &PoolInfo{Address: common.HexToAddress(poolStr)})
}
return path
}
func (sas *ArbitrageService) rankOpportunities(opportunities []*pkgtypes.ArbitrageOpportunity) {
for i := 0; i < len(opportunities); i++ {
for j := i + 1; j < len(opportunities); j++ {
@@ -721,7 +881,8 @@ func (sas *ArbitrageService) processExecutionResult(result *ExecutionResult) {
sas.stats.TotalProfitRealized.Add(sas.stats.TotalProfitRealized, result.ProfitRealized)
}
gasCost := new(big.Int).Mul(result.GasPrice, big.NewInt(int64(result.GasUsed)))
gasUsedBigInt := new(big.Int).SetUint64(result.GasUsed)
gasCost := new(big.Int).Mul(result.GasPrice, gasUsedBigInt)
sas.stats.TotalGasSpent.Add(sas.stats.TotalGasSpent, gasCost)
sas.stats.LastExecutionTime = time.Now()
@@ -738,8 +899,9 @@ func (sas *ArbitrageService) processExecutionResult(result *ExecutionResult) {
}
if result.Success {
profitDisplay := ethAmountString(nil, nil, result.ProfitRealized)
sas.logger.Info(fmt.Sprintf("Arbitrage execution successful: TX %s, Profit: %s ETH, Gas: %d",
result.TransactionHash.Hex(), formatEther(result.ProfitRealized), result.GasUsed))
result.TransactionHash.Hex(), profitDisplay, result.GasUsed))
} else {
sas.logger.Error(fmt.Sprintf("Arbitrage execution failed: TX %s, Error: %v",
result.TransactionHash.Hex(), result.Error))
@@ -783,14 +945,16 @@ func (sas *ArbitrageService) logStats() {
}
// Log comprehensive stats
profitDisplay := ethAmountString(nil, nil, totalProfit)
gasDisplay := ethAmountString(nil, nil, totalGas)
sas.logger.Info(fmt.Sprintf("Arbitrage Service Stats - Detected: %d, Executed: %d, Successful: %d, "+
"Success Rate: %.2f%%, Total Profit: %s ETH, Total Gas: %s ETH, Avg Execution: %v, Last: %v",
detected,
executed,
successful,
successRate,
formatEther(totalProfit),
formatEther(totalGas),
profitDisplay,
gasDisplay,
avgExecutionTime,
lastExecution.Format("15:04:05")))
}
@@ -1003,7 +1167,7 @@ func (sas *ArbitrageService) parseSwapLog(log *types.Log, tx *types.Transaction,
}
// Parse the event data
if len(log.Topics) < 3 || len(log.Data) < 192 { // 6 * 32 bytes
if len(log.Topics) < 3 || len(log.Data) < 160 { // 5 * 32 bytes for amount0, amount1, sqrtPriceX96, liquidity, tick
return nil
}
@@ -1012,16 +1176,27 @@ func (sas *ArbitrageService) parseSwapLog(log *types.Log, tx *types.Transaction,
// recipient := common.BytesToAddress(log.Topics[2].Bytes())
// Extract non-indexed parameters from data
amount0 := new(big.Int).SetBytes(log.Data[0:32])
amount1 := new(big.Int).SetBytes(log.Data[32:64])
// Parse signed amounts correctly (amount0 and amount1 are int256)
amount0, err := sas.parseSignedInt256(log.Data[0:32])
if err != nil {
return nil
}
amount1, err := sas.parseSignedInt256(log.Data[32:64])
if err != nil {
return nil
}
sqrtPriceX96 := new(big.Int).SetBytes(log.Data[64:96])
liquidity := new(big.Int).SetBytes(log.Data[96:128])
// Extract tick (int24, but stored as int256)
tickBytes := log.Data[128:160]
tick := new(big.Int).SetBytes(tickBytes)
if tick.Bit(255) == 1 { // Check if negative (two's complement)
tick.Sub(tick, new(big.Int).Lsh(big.NewInt(1), 256))
tick, err := sas.parseSignedInt24(log.Data[128:160])
if err != nil {
return nil
}
// CRITICAL FIX: Validate pool address is not zero before processing
if log.Address == (common.Address{}) {
return nil // Skip events with zero pool addresses
}
// Get pool tokens by querying the actual pool contract
@@ -1030,6 +1205,12 @@ func (sas *ArbitrageService) parseSwapLog(log *types.Log, tx *types.Transaction,
return nil // Skip if we can't get pool tokens
}
// DEBUG: Log details about this swap event creation
if log.Address == (common.Address{}) {
sas.logger.Error(fmt.Sprintf("ZERO ADDRESS DEBUG [ARBITRAGE-1]: Creating SimpleSwapEvent with zero address - TxHash: %s, LogIndex: %d, BlockNumber: %d, LogTopics: %d, LogData: %d bytes",
tx.Hash().Hex(), log.Index, log.BlockNumber, len(log.Topics), len(log.Data)))
}
return &SimpleSwapEvent{
TxHash: tx.Hash(),
PoolAddress: log.Address,
@@ -1039,13 +1220,70 @@ func (sas *ArbitrageService) parseSwapLog(log *types.Log, tx *types.Transaction,
Amount1: amount1,
SqrtPriceX96: sqrtPriceX96,
Liquidity: liquidity,
Tick: int32(tick.Int64()),
Tick: tick,
BlockNumber: blockNumber,
LogIndex: log.Index,
Timestamp: time.Now(),
}
}
// parseSignedInt256 correctly parses a signed 256-bit integer from bytes
func (sas *ArbitrageService) parseSignedInt256(data []byte) (*big.Int, error) {
if len(data) != 32 {
return nil, fmt.Errorf("invalid data length for int256: got %d, need 32", len(data))
}
value := new(big.Int).SetBytes(data)
// Check if the value is negative (MSB set)
if len(data) > 0 && data[0]&0x80 != 0 {
// Convert from two's complement
// Subtract 2^256 to get the negative value
maxUint256 := new(big.Int)
maxUint256.Lsh(big.NewInt(1), 256)
value.Sub(value, maxUint256)
}
return value, nil
}
// parseSignedInt24 correctly parses a signed 24-bit integer stored in a 32-byte field
func (sas *ArbitrageService) parseSignedInt24(data []byte) (int32, error) {
if len(data) != 32 {
return 0, fmt.Errorf("invalid data length for int24: got %d, need 32", len(data))
}
signByte := data[28]
if signByte != 0x00 && signByte != 0xFF {
return 0, fmt.Errorf("invalid sign extension byte 0x%02x for int24", signByte)
}
if signByte == 0x00 && data[29]&0x80 != 0 {
return 0, fmt.Errorf("value uses more than 23 bits for positive int24")
}
if signByte == 0xFF && data[29]&0x80 == 0 {
return 0, fmt.Errorf("value uses more than 23 bits for negative int24")
}
// Extract the last 4 bytes (since int24 is stored as int256)
value := binary.BigEndian.Uint32(data[28:32])
// Convert to int24 by masking and sign-extending
int24Value := int32(safeConvertUint32ToInt32(value & 0xFFFFFF)) // Mask to 24 bits
// Check if negative (bit 23 set)
if int24Value&0x800000 != 0 {
// Sign extend to int32
int24Value |= ^0xFFFFFF // Set all bits above bit 23 to 1 for negative numbers
}
// Validate range for int24
if int24Value < -8388608 || int24Value > 8388607 {
return 0, fmt.Errorf("value %d out of range for int24", int24Value)
}
return int24Value, nil
}
// getPoolTokens retrieves token addresses for a Uniswap V3 pool with caching
func (sas *ArbitrageService) getPoolTokens(poolAddress common.Address) (token0, token1 common.Address, err error) {
// Check cache first
@@ -1104,9 +1342,10 @@ func (sas *ArbitrageService) getSwapEventsFromBlock(blockNumber uint64) []*Simpl
swapEventSig := common.HexToHash("0xc42079f94a6350d7e6235f29174924f928cc2ac818eb64fed8004e115fbcca67")
// Create filter query for this specific block
blockNumberBigInt := new(big.Int).SetUint64(blockNumber)
query := ethereum.FilterQuery{
FromBlock: big.NewInt(int64(blockNumber)),
ToBlock: big.NewInt(int64(blockNumber)),
FromBlock: blockNumberBigInt,
ToBlock: blockNumberBigInt,
Topics: [][]common.Hash{{swapEventSig}},
}
@@ -1246,22 +1485,36 @@ func (sas *ArbitrageService) createArbitrumMonitor() (*monitor.ArbitrumMonitor,
func (sas *ArbitrageService) parseSwapEvent(log types.Log, blockNumber uint64) *SimpleSwapEvent {
// Validate log structure
if len(log.Topics) < 3 || len(log.Data) < 192 { // 6 * 32 bytes
if len(log.Topics) < 3 || len(log.Data) < 160 { // 5 * 32 bytes for amount0, amount1, sqrtPriceX96, liquidity, tick
sas.logger.Debug(fmt.Sprintf("Invalid log structure: topics=%d, data_len=%d", len(log.Topics), len(log.Data)))
return nil
}
// Extract non-indexed parameters from data
amount0 := new(big.Int).SetBytes(log.Data[0:32])
amount1 := new(big.Int).SetBytes(log.Data[32:64])
// Parse signed amounts correctly (amount0 and amount1 are int256)
amount0, err := sas.parseSignedInt256(log.Data[0:32])
if err != nil {
sas.logger.Debug(fmt.Sprintf("Failed to parse amount0: %v", err))
return nil
}
amount1, err := sas.parseSignedInt256(log.Data[32:64])
if err != nil {
sas.logger.Debug(fmt.Sprintf("Failed to parse amount1: %v", err))
return nil
}
sqrtPriceX96 := new(big.Int).SetBytes(log.Data[64:96])
liquidity := new(big.Int).SetBytes(log.Data[96:128])
// Extract tick (int24, but stored as int256)
tickBytes := log.Data[128:160]
tick := new(big.Int).SetBytes(tickBytes)
if tick.Bit(255) == 1 { // Check if negative (two's complement)
tick.Sub(tick, new(big.Int).Lsh(big.NewInt(1), 256))
tick, err := sas.parseSignedInt24(log.Data[128:160])
if err != nil {
sas.logger.Debug(fmt.Sprintf("Failed to parse tick: %v", err))
return nil
}
// CRITICAL FIX: Validate pool address is not zero before processing
if log.Address == (common.Address{}) {
return nil // Skip events with zero pool addresses
}
// Get pool tokens by querying the actual pool contract
@@ -1274,6 +1527,12 @@ func (sas *ArbitrageService) parseSwapEvent(log types.Log, blockNumber uint64) *
sas.logger.Debug(fmt.Sprintf("Successfully got pool tokens: %s/%s for pool %s",
token0.Hex(), token1.Hex(), log.Address.Hex()))
// DEBUG: Log details about this swap event creation
if log.Address == (common.Address{}) {
sas.logger.Error(fmt.Sprintf("ZERO ADDRESS DEBUG [ARBITRAGE-2]: Creating SimpleSwapEvent with zero address - TxHash: %s, LogIndex: %d, BlockNumber: %d, LogTopics: %d, LogData: %d bytes",
log.TxHash.Hex(), log.Index, log.BlockNumber, len(log.Topics), len(log.Data)))
}
return &SimpleSwapEvent{
TxHash: log.TxHash,
PoolAddress: log.Address,
@@ -1283,7 +1542,7 @@ func (sas *ArbitrageService) parseSwapEvent(log types.Log, blockNumber uint64) *
Amount1: amount1,
SqrtPriceX96: sqrtPriceX96,
Liquidity: liquidity,
Tick: int32(tick.Int64()),
Tick: tick,
BlockNumber: blockNumber,
LogIndex: log.Index,
Timestamp: time.Now(),
@@ -1414,7 +1673,7 @@ func (sas *ArbitrageService) ScanTokenPairs(ctx context.Context, pairs []TokenPa
continue
}
if opportunity.NetProfit.Value.Cmp(big.NewInt(sas.config.MinProfitWei)) > 0 {
if opportunity.NetProfit.Cmp(big.NewInt(sas.config.MinProfitWei)) > 0 {
allOpportunities = append(allOpportunities, opportunity)
}
}

View File

@@ -0,0 +1,161 @@
package arbitrage
import (
"testing"
)
func TestParseSignedInt256(t *testing.T) {
sas := &ArbitrageService{}
tests := []struct {
name string
input []byte
expected string
}{
{
name: "positive value",
input: make([]byte, 32), // All zeros = 0
expected: "0",
},
{
name: "negative value",
input: func() []byte {
// Create a -1 value in two's complement (all 1s)
data := make([]byte, 32)
for i := range data {
data[i] = 0xFF
}
return data
}(),
expected: "-1",
},
{
name: "large positive value",
input: func() []byte {
data := make([]byte, 32)
data[31] = 0x01 // Small positive number
return data
}(),
expected: "1",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, err := sas.parseSignedInt256(tt.input)
if err != nil {
t.Fatalf("parseSignedInt256() error = %v", err)
}
if result.String() != tt.expected {
t.Errorf("parseSignedInt256() = %v, want %v", result.String(), tt.expected)
}
})
}
}
func TestParseSignedInt24(t *testing.T) {
sas := &ArbitrageService{}
tests := []struct {
name string
input []byte
expected int32
}{
{
name: "zero value",
input: make([]byte, 32), // All zeros
expected: 0,
},
{
name: "positive value",
input: func() []byte {
data := make([]byte, 32)
data[31] = 0x01 // 1
return data
}(),
expected: 1,
},
{
name: "negative value (-1)",
input: func() []byte {
data := make([]byte, 32)
data[28] = 0xFF
// Set the 24-bit value to all 1s (which is -1 in two's complement)
data[29] = 0xFF
data[30] = 0xFF
data[31] = 0xFF
return data
}(),
expected: -1,
},
{
name: "max positive int24",
input: func() []byte {
data := make([]byte, 32)
// 0x7FFFFF = 8388607 (max int24)
data[29] = 0x7F
data[30] = 0xFF
data[31] = 0xFF
return data
}(),
expected: 8388607,
},
{
name: "min negative int24",
input: func() []byte {
data := make([]byte, 32)
data[28] = 0xFF
// 0x800000 = -8388608 (min int24)
data[29] = 0x80
data[30] = 0x00
data[31] = 0x00
return data
}(),
expected: -8388608,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, err := sas.parseSignedInt24(tt.input)
if err != nil {
t.Fatalf("parseSignedInt24() error = %v", err)
}
if result != tt.expected {
t.Errorf("parseSignedInt24() = %v, want %v", result, tt.expected)
}
})
}
}
func TestParseSignedInt24Errors(t *testing.T) {
sas := &ArbitrageService{}
tests := []struct {
name string
input []byte
}{
{
name: "invalid length",
input: make([]byte, 16), // Wrong length
},
{
name: "out of range positive",
input: func() []byte {
data := make([]byte, 32)
// Set a value > 8388607
data[28] = 0x01 // This makes it > 24 bits
return data
}(),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, err := sas.parseSignedInt24(tt.input)
if err == nil {
t.Errorf("parseSignedInt24() expected error but got none")
}
})
}
}

View File

@@ -5,6 +5,7 @@ import (
"time"
"github.com/ethereum/go-ethereum/common"
"github.com/fraktal/mev-beta/pkg/math"
"github.com/fraktal/mev-beta/pkg/mev"
"github.com/fraktal/mev-beta/pkg/types"