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>
This commit is contained in:
408
orig/tools/math-audit/internal/property_tests.go
Normal file
408
orig/tools/math-audit/internal/property_tests.go
Normal file
@@ -0,0 +1,408 @@
|
||||
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))
|
||||
}
|
||||
Reference in New Issue
Block a user