package internal import ( "context" "fmt" stdmath "math" "math/big" "math/rand" "time" pkgmath "github.com/fraktal/mev-beta/pkg/math" ) // PropertyTestSuite implements mathematical property testing type PropertyTestSuite struct { auditor *MathAuditor converter *pkgmath.DecimalConverter rand *rand.Rand } // NewPropertyTestSuite creates a new property test suite func NewPropertyTestSuite(auditor *MathAuditor) *PropertyTestSuite { return &PropertyTestSuite{ auditor: auditor, converter: pkgmath.NewDecimalConverter(), rand: rand.New(rand.NewSource(42)), // Deterministic seed } } // PropertyTestResult represents the result of a property test type PropertyTestResult struct { PropertyName string `json:"property_name"` TestCount int `json:"test_count"` PassCount int `json:"pass_count"` FailCount int `json:"fail_count"` MaxError float64 `json:"max_error_bp"` AvgError float64 `json:"avg_error_bp"` Duration time.Duration `json:"duration"` Failures []*PropertyFailure `json:"failures,omitempty"` } // PropertyFailure represents a property test failure type PropertyFailure struct { Input string `json:"input"` Expected string `json:"expected"` Actual string `json:"actual"` ErrorBP float64 `json:"error_bp"` Description string `json:"description"` } // RunAllPropertyTests runs all mathematical property tests func (pts *PropertyTestSuite) RunAllPropertyTests(ctx context.Context) ([]*PropertyTestResult, error) { var results []*PropertyTestResult // Test monotonicity properties if result, err := pts.TestPriceMonotonicity(ctx, 1000); err == nil { results = append(results, result) } // Test round-trip properties if result, err := pts.TestRoundTripProperties(ctx, 1000); err == nil { results = append(results, result) } // Test symmetry properties if result, err := pts.TestSymmetryProperties(ctx, 1000); err == nil { results = append(results, result) } // Test boundary properties if result, err := pts.TestBoundaryProperties(ctx, 500); err == nil { results = append(results, result) } // Test arithmetic properties if result, err := pts.TestArithmeticProperties(ctx, 1000); err == nil { results = append(results, result) } return results, nil } // TestPriceMonotonicity tests that price functions are monotonic func (pts *PropertyTestSuite) TestPriceMonotonicity(ctx context.Context, testCount int) (*PropertyTestResult, error) { startTime := time.Now() result := &PropertyTestResult{ PropertyName: "Price Monotonicity", TestCount: testCount, } var totalError float64 for i := 0; i < testCount; i++ { // Generate two reserve ratios where ratio1 > ratio2 reserve0_1 := pts.generateRandomReserve() reserve1_1 := pts.generateRandomReserve() reserve0_2 := reserve0_1 reserve1_2 := new(big.Int).Mul(reserve1_1, big.NewInt(2)) // Double reserve1 to decrease price // Calculate prices price1, _ := pts.auditor.calculateUniswapV2Price( &pkgmath.UniversalDecimal{Value: reserve0_1, Decimals: 18, Symbol: "TOKEN0"}, &pkgmath.UniversalDecimal{Value: reserve1_1, Decimals: 18, Symbol: "TOKEN1"}, ) price2, _ := pts.auditor.calculateUniswapV2Price( &pkgmath.UniversalDecimal{Value: reserve0_2, Decimals: 18, Symbol: "TOKEN0"}, &pkgmath.UniversalDecimal{Value: reserve1_2, Decimals: 18, Symbol: "TOKEN1"}, ) if price1 == nil || price2 == nil { continue } // Price2 should be greater than Price1 (more reserve1 means higher price per token0) if price2.Value.Cmp(price1.Value) <= 0 { failure := &PropertyFailure{ Input: fmt.Sprintf("Reserve0: %s, Reserve1_1: %s, Reserve1_2: %s", reserve0_1.String(), reserve1_1.String(), reserve1_2.String()), Expected: fmt.Sprintf("Price2 > Price1 (%s)", price1.Value.String()), Actual: fmt.Sprintf("Price2 = %s", price2.Value.String()), ErrorBP: 100.0, // Categorical error Description: "Monotonicity violation: increasing reserve should increase price", } result.Failures = append(result.Failures, failure) result.FailCount++ totalError += 100.0 } else { result.PassCount++ } } if result.TestCount > 0 { result.AvgError = totalError / float64(result.TestCount) } result.Duration = time.Since(startTime) return result, nil } // TestRoundTripProperties tests that conversions are reversible func (pts *PropertyTestSuite) TestRoundTripProperties(ctx context.Context, testCount int) (*PropertyTestResult, error) { startTime := time.Now() result := &PropertyTestResult{ PropertyName: "Round Trip Properties", TestCount: testCount, } var totalError float64 for i := 0; i < testCount; i++ { // Generate random price originalPrice := pts.generateRandomPrice() // Convert to UniversalDecimal and back priceDecimal, err := pts.converter.FromString(fmt.Sprintf("%.18f", originalPrice), 18, "PRICE") if err != nil { continue } // Convert back to float - use a simple division approach recoveredPrice := 0.0 if priceDecimal.Value.Cmp(big.NewInt(0)) > 0 { priceFloat := new(big.Float).SetInt(priceDecimal.Value) scaleFactor := new(big.Float).SetInt(new(big.Int).Exp(big.NewInt(10), big.NewInt(int64(priceDecimal.Decimals)), nil)) priceFloat.Quo(priceFloat, scaleFactor) recoveredPrice, _ = priceFloat.Float64() } // Calculate error errorBP := stdmath.Abs(recoveredPrice-originalPrice) / originalPrice * 10000 totalError += errorBP if errorBP > pts.auditor.tolerance*10000 { failure := &PropertyFailure{ Input: fmt.Sprintf("%.18f", originalPrice), Expected: fmt.Sprintf("%.18f", originalPrice), Actual: fmt.Sprintf("%.18f", recoveredPrice), ErrorBP: errorBP, Description: "Round-trip conversion error exceeds tolerance", } result.Failures = append(result.Failures, failure) result.FailCount++ } else { result.PassCount++ } if errorBP > result.MaxError { result.MaxError = errorBP } } if result.TestCount > 0 { result.AvgError = totalError / float64(result.TestCount) } result.Duration = time.Since(startTime) return result, nil } // TestSymmetryProperties tests mathematical symmetries func (pts *PropertyTestSuite) TestSymmetryProperties(ctx context.Context, testCount int) (*PropertyTestResult, error) { startTime := time.Now() result := &PropertyTestResult{ PropertyName: "Symmetry Properties", TestCount: testCount, } var totalError float64 for i := 0; i < testCount; i++ { // Test that swapping tokens gives reciprocal price reserve0 := pts.generateRandomReserve() reserve1 := pts.generateRandomReserve() // Calculate price TOKEN1/TOKEN0 price1, _ := pts.auditor.calculateUniswapV2Price( &pkgmath.UniversalDecimal{Value: reserve0, Decimals: 18, Symbol: "TOKEN0"}, &pkgmath.UniversalDecimal{Value: reserve1, Decimals: 18, Symbol: "TOKEN1"}, ) // Calculate price TOKEN0/TOKEN1 (swapped) price2, _ := pts.auditor.calculateUniswapV2Price( &pkgmath.UniversalDecimal{Value: reserve1, Decimals: 18, Symbol: "TOKEN1"}, &pkgmath.UniversalDecimal{Value: reserve0, Decimals: 18, Symbol: "TOKEN0"}, ) if price1 == nil || price2 == nil { continue } // price1 * price2 should equal 1 (within tolerance) product := new(big.Int).Mul(price1.Value, price2.Value) // Scale product to compare with 10^36 (18 + 18 decimals) expected := new(big.Int).Exp(big.NewInt(10), big.NewInt(36), nil) // Calculate relative error diff := new(big.Int).Sub(product, expected) if diff.Sign() < 0 { diff.Neg(diff) } expectedFloat, _ := new(big.Float).SetInt(expected).Float64() diffFloat, _ := new(big.Float).SetInt(diff).Float64() errorBP := 0.0 if expectedFloat > 0 { errorBP = (diffFloat / expectedFloat) * 10000 } totalError += errorBP if errorBP > pts.auditor.tolerance*10000 { failure := &PropertyFailure{ Input: fmt.Sprintf("Reserve0: %s, Reserve1: %s", reserve0.String(), reserve1.String()), Expected: "price1 * price2 = 1", Actual: fmt.Sprintf("price1 * price2 = %s", product.String()), ErrorBP: errorBP, Description: "Symmetry violation: reciprocal prices don't multiply to 1", } result.Failures = append(result.Failures, failure) result.FailCount++ } else { result.PassCount++ } if errorBP > result.MaxError { result.MaxError = errorBP } } if result.TestCount > 0 { result.AvgError = totalError / float64(result.TestCount) } result.Duration = time.Since(startTime) return result, nil } // TestBoundaryProperties tests behavior at boundaries func (pts *PropertyTestSuite) TestBoundaryProperties(ctx context.Context, testCount int) (*PropertyTestResult, error) { startTime := time.Now() result := &PropertyTestResult{ PropertyName: "Boundary Properties", TestCount: testCount, } boundaryValues := []*big.Int{ big.NewInt(1), // Minimum big.NewInt(1000), // Small new(big.Int).Exp(big.NewInt(10), big.NewInt(18), nil), // 1 token new(big.Int).Exp(big.NewInt(10), big.NewInt(21), nil), // 1000 tokens new(big.Int).Exp(big.NewInt(10), big.NewInt(25), nil), // Large value } for i := 0; i < testCount; i++ { // Test combinations of boundary values reserve0 := boundaryValues[pts.rand.Intn(len(boundaryValues))] reserve1 := boundaryValues[pts.rand.Intn(len(boundaryValues))] // Skip if reserves are equal to zero if reserve0.Cmp(big.NewInt(0)) == 0 || reserve1.Cmp(big.NewInt(0)) == 0 { continue } price, err := pts.auditor.calculateUniswapV2Price( &pkgmath.UniversalDecimal{Value: reserve0, Decimals: 18, Symbol: "TOKEN0"}, &pkgmath.UniversalDecimal{Value: reserve1, Decimals: 18, Symbol: "TOKEN1"}, ) if err != nil { failure := &PropertyFailure{ Input: fmt.Sprintf("Reserve0: %s, Reserve1: %s", reserve0.String(), reserve1.String()), Expected: "Valid price calculation", Actual: fmt.Sprintf("Error: %v", err), ErrorBP: 10000.0, Description: "Boundary value calculation failed", } result.Failures = append(result.Failures, failure) result.FailCount++ } else if price == nil || price.Value.Cmp(big.NewInt(0)) <= 0 { failure := &PropertyFailure{ Input: fmt.Sprintf("Reserve0: %s, Reserve1: %s", reserve0.String(), reserve1.String()), Expected: "Positive price", Actual: "Zero or negative price", ErrorBP: 10000.0, Description: "Boundary value produced invalid price", } result.Failures = append(result.Failures, failure) result.FailCount++ } else { result.PassCount++ } } result.Duration = time.Since(startTime) return result, nil } // TestArithmeticProperties tests arithmetic operation properties func (pts *PropertyTestSuite) TestArithmeticProperties(ctx context.Context, testCount int) (*PropertyTestResult, error) { startTime := time.Now() result := &PropertyTestResult{ PropertyName: "Arithmetic Properties", TestCount: testCount, } var totalError float64 for i := 0; i < testCount; i++ { // Test that addition is commutative: a + b = b + a a := pts.generateRandomAmount() b := pts.generateRandomAmount() decimalA, _ := pkgmath.NewUniversalDecimal(a, 18, "TOKENA") decimalB, _ := pkgmath.NewUniversalDecimal(b, 18, "TOKENB") // Calculate a + b sum1 := new(big.Int).Add(decimalA.Value, decimalB.Value) // Calculate b + a sum2 := new(big.Int).Add(decimalB.Value, decimalA.Value) if sum1.Cmp(sum2) != 0 { failure := &PropertyFailure{ Input: fmt.Sprintf("A: %s, B: %s", a.String(), b.String()), Expected: sum1.String(), Actual: sum2.String(), ErrorBP: 10000.0, Description: "Addition is not commutative", } result.Failures = append(result.Failures, failure) result.FailCount++ totalError += 10000.0 } else { result.PassCount++ } } if result.TestCount > 0 { result.AvgError = totalError / float64(result.TestCount) } result.Duration = time.Since(startTime) return result, nil } // Helper functions for generating test data func (pts *PropertyTestSuite) generateRandomPrice() float64 { // Generate prices between 0.000001 and 1000000 exponent := pts.rand.Float64()*12 - 6 // -6 to 6 return stdmath.Pow(10, exponent) } func (pts *PropertyTestSuite) generateRandomReserve() *big.Int { // Generate reserves between 1 and 10^25 exp := pts.rand.Intn(25) + 1 base := big.NewInt(int64(pts.rand.Intn(9) + 1)) return new(big.Int).Exp(big.NewInt(10), big.NewInt(int64(exp)), nil).Mul(base, new(big.Int).Exp(big.NewInt(10), big.NewInt(int64(exp)), nil)) } func (pts *PropertyTestSuite) generateRandomAmount() *big.Int { // Generate amounts between 1 and 10^24 exp := pts.rand.Intn(24) + 1 base := big.NewInt(int64(pts.rand.Intn(9) + 1)) return new(big.Int).Exp(big.NewInt(10), big.NewInt(int64(exp)), nil).Mul(base, new(big.Int).Exp(big.NewInt(10), big.NewInt(int64(exp)), nil)) }