Files
mev-beta/pkg/dex/detector.go
2025-11-08 10:37:52 -06:00

311 lines
10 KiB
Go

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
}