Sequencer is working (minimal parsing)
This commit is contained in:
157
test/benchmarks/pricing_bench_test.go
Normal file
157
test/benchmarks/pricing_bench_test.go
Normal file
@@ -0,0 +1,157 @@
|
||||
package benchmarks
|
||||
|
||||
import (
|
||||
"math/big"
|
||||
"testing"
|
||||
|
||||
"github.com/fraktal/mev-beta/pkg/uniswap"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// BenchmarkSqrtPriceX96ToPrice benchmarks the SqrtPriceX96ToPrice function
|
||||
func BenchmarkSqrtPriceX96ToPrice(b *testing.B) {
|
||||
sqrtPriceX96 := new(big.Int)
|
||||
sqrtPriceX96.SetString("79228162514264337593543950336", 10) // 2^96
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = uniswap.SqrtPriceX96ToPrice(sqrtPriceX96)
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkPriceToSqrtPriceX96 benchmarks the PriceToSqrtPriceX96 function
|
||||
func BenchmarkPriceToSqrtPriceX96(b *testing.B) {
|
||||
price := new(big.Float).SetFloat64(1.0)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = uniswap.PriceToSqrtPriceX96(price)
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkTickToSqrtPriceX96 benchmarks the TickToSqrtPriceX96 function
|
||||
func BenchmarkTickToSqrtPriceX96(b *testing.B) {
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = uniswap.TickToSqrtPriceX96(0)
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkSqrtPriceX96ToTick benchmarks the SqrtPriceX96ToTick function
|
||||
func BenchmarkSqrtPriceX96ToTick(b *testing.B) {
|
||||
sqrtPriceX96 := new(big.Int)
|
||||
sqrtPriceX96.SetString("79228162514264337593543950336", 10) // 2^96
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = uniswap.SqrtPriceX96ToTick(sqrtPriceX96)
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkPricingConversionsSequential benchmarks sequential pricing conversions
|
||||
func BenchmarkPricingConversionsSequential(b *testing.B) {
|
||||
price := new(big.Float).SetFloat64(1.0)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
sqrtPriceX96 := uniswap.PriceToSqrtPriceX96(price)
|
||||
tick := uniswap.SqrtPriceX96ToTick(sqrtPriceX96)
|
||||
backToSqrt := uniswap.TickToSqrtPriceX96(tick)
|
||||
_ = uniswap.SqrtPriceX96ToPrice(backToSqrt)
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkPricingCalculationRealistic benchmarks realistic pricing calculations
|
||||
func BenchmarkPricingCalculationRealistic(b *testing.B) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
sqrtPriceX96 string
|
||||
}{
|
||||
{"ETH_USDC_1800", "2231455953840924584200896000"}, // ~1800 USDC per ETH
|
||||
{"ETH_USDC_3000", "2890903041336652768307200000"}, // ~3000 USDC per ETH
|
||||
{"WBTC_ETH_15", "977228162514264337593543950"}, // ~15 ETH per WBTC
|
||||
{"DAI_USDC_1", "79228162514264337593543950336"}, // ~1 DAI per USDC
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
b.Run(tc.name, func(b *testing.B) {
|
||||
sqrtPriceX96 := new(big.Int)
|
||||
sqrtPriceX96.SetString(tc.sqrtPriceX96, 10)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
price := uniswap.SqrtPriceX96ToPrice(sqrtPriceX96)
|
||||
tick := uniswap.SqrtPriceX96ToTick(sqrtPriceX96)
|
||||
backToSqrt := uniswap.TickToSqrtPriceX96(tick)
|
||||
_ = uniswap.SqrtPriceX96ToPrice(backToSqrt)
|
||||
|
||||
// Verify we get similar price back (within reasonable precision)
|
||||
require.NotNil(b, price)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkExtremePriceValues benchmarks extreme price value conversions
|
||||
func BenchmarkExtremePriceValues(b *testing.B) {
|
||||
extremeCases := []struct {
|
||||
name string
|
||||
price float64
|
||||
}{
|
||||
{"VeryLow_0.000001", 0.000001},
|
||||
{"Low_0.01", 0.01},
|
||||
{"Normal_1.0", 1.0},
|
||||
{"High_100.0", 100.0},
|
||||
{"VeryHigh_1000000.0", 1000000.0},
|
||||
}
|
||||
|
||||
for _, tc := range extremeCases {
|
||||
b.Run(tc.name, func(b *testing.B) {
|
||||
price := new(big.Float).SetFloat64(tc.price)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
sqrtPriceX96 := uniswap.PriceToSqrtPriceX96(price)
|
||||
tick := uniswap.SqrtPriceX96ToTick(sqrtPriceX96)
|
||||
backToSqrt := uniswap.TickToSqrtPriceX96(tick)
|
||||
_ = uniswap.SqrtPriceX96ToPrice(backToSqrt)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkBigIntOperations benchmarks the underlying big.Int operations
|
||||
func BenchmarkBigIntOperations(b *testing.B) {
|
||||
b.Run("BigInt_Multiplication", func(b *testing.B) {
|
||||
x := big.NewInt(1000000)
|
||||
y := big.NewInt(2000000)
|
||||
result := new(big.Int)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
result.Mul(x, y)
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("BigInt_Division", func(b *testing.B) {
|
||||
x := big.NewInt(1000000000000)
|
||||
y := big.NewInt(1000000)
|
||||
result := new(big.Int)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
result.Div(x, y)
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("BigFloat_Operations", func(b *testing.B) {
|
||||
x := big.NewFloat(1000000.5)
|
||||
y := big.NewFloat(2000000.3)
|
||||
result := new(big.Float)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
result.Mul(x, y)
|
||||
}
|
||||
})
|
||||
}
|
||||
270
test/fuzzing/price_fuzzing_test.go
Normal file
270
test/fuzzing/price_fuzzing_test.go
Normal file
@@ -0,0 +1,270 @@
|
||||
package fuzzing
|
||||
|
||||
import (
|
||||
"math/big"
|
||||
"math/rand"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/fraktal/mev-beta/pkg/uniswap"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func init() {
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
}
|
||||
|
||||
// FuzzPricingConversions performs fuzz testing on pricing conversion functions
|
||||
func FuzzPricingConversions(f *testing.F) {
|
||||
// Add seed values for fuzzing
|
||||
f.Add(float64(1.0))
|
||||
f.Add(float64(0.001))
|
||||
f.Add(float64(1000.0))
|
||||
f.Add(float64(0.000001))
|
||||
f.Add(float64(1000000.0))
|
||||
|
||||
f.Fuzz(func(t *testing.T, price float64) {
|
||||
// Skip invalid inputs
|
||||
if price <= 0 || price != price { // NaN check
|
||||
t.Skip("Invalid price input")
|
||||
}
|
||||
|
||||
// Skip extreme values that would cause overflow
|
||||
if price > 1e15 || price < 1e-15 {
|
||||
t.Skip("Price too extreme")
|
||||
}
|
||||
|
||||
// Convert price to sqrtPriceX96 and back
|
||||
priceBigFloat := new(big.Float).SetFloat64(price)
|
||||
sqrtPriceX96 := uniswap.PriceToSqrtPriceX96(priceBigFloat)
|
||||
|
||||
// Verify sqrtPriceX96 is positive
|
||||
require.True(t, sqrtPriceX96.Sign() > 0, "sqrtPriceX96 must be positive")
|
||||
|
||||
convertedPrice := uniswap.SqrtPriceX96ToPrice(sqrtPriceX96)
|
||||
convertedPriceFloat, accuracy := convertedPrice.Float64()
|
||||
|
||||
// Verify conversion accuracy
|
||||
require.Equal(t, big.Exact, accuracy, "Price conversion should be exact")
|
||||
require.True(t, convertedPriceFloat > 0, "Converted price must be positive")
|
||||
|
||||
// Check round-trip consistency (allow some tolerance for floating point precision)
|
||||
tolerance := 0.01 // 1% tolerance
|
||||
if price > 0.01 && price < 100000 { // For reasonable price ranges
|
||||
relativeError := abs(price-convertedPriceFloat) / price
|
||||
assert.True(t, relativeError < tolerance,
|
||||
"Round-trip conversion failed: original=%.6f, converted=%.6f, error=%.6f",
|
||||
price, convertedPriceFloat, relativeError)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// FuzzTickConversions performs fuzz testing on tick conversion functions
|
||||
func FuzzTickConversions(f *testing.F) {
|
||||
// Add seed values for fuzzing
|
||||
f.Add(int(0))
|
||||
f.Add(int(100000))
|
||||
f.Add(int(-100000))
|
||||
f.Add(int(500000))
|
||||
f.Add(int(-500000))
|
||||
|
||||
f.Fuzz(func(t *testing.T, tick int) {
|
||||
// Skip ticks outside valid range
|
||||
if tick < -887272 || tick > 887272 {
|
||||
t.Skip("Tick outside valid range")
|
||||
}
|
||||
|
||||
// Convert tick to sqrtPriceX96 and back
|
||||
sqrtPriceX96 := uniswap.TickToSqrtPriceX96(tick)
|
||||
|
||||
// Verify sqrtPriceX96 is positive
|
||||
require.True(t, sqrtPriceX96.Sign() > 0, "sqrtPriceX96 must be positive")
|
||||
|
||||
convertedTick := uniswap.SqrtPriceX96ToTick(sqrtPriceX96)
|
||||
|
||||
// Check round-trip consistency (should be exact for ticks)
|
||||
tickDifference := abs64(int64(tick) - int64(convertedTick))
|
||||
assert.True(t, tickDifference <= 1,
|
||||
"Tick round-trip failed: original=%d, converted=%d, difference=%d",
|
||||
tick, convertedTick, tickDifference)
|
||||
})
|
||||
}
|
||||
|
||||
// FuzzSqrtPriceX96Operations performs fuzz testing on sqrtPriceX96 operations
|
||||
func FuzzSqrtPriceX96Operations(f *testing.F) {
|
||||
// Add seed values for fuzzing
|
||||
f.Add("79228162514264337593543950336") // 2^96 (price = 1)
|
||||
f.Add("158456325028528675187087900672") // 2 * 2^96 (price = 4)
|
||||
f.Add("39614081257132168796771975168") // 2^95 (price = 0.25)
|
||||
f.Add("1122334455667788990011223344") // Random value
|
||||
f.Add("999888777666555444333222111") // Another random value
|
||||
|
||||
f.Fuzz(func(t *testing.T, sqrtPriceX96Str string) {
|
||||
sqrtPriceX96 := new(big.Int)
|
||||
_, ok := sqrtPriceX96.SetString(sqrtPriceX96Str, 10)
|
||||
if !ok {
|
||||
t.Skip("Invalid sqrtPriceX96 string")
|
||||
}
|
||||
|
||||
// Skip if sqrtPriceX96 is zero or negative
|
||||
if sqrtPriceX96.Sign() <= 0 {
|
||||
t.Skip("sqrtPriceX96 must be positive")
|
||||
}
|
||||
|
||||
// Skip extremely large values to prevent overflow
|
||||
maxValue := new(big.Int)
|
||||
maxValue.SetString("1461446703485210103287273052203988822378723970341", 10)
|
||||
if sqrtPriceX96.Cmp(maxValue) > 0 {
|
||||
t.Skip("sqrtPriceX96 too large")
|
||||
}
|
||||
|
||||
// Skip extremely small values
|
||||
minValue := new(big.Int)
|
||||
minValue.SetString("4295128739", 10)
|
||||
if sqrtPriceX96.Cmp(minValue) < 0 {
|
||||
t.Skip("sqrtPriceX96 too small")
|
||||
}
|
||||
|
||||
// Convert sqrtPriceX96 to price
|
||||
price := uniswap.SqrtPriceX96ToPrice(sqrtPriceX96)
|
||||
require.NotNil(t, price, "Price should not be nil")
|
||||
|
||||
priceFloat, accuracy := price.Float64()
|
||||
if accuracy != big.Exact {
|
||||
t.Skip("Price conversion not exact, value too large")
|
||||
}
|
||||
|
||||
require.True(t, priceFloat > 0, "Price must be positive")
|
||||
|
||||
// Convert sqrtPriceX96 to tick
|
||||
tick := uniswap.SqrtPriceX96ToTick(sqrtPriceX96)
|
||||
|
||||
// Verify tick is in valid range
|
||||
assert.True(t, tick >= -887272 && tick <= 887272,
|
||||
"Tick %d outside valid range", tick)
|
||||
|
||||
// Convert back to sqrtPriceX96
|
||||
backToSqrtPrice := uniswap.TickToSqrtPriceX96(tick)
|
||||
|
||||
// Check consistency (allow small difference due to rounding)
|
||||
diff := new(big.Int).Sub(sqrtPriceX96, backToSqrtPrice)
|
||||
diff.Abs(diff)
|
||||
|
||||
// Allow difference of up to 0.01% of original value
|
||||
tolerance := new(big.Int).Div(sqrtPriceX96, big.NewInt(10000))
|
||||
assert.True(t, diff.Cmp(tolerance) <= 0,
|
||||
"sqrtPriceX96 round-trip failed: original=%s, converted=%s, diff=%s",
|
||||
sqrtPriceX96.String(), backToSqrtPrice.String(), diff.String())
|
||||
})
|
||||
}
|
||||
|
||||
// FuzzPriceImpactCalculations performs fuzz testing on price impact calculations
|
||||
func FuzzPriceImpactCalculations(f *testing.F) {
|
||||
// Add seed values for fuzzing
|
||||
f.Add(int64(1000000), int64(1000000000000000000)) // Small swap, large liquidity
|
||||
f.Add(int64(1000000000), int64(1000000000000000000)) // Large swap, large liquidity
|
||||
f.Add(int64(1000000), int64(1000000000)) // Small swap, small liquidity
|
||||
f.Add(int64(100000000), int64(1000000000)) // Large swap, small liquidity
|
||||
|
||||
f.Fuzz(func(t *testing.T, swapAmount int64, liquidity int64) {
|
||||
// Skip invalid inputs
|
||||
if swapAmount <= 0 || liquidity <= 0 {
|
||||
t.Skip("Invalid swap amount or liquidity")
|
||||
}
|
||||
|
||||
// Skip extreme values
|
||||
if swapAmount > 1e18 || liquidity > 1e18 {
|
||||
t.Skip("Values too extreme")
|
||||
}
|
||||
|
||||
// Calculate price impact as percentage
|
||||
// Simple approximation: impact ≈ (swapAmount / liquidity) * 100
|
||||
impactFloat := float64(swapAmount) / float64(liquidity) * 100
|
||||
|
||||
// Verify price impact is reasonable
|
||||
assert.True(t, impactFloat >= 0, "Price impact must be non-negative")
|
||||
assert.True(t, impactFloat <= 100, "Price impact should not exceed 100%")
|
||||
|
||||
// For very large swaps relative to liquidity, impact should be significant
|
||||
if float64(swapAmount) > float64(liquidity)*0.1 {
|
||||
assert.True(t, impactFloat > 1, "Large swaps should have significant price impact")
|
||||
}
|
||||
|
||||
// For very small swaps relative to liquidity, impact should be minimal
|
||||
if float64(swapAmount) < float64(liquidity)*0.001 {
|
||||
assert.True(t, impactFloat < 1, "Small swaps should have minimal price impact")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// FuzzMathematicalProperties performs fuzz testing on mathematical properties
|
||||
func FuzzMathematicalProperties(f *testing.F) {
|
||||
// Add seed values for fuzzing
|
||||
f.Add(float64(1.0), float64(2.0))
|
||||
f.Add(float64(0.5), float64(0.25))
|
||||
f.Add(float64(100.0), float64(200.0))
|
||||
f.Add(float64(0.001), float64(0.002))
|
||||
|
||||
f.Fuzz(func(t *testing.T, price1 float64, price2 float64) {
|
||||
// Skip invalid inputs
|
||||
if price1 <= 0 || price2 <= 0 || price1 != price1 || price2 != price2 {
|
||||
t.Skip("Invalid price inputs")
|
||||
}
|
||||
|
||||
// Skip extreme values
|
||||
if price1 > 1e10 || price2 > 1e10 || price1 < 1e-10 || price2 < 1e-10 {
|
||||
t.Skip("Prices too extreme")
|
||||
}
|
||||
|
||||
// Convert prices to sqrtPriceX96
|
||||
price1BigFloat := new(big.Float).SetFloat64(price1)
|
||||
price2BigFloat := new(big.Float).SetFloat64(price2)
|
||||
|
||||
sqrtPrice1 := uniswap.PriceToSqrtPriceX96(price1BigFloat)
|
||||
sqrtPrice2 := uniswap.PriceToSqrtPriceX96(price2BigFloat)
|
||||
|
||||
// Test monotonicity: if price1 < price2, then sqrtPrice1 < sqrtPrice2
|
||||
if price1 < price2 {
|
||||
assert.True(t, sqrtPrice1.Cmp(sqrtPrice2) < 0,
|
||||
"Monotonicity violated: price1=%.6f < price2=%.6f but sqrtPrice1=%s >= sqrtPrice2=%s",
|
||||
price1, price2, sqrtPrice1.String(), sqrtPrice2.String())
|
||||
} else if price1 > price2 {
|
||||
assert.True(t, sqrtPrice1.Cmp(sqrtPrice2) > 0,
|
||||
"Monotonicity violated: price1=%.6f > price2=%.6f but sqrtPrice1=%s <= sqrtPrice2=%s",
|
||||
price1, price2, sqrtPrice1.String(), sqrtPrice2.String())
|
||||
}
|
||||
|
||||
// Test that sqrt(price1 * price2) ≈ geometric mean of sqrtPrices
|
||||
geometricMean := price1 * price2
|
||||
if geometricMean > 0 {
|
||||
geometricMeanSqrt := new(big.Float).SetFloat64(geometricMean)
|
||||
geometricMeanSqrt.Sqrt(geometricMeanSqrt)
|
||||
|
||||
geometricMeanSqrtX96 := uniswap.PriceToSqrtPriceX96(geometricMeanSqrt)
|
||||
|
||||
// Calculate geometric mean of sqrtPrices
|
||||
// This is complex with big integers, so we'll skip this test for extreme values
|
||||
if sqrtPrice1.BitLen() < 200 && sqrtPrice2.BitLen() < 200 {
|
||||
// Test passes if we can perform the calculation without overflow
|
||||
assert.NotNil(t, geometricMeanSqrtX96)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Helper function for absolute value of float64
|
||||
func abs(x float64) float64 {
|
||||
if x < 0 {
|
||||
return -x
|
||||
}
|
||||
return x
|
||||
}
|
||||
|
||||
// Helper function for absolute value of int64
|
||||
func abs64(x int64) int64 {
|
||||
if x < 0 {
|
||||
return -x
|
||||
}
|
||||
return x
|
||||
}
|
||||
195
test/integration/arbitrum_integration_test.go
Normal file
195
test/integration/arbitrum_integration_test.go
Normal file
@@ -0,0 +1,195 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"math/big"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/fraktal/mev-beta/internal/logger"
|
||||
"github.com/fraktal/mev-beta/pkg/arbitrum"
|
||||
"github.com/fraktal/mev-beta/test/mocks"
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// TestL2MessageParsingAccuracy tests the accuracy of L2 message parsing
|
||||
func TestL2MessageParsingAccuracy(t *testing.T) {
|
||||
log := logger.New("info", "text", "")
|
||||
parser := arbitrum.NewL2MessageParser(log)
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
protocol string
|
||||
expectedTokens []common.Address
|
||||
expectedFee uint32
|
||||
}{
|
||||
{
|
||||
name: "UniswapV3_USDC_WETH",
|
||||
protocol: "UniswapV3",
|
||||
expectedTokens: []common.Address{
|
||||
common.HexToAddress("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"), // USDC
|
||||
common.HexToAddress("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"), // WETH
|
||||
},
|
||||
expectedFee: 3000,
|
||||
},
|
||||
{
|
||||
name: "SushiSwap_USDC_WETH",
|
||||
protocol: "SushiSwap",
|
||||
expectedTokens: []common.Address{
|
||||
common.HexToAddress("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"), // USDC
|
||||
common.HexToAddress("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"), // WETH
|
||||
},
|
||||
expectedFee: 3000,
|
||||
},
|
||||
{
|
||||
name: "Camelot_ARB_WETH",
|
||||
protocol: "Camelot",
|
||||
expectedTokens: []common.Address{
|
||||
common.HexToAddress("0x912CE59144191C1204E64559FE8253a0e49E6548"), // ARB
|
||||
common.HexToAddress("0x82aF49447D8a07e3bd95BD0d56f35241523fBab1"), // WETH on Arbitrum
|
||||
},
|
||||
expectedFee: 3000,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
// Create mock transaction with DEX interaction
|
||||
poolAddress := common.HexToAddress("0xE592427A0AEce92De3Edee1F18E0157C05861564") // Uniswap V3 Router
|
||||
|
||||
// Create mock transaction data for swap
|
||||
swapData := createMockSwapData(tc.expectedTokens[0], tc.expectedTokens[1], tc.expectedFee)
|
||||
tx := mocks.CreateMockTransaction(poolAddress, swapData)
|
||||
|
||||
// Parse DEX interaction
|
||||
interaction, err := parser.ParseDEXInteraction(tx)
|
||||
|
||||
if tc.protocol == "UniswapV3" {
|
||||
// UniswapV3 should be successfully parsed
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, interaction)
|
||||
assert.Equal(t, tc.protocol, interaction.Protocol)
|
||||
// Note: Fee field not available in current DEXInteraction struct
|
||||
assert.Equal(t, tc.protocol, interaction.Protocol)
|
||||
} else {
|
||||
// Other protocols might not be implemented yet, so we allow nil results
|
||||
if interaction != nil {
|
||||
assert.Equal(t, tc.protocol, interaction.Protocol)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestL2MessageLatency tests the latency of L2 message processing
|
||||
func TestL2MessageLatency(t *testing.T) {
|
||||
log := logger.New("info", "text", "")
|
||||
parser := arbitrum.NewL2MessageParser(log)
|
||||
|
||||
const numMessages = 100
|
||||
const maxLatencyMs = 10 // Maximum acceptable latency in milliseconds
|
||||
|
||||
for i := 0; i < numMessages; i++ {
|
||||
// Create L2 message
|
||||
l2Message := mocks.CreateMockL2Message()
|
||||
|
||||
// Measure parsing time
|
||||
startTime := time.Now()
|
||||
|
||||
if l2Message.ParsedTx != nil {
|
||||
_, err := parser.ParseDEXInteraction(l2Message.ParsedTx)
|
||||
// Error is expected for mock data, just measure timing
|
||||
_ = err
|
||||
}
|
||||
|
||||
latency := time.Since(startTime)
|
||||
latencyMs := latency.Nanoseconds() / 1000000
|
||||
|
||||
// Verify latency is acceptable
|
||||
assert.LessOrEqual(t, latencyMs, int64(maxLatencyMs),
|
||||
"L2 message processing latency too high: %dms", latencyMs)
|
||||
}
|
||||
}
|
||||
|
||||
// TestMultiProtocolDetection tests detection of multiple DEX protocols
|
||||
func TestMultiProtocolDetection(t *testing.T) {
|
||||
log := logger.New("info", "text", "")
|
||||
parser := arbitrum.NewL2MessageParser(log)
|
||||
|
||||
protocols := []string{"UniswapV3", "SushiSwap", "Camelot", "Balancer", "Curve"}
|
||||
|
||||
for _, protocol := range protocols {
|
||||
t.Run(protocol, func(t *testing.T) {
|
||||
// Create mock transaction for each protocol
|
||||
poolAddress := getProtocolPoolAddress(protocol)
|
||||
swapData := createMockSwapDataForProtocol(protocol)
|
||||
tx := mocks.CreateMockTransaction(poolAddress, swapData)
|
||||
|
||||
// Parse DEX interaction
|
||||
interaction, err := parser.ParseDEXInteraction(tx)
|
||||
|
||||
// For UniswapV3, we expect successful parsing
|
||||
// For others, we may not have full implementation yet
|
||||
if protocol == "UniswapV3" {
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, interaction)
|
||||
assert.Equal(t, protocol, interaction.Protocol)
|
||||
} else {
|
||||
// Log the results for other protocols
|
||||
if err != nil {
|
||||
t.Logf("Protocol %s not fully implemented yet: %v", protocol, err)
|
||||
} else if interaction != nil {
|
||||
t.Logf("Protocol %s detected: %+v", protocol, interaction)
|
||||
} else {
|
||||
t.Logf("Protocol %s: no interaction detected (expected for mock data)", protocol)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Helper functions for test data creation
|
||||
|
||||
func createMockSwapData(token0, token1 common.Address, fee uint32) []byte {
|
||||
// exactInputSingle selector: 0x414bf389
|
||||
selector := []byte{0x41, 0x4b, 0xf3, 0x89}
|
||||
|
||||
// Create a mock payload for exactInputSingle
|
||||
payload := make([]byte, 256)
|
||||
|
||||
// tokenIn (address)
|
||||
copy(payload[12:32], token0.Bytes())
|
||||
// tokenOut (address)
|
||||
copy(payload[44:64], token1.Bytes())
|
||||
// amountIn (uint256)
|
||||
amountIn := new(big.Int).SetInt64(1000000000000000000) // 1 ETH
|
||||
amountInBytes := amountIn.Bytes()
|
||||
copy(payload[192-len(amountInBytes):192], amountInBytes)
|
||||
|
||||
return append(selector, payload...)
|
||||
}
|
||||
|
||||
func createMockSwapDataForProtocol(protocol string) []byte {
|
||||
// For testing, we'll just use the same mock data for all protocols.
|
||||
// In a real scenario, this would generate protocol-specific data.
|
||||
token0 := common.HexToAddress("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48") // USDC
|
||||
token1 := common.HexToAddress("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2") // WETH
|
||||
return createMockSwapData(token0, token1, 3000)
|
||||
}
|
||||
|
||||
func getProtocolPoolAddress(protocol string) common.Address {
|
||||
// Return known pool addresses for different protocols on Arbitrum
|
||||
protocolPools := map[string]string{
|
||||
"UniswapV3": "0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640",
|
||||
"SushiSwap": "0x905dfCD5649217c42684f23958568e533C711Aa3",
|
||||
"Camelot": "0x84652bb2539513BAf36e225c930Fdd8eaa63CE27",
|
||||
"Balancer": "0x32dF62dc3aEd2cD6224193052Ce665DC18165841",
|
||||
"Curve": "0x7f90122BF0700F9E7e1F688fe926940E8839F353",
|
||||
}
|
||||
|
||||
if addr, exists := protocolPools[protocol]; exists {
|
||||
return common.HexToAddress(addr)
|
||||
}
|
||||
return common.HexToAddress("0x0000000000000000000000000000000000000000")
|
||||
}
|
||||
32
test/mocks/arbitrum_mock_test.go
Normal file
32
test/mocks/arbitrum_mock_test.go
Normal file
@@ -0,0 +1,32 @@
|
||||
package mocks
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestMockDEXInteraction tests mock DEX interaction creation
|
||||
func TestMockDEXInteraction(t *testing.T) {
|
||||
dexInteraction := CreateMockDEXInteraction()
|
||||
|
||||
assert.Equal(t, "UniswapV3", dexInteraction.Protocol)
|
||||
assert.Equal(t, common.HexToAddress("0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640"), dexInteraction.Pool)
|
||||
assert.Equal(t, common.HexToAddress("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"), dexInteraction.TokenIn)
|
||||
assert.Equal(t, common.HexToAddress("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"), dexInteraction.TokenOut)
|
||||
assert.NotNil(t, dexInteraction.AmountIn)
|
||||
assert.NotNil(t, dexInteraction.AmountOut)
|
||||
}
|
||||
|
||||
// TestMockTransaction tests mock transaction creation
|
||||
func TestMockTransaction(t *testing.T) {
|
||||
to := common.HexToAddress("0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640")
|
||||
data := []byte("test_data")
|
||||
|
||||
tx := CreateMockTransaction(to, data)
|
||||
|
||||
assert.Equal(t, to, *tx.To())
|
||||
assert.Equal(t, data, tx.Data())
|
||||
assert.Equal(t, uint64(1), tx.Nonce())
|
||||
}
|
||||
56
test/mocks/mock_types.go
Normal file
56
test/mocks/mock_types.go
Normal file
@@ -0,0 +1,56 @@
|
||||
package mocks
|
||||
|
||||
import (
|
||||
"math/big"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/core/types"
|
||||
"github.com/fraktal/mev-beta/pkg/arbitrum"
|
||||
)
|
||||
|
||||
// CreateMockL2Message creates a realistic L2 message for testing
|
||||
func CreateMockL2Message() *arbitrum.L2Message {
|
||||
// Create a mock transaction
|
||||
to := common.HexToAddress("0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640") // Uniswap V3 pool
|
||||
tx := types.NewTransaction(
|
||||
1, // nonce
|
||||
to, // to
|
||||
big.NewInt(0), // value
|
||||
21000, // gas limit
|
||||
big.NewInt(20000000000), // gas price (20 gwei)
|
||||
[]byte{}, // data
|
||||
)
|
||||
|
||||
return &arbitrum.L2Message{
|
||||
Type: arbitrum.L2Transaction,
|
||||
MessageNumber: big.NewInt(12345),
|
||||
ParsedTx: tx,
|
||||
InnerTxs: []*types.Transaction{tx},
|
||||
}
|
||||
}
|
||||
|
||||
// CreateMockDEXInteraction creates a realistic DEX interaction for testing
|
||||
func CreateMockDEXInteraction() *arbitrum.DEXInteraction {
|
||||
return &arbitrum.DEXInteraction{
|
||||
Protocol: "UniswapV3",
|
||||
Pool: common.HexToAddress("0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640"),
|
||||
TokenIn: common.HexToAddress("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"), // USDC
|
||||
TokenOut: common.HexToAddress("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"), // WETH
|
||||
AmountIn: big.NewInt(1000000000), // 1000 USDC
|
||||
AmountOut: big.NewInt(500000000000000000), // 0.5 ETH
|
||||
MessageNumber: big.NewInt(12345),
|
||||
Timestamp: uint64(1234567890),
|
||||
}
|
||||
}
|
||||
|
||||
// CreateMockTransaction creates a realistic transaction for testing
|
||||
func CreateMockTransaction(to common.Address, data []byte) *types.Transaction {
|
||||
return types.NewTransaction(
|
||||
1, // nonce
|
||||
to, // to
|
||||
big.NewInt(0), // value
|
||||
21000, // gas limit
|
||||
big.NewInt(20000000000), // gas price (20 gwei)
|
||||
data, // data
|
||||
)
|
||||
}
|
||||
257
test/property/pricing_property_test.go
Normal file
257
test/property/pricing_property_test.go
Normal file
@@ -0,0 +1,257 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user