310 lines
10 KiB
Go
310 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
|
|
} |