Completed clean root directory structure: - Root now contains only: .git, .env, docs/, orig/ - Moved all remaining files and directories to orig/: - Config files (.claude, .dockerignore, .drone.yml, etc.) - All .env variants (except active .env) - Git config (.gitconfig, .github, .gitignore, etc.) - Tool configs (.golangci.yml, .revive.toml, etc.) - Documentation (*.md files, @prompts) - Build files (Dockerfiles, Makefile, go.mod, go.sum) - Docker compose files - All source directories (scripts, tests, tools, etc.) - Runtime directories (logs, monitoring, reports) - Dependency files (node_modules, lib, cache) - Special files (--delete) - Removed empty runtime directories (bin/, data/) V2 structure is now clean: - docs/planning/ - V2 planning documents - orig/ - Complete V1 codebase preserved - .env - Active environment config (not in git) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
882 lines
23 KiB
Go
882 lines
23 KiB
Go
//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)
|
|
}
|