package dex import ( "context" "fmt" "math/big" "time" "github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/ethclient" ) // PoolType represents the detected pool/exchange type type PoolType string const ( PoolTypeUnknown PoolType = "unknown" PoolTypeUniswapV2 PoolType = "uniswap_v2" PoolTypeUniswapV3 PoolType = "uniswap_v3" PoolTypeUniswapV4 PoolType = "uniswap_v4" PoolTypeSushiswap PoolType = "sushiswap" PoolTypeBalancer PoolType = "balancer" PoolTypeCurve PoolType = "curve" PoolTypeAlgebraV1 PoolType = "algebra_v1" PoolTypeAlgebraV19 PoolType = "algebra_v1.9" PoolTypeAlgebraIntegral PoolType = "algebra_integral" PoolTypeCamelot PoolType = "camelot" PoolTypeKyberswap PoolType = "kyberswap" PoolTypePancakeV3 PoolType = "pancake_v3" ) // PoolDetector identifies pool/exchange types using unique signatures type PoolDetector struct { client *ethclient.Client } // NewPoolDetector creates a new pool detector func NewPoolDetector(client *ethclient.Client) *PoolDetector { return &PoolDetector{ client: client, } } // PoolInfo contains detected pool information type PoolInfo struct { Address common.Address Type PoolType Token0 common.Address Token1 common.Address Fee *big.Int Version string Confidence float64 DetectedAt time.Time Properties map[string]interface{} } // DetectPoolType identifies the pool type using unique method signatures func (pd *PoolDetector) DetectPoolType(ctx context.Context, poolAddr common.Address) (*PoolInfo, error) { // First check if contract exists code, err := pd.client.CodeAt(ctx, poolAddr, nil) if err != nil { return nil, fmt.Errorf("failed to get contract code: %w", err) } if len(code) == 0 { return nil, fmt.Errorf("no contract at address %s", poolAddr.Hex()) } info := &PoolInfo{ Address: poolAddr, Type: PoolTypeUnknown, Properties: make(map[string]interface{}), DetectedAt: time.Now(), } // Method selectors for detection selectors := map[string][]byte{ "token0": {0x0d, 0xfe, 0x16, 0x81}, // Common to many DEXs "token1": {0xd2, 0x1c, 0xec, 0xd4}, // Common to many DEXs "fee": {0xdd, 0xca, 0x3f, 0x43}, // UniswapV3 "slot0": {0x38, 0x50, 0xc7, 0xbd}, // UniswapV3 "globalState": {0x13, 0xaf, 0x40, 0x35}, // Algebra "getReserves": {0x09, 0x02, 0xf1, 0xac}, // UniswapV2 "liquidity": {0x1a, 0x68, 0x6d, 0x0f}, // UniswapV3 "factory": {0xc4, 0x5a, 0x01, 0x55}, // Common "tickSpacing": {0xd0, 0xc9, 0x38, 0x91}, // UniswapV3 "maxLiquidityPerTick": {0x70, 0xcf, 0x75, 0x4a}, // UniswapV3 } // Test each selector results := make(map[string]bool) for name, selector := range selectors { result, err := pd.client.CallContract(ctx, ethereum.CallMsg{ To: &poolAddr, Data: selector, }, nil) results[name] = err == nil && len(result) > 0 } // Detection logic based on unique combinations hasToken0 := results["token0"] hasToken1 := results["token1"] hasFee := results["fee"] hasSlot0 := results["slot0"] hasGlobalState := results["globalState"] hasGetReserves := results["getReserves"] hasLiquidity := results["liquidity"] hasTickSpacing := results["tickSpacing"] hasMaxLiquidityPerTick := results["maxLiquidityPerTick"] // UniswapV3: Has slot0, fee, tickSpacing, maxLiquidityPerTick if hasToken0 && hasToken1 && hasSlot0 && hasFee && hasTickSpacing && hasMaxLiquidityPerTick { info.Type = PoolTypeUniswapV3 info.Version = "3" info.Confidence = 0.95 info.Properties["has_concentrated_liquidity"] = true // Get fee tier if feeData, err := pd.getUint24(ctx, poolAddr, selectors["fee"]); err == nil { info.Fee = feeData info.Properties["fee_tier"] = feeData.Uint64() } } else if hasToken0 && hasToken1 && hasGlobalState && !hasSlot0 { // Algebra-based (Camelot, QuickSwap V3): Has globalState instead of slot0 // Further distinguish between Algebra versions if hasDirectionalFees := pd.checkDirectionalFees(ctx, poolAddr); hasDirectionalFees { info.Type = PoolTypeAlgebraIntegral info.Version = "integral" info.Properties["has_directional_fees"] = true } else { info.Type = PoolTypeAlgebraV19 info.Version = "1.9" } info.Confidence = 0.90 info.Properties["has_concentrated_liquidity"] = true } else if hasToken0 && hasToken1 && hasGetReserves && !hasSlot0 && !hasGlobalState { // UniswapV2/Sushiswap: Has getReserves, no slot0 // Check factory to distinguish between V2 and Sushiswap if factory := pd.getFactory(ctx, poolAddr); factory != nil { if pd.isUniswapV2Factory(*factory) { info.Type = PoolTypeUniswapV2 info.Version = "2" } else if pd.isSushiswapFactory(*factory) { info.Type = PoolTypeSushiswap info.Version = "1" } } else { info.Type = PoolTypeUniswapV2 // Default to V2 pattern info.Version = "2" } info.Confidence = 0.85 info.Properties["has_constant_product"] = true } else if hasToken0 && hasToken1 && hasSlot0 && hasFee && hasLiquidity { // PancakeSwap V3: Similar to UniswapV3 but different factory if factory := pd.getFactory(ctx, poolAddr); factory != nil && pd.isPancakeV3Factory(*factory) { info.Type = PoolTypePancakeV3 info.Version = "3" info.Confidence = 0.85 } else { // Generic V3-like pool info.Type = PoolTypeUniswapV3 info.Version = "3-compatible" info.Confidence = 0.70 } } // Get token addresses if detected if hasToken0 && hasToken1 { if token0, err := pd.getAddress(ctx, poolAddr, selectors["token0"]); err == nil { info.Token0 = *token0 } if token1, err := pd.getAddress(ctx, poolAddr, selectors["token1"]); err == nil { info.Token1 = *token1 } } // If still unknown but has basic token methods if info.Type == PoolTypeUnknown && hasToken0 && hasToken1 { info.Type = PoolTypeUnknown info.Confidence = 0.30 info.Properties["has_basic_methods"] = true } return info, nil } // DetectFromTransaction detects pool type from transaction data func (pd *PoolDetector) DetectFromTransaction(ctx context.Context, txData []byte, to common.Address) (*PoolInfo, error) { if len(txData) < 4 { return nil, fmt.Errorf("transaction data too short") } // Get method selector (first 4 bytes) selector := txData[:4] // Common swap selectors by protocol swapSelectors := map[string]PoolType{ "0x128acb08": PoolTypeUniswapV3, // swap (V3) "0x5c11d795": PoolTypeUniswapV2, // swapExactTokensForTokensSupportingFeeOnTransferTokens "0x38ed1739": PoolTypeUniswapV2, // swapExactTokensForTokens "0x8803dbee": PoolTypeUniswapV2, // swapTokensForExactTokens "0x04e45aaf": PoolTypeUniswapV3, // exactInputSingle "0x414bf389": PoolTypeUniswapV3, // exactInputSingle (SwapRouter02) "0xac9650d8": PoolTypeUniswapV3, // multicall (V3) "0x5ae401dc": PoolTypeUniswapV3, // multicall with deadline } selectorHex := fmt.Sprintf("0x%x", selector) info := &PoolInfo{ Address: to, Type: PoolTypeUnknown, Properties: make(map[string]interface{}), DetectedAt: time.Now(), } // Check known selectors if poolType, found := swapSelectors[selectorHex]; found { info.Type = poolType info.Confidence = 0.75 info.Properties["detected_from"] = "transaction" info.Properties["method_selector"] = selectorHex // Try to extract pool address from calldata if poolAddr := pd.extractPoolFromCalldata(txData); poolAddr != nil { // Detect the actual pool (not router) return pd.DetectPoolType(ctx, *poolAddr) } } return info, nil } // Helper methods func (pd *PoolDetector) getAddress(ctx context.Context, contract common.Address, selector []byte) (*common.Address, error) { result, err := pd.client.CallContract(ctx, ethereum.CallMsg{ To: &contract, Data: selector, }, nil) if err != nil || len(result) < 32 { return nil, err } addr := common.BytesToAddress(result[12:32]) return &addr, nil } func (pd *PoolDetector) getUint24(ctx context.Context, contract common.Address, selector []byte) (*big.Int, error) { result, err := pd.client.CallContract(ctx, ethereum.CallMsg{ To: &contract, Data: selector, }, nil) if err != nil || len(result) < 32 { return nil, err } return new(big.Int).SetBytes(result[:32]), nil } func (pd *PoolDetector) getFactory(ctx context.Context, poolAddr common.Address) *common.Address { factorySelector := []byte{0xc4, 0x5a, 0x01, 0x55} addr, _ := pd.getAddress(ctx, poolAddr, factorySelector) return addr } func (pd *PoolDetector) checkDirectionalFees(ctx context.Context, poolAddr common.Address) bool { // Check for directional fee methods (Algebra Integral specific) feeZtoOSelector := []byte{0x8b, 0x94, 0xc9, 0xae} // feeZtoO() result, err := pd.client.CallContract(ctx, ethereum.CallMsg{ To: &poolAddr, Data: feeZtoOSelector, }, nil) return err == nil && len(result) > 0 } func (pd *PoolDetector) isUniswapV2Factory(factory common.Address) bool { // Known UniswapV2 factory addresses on Arbitrum knownFactories := []common.Address{ common.HexToAddress("0xc35DADB65012eC5796536bD9864eD8773aBc74C4"), // Sushiswap common.HexToAddress("0xf1D7CC64Fb4452F05c498126312eBE29f30Fbcf9"), // Shibaswap } for _, known := range knownFactories { if factory == known { return true } } return false } func (pd *PoolDetector) isSushiswapFactory(factory common.Address) bool { return factory == common.HexToAddress("0xc35DADB65012eC5796536bD9864eD8773aBc74C4") } func (pd *PoolDetector) isPancakeV3Factory(factory common.Address) bool { // PancakeSwap V3 factory on Arbitrum return factory == common.HexToAddress("0x0BFbCF9fa4f9C56B0F40a671Ad40E0805A091865") } func (pd *PoolDetector) extractPoolFromCalldata(data []byte) *common.Address { // Try to extract pool address from common positions in calldata // This is protocol-specific and would need expansion if len(data) >= 68 { // 4 (selector) + 32 + 32 // Check if bytes 36-68 look like an address possibleAddr := common.BytesToAddress(data[36:68]) if possibleAddr != (common.Address{}) { return &possibleAddr } } return nil }