Files
mev-beta/orig/tools/math-audit/internal/property_tests.go
Administrator c54c569f30 refactor: move all remaining files to orig/ directory
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>
2025-11-10 10:53:05 +01:00

409 lines
13 KiB
Go

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))
}