feat: create v2-prep branch with comprehensive planning
Restructured project for V2 refactor: **Structure Changes:** - Moved all V1 code to orig/ folder (preserved with git mv) - Created docs/planning/ directory - Added orig/README_V1.md explaining V1 preservation **Planning Documents:** - 00_V2_MASTER_PLAN.md: Complete architecture overview - Executive summary of critical V1 issues - High-level component architecture diagrams - 5-phase implementation roadmap - Success metrics and risk mitigation - 07_TASK_BREAKDOWN.md: Atomic task breakdown - 99+ hours of detailed tasks - Every task < 2 hours (atomic) - Clear dependencies and success criteria - Organized by implementation phase **V2 Key Improvements:** - Per-exchange parsers (factory pattern) - Multi-layer strict validation - Multi-index pool cache - Background validation pipeline - Comprehensive observability **Critical Issues Addressed:** - Zero address tokens (strict validation + cache enrichment) - Parsing accuracy (protocol-specific parsers) - No audit trail (background validation channel) - Inefficient lookups (multi-index cache) - Stats disconnection (event-driven metrics) Next Steps: 1. Review planning documents 2. Begin Phase 1: Foundation (P1-001 through P1-010) 3. Implement parsers in Phase 2 4. Build cache system in Phase 3 5. Add validation pipeline in Phase 4 6. Migrate and test in Phase 5 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
310
orig/pkg/dex/detector.go
Normal file
310
orig/pkg/dex/detector.go
Normal file
@@ -0,0 +1,310 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user