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

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

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

603 lines
18 KiB
Go

package internal
import (
"context"
"encoding/json"
"fmt"
"log"
"math/big"
"os"
"path/filepath"
"sort"
"strings"
"time"
"github.com/fraktal/mev-beta/pkg/arbitrage"
"github.com/fraktal/mev-beta/pkg/math"
)
type ValidatorConfig struct {
Exchanges string
MinProfitBP float64
MaxSlippage float64
OutputDir string
Verbose bool
DryRun bool
TestMode bool
}
type OpportunityValidator struct {
config *ValidatorConfig
supportedExchanges []string
detectionEngine *arbitrage.DetectionEngine
calculator *math.ArbitrageCalculator
results *ValidationResults
}
type ValidationResults struct {
TotalOpportunities int `json:"total_opportunities"`
ValidOpportunities int `json:"valid_opportunities"`
InvalidOpportunities int `json:"invalid_opportunities"`
AverageProfitBP float64 `json:"average_profit_bp"`
MaxProfitBP float64 `json:"max_profit_bp"`
OpportunityDetails []OpportunityResult `json:"opportunity_details"`
ExchangeBreakdown map[string]int `json:"exchange_breakdown"`
ValidationErrors []ValidationError `json:"validation_errors"`
Timestamp time.Time `json:"timestamp"`
DurationMs int64 `json:"duration_ms"`
}
type OpportunityResult struct {
ID string `json:"id"`
Exchange1 string `json:"exchange1"`
Exchange2 string `json:"exchange2"`
TokenA string `json:"token_a"`
TokenB string `json:"token_b"`
ProfitBP float64 `json:"profit_bp"`
SlippageBP float64 `json:"slippage_bp"`
GasCostETH float64 `json:"gas_cost_eth"`
NetProfitBP float64 `json:"net_profit_bp"`
ExecutionTime time.Time `json:"execution_time"`
Valid bool `json:"valid"`
ValidationNotes []string `json:"validation_notes"`
}
type ValidationError struct {
Type string `json:"type"`
Message string `json:"message"`
Exchange string `json:"exchange,omitempty"`
TokenPair string `json:"token_pair,omitempty"`
Timestamp time.Time `json:"timestamp"`
}
func NewOpportunityValidator(config *ValidatorConfig) (*OpportunityValidator, error) {
// Parse supported exchanges
exchanges := strings.Split(config.Exchanges, ",")
for i, exchange := range exchanges {
exchanges[i] = strings.TrimSpace(exchange)
}
// Initialize detection engine
detectionEngine, err := arbitrage.NewDetectionEngine(&arbitrage.DetectionEngineConfig{
MinProfitBasisPoints: config.MinProfitBP,
MaxSlippageBasisPoints: config.MaxSlippage,
EnabledExchanges: exchanges,
})
if err != nil {
return nil, fmt.Errorf("failed to initialize detection engine: %w", err)
}
// Initialize arbitrage calculator
calculator := math.NewArbitrageCalculator()
return &OpportunityValidator{
config: config,
supportedExchanges: exchanges,
detectionEngine: detectionEngine,
calculator: calculator,
results: &ValidationResults{
OpportunityDetails: make([]OpportunityResult, 0),
ExchangeBreakdown: make(map[string]int),
ValidationErrors: make([]ValidationError, 0),
Timestamp: time.Now(),
},
}, nil
}
func (ov *OpportunityValidator) ValidateOpportunities(ctx context.Context) error {
startTime := time.Now()
defer func() {
ov.results.DurationMs = time.Since(startTime).Milliseconds()
}()
if ov.config.TestMode {
return ov.validateTestOpportunities(ctx)
}
return ov.validateRealOpportunities(ctx)
}
func (ov *OpportunityValidator) validateTestOpportunities(ctx context.Context) error {
// Create simulated test opportunities
testOpportunities := ov.generateTestOpportunities()
for _, opportunity := range testOpportunities {
select {
case <-ctx.Done():
return ctx.Err()
default:
result := ov.validateSingleOpportunity(opportunity)
ov.results.OpportunityDetails = append(ov.results.OpportunityDetails, result)
ov.updateStatistics(result)
if ov.config.Verbose {
fmt.Printf("Validated opportunity %s: Valid=%t, Profit=%.2f bp\n",
result.ID, result.Valid, result.ProfitBP)
}
}
}
return nil
}
func (ov *OpportunityValidator) validateRealOpportunities(ctx context.Context) error {
// In real mode, we would connect to live data sources
// For now, implement basic validation framework
if ov.config.Verbose {
fmt.Println("Scanning for real-time arbitrage opportunities...")
}
// This would integrate with the actual market scanner
// For demonstration, create a few real-world scenarios
realOpportunities := ov.generateRealWorldScenarios()
for _, opportunity := range realOpportunities {
select {
case <-ctx.Done():
return ctx.Err()
default:
result := ov.validateSingleOpportunity(opportunity)
ov.results.OpportunityDetails = append(ov.results.OpportunityDetails, result)
ov.updateStatistics(result)
if ov.config.Verbose {
fmt.Printf("Validated real opportunity %s: Valid=%t, Profit=%.2f bp\n",
result.ID, result.Valid, result.ProfitBP)
}
}
}
return nil
}
func (ov *OpportunityValidator) validateSingleOpportunity(opportunity *TestOpportunity) OpportunityResult {
result := OpportunityResult{
ID: opportunity.ID,
Exchange1: opportunity.Exchange1,
Exchange2: opportunity.Exchange2,
TokenA: opportunity.TokenA,
TokenB: opportunity.TokenB,
ExecutionTime: time.Now(),
ValidationNotes: make([]string, 0),
}
// Calculate profit using arbitrage calculator
profitCalculation, err := ov.calculateProfit(opportunity)
if err != nil {
ov.addValidationError("profit_calculation", err.Error(), opportunity.Exchange1,
fmt.Sprintf("%s/%s", opportunity.TokenA, opportunity.TokenB))
result.Valid = false
result.ValidationNotes = append(result.ValidationNotes, fmt.Sprintf("Profit calculation failed: %v", err))
return result
}
result.ProfitBP = profitCalculation.ProfitBP
result.SlippageBP = profitCalculation.SlippageBP
result.GasCostETH = profitCalculation.GasCostETH
result.NetProfitBP = profitCalculation.NetProfitBP
// Validate profit threshold
if result.ProfitBP < ov.config.MinProfitBP {
result.Valid = false
result.ValidationNotes = append(result.ValidationNotes,
fmt.Sprintf("Profit %.2f bp below minimum threshold %.2f bp", result.ProfitBP, ov.config.MinProfitBP))
}
// Validate slippage threshold
if result.SlippageBP > ov.config.MaxSlippage {
result.Valid = false
result.ValidationNotes = append(result.ValidationNotes,
fmt.Sprintf("Slippage %.2f bp exceeds maximum %.2f bp", result.SlippageBP, ov.config.MaxSlippage))
}
// Validate net profit after gas costs
if result.NetProfitBP <= 0 {
result.Valid = false
result.ValidationNotes = append(result.ValidationNotes,
fmt.Sprintf("Net profit %.2f bp not profitable after gas costs", result.NetProfitBP))
}
// Additional exchange-specific validations
if err := ov.validateExchangeSpecific(opportunity); err != nil {
result.Valid = false
result.ValidationNotes = append(result.ValidationNotes, fmt.Sprintf("Exchange validation failed: %v", err))
}
// If no validation errors, mark as valid
if len(result.ValidationNotes) == 0 {
result.Valid = true
result.ValidationNotes = append(result.ValidationNotes, "All validations passed")
}
return result
}
type TestOpportunity struct {
ID string
Exchange1 string
Exchange2 string
TokenA string
TokenB string
Price1 *big.Float
Price2 *big.Float
Liquidity *big.Float
GasLimit uint64
}
type ProfitCalculation struct {
ProfitBP float64
SlippageBP float64
GasCostETH float64
NetProfitBP float64
}
func (ov *OpportunityValidator) calculateProfit(opportunity *TestOpportunity) (*ProfitCalculation, error) {
// Convert prices to UniversalDecimal for precise calculations
price1UD, err := math.NewUniversalDecimal(opportunity.Price1, 18)
if err != nil {
return nil, fmt.Errorf("failed to convert price1: %w", err)
}
price2UD, err := math.NewUniversalDecimal(opportunity.Price2, 18)
if err != nil {
return nil, fmt.Errorf("failed to convert price2: %w", err)
}
// Calculate profit basis points
priceDiff := price2UD.Sub(price1UD)
profitRatio := priceDiff.Div(price1UD)
profitBP := profitRatio.Mul(math.NewUniversalDecimalFromFloat(10000, 18)).Float64()
// Estimate slippage based on liquidity
liquidityUD, err := math.NewUniversalDecimal(opportunity.Liquidity, 18)
if err != nil {
return nil, fmt.Errorf("failed to convert liquidity: %w", err)
}
// Simple slippage model: inversely proportional to liquidity
baseSlippage := 10.0 // 10 bp base slippage
liquidityFactor := liquidityUD.Float64() / 1000000.0 // Normalize to millions
slippageBP := baseSlippage / (1.0 + liquidityFactor)
// Estimate gas cost (simplified)
gasPriceGwei := 0.5 // 0.5 gwei on Arbitrum
gasCostETH := float64(opportunity.GasLimit) * gasPriceGwei * 1e-9
// Convert gas cost to basis points (assuming 1 ETH trade size)
gasCostBP := gasCostETH * 10000.0
// Calculate net profit
netProfitBP := profitBP - slippageBP - gasCostBP
return &ProfitCalculation{
ProfitBP: profitBP,
SlippageBP: slippageBP,
GasCostETH: gasCostETH,
NetProfitBP: netProfitBP,
}, nil
}
func (ov *OpportunityValidator) validateExchangeSpecific(opportunity *TestOpportunity) error {
// Validate exchange-specific constraints
switch opportunity.Exchange1 {
case "uniswap_v2":
return ov.validateUniswapV2(opportunity)
case "uniswap_v3":
return ov.validateUniswapV3(opportunity)
case "curve":
return ov.validateCurve(opportunity)
case "balancer":
return ov.validateBalancer(opportunity)
default:
return fmt.Errorf("unsupported exchange: %s", opportunity.Exchange1)
}
}
func (ov *OpportunityValidator) validateUniswapV2(opportunity *TestOpportunity) error {
// Uniswap V2 specific validations
if opportunity.Price1.Cmp(big.NewFloat(0)) <= 0 {
return fmt.Errorf("invalid price for Uniswap V2")
}
return nil
}
func (ov *OpportunityValidator) validateUniswapV3(opportunity *TestOpportunity) error {
// Uniswap V3 specific validations
if opportunity.Liquidity.Cmp(big.NewFloat(0)) <= 0 {
return fmt.Errorf("insufficient liquidity for Uniswap V3")
}
return nil
}
func (ov *OpportunityValidator) validateCurve(opportunity *TestOpportunity) error {
// Curve specific validations
return nil
}
func (ov *OpportunityValidator) validateBalancer(opportunity *TestOpportunity) error {
// Balancer specific validations
return nil
}
func (ov *OpportunityValidator) generateTestOpportunities() []*TestOpportunity {
opportunities := make([]*TestOpportunity, 0)
// ETH/USDC opportunities across different exchanges
opportunities = append(opportunities, &TestOpportunity{
ID: "test_eth_usdc_uniswap_v2_v3",
Exchange1: "uniswap_v2",
Exchange2: "uniswap_v3",
TokenA: "ETH",
TokenB: "USDC",
Price1: big.NewFloat(2000.0),
Price2: big.NewFloat(2002.5),
Liquidity: big.NewFloat(5000000.0),
GasLimit: 150000,
})
opportunities = append(opportunities, &TestOpportunity{
ID: "test_eth_usdc_curve_balancer",
Exchange1: "curve",
Exchange2: "balancer",
TokenA: "ETH",
TokenB: "USDC",
Price1: big.NewFloat(2001.0),
Price2: big.NewFloat(2004.0),
Liquidity: big.NewFloat(10000000.0),
GasLimit: 180000,
})
// WBTC/ETH opportunities
opportunities = append(opportunities, &TestOpportunity{
ID: "test_wbtc_eth_uniswap",
Exchange1: "uniswap_v2",
Exchange2: "uniswap_v3",
TokenA: "WBTC",
TokenB: "ETH",
Price1: big.NewFloat(15.5),
Price2: big.NewFloat(15.52),
Liquidity: big.NewFloat(2000000.0),
GasLimit: 200000,
})
// Low profit opportunity (should fail validation)
opportunities = append(opportunities, &TestOpportunity{
ID: "test_low_profit",
Exchange1: "uniswap_v2",
Exchange2: "curve",
TokenA: "USDC",
TokenB: "USDT",
Price1: big.NewFloat(1.0),
Price2: big.NewFloat(1.0005),
Liquidity: big.NewFloat(1000000.0),
GasLimit: 120000,
})
// High slippage opportunity (should fail validation)
opportunities = append(opportunities, &TestOpportunity{
ID: "test_high_slippage",
Exchange1: "uniswap_v3",
Exchange2: "balancer",
TokenA: "ETH",
TokenB: "RARE_TOKEN",
Price1: big.NewFloat(100.0),
Price2: big.NewFloat(105.0),
Liquidity: big.NewFloat(10000.0), // Low liquidity
GasLimit: 250000,
})
return opportunities
}
func (ov *OpportunityValidator) generateRealWorldScenarios() []*TestOpportunity {
// Generate more realistic scenarios based on actual market conditions
return []*TestOpportunity{
{
ID: "real_eth_usdc_arbitrum",
Exchange1: "uniswap_v3",
Exchange2: "curve",
TokenA: "ETH",
TokenB: "USDC",
Price1: big.NewFloat(2456.78),
Price2: big.NewFloat(2459.12),
Liquidity: big.NewFloat(8500000.0),
GasLimit: 165000,
},
{
ID: "real_wbtc_eth_arbitrum",
Exchange1: "balancer",
Exchange2: "uniswap_v2",
TokenA: "WBTC",
TokenB: "ETH",
Price1: big.NewFloat(16.234),
Price2: big.NewFloat(16.251),
Liquidity: big.NewFloat(3200000.0),
GasLimit: 195000,
},
}
}
func (ov *OpportunityValidator) updateStatistics(result OpportunityResult) {
ov.results.TotalOpportunities++
if result.Valid {
ov.results.ValidOpportunities++
} else {
ov.results.InvalidOpportunities++
}
// Update exchange breakdown
ov.results.ExchangeBreakdown[result.Exchange1]++
ov.results.ExchangeBreakdown[result.Exchange2]++
// Update profit statistics
if result.Valid && result.ProfitBP > 0 {
if ov.results.MaxProfitBP < result.ProfitBP {
ov.results.MaxProfitBP = result.ProfitBP
}
// Calculate running average
totalProfit := ov.results.AverageProfitBP * float64(ov.results.ValidOpportunities-1)
ov.results.AverageProfitBP = (totalProfit + result.ProfitBP) / float64(ov.results.ValidOpportunities)
}
}
func (ov *OpportunityValidator) addValidationError(errorType, message, exchange, tokenPair string) {
ov.results.ValidationErrors = append(ov.results.ValidationErrors, ValidationError{
Type: errorType,
Message: message,
Exchange: exchange,
TokenPair: tokenPair,
Timestamp: time.Now(),
})
}
func (ov *OpportunityValidator) MonitorOpportunities(ctx context.Context) error {
if ov.config.Verbose {
fmt.Println("Starting real-time opportunity monitoring...")
}
ticker := time.NewTicker(30 * time.Second) // Check every 30 seconds
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return ctx.Err()
case <-ticker.C:
// Reset results for new monitoring cycle
ov.results = &ValidationResults{
OpportunityDetails: make([]OpportunityResult, 0),
ExchangeBreakdown: make(map[string]int),
ValidationErrors: make([]ValidationError, 0),
Timestamp: time.Now(),
}
if err := ov.validateRealOpportunities(ctx); err != nil {
log.Printf("Error during opportunity validation: %v", err)
continue
}
if err := ov.GenerateReport(); err != nil {
log.Printf("Error generating report: %v", err)
continue
}
if ov.config.Verbose {
fmt.Printf("Monitoring cycle complete. Found %d valid opportunities out of %d total\n",
ov.results.ValidOpportunities, ov.results.TotalOpportunities)
}
}
}
}
func (ov *OpportunityValidator) GenerateReport() error {
// Sort opportunities by profit (descending)
sort.Slice(ov.results.OpportunityDetails, func(i, j int) bool {
return ov.results.OpportunityDetails[i].ProfitBP > ov.results.OpportunityDetails[j].ProfitBP
})
// Generate JSON report
jsonReport, err := json.MarshalIndent(ov.results, "", " ")
if err != nil {
return fmt.Errorf("failed to marshal results: %w", err)
}
// Save JSON report
timestamp := time.Now().Format("2006-01-02_15-04-05")
jsonPath := filepath.Join(ov.config.OutputDir, fmt.Sprintf("opportunity_validation_%s.json", timestamp))
if err := os.WriteFile(jsonPath, jsonReport, 0644); err != nil {
return fmt.Errorf("failed to write JSON report: %w", err)
}
// Generate summary report
summaryPath := filepath.Join(ov.config.OutputDir, fmt.Sprintf("opportunity_summary_%s.txt", timestamp))
if err := ov.generateSummaryReport(summaryPath); err != nil {
return fmt.Errorf("failed to generate summary report: %w", err)
}
if ov.config.Verbose {
fmt.Printf("Reports generated:\n")
fmt.Printf(" JSON: %s\n", jsonPath)
fmt.Printf(" Summary: %s\n", summaryPath)
}
return nil
}
func (ov *OpportunityValidator) generateSummaryReport(filePath string) error {
summary := fmt.Sprintf(`Arbitrage Opportunity Validation Report
Generated: %s
Duration: %d ms
SUMMARY
=======
Total Opportunities Analyzed: %d
Valid Opportunities: %d
Invalid Opportunities: %d
Success Rate: %.1f%%
PROFIT STATISTICS
================
Average Profit: %.2f bp
Maximum Profit: %.2f bp
EXCHANGE BREAKDOWN
==================
`, ov.results.Timestamp.Format("2006-01-02 15:04:05"),
ov.results.DurationMs,
ov.results.TotalOpportunities,
ov.results.ValidOpportunities,
ov.results.InvalidOpportunities,
float64(ov.results.ValidOpportunities)/float64(ov.results.TotalOpportunities)*100,
ov.results.AverageProfitBP,
ov.results.MaxProfitBP)
for exchange, count := range ov.results.ExchangeBreakdown {
summary += fmt.Sprintf("%s: %d opportunities\n", exchange, count)
}
summary += "\nTOP OPPORTUNITIES\n=================\n"
for i, opp := range ov.results.OpportunityDetails {
if i >= 5 { // Show top 5
break
}
status := "INVALID"
if opp.Valid {
status = "VALID"
}
summary += fmt.Sprintf("%d. %s (%s/%s) - %.2f bp - %s\n",
i+1, opp.ID, opp.Exchange1, opp.Exchange2, opp.ProfitBP, status)
}
if len(ov.results.ValidationErrors) > 0 {
summary += "\nVALIDATION ERRORS\n================\n"
for _, err := range ov.results.ValidationErrors {
summary += fmt.Sprintf("- %s: %s\n", err.Type, err.Message)
}
}
return os.WriteFile(filePath, []byte(summary), 0644)
}