package property import ( "math/big" "math/rand" "testing" "time" "github.com/fraktal/mev-beta/pkg/uniswap" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) // Property-based testing for Uniswap V3 pricing functions // These tests verify mathematical properties that should hold for all valid inputs func init() { rand.Seed(time.Now().UnixNano()) } // generateRandomPrice generates a random price within realistic bounds func generateRandomPrice() float64 { // Generate prices between 0.000001 and 1000000 (6 orders of magnitude) exponent := rand.Float64()*12 - 6 // -6 to 6 return 10 * exponent } // generateRandomTick generates a random tick within valid bounds func generateRandomTick() int { // Uniswap V3 tick range is approximately -887272 to 887272 return rand.Intn(1774544) - 887272 } // TestPriceConversionRoundTrip verifies that price->sqrt->price conversions are consistent func TestPriceConversionRoundTrip(t *testing.T) { const numTests = 1000 const tolerance = 0.001 // 0.1% tolerance for floating point precision for i := 0; i < numTests; i++ { originalPrice := generateRandomPrice() if originalPrice <= 0 || originalPrice > 1e10 { // Skip extreme values continue } // Convert price to sqrtPriceX96 and back priceBigFloat := new(big.Float).SetFloat64(originalPrice) sqrtPriceX96 := uniswap.PriceToSqrtPriceX96(priceBigFloat) convertedPrice := uniswap.SqrtPriceX96ToPrice(sqrtPriceX96) convertedPriceFloat, _ := convertedPrice.Float64() // Verify round-trip consistency within tolerance relativeError := abs(originalPrice-convertedPriceFloat) / originalPrice assert.True(t, relativeError < tolerance, "Round-trip conversion failed: original=%.6f, converted=%.6f, error=%.6f", originalPrice, convertedPriceFloat, relativeError) } } // TestTickConversionRoundTrip verifies that tick->sqrt->tick conversions are consistent func TestTickConversionRoundTrip(t *testing.T) { const numTests = 1000 for i := 0; i < numTests; i++ { originalTick := generateRandomTick() // Convert tick to sqrtPriceX96 and back sqrtPriceX96 := uniswap.TickToSqrtPriceX96(originalTick) convertedTick := uniswap.SqrtPriceX96ToTick(sqrtPriceX96) // For tick conversions, we expect exact equality or at most 1 tick difference // due to rounding in the logarithmic calculations tickDifference := abs64(int64(originalTick) - int64(convertedTick)) assert.True(t, tickDifference <= 1, "Tick round-trip failed: original=%d, converted=%d, difference=%d", originalTick, convertedTick, tickDifference) } } // TestPriceMonotonicity verifies that price increases monotonically with tick func TestPriceMonotonicity(t *testing.T) { const numTests = 100 const tickStep = 1000 for i := 0; i < numTests; i++ { baseTick := generateRandomTick() if baseTick > 800000 { // Ensure we don't overflow baseTick = 800000 } tick1 := baseTick tick2 := baseTick + tickStep sqrtPrice1 := uniswap.TickToSqrtPriceX96(tick1) sqrtPrice2 := uniswap.TickToSqrtPriceX96(tick2) price1 := uniswap.SqrtPriceX96ToPrice(sqrtPrice1) price2 := uniswap.SqrtPriceX96ToPrice(sqrtPrice2) price1Float, _ := price1.Float64() price2Float, _ := price2.Float64() // Higher tick should result in higher price assert.True(t, price2Float > price1Float, "Price monotonicity violated: tick1=%d, price1=%.6f, tick2=%d, price2=%.6f", tick1, price1Float, tick2, price2Float) } } // TestSqrtPriceX96Bounds verifies that sqrtPriceX96 values are within expected bounds func TestSqrtPriceX96Bounds(t *testing.T) { const numTests = 1000 // Define reasonable bounds for sqrtPriceX96 minBound := new(big.Int) minBound.SetString("4295128739", 10) // Very small price maxBound := new(big.Int) maxBound.SetString("1461446703485210103287273052203988822378723970341", 10) // Very large price for i := 0; i < numTests; i++ { tick := generateRandomTick() sqrtPriceX96 := uniswap.TickToSqrtPriceX96(tick) // Verify bounds assert.True(t, sqrtPriceX96.Cmp(minBound) >= 0, "sqrtPriceX96 below minimum bound: tick=%d, sqrtPriceX96=%s", tick, sqrtPriceX96.String()) assert.True(t, sqrtPriceX96.Cmp(maxBound) <= 0, "sqrtPriceX96 above maximum bound: tick=%d, sqrtPriceX96=%s", tick, sqrtPriceX96.String()) } } // TestPriceSymmetry verifies that inverse prices work correctly func TestPriceSymmetry(t *testing.T) { const numTests = 100 const tolerance = 0.001 for i := 0; i < numTests; i++ { originalPrice := generateRandomPrice() if originalPrice <= 0 || originalPrice > 1e6 { continue } // Calculate inverse price inversePrice := 1.0 / originalPrice // Convert both to sqrtPriceX96 priceBigFloat := new(big.Float).SetFloat64(originalPrice) inverseBigFloat := new(big.Float).SetFloat64(inversePrice) sqrtPrice := uniswap.PriceToSqrtPriceX96(priceBigFloat) sqrtInverse := uniswap.PriceToSqrtPriceX96(inverseBigFloat) // Convert back to prices convertedPrice := uniswap.SqrtPriceX96ToPrice(sqrtPrice) convertedInverse := uniswap.SqrtPriceX96ToPrice(sqrtInverse) convertedPriceFloat, _ := convertedPrice.Float64() convertedInverseFloat, _ := convertedInverse.Float64() // Verify that price * inverse ≈ 1 product := convertedPriceFloat * convertedInverseFloat assert.InDelta(t, 1.0, product, tolerance, "Price symmetry failed: price=%.6f, inverse=%.6f, product=%.6f", convertedPriceFloat, convertedInverseFloat, product) } } // TestEdgeCases tests edge cases and boundary conditions func TestEdgeCases(t *testing.T) { testCases := []struct { name string tick int shouldPass bool description string }{ {"MinTick", -887272, true, "Minimum valid tick"}, {"MaxTick", 887272, true, "Maximum valid tick"}, {"ZeroTick", 0, true, "Zero tick (price = 1)"}, {"NegativeTick", -100000, true, "Negative tick"}, {"PositiveTick", 100000, true, "Positive tick"}, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { if tc.shouldPass { // Test tick to sqrtPrice conversion sqrtPriceX96 := uniswap.TickToSqrtPriceX96(tc.tick) require.NotNil(t, sqrtPriceX96, "sqrtPriceX96 should not be nil for %s", tc.description) // Test sqrtPrice to price conversion price := uniswap.SqrtPriceX96ToPrice(sqrtPriceX96) require.NotNil(t, price, "price should not be nil for %s", tc.description) // Test round-trip: tick -> sqrt -> tick convertedTick := uniswap.SqrtPriceX96ToTick(sqrtPriceX96) tickDiff := abs64(int64(tc.tick) - int64(convertedTick)) assert.True(t, tickDiff <= 1, "Round-trip tick conversion failed for %s: original=%d, converted=%d", tc.description, tc.tick, convertedTick) } }) } } // TestPricePrecision verifies precision of price calculations func TestPricePrecision(t *testing.T) { knownCases := []struct { name string sqrtPriceX96 string expectedPrice float64 tolerance float64 }{ { name: "Price_1_ETH_USDC", sqrtPriceX96: "79228162514264337593543950336", // 2^96, price = 1 expectedPrice: 1.0, tolerance: 0.0001, }, { name: "Price_4_ETH_USDC", sqrtPriceX96: "158456325028528675187087900672", // 2 * 2^96, price = 4 expectedPrice: 4.0, tolerance: 0.01, }, } for _, tc := range knownCases { t.Run(tc.name, func(t *testing.T) { sqrtPriceX96 := new(big.Int) _, ok := sqrtPriceX96.SetString(tc.sqrtPriceX96, 10) require.True(t, ok, "Failed to parse sqrtPriceX96") price := uniswap.SqrtPriceX96ToPrice(sqrtPriceX96) priceFloat, accuracy := price.Float64() require.Equal(t, big.Exact, accuracy, "Price conversion should be exact") assert.InDelta(t, tc.expectedPrice, priceFloat, tc.tolerance, "Price precision test failed for %s", tc.name) }) } } // Helper functions func abs(x float64) float64 { if x < 0 { return -x } return x } func abs64(x int64) int64 { if x < 0 { return -x } return x }