//go:build integration && legacy && forked // +build integration,legacy,forked package test_main import ( "crypto/rand" "fmt" "math/big" "strings" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/fraktal/mev-beta/internal/logger" "github.com/fraktal/mev-beta/pkg/arbitrum" "github.com/fraktal/mev-beta/pkg/events" "github.com/fraktal/mev-beta/pkg/oracle" ) // FuzzTestSuite manages fuzzing and robustness testing type FuzzTestSuite struct { l2Parser *arbitrum.ArbitrumL2Parser eventParser *events.EventParser logger *logger.Logger oracle *oracle.PriceOracle // Fuzzing configuration maxFuzzIterations int maxInputSize int timeoutPerTest time.Duration crashDetectionMode bool memoryLimitMB int64 } // FuzzResult tracks the results of fuzzing operations type FuzzResult struct { TestName string `json:"test_name"` TotalTests int `json:"total_tests"` CrashCount int `json:"crash_count"` ErrorCount int `json:"error_count"` SuccessCount int `json:"success_count"` TimeoutCount int `json:"timeout_count"` UniqueErrors []string `json:"unique_errors"` MaxMemoryUsageMB float64 `json:"max_memory_usage_mb"` TotalDuration time.Duration `json:"total_duration"` InterestingInputs []string `json:"interesting_inputs"` } func NewFuzzTestSuite(t *testing.T) *FuzzTestSuite { // Setup with minimal logging to reduce overhead testLogger := logger.NewLogger(logger.Config{ Level: "error", // Only log errors during fuzzing Format: "json", }) testOracle, err := oracle.NewPriceOracle(&oracle.Config{ Providers: []oracle.Provider{ {Name: "mock", Type: "mock"}, }, }, testLogger) require.NoError(t, err, "Failed to create price oracle") l2Parser, err := arbitrum.NewArbitrumL2Parser("https://mock-rpc", testLogger, testOracle) require.NoError(t, err, "Failed to create L2 parser") eventParser := events.NewEventParser() return &FuzzTestSuite{ l2Parser: l2Parser, eventParser: eventParser, logger: testLogger, oracle: testOracle, maxFuzzIterations: 10000, maxInputSize: 8192, timeoutPerTest: 100 * time.Millisecond, crashDetectionMode: true, memoryLimitMB: 1024, } } func TestFuzzingRobustness(t *testing.T) { if testing.Short() { t.Skip("Skipping fuzzing tests in short mode") } suite := NewFuzzTestSuite(t) defer suite.l2Parser.Close() // Core fuzzing tests t.Run("FuzzTransactionData", func(t *testing.T) { suite.fuzzTransactionData(t) }) t.Run("FuzzFunctionSelectors", func(t *testing.T) { suite.fuzzFunctionSelectors(t) }) t.Run("FuzzAmountValues", func(t *testing.T) { suite.fuzzAmountValues(t) }) t.Run("FuzzAddressValues", func(t *testing.T) { suite.fuzzAddressValues(t) }) t.Run("FuzzEncodedPaths", func(t *testing.T) { suite.fuzzEncodedPaths(t) }) t.Run("FuzzMulticallData", func(t *testing.T) { suite.fuzzMulticallData(t) }) t.Run("FuzzEventLogs", func(t *testing.T) { suite.fuzzEventLogs(t) }) t.Run("FuzzConcurrentAccess", func(t *testing.T) { suite.fuzzConcurrentAccess(t) }) t.Run("FuzzMemoryExhaustion", func(t *testing.T) { suite.fuzzMemoryExhaustion(t) }) } func (suite *FuzzTestSuite) fuzzTransactionData(t *testing.T) { result := &FuzzResult{ TestName: "TransactionDataFuzz", UniqueErrors: make([]string, 0), InterestingInputs: make([]string, 0), } startTime := time.Now() defer func() { result.TotalDuration = time.Since(startTime) suite.reportFuzzResult(t, result) }() for i := 0; i < suite.maxFuzzIterations; i++ { result.TotalTests++ // Generate random transaction data txData := suite.generateRandomTransactionData() // Test parsing with timeout parseResult := suite.testParsingWithTimeout(txData, suite.timeoutPerTest) switch parseResult.Status { case "success": result.SuccessCount++ case "error": result.ErrorCount++ suite.addUniqueError(result, parseResult.Error) case "timeout": result.TimeoutCount++ case "crash": result.CrashCount++ result.InterestingInputs = append(result.InterestingInputs, txData.Input) } // Update memory usage tracking if parseResult.MemoryUsageMB > result.MaxMemoryUsageMB { result.MaxMemoryUsageMB = parseResult.MemoryUsageMB } // Log progress if i%1000 == 0 && i > 0 { t.Logf("Fuzzing progress: %d/%d iterations", i, suite.maxFuzzIterations) } } } func (suite *FuzzTestSuite) fuzzFunctionSelectors(t *testing.T) { result := &FuzzResult{ TestName: "FunctionSelectorFuzz", UniqueErrors: make([]string, 0), InterestingInputs: make([]string, 0), } startTime := time.Now() defer func() { result.TotalDuration = time.Since(startTime) suite.reportFuzzResult(t, result) }() // Known function selectors to test variations of knownSelectors := []string{ "0x38ed1739", // swapExactTokensForTokens "0x414bf389", // exactInputSingle "0xac9650d8", // multicall "0x7c025200", // 1inch swap "0xdb3e2198", // exactOutputSingle } for i := 0; i < suite.maxFuzzIterations/2; i++ { result.TotalTests++ var selector string if i%2 == 0 && len(knownSelectors) > 0 { // 50% use known selectors with random modifications baseSelector := knownSelectors[i%len(knownSelectors)] selector = suite.mutateSelector(baseSelector) } else { // 50% completely random selectors selector = suite.generateRandomSelector() } // Generate transaction data with fuzzed selector txData := &TransactionData{ Hash: fmt.Sprintf("0xfuzz_selector_%d", i), From: "0x1234567890123456789012345678901234567890", To: "0xE592427A0AEce92De3Edee1F18E0157C05861564", Input: selector + suite.generateRandomHex(256), Value: "0", } parseResult := suite.testParsingWithTimeout(txData, suite.timeoutPerTest) switch parseResult.Status { case "success": result.SuccessCount++ case "error": result.ErrorCount++ suite.addUniqueError(result, parseResult.Error) case "timeout": result.TimeoutCount++ case "crash": result.CrashCount++ result.InterestingInputs = append(result.InterestingInputs, selector) } } } func (suite *FuzzTestSuite) fuzzAmountValues(t *testing.T) { result := &FuzzResult{ TestName: "AmountValueFuzz", UniqueErrors: make([]string, 0), InterestingInputs: make([]string, 0), } startTime := time.Now() defer func() { result.TotalDuration = time.Since(startTime) suite.reportFuzzResult(t, result) }() // Test extreme amount values extremeAmounts := []string{ "0", "1", "115792089237316195423570985008687907853269984665640564039457584007913129639935", // max uint256 "57896044618658097711785492504343953926634992332820282019728792003956564819968", // max int256 "1000000000000000000000000000000000000000000000000000000000000000000000000000", // > max uint256 strings.Repeat("9", 1000), // Very large number } for i, amount := range extremeAmounts { for j := 0; j < 100; j++ { // Test each extreme amount multiple times result.TotalTests++ // Create transaction data with extreme amount txData := &TransactionData{ Hash: fmt.Sprintf("0xfuzz_amount_%d_%d", i, j), From: "0x1234567890123456789012345678901234567890", To: "0xE592427A0AEce92De3Edee1F18E0157C05861564", Input: "0x414bf389" + suite.encodeAmountInParams(amount), Value: amount, } parseResult := suite.testParsingWithTimeout(txData, suite.timeoutPerTest) switch parseResult.Status { case "success": result.SuccessCount++ case "error": result.ErrorCount++ suite.addUniqueError(result, parseResult.Error) case "timeout": result.TimeoutCount++ case "crash": result.CrashCount++ result.InterestingInputs = append(result.InterestingInputs, amount) } } } // Test random amounts for i := 0; i < suite.maxFuzzIterations/4; i++ { result.TotalTests++ amount := suite.generateRandomAmount() txData := &TransactionData{ Hash: fmt.Sprintf("0xfuzz_random_amount_%d", i), From: "0x1234567890123456789012345678901234567890", To: "0xE592427A0AEce92De3Edee1F18E0157C05861564", Input: "0x414bf389" + suite.encodeAmountInParams(amount), Value: amount, } parseResult := suite.testParsingWithTimeout(txData, suite.timeoutPerTest) switch parseResult.Status { case "success": result.SuccessCount++ case "error": result.ErrorCount++ suite.addUniqueError(result, parseResult.Error) case "timeout": result.TimeoutCount++ case "crash": result.CrashCount++ } } } func (suite *FuzzTestSuite) fuzzAddressValues(t *testing.T) { result := &FuzzResult{ TestName: "AddressFuzz", UniqueErrors: make([]string, 0), InterestingInputs: make([]string, 0), } startTime := time.Now() defer func() { result.TotalDuration = time.Since(startTime) suite.reportFuzzResult(t, result) }() // Test extreme address values extremeAddresses := []string{ "0x0000000000000000000000000000000000000000", // Zero address "0xffffffffffffffffffffffffffffffffffffffff", // Max address "0x1111111111111111111111111111111111111111", // Repeated pattern "0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef", // Known pattern "0x", // Empty "0x123", // Too short "0x12345678901234567890123456789012345678901", // Too long "invalid_address", // Invalid format } for i, address := range extremeAddresses { for j := 0; j < 50; j++ { result.TotalTests++ txData := &TransactionData{ Hash: fmt.Sprintf("0xfuzz_address_%d_%d", i, j), From: address, To: suite.generateRandomAddress(), Input: "0x38ed1739" + suite.generateRandomHex(320), Value: "0", } parseResult := suite.testParsingWithTimeout(txData, suite.timeoutPerTest) switch parseResult.Status { case "success": result.SuccessCount++ case "error": result.ErrorCount++ suite.addUniqueError(result, parseResult.Error) case "timeout": result.TimeoutCount++ case "crash": result.CrashCount++ result.InterestingInputs = append(result.InterestingInputs, address) } } } } func (suite *FuzzTestSuite) fuzzEncodedPaths(t *testing.T) { result := &FuzzResult{ TestName: "EncodedPathFuzz", UniqueErrors: make([]string, 0), InterestingInputs: make([]string, 0), } startTime := time.Now() defer func() { result.TotalDuration = time.Since(startTime) suite.reportFuzzResult(t, result) }() for i := 0; i < suite.maxFuzzIterations/2; i++ { result.TotalTests++ // Generate Uniswap V3 encoded path with random data pathData := suite.generateRandomV3Path() txData := &TransactionData{ Hash: fmt.Sprintf("0xfuzz_path_%d", i), From: "0x1234567890123456789012345678901234567890", To: "0xE592427A0AEce92De3Edee1F18E0157C05861564", Input: "0xc04b8d59" + pathData, // exactInput selector Value: "0", } parseResult := suite.testParsingWithTimeout(txData, suite.timeoutPerTest) switch parseResult.Status { case "success": result.SuccessCount++ case "error": result.ErrorCode++ suite.addUniqueError(result, parseResult.Error) case "timeout": result.TimeoutCount++ case "crash": result.CrashCount++ result.InterestingInputs = append(result.InterestingInputs, pathData) } } } func (suite *FuzzTestSuite) fuzzMulticallData(t *testing.T) { result := &FuzzResult{ TestName: "MulticallFuzz", UniqueErrors: make([]string, 0), InterestingInputs: make([]string, 0), } startTime := time.Now() defer func() { result.TotalDuration = time.Since(startTime) suite.reportFuzzResult(t, result) }() for i := 0; i < suite.maxFuzzIterations/4; i++ { result.TotalTests++ // Generate multicall data with random number of calls multicallData := suite.generateRandomMulticallData() txData := &TransactionData{ Hash: fmt.Sprintf("0xfuzz_multicall_%d", i), From: "0x1234567890123456789012345678901234567890", To: "0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45", Input: "0xac9650d8" + multicallData, Value: "0", } parseResult := suite.testParsingWithTimeout(txData, suite.timeoutPerTest*2) // Double timeout for multicall switch parseResult.Status { case "success": result.SuccessCount++ case "error": result.ErrorCount++ suite.addUniqueError(result, parseResult.Error) case "timeout": result.TimeoutCount++ case "crash": result.CrashCount++ } } } func (suite *FuzzTestSuite) fuzzEventLogs(t *testing.T) { result := &FuzzResult{ TestName: "EventLogFuzz", UniqueErrors: make([]string, 0), InterestingInputs: make([]string, 0), } startTime := time.Now() defer func() { result.TotalDuration = time.Since(startTime) suite.reportFuzzResult(t, result) }() // This would test the events parser with fuzzing // For now, we'll skip detailed implementation but structure is here t.Skip("Event log fuzzing not yet implemented") } func (suite *FuzzTestSuite) fuzzConcurrentAccess(t *testing.T) { result := &FuzzResult{ TestName: "ConcurrentAccessFuzz", UniqueErrors: make([]string, 0), InterestingInputs: make([]string, 0), } startTime := time.Now() defer func() { result.TotalDuration = time.Since(startTime) suite.reportFuzzResult(t, result) }() // Test concurrent access with random data workers := 10 iterationsPerWorker := suite.maxFuzzIterations / workers results := make(chan ParseResult, workers*iterationsPerWorker) for w := 0; w < workers; w++ { go func(workerID int) { for i := 0; i < iterationsPerWorker; i++ { txData := suite.generateRandomTransactionData() txData.Hash = fmt.Sprintf("0xfuzz_concurrent_%d_%d", workerID, i) parseResult := suite.testParsingWithTimeout(txData, suite.timeoutPerTest) results <- parseResult } }(w) } // Collect results for i := 0; i < workers*iterationsPerWorker; i++ { result.TotalTests++ parseResult := <-results switch parseResult.Status { case "success": result.SuccessCount++ case "error": result.ErrorCount++ suite.addUniqueError(result, parseResult.Error) case "timeout": result.TimeoutCount++ case "crash": result.CrashCount++ } } } func (suite *FuzzTestSuite) fuzzMemoryExhaustion(t *testing.T) { result := &FuzzResult{ TestName: "MemoryExhaustionFuzz", UniqueErrors: make([]string, 0), InterestingInputs: make([]string, 0), } startTime := time.Now() defer func() { result.TotalDuration = time.Since(startTime) suite.reportFuzzResult(t, result) }() // Test with increasingly large inputs baseSizes := []int{1024, 4096, 16384, 65536, 262144, 1048576} for _, baseSize := range baseSizes { for i := 0; i < 10; i++ { result.TotalTests++ // Generate large input data largeInput := "0x414bf389" + suite.generateRandomHex(baseSize) txData := &TransactionData{ Hash: fmt.Sprintf("0xfuzz_memory_%d_%d", baseSize, i), From: "0x1234567890123456789012345678901234567890", To: "0xE592427A0AEce92De3Edee1F18E0157C05861564", Input: largeInput, Value: "0", } parseResult := suite.testParsingWithTimeout(txData, suite.timeoutPerTest*5) // Longer timeout for large inputs switch parseResult.Status { case "success": result.SuccessCount++ case "error": result.ErrorCount++ suite.addUniqueError(result, parseResult.Error) case "timeout": result.TimeoutCount++ case "crash": result.CrashCount++ result.InterestingInputs = append(result.InterestingInputs, fmt.Sprintf("size_%d", len(largeInput))) } // Check memory usage if parseResult.MemoryUsageMB > result.MaxMemoryUsageMB { result.MaxMemoryUsageMB = parseResult.MemoryUsageMB } } } } // Helper types and functions type TransactionData struct { Hash string From string To string Input string Value string } type ParseResult struct { Status string // "success", "error", "timeout", "crash" Error string Duration time.Duration MemoryUsageMB float64 } func (suite *FuzzTestSuite) testParsingWithTimeout(txData *TransactionData, timeout time.Duration) ParseResult { start := time.Now() // Create a channel to capture the result resultChan := make(chan ParseResult, 1) go func() { defer func() { if r := recover(); r != nil { resultChan <- ParseResult{ Status: "crash", Error: fmt.Sprintf("panic: %v", r), Duration: time.Since(start), } } }() // Convert to RawL2Transaction rawTx := arbitrum.RawL2Transaction{ Hash: txData.Hash, From: txData.From, To: txData.To, Input: txData.Input, Value: txData.Value, } _, err := suite.l2Parser.ParseDEXTransaction(rawTx) result := ParseResult{ Duration: time.Since(start), } if err != nil { result.Status = "error" result.Error = err.Error() } else { result.Status = "success" } resultChan <- result }() select { case result := <-resultChan: return result case <-time.After(timeout): return ParseResult{ Status: "timeout", Duration: timeout, } } } func (suite *FuzzTestSuite) generateRandomTransactionData() *TransactionData { return &TransactionData{ Hash: suite.generateRandomHash(), From: suite.generateRandomAddress(), To: suite.generateRandomAddress(), Input: suite.generateRandomSelector() + suite.generateRandomHex(256+suite.randomInt(512)), Value: suite.generateRandomAmount(), } } func (suite *FuzzTestSuite) generateRandomHash() string { return "0x" + suite.generateRandomHex(64) } func (suite *FuzzTestSuite) generateRandomAddress() string { return "0x" + suite.generateRandomHex(40) } func (suite *FuzzTestSuite) generateRandomSelector() string { return "0x" + suite.generateRandomHex(8) } func (suite *FuzzTestSuite) generateRandomHex(length int) string { bytes := make([]byte, length/2) rand.Read(bytes) return fmt.Sprintf("%x", bytes) } func (suite *FuzzTestSuite) generateRandomAmount() string { // Generate various types of amounts switch suite.randomInt(5) { case 0: return "0" case 1: return "1" case 2: // Small amount amount := big.NewInt(int64(suite.randomInt(1000000))) return amount.String() case 3: // Medium amount amount := new(big.Int).Exp(big.NewInt(10), big.NewInt(18), nil) amount.Mul(amount, big.NewInt(int64(suite.randomInt(1000)))) return amount.String() case 4: // Large amount bytes := make([]byte, 32) rand.Read(bytes) amount := new(big.Int).SetBytes(bytes) return amount.String() default: return "1000000000000000000" // 1 ETH } } func (suite *FuzzTestSuite) generateRandomV3Path() string { // Generate Uniswap V3 encoded path pathLength := 2 + suite.randomInt(3) // 2-4 tokens path := "" for i := 0; i < pathLength; i++ { // Add token address (20 bytes) path += suite.generateRandomHex(40) if i < pathLength-1 { // Add fee (3 bytes) fees := []string{"000064", "0001f4", "000bb8", "002710"} // 100, 500, 3000, 10000 path += fees[suite.randomInt(len(fees))] } } // Encode as ABI bytes return suite.encodeABIBytes(path) } func (suite *FuzzTestSuite) generateRandomMulticallData() string { callCount := 1 + suite.randomInt(10) // 1-10 calls // Start with array encoding data := suite.encodeUint256(uint64(callCount)) // Array length // Encode call data offsets baseOffset := uint64(32 + callCount*32) // Skip length + offsets currentOffset := baseOffset for i := 0; i < callCount; i++ { data += suite.encodeUint256(currentOffset) currentOffset += 32 + uint64(64+suite.randomInt(256)) // Length + random data } // Encode actual call data for i := 0; i < callCount; i++ { callDataLength := 64 + suite.randomInt(256) data += suite.encodeUint256(uint64(callDataLength)) data += suite.generateRandomHex(callDataLength) } return data } func (suite *FuzzTestSuite) mutateSelector(selector string) string { // Remove 0x prefix hex := selector[2:] // Mutate a random byte bytes := []byte(hex) if len(bytes) > 0 { pos := suite.randomInt(len(bytes)) // Flip a bit if bytes[pos] >= '0' && bytes[pos] <= '9' { bytes[pos] = 'a' + (bytes[pos] - '0') } else if bytes[pos] >= 'a' && bytes[pos] <= 'f' { bytes[pos] = '0' + (bytes[pos] - 'a') } } return "0x" + string(bytes) } func (suite *FuzzTestSuite) encodeAmountInParams(amount string) string { // Simplified encoding for exactInputSingle params amountHex := suite.encodeBigInt(amount) return amountHex + strings.Repeat("0", 7*64) // Pad other parameters } func (suite *FuzzTestSuite) encodeBigInt(amount string) string { bigInt, ok := new(big.Int).SetString(amount, 10) if !ok { // Invalid amount, return zero bigInt = big.NewInt(0) } // Pad to 32 bytes (64 hex chars) hex := fmt.Sprintf("%064x", bigInt) if len(hex) > 64 { hex = hex[len(hex)-64:] // Take last 64 chars if too long } return hex } func (suite *FuzzTestSuite) encodeUint256(value uint64) string { return fmt.Sprintf("%064x", value) } func (suite *FuzzTestSuite) encodeABIBytes(hexData string) string { // Encode as ABI bytes type length := len(hexData) / 2 lengthHex := suite.encodeUint256(uint64(length)) // Pad data to 32-byte boundary paddedData := hexData if len(paddedData)%64 != 0 { paddedData += strings.Repeat("0", 64-(len(paddedData)%64)) } return lengthHex + paddedData } func (suite *FuzzTestSuite) randomInt(max int) int { if max <= 0 { return 0 } bytes := make([]byte, 4) rand.Read(bytes) val := int(bytes[0])<<24 | int(bytes[1])<<16 | int(bytes[2])<<8 | int(bytes[3]) if val < 0 { val = -val } return val % max } func (suite *FuzzTestSuite) addUniqueError(result *FuzzResult, errorMsg string) { // Add error to unique errors list if not already present for _, existing := range result.UniqueErrors { if existing == errorMsg { return } } if len(result.UniqueErrors) < 50 { // Limit unique errors result.UniqueErrors = append(result.UniqueErrors, errorMsg) } } func (suite *FuzzTestSuite) reportFuzzResult(t *testing.T, result *FuzzResult) { t.Logf("\n=== FUZZING RESULTS: %s ===", result.TestName) t.Logf("Total Tests: %d", result.TotalTests) t.Logf("Success: %d (%.2f%%)", result.SuccessCount, float64(result.SuccessCount)/float64(result.TotalTests)*100) t.Logf("Errors: %d (%.2f%%)", result.ErrorCount, float64(result.ErrorCount)/float64(result.TotalTests)*100) t.Logf("Timeouts: %d (%.2f%%)", result.TimeoutCount, float64(result.TimeoutCount)/float64(result.TotalTests)*100) t.Logf("Crashes: %d (%.2f%%)", result.CrashCount, float64(result.CrashCount)/float64(result.TotalTests)*100) t.Logf("Duration: %v", result.TotalDuration) t.Logf("Max Memory: %.2f MB", result.MaxMemoryUsageMB) t.Logf("Unique Errors: %d", len(result.UniqueErrors)) // Print first few unique errors for i, err := range result.UniqueErrors { if i >= 5 { t.Logf("... and %d more errors", len(result.UniqueErrors)-5) break } t.Logf(" Error %d: %s", i+1, err) } // Print interesting inputs that caused crashes if len(result.InterestingInputs) > 0 { t.Logf("Interesting inputs (first 3):") for i, input := range result.InterestingInputs { if i >= 3 { break } t.Logf(" %s", input) } } // Validate fuzzing results crashRate := float64(result.CrashCount) / float64(result.TotalTests) * 100 assert.True(t, crashRate < 1.0, "Crash rate (%.2f%%) should be below 1%%", crashRate) timeoutRate := float64(result.TimeoutCount) / float64(result.TotalTests) * 100 assert.True(t, timeoutRate < 5.0, "Timeout rate (%.2f%%) should be below 5%%", timeoutRate) assert.True(t, result.MaxMemoryUsageMB < float64(suite.memoryLimitMB), "Max memory usage (%.2f MB) should be below limit (%d MB)", result.MaxMemoryUsageMB, suite.memoryLimitMB) }