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>
409 lines
13 KiB
Go
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))
|
|
}
|