- Added comprehensive bounds checking to prevent buffer overruns in multicall parsing - Implemented graduated validation system (Strict/Moderate/Permissive) to reduce false positives - Added LRU caching system for address validation with 10-minute TTL - Enhanced ABI decoder with missing Universal Router and Arbitrum-specific DEX signatures - Fixed duplicate function declarations and import conflicts across multiple files - Added error recovery mechanisms with multiple fallback strategies - Updated tests to handle new validation behavior for suspicious addresses - Fixed parser test expectations for improved validation system - Applied gofmt formatting fixes to ensure code style compliance - Fixed mutex copying issues in monitoring package by introducing MetricsSnapshot - Resolved critical security vulnerabilities in heuristic address extraction - Progress: Updated TODO audit from 10% to 35% complete 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
327 lines
11 KiB
Go
327 lines
11 KiB
Go
//go:build legacy_pools
|
|
// +build legacy_pools
|
|
|
|
package pools
|
|
|
|
import (
|
|
"math/big"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/fraktal/mev-beta/internal/logger"
|
|
)
|
|
|
|
// TestNewPoolDiscovery tests the creation of a new PoolDiscovery
|
|
func TestNewPoolDiscovery(t *testing.T) {
|
|
logger := logger.New("info", "text", "")
|
|
|
|
// Test with nil client (for testing purposes)
|
|
pd := NewPoolDiscovery(nil, logger)
|
|
|
|
require.NotNil(t, pd)
|
|
assert.NotNil(t, pd.pools)
|
|
assert.NotNil(t, pd.exchanges)
|
|
assert.NotNil(t, pd.eventSignatures)
|
|
assert.NotNil(t, pd.knownFactories)
|
|
assert.NotNil(t, pd.minLiquidityThreshold)
|
|
assert.Equal(t, 0.01, pd.priceImpactThreshold)
|
|
}
|
|
|
|
// TestInitializeEventSignatures tests the initialization of event signatures
|
|
func TestInitializeEventSignatures(t *testing.T) {
|
|
logger := logger.New("info", "text", "")
|
|
pd := NewPoolDiscovery(nil, logger)
|
|
|
|
// Check that key event signatures are present
|
|
assert.Contains(t, pd.eventSignatures, "0x0d3648bd0f6ba80134a33ba9275ac585d9d315f0ad8355cddefde31afa28d0e9") // PairCreated
|
|
assert.Contains(t, pd.eventSignatures, "0x783cca1c0412dd0d695e784568c96da2e9c22ff989357a2e8b1d9b2b4e6b7118") // PoolCreated
|
|
assert.Contains(t, pd.eventSignatures, "0xd78ad95fa46c994b6551d0da85fc275fe613ce37657fb8d5e3d130840159d822") // Swap
|
|
}
|
|
|
|
// TestInitializeKnownFactories tests the initialization of known factories
|
|
func TestInitializeKnownFactories(t *testing.T) {
|
|
logger := logger.New("info", "text", "")
|
|
pd := NewPoolDiscovery(nil, logger)
|
|
|
|
// Check that key factories are present
|
|
assert.Contains(t, pd.knownFactories, "0xf1d7cc64fb4452f05c498126312ebe29f30fbcf9") // Uniswap V2
|
|
assert.Contains(t, pd.knownFactories, "0x1f98431c8ad98523631ae4a59f267346ea31f984") // Uniswap V3
|
|
assert.Contains(t, pd.knownFactories, "0xc35dadb65012ec5796536bd9864ed8773abc74c4") // SushiSwap
|
|
}
|
|
|
|
// TestPoolDiscovery_GetPoolCount tests getting pool count
|
|
func TestPoolDiscovery_GetPoolCount(t *testing.T) {
|
|
logger := logger.New("info", "text", "")
|
|
pd := NewPoolDiscovery(nil, logger)
|
|
|
|
// Initially should be zero
|
|
count := pd.GetPoolCount()
|
|
assert.Equal(t, 0, count)
|
|
|
|
// Add a test pool
|
|
pool := &Pool{
|
|
Address: "0x1234567890123456789012345678901234567890",
|
|
Token0: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
|
|
Token1: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
|
|
Fee: 3000,
|
|
Protocol: "UniswapV3",
|
|
Factory: "0x1f98431c8ad98523631ae4a59f267346ea31f984",
|
|
LastUpdated: time.Now(),
|
|
TotalVolume: big.NewInt(0),
|
|
SwapCount: 0,
|
|
}
|
|
|
|
pd.pools["0x1234567890123456789012345678901234567890"] = pool
|
|
|
|
// Should now be one
|
|
count = pd.GetPoolCount()
|
|
assert.Equal(t, 1, count)
|
|
}
|
|
|
|
// TestPoolDiscovery_GetExchangeCount tests getting exchange count
|
|
func TestPoolDiscovery_GetExchangeCount(t *testing.T) {
|
|
logger := logger.New("info", "text", "")
|
|
pd := NewPoolDiscovery(nil, logger)
|
|
|
|
// Initially should be zero
|
|
count := pd.GetExchangeCount()
|
|
assert.Equal(t, 0, count)
|
|
|
|
// Add a test exchange
|
|
exchange := &Exchange{
|
|
Name: "TestExchange",
|
|
Router: "0x1234567890123456789012345678901234567890",
|
|
Factory: "0x1f98431c8ad98523631ae4a59f267346ea31f984",
|
|
Protocol: "UniswapV3",
|
|
Version: "1.0",
|
|
Discovered: time.Now(),
|
|
PoolCount: 0,
|
|
TotalVolume: big.NewInt(0),
|
|
}
|
|
|
|
pd.exchanges["0x1234567890123456789012345678901234567890"] = exchange
|
|
|
|
// Should now be one
|
|
count = pd.GetExchangeCount()
|
|
assert.Equal(t, 1, count)
|
|
}
|
|
|
|
// TestPoolDiscovery_GetPool tests getting a pool by address
|
|
func TestPoolDiscovery_GetPool(t *testing.T) {
|
|
logger := logger.New("info", "text", "")
|
|
pd := NewPoolDiscovery(nil, logger)
|
|
|
|
// Test getting non-existent pool
|
|
pool, exists := pd.GetPool("0x1234567890123456789012345678901234567890")
|
|
assert.False(t, exists)
|
|
assert.Nil(t, pool)
|
|
|
|
// Add a test pool
|
|
testPool := &Pool{
|
|
Address: "0x1234567890123456789012345678901234567890",
|
|
Token0: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
|
|
Token1: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
|
|
Fee: 3000,
|
|
Protocol: "UniswapV3",
|
|
Factory: "0x1f98431c8ad98523631ae4a59f267346ea31f984",
|
|
LastUpdated: time.Now(),
|
|
TotalVolume: big.NewInt(1000000000000000000),
|
|
SwapCount: 5,
|
|
}
|
|
|
|
pd.pools["0x1234567890123456789012345678901234567890"] = testPool
|
|
|
|
// Test getting existing pool
|
|
pool, exists = pd.GetPool("0x1234567890123456789012345678901234567890")
|
|
assert.True(t, exists)
|
|
assert.NotNil(t, pool)
|
|
assert.Equal(t, "0x1234567890123456789012345678901234567890", pool.Address)
|
|
assert.Equal(t, "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", pool.Token0)
|
|
assert.Equal(t, "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", pool.Token1)
|
|
assert.Equal(t, uint32(3000), pool.Fee)
|
|
assert.Equal(t, "UniswapV3", pool.Protocol)
|
|
assert.Equal(t, "0x1f98431c8ad98523631ae4a59f267346ea31f984", pool.Factory)
|
|
assert.Equal(t, int64(1000000000000000000), pool.TotalVolume.Int64())
|
|
assert.Equal(t, uint64(5), pool.SwapCount)
|
|
}
|
|
|
|
// TestPoolDiscovery_GetAllPools tests getting all pools
|
|
func TestPoolDiscovery_GetAllPools(t *testing.T) {
|
|
logger := logger.New("info", "text", "")
|
|
pd := NewPoolDiscovery(nil, logger)
|
|
|
|
// Test getting all pools when empty
|
|
pools := pd.GetAllPools()
|
|
assert.Empty(t, pools)
|
|
|
|
// Add test pools
|
|
pool1 := &Pool{
|
|
Address: "0x1234567890123456789012345678901234567890",
|
|
Token0: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
|
|
Token1: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
|
|
Fee: 3000,
|
|
Protocol: "UniswapV3",
|
|
Factory: "0x1f98431c8ad98523631ae4a59f267346ea31f984",
|
|
LastUpdated: time.Now(),
|
|
TotalVolume: big.NewInt(1000000000000000000),
|
|
SwapCount: 5,
|
|
}
|
|
|
|
pool2 := &Pool{
|
|
Address: "0x0987654321098765432109876543210987654321",
|
|
Token0: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
|
|
Token1: "0x1f98431c8ad98523631ae4a59f267346ea31f984",
|
|
Fee: 500,
|
|
Protocol: "UniswapV2",
|
|
Factory: "0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f",
|
|
LastUpdated: time.Now(),
|
|
TotalVolume: big.NewInt(2000000000000000000),
|
|
SwapCount: 10,
|
|
}
|
|
|
|
pd.pools["0x1234567890123456789012345678901234567890"] = pool1
|
|
pd.pools["0x0987654321098765432109876543210987654321"] = pool2
|
|
|
|
// Test getting all pools
|
|
pools = pd.GetAllPools()
|
|
assert.Len(t, pools, 2)
|
|
assert.Contains(t, pools, "0x1234567890123456789012345678901234567890")
|
|
assert.Contains(t, pools, "0x0987654321098765432109876543210987654321")
|
|
|
|
// Verify pool data
|
|
retrievedPool1 := pools["0x1234567890123456789012345678901234567890"]
|
|
assert.Equal(t, pool1.Address, retrievedPool1.Address)
|
|
assert.Equal(t, pool1.Token0, retrievedPool1.Token0)
|
|
assert.Equal(t, pool1.Token1, retrievedPool1.Token1)
|
|
assert.Equal(t, pool1.Fee, retrievedPool1.Fee)
|
|
assert.Equal(t, pool1.Protocol, retrievedPool1.Protocol)
|
|
assert.Equal(t, pool1.Factory, retrievedPool1.Factory)
|
|
|
|
retrievedPool2 := pools["0x0987654321098765432109876543210987654321"]
|
|
assert.Equal(t, pool2.Address, retrievedPool2.Address)
|
|
assert.Equal(t, pool2.Token0, retrievedPool2.Token0)
|
|
assert.Equal(t, pool2.Token1, retrievedPool2.Token1)
|
|
assert.Equal(t, pool2.Fee, retrievedPool2.Fee)
|
|
assert.Equal(t, pool2.Protocol, retrievedPool2.Protocol)
|
|
assert.Equal(t, pool2.Factory, retrievedPool2.Factory)
|
|
}
|
|
|
|
// TestPoolDiscovery_PersistData tests data persistence functionality
|
|
func TestPoolDiscovery_PersistData(t *testing.T) {
|
|
logger := logger.New("info", "text", "")
|
|
pd := NewPoolDiscovery(nil, logger)
|
|
|
|
// Add test data
|
|
pool := &Pool{
|
|
Address: "0x1234567890123456789012345678901234567890",
|
|
Token0: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
|
|
Token1: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
|
|
Fee: 3000,
|
|
Protocol: "UniswapV3",
|
|
Factory: "0x1f98431c8ad98523631ae4a59f267346ea31f984",
|
|
LastUpdated: time.Now(),
|
|
TotalVolume: big.NewInt(1000000000000000000),
|
|
SwapCount: 5,
|
|
}
|
|
|
|
pd.pools["0x1234567890123456789012345678901234567890"] = pool
|
|
|
|
// Test persistence (this will create files in the data directory)
|
|
// Note: This test doesn't verify file contents, but ensures the method doesn't panic
|
|
pd.persistData()
|
|
|
|
// Test loading persisted data
|
|
pd.loadPersistedData()
|
|
|
|
// Verify data is still there
|
|
assert.Len(t, pd.pools, 1)
|
|
_, exists := pd.pools["0x1234567890123456789012345678901234567890"]
|
|
assert.True(t, exists)
|
|
}
|
|
|
|
// TestCalculatePriceImpact tests price impact calculation
|
|
func TestCalculatePriceImpact(t *testing.T) {
|
|
logger := logger.New("info", "text", "")
|
|
pd := NewPoolDiscovery(nil, logger)
|
|
|
|
// Create a test pool with reserves
|
|
pool := &Pool{
|
|
Reserves0: func() *big.Int { val, _ := big.NewInt(0).SetString("10000000000000000000", 10); return val }(), // 10 tokens
|
|
Reserves1: func() *big.Int { val, _ := big.NewInt(0).SetString("20000000000000000000", 10); return val }(), // 20 tokens
|
|
}
|
|
|
|
// Test with zero reserves (should return 0)
|
|
emptyPool := &Pool{}
|
|
priceImpact := pd.calculatePriceImpact(emptyPool, big.NewInt(1000000000000000000), big.NewInt(1000000000000000000))
|
|
assert.Equal(t, 0.0, priceImpact)
|
|
|
|
// Test with valid reserves
|
|
amountIn := big.NewInt(1000000000000000000) // 1 token
|
|
amountOut := big.NewInt(1990000000000000000) // ~1.99 tokens (due to 0.5% fee)
|
|
priceImpact = pd.calculatePriceImpact(pool, amountIn, amountOut)
|
|
|
|
// Price impact should be positive (small value due to small trade size)
|
|
assert.True(t, priceImpact >= 0.0)
|
|
|
|
// Test with larger trade
|
|
largeAmountIn := func() *big.Int { val, _ := big.NewInt(0).SetString("1000000000000000000", 10); return val }() // 1 token
|
|
largeAmountOut := func() *big.Int { val, _ := big.NewInt(0).SetString("1800000000000000000", 10); return val }() // ~1.8 tokens (larger price impact)
|
|
priceImpact = pd.calculatePriceImpact(pool, largeAmountIn, largeAmountOut)
|
|
|
|
// Larger trade should have larger price impact
|
|
assert.True(t, priceImpact >= 0.0)
|
|
}
|
|
|
|
// TestParseSwapData tests parsing of swap data
|
|
func TestParseSwapData(t *testing.T) {
|
|
logger := logger.New("info", "text", "")
|
|
pd := NewPoolDiscovery(nil, logger)
|
|
|
|
// Test with empty data
|
|
result := pd.parseSwapData("", "UniswapV2")
|
|
assert.Nil(t, result)
|
|
|
|
// Test with short data
|
|
result = pd.parseSwapData("0x", "UniswapV2")
|
|
assert.Nil(t, result)
|
|
|
|
// Test parseLiquidityData
|
|
data := "0x0000000000000000000000000000000000000000000000000000000000000001" +
|
|
"0000000000000000000000000000000000000000000000000000000000000002"
|
|
|
|
result2 := pd.parseLiquidityData(data, "Mint")
|
|
assert.NotNil(t, result2)
|
|
assert.Equal(t, int64(1), result2.AmountIn.Int64())
|
|
assert.Equal(t, int64(2), result2.AmountOut.Int64())
|
|
|
|
// Test parseSyncData
|
|
syncData := "0000000000000000000000000000000000000000000000000000000000000001" +
|
|
"0000000000000000000000000000000000000000000000000000000000000002"
|
|
|
|
result3 := pd.parseSyncData(syncData)
|
|
assert.NotNil(t, result3)
|
|
assert.Equal(t, int64(1), result3.Reserve0.Int64())
|
|
assert.Equal(t, int64(2), result3.Reserve1.Int64())
|
|
}
|
|
|
|
// TestPoolDiscovery_AddressFromTopic tests address extraction from topics
|
|
func TestPoolDiscovery_AddressFromTopic(t *testing.T) {
|
|
logger := logger.New("info", "text", "")
|
|
pd := NewPoolDiscovery(nil, logger)
|
|
|
|
// Test with invalid topic
|
|
result := pd.addressFromTopic(nil)
|
|
assert.Equal(t, "", result)
|
|
|
|
// Test with short topic
|
|
result = pd.addressFromTopic("0x1234")
|
|
assert.Equal(t, "", result)
|
|
|
|
// Test with valid topic (this function expects the full 32-byte topic)
|
|
result = pd.addressFromTopic("0x0000000000000000000000001234567890123456789012345678901234567890")
|
|
assert.Equal(t, "0x1234567890123456789012345678901234567890", result)
|
|
}
|