// Package arbitrum provides functionality for decoding and analyzing Arbitrum blockchain transactions, // with a particular focus on DEX (Decentralized Exchange) transaction analysis and ABI decoding. // This package is critical for the MEV bot's ability to identify arbitrage opportunities by // understanding the structure and content of DEX transactions. package arbitrum import ( "context" "encoding/hex" "fmt" "math/big" "strings" "time" "github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/ethclient" ) // ABIDecoder handles proper ABI (Application Binary Interface) decoding for DEX transactions. // This is a critical component of the MEV bot that enables it to understand and parse // complex transaction data from various decentralized exchanges on Arbitrum. // // The decoder performs several key functions: // 1. Decodes transaction input data to extract function calls and parameters // 2. Validates contract types to prevent ERC-20/pool confusion (critical fix) // 3. Supports multiple DEX protocols with their specific ABI patterns // 4. Provides runtime contract validation using RPC calls type ABIDecoder struct { // ABI definitions for major DEX protocols supported on Arbitrum // These are used for precise transaction decoding when available uniswapV2ABI abi.ABI // Uniswap V2 and compatible protocols (SushiSwap, etc.) uniswapV3ABI abi.ABI // Uniswap V3 and compatible protocols (Camelot V3, etc.) sushiswapABI abi.ABI // SushiSwap-specific functions camelotABI abi.ABI // Camelot DEX on Arbitrum balancerABI abi.ABI // Balancer V2 pools curveABI abi.ABI // Curve Finance stable pools oneInchABI abi.ABI // 1inch aggregator // Function signature mappings for fast lookup of transaction types // Maps 4-byte function selectors (e.g., "0x38ed1739") to human-readable signatures functionSignatures map[string]string // Contract type validation components (CRITICAL FIX IMPLEMENTATION) // These prevent the costly error where ERC-20 tokens are misclassified as pool contracts client *ethclient.Client // RPC client for runtime contract validation enableValidation bool // Flag to enable/disable validation (default: true for safety) } // SwapParams represents the decoded parameters from a DEX swap transaction. // This structure contains all the essential information needed to analyze // arbitrage opportunities and understand the economic impact of a trade. type SwapParams struct { // Trade amounts and constraints AmountIn *big.Int // Input amount being swapped (in token decimals) AmountOut *big.Int // Expected output amount (in token decimals) MinAmountOut *big.Int // Minimum acceptable output (slippage protection) // Token and address information TokenIn common.Address // Address of the input token contract TokenOut common.Address // Address of the output token contract Recipient common.Address // Address that will receive the output tokens Pool common.Address // Address of the liquidity pool being used Path []common.Address // Multi-hop path for complex swaps (token0 -> token1 -> token2) // Transaction constraints and fees Deadline *big.Int // Unix timestamp after which the transaction is invalid Fee *big.Int // Fee tier for Uniswap V3 pools (e.g., 500 = 0.05%) } // NewABIDecoder creates a new ABI decoder with support for all major Arbitrum DEX protocols. // The decoder is initialized with comprehensive function signature mappings and validation enabled by default. // This is the primary entry point for creating a decoder instance. // // Returns: // - *ABIDecoder: Configured decoder ready for transaction analysis // - error: Any initialization errors func NewABIDecoder() (*ABIDecoder, error) { decoder := &ABIDecoder{ // Initialize the function signature lookup table functionSignatures: make(map[string]string), // Enable validation by default to prevent contract type confusion enableValidation: true, } // Load all known DEX function signatures for fast transaction identification if err := decoder.initializeFunctionSignatures(); err != nil { return nil, fmt.Errorf("failed to initialize function signatures: %w", err) } return decoder, nil } // ValidateInputData performs enhanced input validation for ABI decoding (exported for testing) func (d *ABIDecoder) ValidateInputData(data []byte, context string) error { // Enhanced bounds checking if data == nil { return fmt.Errorf("ABI decoding validation failed: input data is nil in context %s", context) } // Check minimum size requirements if len(data) < 4 { return fmt.Errorf("ABI decoding validation failed: insufficient data length %d (minimum 4 bytes) in context %s", len(data), context) } // Check maximum size to prevent DoS const maxDataSize = 1024 * 1024 // 1MB limit if len(data) > maxDataSize { return fmt.Errorf("ABI decoding validation failed: data size %d exceeds maximum %d in context %s", len(data), maxDataSize, context) } // Validate data alignment (ABI data should be 32-byte aligned after function selector) payloadSize := len(data) - 4 // Exclude function selector if payloadSize > 0 && payloadSize%32 != 0 { return fmt.Errorf("ABI decoding validation failed: payload size %d not 32-byte aligned in context %s", payloadSize, context) } return nil } // ValidateABIParameter performs enhanced ABI parameter validation (exported for testing) func (d *ABIDecoder) ValidateABIParameter(data []byte, offset, size int, paramType string, context string) error { if offset < 0 { return fmt.Errorf("ABI parameter validation failed: negative offset %d for %s in context %s", offset, paramType, context) } if offset+size > len(data) { return fmt.Errorf("ABI parameter validation failed: parameter bounds [%d:%d] exceed data length %d for %s in context %s", offset, offset+size, len(data), paramType, context) } if size <= 0 { return fmt.Errorf("ABI parameter validation failed: invalid parameter size %d for %s in context %s", size, paramType, context) } // Specific validation for address parameters if paramType == "address" && size == 32 { // Check that first 12 bytes are zero for address type for i := 0; i < 12; i++ { if data[offset+i] != 0 { return fmt.Errorf("ABI parameter validation failed: invalid address padding for %s in context %s", paramType, context) } } } return nil } // ValidateArrayBounds performs enhanced array bounds validation (exported for testing) func (d *ABIDecoder) ValidateArrayBounds(data []byte, arrayOffset, arrayLength uint64, elementSize int, context string) error { if arrayOffset >= uint64(len(data)) { return fmt.Errorf("ABI array validation failed: array offset %d exceeds data length %d in context %s", arrayOffset, len(data), context) } // Reasonable array length limits const maxArrayLength = 10000 if arrayLength > maxArrayLength { return fmt.Errorf("ABI array validation failed: array length %d exceeds maximum %d in context %s", arrayLength, maxArrayLength, context) } // Check total array size doesn't exceed bounds totalArraySize := arrayLength * uint64(elementSize) if arrayOffset+32+totalArraySize > uint64(len(data)) { return fmt.Errorf("ABI array validation failed: array bounds [%d:%d] exceed data length %d in context %s", arrayOffset, arrayOffset+32+totalArraySize, len(data), context) } return nil } // WithClient enables runtime contract validation by providing an RPC client. // When a client is provided, the decoder can perform on-chain contract calls // to verify contract types and prevent ERC-20/pool confusion errors. // // This is essential for production use to avoid the costly misclassification // that was causing 535K+ log messages and transaction failures. // // Parameters: // - client: Ethereum RPC client for contract validation // // Returns: // - *ABIDecoder: The decoder instance for method chaining func (d *ABIDecoder) WithClient(client *ethclient.Client) *ABIDecoder { d.client = client return d } // WithValidation controls whether contract type validation is performed during decoding. // Validation is highly recommended for production use to prevent costly errors. // // When validation is enabled: // - Contracts are verified to match their expected types // - ERC-20 tokens are prevented from being used as pool contracts // - Runtime checks are performed using RPC calls // // Parameters: // - enabled: true to enable validation (recommended), false to disable // // Returns: // - *ABIDecoder: The decoder instance for method chaining func (d *ABIDecoder) WithValidation(enabled bool) *ABIDecoder { d.enableValidation = enabled return d } // initializeFunctionSignatures sets up known DEX function signatures func (d *ABIDecoder) initializeFunctionSignatures() error { // Uniswap V2 signatures d.functionSignatures["0x38ed1739"] = "swapExactTokensForTokens(uint256,uint256,address[],address,uint256)" d.functionSignatures["0x8803dbee"] = "swapTokensForExactTokens(uint256,uint256,address[],address,uint256)" d.functionSignatures["0x7ff36ab5"] = "swapExactETHForTokens(uint256,address[],address,uint256)" d.functionSignatures["0x4a25d94a"] = "swapTokensForExactETH(uint256,uint256,address[],address,uint256)" d.functionSignatures["0x18cbafe5"] = "swapExactTokensForETH(uint256,uint256,address[],address,uint256)" d.functionSignatures["0xfb3bdb41"] = "swapETHForExactTokens(uint256,address[],address,uint256)" // Uniswap V3 signatures d.functionSignatures["0x414bf389"] = "exactInputSingle((address,address,uint24,address,uint256,uint256,uint256,uint160))" d.functionSignatures["0xc04b8d59"] = "exactInput((bytes,address,uint256,uint256,uint256))" d.functionSignatures["0xdb3e2198"] = "exactOutputSingle((address,address,uint24,address,uint256,uint256,uint256,uint160))" d.functionSignatures["0xf28c0498"] = "exactOutput((bytes,address,uint256,uint256,uint256))" // SushiSwap (same as Uniswap V2) // Camelot V3 (same as Uniswap V3) // 1inch aggregator signatures d.functionSignatures["0x7c025200"] = "swap(address,(address,address,address,address,uint256,uint256,uint256),bytes,bytes)" d.functionSignatures["0xe449022e"] = "uniswapV3Swap(uint256,uint256,uint256[])" // Balancer V2 signatures d.functionSignatures["0x52bbbe29"] = "swap((bytes32,uint8,address,address,uint256,bytes),(address,bool,address,bool),uint256,uint256)" // Radiant (lending protocol with swap functionality) d.functionSignatures["0xa9059cbb"] = "transfer(address,uint256)" // ERC-20 transfer d.functionSignatures["0x23b872dd"] = "transferFrom(address,address,uint256)" // ERC-20 transferFrom d.functionSignatures["0xe8e33700"] = "deposit(address,uint256,address,uint16)" // Aave-style deposit d.functionSignatures["0x69328dec"] = "withdraw(address,uint256,address)" // Aave-style withdraw d.functionSignatures["0xa415bcad"] = "borrow(address,uint256,uint256,uint16,address)" // Aave-style borrow d.functionSignatures["0x563dd613"] = "repay(address,uint256,uint256,address)" // Aave-style repay // Curve Finance signatures d.functionSignatures["0x3df02124"] = "exchange(int128,int128,uint256,uint256)" // Curve exchange d.functionSignatures["0xa6417ed6"] = "exchange_underlying(int128,int128,uint256,uint256)" // Curve exchange underlying // Trader Joe (Arbitrum DEX) d.functionSignatures["0x18cbafe5"] = "swapExactTokensForTokens(uint256,uint256,address[],address,uint256)" // Same as Uniswap V2 d.functionSignatures["0x791ac947"] = "swapExactTokensForTokensSupportingFeeOnTransferTokens(uint256,uint256,address[],address,uint256)" // GMX (perpetual trading) d.functionSignatures["0x0809dd62"] = "swap(address[],uint256,uint256,address)" // GMX swap d.functionSignatures["0x29a5408c"] = "increasePosition(address[],address,uint256,uint256,uint256,bool,uint256)" // GMX position // Arbitrum-specific multicall patterns d.functionSignatures["0xac9650d8"] = "multicall(bytes[])" // Common multicall d.functionSignatures["0x1f0464d1"] = "multicall(bytes[])" // Alternative multicall d.functionSignatures["0x5ae401dc"] = "multicall(uint256,bytes[])" // Multicall with deadline // Universal Router signatures (critical for Arbitrum) d.functionSignatures["0x24856bc3"] = "execute(bytes,bytes[])" // Universal Router execute d.functionSignatures["0x3593564c"] = "execute(bytes,bytes[],uint256)" // Universal Router execute with deadline d.functionSignatures["0x0000000c"] = "V3_SWAP_EXACT_IN" // Universal Router command d.functionSignatures["0x0000000d"] = "V3_SWAP_EXACT_OUT" // Universal Router command d.functionSignatures["0x00000008"] = "V2_SWAP_EXACT_IN" // Universal Router V2 command d.functionSignatures["0x00000009"] = "V2_SWAP_EXACT_OUT" // Universal Router V2 command // Additional Arbitrum DEX signatures d.functionSignatures["0x128acb08"] = "initialize(uint160)" // Uniswap V3 pool initialize d.functionSignatures["0x883164f6"] = "mint(address,int24,int24,uint128,bytes)" // Uniswap V3 mint d.functionSignatures["0x128acb09"] = "collect(address,int24,int24,uint128,uint128)" // Uniswap V3 collect // Camelot DEX (Arbitrum-specific) d.functionSignatures["0x022c0d9f"] = "swap(uint256,uint256,address,bytes)" // Camelot swap d.functionSignatures["0x4515cef3"] = "deposit(address,uint256)" // Camelot deposit // Radiant Capital (Arbitrum lending with swaps) d.functionSignatures["0x7535d246"] = "mint(uint256)" // Radiant mint d.functionSignatures["0x852a12e3"] = "repayBorrow(uint256)" // Radiant repay return nil } // DecodeSwapTransaction decodes a swap transaction based on protocol and function signature func (d *ABIDecoder) DecodeSwapTransaction(protocol, txData string) (*SwapParams, error) { if len(txData) < 10 { // 0x + 4 bytes function selector return nil, fmt.Errorf("transaction data too short") } // Remove 0x prefix if present if strings.HasPrefix(txData, "0x") { txData = txData[2:] } data, err := hex.DecodeString(txData) if err != nil { return nil, fmt.Errorf("failed to decode hex data: %w", err) } if len(data) < 4 { return nil, fmt.Errorf("insufficient data for function selector") } // Extract function selector selector := "0x" + hex.EncodeToString(data[:4]) // Get function signature functionSig, exists := d.functionSignatures[selector] if !exists { // Try to decode as generic swap if signature unknown return d.decodeGenericSwap(data, protocol) } // Handle multicall transactions first if strings.Contains(functionSig, "multicall") { return d.decodeMulticall(data, protocol) } // Decode based on protocol and function switch protocol { case "uniswap_v2", "sushiswap", "camelot_v2", "trader_joe": return d.decodeUniswapV2Swap(data, functionSig) case "uniswap_v3", "camelot_v3", "algebra": return d.decodeUniswapV3Swap(data, functionSig) case "1inch": return d.decode1inchSwap(data, functionSig) case "balancer_v2": return d.decodeBalancerSwap(data, functionSig) case "curve": return d.decodeCurveSwap(data, functionSig) case "radiant", "aave", "compound": return d.decodeLendingSwap(data, functionSig) case "gmx": return d.decodeGMXSwap(data, functionSig) default: return d.decodeGenericSwap(data, protocol) } } // decodeUniswapV2Swap decodes Uniswap V2 style swap transactions func (d *ABIDecoder) decodeUniswapV2Swap(data []byte, functionSig string) (*SwapParams, error) { if len(data) < 4 { return nil, fmt.Errorf("insufficient data") } params := &SwapParams{} // Skip function selector (first 4 bytes) data = data[4:] // Parse based on function type if strings.Contains(functionSig, "swapExactTokensForTokens") { if len(data) < 160 { // 5 * 32 bytes minimum return nil, fmt.Errorf("insufficient data for swapExactTokensForTokens") } // amountIn (32 bytes) params.AmountIn = new(big.Int).SetBytes(data[0:32]) // amountOutMin (32 bytes) params.MinAmountOut = new(big.Int).SetBytes(data[32:64]) // path offset (32 bytes) - skip this, get actual path pathOffset := new(big.Int).SetBytes(data[64:96]).Uint64() // recipient (32 bytes, but address is last 20 bytes) params.Recipient = common.BytesToAddress(data[96:128]) // deadline (32 bytes) params.Deadline = new(big.Int).SetBytes(data[128:160]) // Parse path array if pathOffset < uint64(len(data)) && pathOffset+32 < uint64(len(data)) { pathLength := new(big.Int).SetBytes(data[pathOffset : pathOffset+32]).Uint64() if pathLength >= 2 && pathOffset+32+pathLength*32 <= uint64(len(data)) { params.Path = make([]common.Address, pathLength) for i := uint64(0); i < pathLength; i++ { start := pathOffset + 32 + i*32 params.Path[i] = common.BytesToAddress(data[start : start+32]) } if len(params.Path) >= 2 { params.TokenIn = params.Path[0] params.TokenOut = params.Path[len(params.Path)-1] } } } } // Handle other Uniswap V2 functions similarly... return params, nil } // decodeUniswapV3Swap decodes Uniswap V3 style swap transactions func (d *ABIDecoder) decodeUniswapV3Swap(data []byte, functionSig string) (*SwapParams, error) { if len(data) < 4 { return nil, fmt.Errorf("insufficient data") } params := &SwapParams{} data = data[4:] // Skip function selector if strings.Contains(functionSig, "exactInputSingle") { // ExactInputSingle struct: (tokenIn, tokenOut, fee, recipient, deadline, amountIn, amountOutMinimum, sqrtPriceLimitX96) if len(data) < 256 { // 8 * 32 bytes return nil, fmt.Errorf("insufficient data for exactInputSingle") } params.TokenIn = common.BytesToAddress(data[0:32]) params.TokenOut = common.BytesToAddress(data[32:64]) params.Fee = new(big.Int).SetBytes(data[64:96]) params.Recipient = common.BytesToAddress(data[96:128]) params.Deadline = new(big.Int).SetBytes(data[128:160]) params.AmountIn = new(big.Int).SetBytes(data[160:192]) params.MinAmountOut = new(big.Int).SetBytes(data[192:224]) // sqrtPriceLimitX96 at data[224:256] - skip for now } // Handle other Uniswap V3 functions... return params, nil } // decode1inchSwap decodes 1inch aggregator swap transactions func (d *ABIDecoder) decode1inchSwap(data []byte, functionSig string) (*SwapParams, error) { params := &SwapParams{} data = data[4:] // Skip function selector // 1inch has complex swap data, extract what we can if len(data) >= 64 { // First parameter is usually the caller/executor // Second parameter contains swap description with tokens and amounts if len(data) >= 160 { // Try to extract token addresses from swap description // This is a simplified extraction - 1inch has complex encoding params.TokenIn = common.BytesToAddress(data[32:64]) params.TokenOut = common.BytesToAddress(data[64:96]) params.AmountIn = new(big.Int).SetBytes(data[96:128]) params.MinAmountOut = new(big.Int).SetBytes(data[128:160]) } } return params, nil } // decodeBalancerSwap decodes Balancer V2 swap transactions func (d *ABIDecoder) decodeBalancerSwap(data []byte, functionSig string) (*SwapParams, error) { params := &SwapParams{} data = data[4:] // Skip function selector // Balancer has complex swap structure with pool ID, tokens, amounts if len(data) >= 128 { // Extract basic information - Balancer encoding is complex // This is a simplified version params.AmountIn = new(big.Int).SetBytes(data[64:96]) params.MinAmountOut = new(big.Int).SetBytes(data[96:128]) } return params, nil } // decodeGenericSwap provides fallback decoding for unknown protocols with enhanced validation func (d *ABIDecoder) decodeGenericSwap(data []byte, protocol string) (*SwapParams, error) { params := &SwapParams{} // Enhanced input validation if err := d.ValidateInputData(data, fmt.Sprintf("decodeGenericSwap-%s", protocol)); err != nil { return nil, err } data = data[4:] // Skip function selector // Enhanced bounds checking for payload if err := d.ValidateABIParameter(data, 0, len(data), "payload", fmt.Sprintf("decodeGenericSwap-%s-payload", protocol)); err != nil { return nil, err } // Try to extract common ERC-20 swap patterns if len(data) >= 128 { // Minimum for token addresses and amounts // Try different common patterns for token addresses // Pattern 1: Direct address parameters at start with validation if len(data) >= 64 { if err := d.ValidateABIParameter(data, 0, 32, "address", fmt.Sprintf("pattern1-tokenIn-%s", protocol)); err != nil { return nil, err } if err := d.ValidateABIParameter(data, 32, 32, "address", fmt.Sprintf("pattern1-tokenOut-%s", protocol)); err != nil { return nil, err } tokenIn := common.BytesToAddress(data[0:32]) tokenOut := common.BytesToAddress(data[32:64]) // Check if these look like valid token addresses (not zero) if d.isValidTokenAddress(tokenIn) && d.isValidTokenAddress(tokenOut) { params.TokenIn = tokenIn params.TokenOut = tokenOut } } // Pattern 2: Try offset-based token extraction with enhanced bounds checking if params.TokenIn == (common.Address{}) && len(data) >= 96 { // Sometimes tokens are at different offsets - validate each access for offset := 0; offset < 128 && offset+32 <= len(data); offset += 32 { if err := d.ValidateABIParameter(data, offset, 32, "address", fmt.Sprintf("pattern2-offset%d-%s", offset, protocol)); err != nil { continue // Skip invalid offsets } addr := common.BytesToAddress(data[offset : offset+32]) if d.isValidTokenAddress(addr) { if params.TokenIn == (common.Address{}) { params.TokenIn = addr } else if params.TokenOut == (common.Address{}) && addr != params.TokenIn { params.TokenOut = addr break } } } } // Pattern 3: Look for array patterns with comprehensive validation if params.TokenIn == (common.Address{}) && len(data) >= 160 { // Look for dynamic arrays which often contain token paths for offset := 32; offset+64 <= len(data); offset += 32 { if err := d.ValidateABIParameter(data, offset, 32, "uint256", fmt.Sprintf("pattern3-offset%d-%s", offset, protocol)); err != nil { continue } // Check if this looks like an array offset possibleOffset := new(big.Int).SetBytes(data[offset : offset+32]).Uint64() if possibleOffset > 32 && possibleOffset < uint64(len(data)-64) { // Validate array header access if err := d.ValidateABIParameter(data, int(possibleOffset), 32, "array-length", fmt.Sprintf("pattern3-arraylen-%s", protocol)); err != nil { continue } // Check if there's an array length at this offset arrayLen := new(big.Int).SetBytes(data[possibleOffset : possibleOffset+32]).Uint64() // Enhanced array validation if err := d.ValidateArrayBounds(data, possibleOffset, arrayLen, 32, fmt.Sprintf("pattern3-array-%s", protocol)); err != nil { continue } if arrayLen >= 2 && arrayLen <= 10 { // Validate array element access before extraction if err := d.ValidateABIParameter(data, int(possibleOffset+32), 32, "address", fmt.Sprintf("pattern3-first-%s", protocol)); err != nil { continue } lastElementOffset := int(possibleOffset + 32 + (arrayLen-1)*32) if err := d.ValidateABIParameter(data, lastElementOffset, 32, "address", fmt.Sprintf("pattern3-last-%s", protocol)); err != nil { continue } // Extract first and last elements as token addresses firstToken := common.BytesToAddress(data[possibleOffset+32 : possibleOffset+64]) lastToken := common.BytesToAddress(data[lastElementOffset : lastElementOffset+32]) if d.isValidTokenAddress(firstToken) && d.isValidTokenAddress(lastToken) { params.TokenIn = firstToken params.TokenOut = lastToken break } } } } } // Extract amounts from common positions if len(data) >= 64 { // Try first amount params.AmountIn = new(big.Int).SetBytes(data[0:32]) if params.AmountIn.Cmp(big.NewInt(0)) == 0 && len(data) >= 96 { params.AmountIn = new(big.Int).SetBytes(data[32:64]) } if params.AmountIn.Cmp(big.NewInt(0)) == 0 && len(data) >= 128 { params.AmountIn = new(big.Int).SetBytes(data[64:96]) } // Try to find amount out (usually after amount in) if len(data) >= 96 && params.AmountIn.Cmp(big.NewInt(0)) > 0 { params.MinAmountOut = new(big.Int).SetBytes(data[64:96]) if params.MinAmountOut.Cmp(big.NewInt(0)) == 0 && len(data) >= 128 { params.MinAmountOut = new(big.Int).SetBytes(data[96:128]) } } } // Try to find recipient address (often near the end) if len(data) >= 160 { // Check last few 32-byte slots for address patterns for i := len(data) - 32; i >= len(data)-96 && i >= 0; i -= 32 { addr := common.BytesToAddress(data[i : i+32]) if d.isValidTokenAddress(addr) { params.Recipient = addr break } } } } // CRITICAL FIX: Validate extracted token addresses before returning if params.TokenIn == (common.Address{}) || params.TokenOut == (common.Address{}) { return nil, fmt.Errorf("failed to extract valid token addresses from transaction data for protocol %s", protocol) } if params.TokenIn == params.TokenOut { return nil, fmt.Errorf("tokenIn and tokenOut cannot be the same: %s", params.TokenIn.Hex()) } // CRITICAL: Validate contract types to prevent ERC-20/pool confusion if d.enableValidation { if err := d.validateContractTypes(params); err != nil { return nil, fmt.Errorf("contract type validation failed: %w", err) } } return params, nil } // isValidTokenAddress checks if an address looks like a valid token address func (d *ABIDecoder) isValidTokenAddress(addr common.Address) bool { // Check if not zero address if addr == (common.Address{}) { return false } // Check if not a common non-token address // Exclude known router/factory addresses that aren't tokens knownContracts := []common.Address{ common.HexToAddress("0x4752ba5dbc23f44d87826276bf6fd6b1c372ad24"), // Uniswap V2 Router common.HexToAddress("0xE592427A0AEce92De3Edee1F18E0157C05861564"), // Uniswap V3 Router common.HexToAddress("0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45"), // Uniswap V3 Router02 common.HexToAddress("0x1b02dA8Cb0d097eB8D57A175b88c7D8b47997506"), // SushiSwap Router common.HexToAddress("0x1F98431c8aD98523631AE4a59f267346ea31F984"), // Uniswap V3 Factory common.HexToAddress("0xc35DADB65012eC5796536bD9864eD8773aBc74C4"), // SushiSwap Factory } for _, contract := range knownContracts { if addr == contract { return false } } // Additional heuristics: valid token addresses typically have: // - Non-zero value // - Not ending in many zeros (contracts often do) // - Not matching common EOA patterns addrBytes := addr.Bytes() zeroCount := 0 for i := len(addrBytes) - 4; i < len(addrBytes); i++ { if addrBytes[i] == 0 { zeroCount++ } } // If last 4 bytes are all zeros, likely not a token if zeroCount >= 3 { return false } return true } // validateContractTypes validates that extracted addresses match expected contract types func (d *ABIDecoder) validateContractTypes(params *SwapParams) error { if d.client == nil { return nil // Skip validation if no client available } ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() // Validate TokenIn is actually an ERC-20 token if params.TokenIn != (common.Address{}) { if err := d.validateIsERC20Token(ctx, params.TokenIn); err != nil { return fmt.Errorf("TokenIn validation failed: %w", err) } } // Validate TokenOut is actually an ERC-20 token if params.TokenOut != (common.Address{}) { if err := d.validateIsERC20Token(ctx, params.TokenOut); err != nil { return fmt.Errorf("TokenOut validation failed: %w", err) } } // Validate Pool address if present (should be a pool, not a token) if params.Pool != (common.Address{}) { if err := d.validateIsPool(ctx, params.Pool); err != nil { return fmt.Errorf("Pool validation failed: %w", err) } // CRITICAL: Ensure pool address is not the same as token addresses if params.Pool == params.TokenIn { return fmt.Errorf("pool address cannot be the same as TokenIn: %s", params.Pool.Hex()) } if params.Pool == params.TokenOut { return fmt.Errorf("pool address cannot be the same as TokenOut: %s", params.Pool.Hex()) } } return nil } // validateIsERC20Token validates that an address is an ERC-20 token contract func (d *ABIDecoder) validateIsERC20Token(ctx context.Context, address common.Address) error { // Test for basic ERC-20 functions erc20Functions := [][]byte{ {0x70, 0xa0, 0x82, 0x31}, // balanceOf(address) {0x31, 0x3c, 0xe5, 0x67}, // decimals() {0x95, 0xd8, 0x9b, 0x41}, // symbol() } successCount := 0 for _, funcSig := range erc20Functions { // Create a test call with zero address as parameter testData := make([]byte, 4+32) copy(testData[:4], funcSig) // Leave the rest as zeros (test parameter) _, err := d.client.CallContract(ctx, ethereum.CallMsg{ To: &address, Data: testData, }, nil) if err == nil { successCount++ } } // Must support at least 2 out of 3 basic ERC-20 functions if successCount < 2 { return fmt.Errorf("address %s does not appear to be an ERC-20 token (supported %d/3 functions)", address.Hex(), successCount) } // CRITICAL: Ensure it's not a pool contract by testing pool-specific functions if d.appearsToBePool(ctx, address) { return fmt.Errorf("address %s appears to be a pool contract, not an ERC-20 token", address.Hex()) } return nil } // validateIsPool validates that an address is a pool contract func (d *ABIDecoder) validateIsPool(ctx context.Context, address common.Address) error { // Test for Uniswap V2/V3 pool functions poolFunctions := [][]byte{ {0x0d, 0xfe, 0x16, 0x81}, // token0() {0xd2, 0x12, 0x20, 0xa7}, // token1() } successCount := 0 for _, funcSig := range poolFunctions { _, err := d.client.CallContract(ctx, ethereum.CallMsg{ To: &address, Data: funcSig, }, nil) if err == nil { successCount++ } } // Must support both token0() and token1() functions if successCount < 2 { return fmt.Errorf("address %s does not appear to be a pool contract (supported %d/2 pool functions)", address.Hex(), successCount) } // CRITICAL: Ensure it's not an ERC-20 token by testing token-specific functions if d.appearsToBeERC20(ctx, address) { return fmt.Errorf("address %s appears to be an ERC-20 token, not a pool contract", address.Hex()) } return nil } // appearsToBePool checks if an address has pool-like characteristics func (d *ABIDecoder) appearsToBePool(ctx context.Context, address common.Address) bool { poolFunctions := [][]byte{ {0x0d, 0xfe, 0x16, 0x81}, // token0() {0xd2, 0x12, 0x20, 0xa7}, // token1() {0x09, 0x02, 0xf1, 0xac}, // getReserves() - V2 {0x38, 0x50, 0xc7, 0xbd}, // slot0() - V3 } successCount := 0 for _, funcSig := range poolFunctions { _, err := d.client.CallContract(ctx, ethereum.CallMsg{ To: &address, Data: funcSig, }, nil) if err == nil { successCount++ } } // If it supports 2+ pool functions, it's likely a pool return successCount >= 2 } // appearsToBeERC20 checks if an address has ERC-20 token characteristics func (d *ABIDecoder) appearsToBeERC20(ctx context.Context, address common.Address) bool { erc20Functions := [][]byte{ {0x70, 0xa0, 0x82, 0x31}, // balanceOf(address) {0x31, 0x3c, 0xe5, 0x67}, // decimals() {0x95, 0xd8, 0x9b, 0x41}, // symbol() {0x18, 0x16, 0x0d, 0xdd}, // totalSupply() } successCount := 0 for _, funcSig := range erc20Functions { testData := make([]byte, 4+32) copy(testData[:4], funcSig) _, err := d.client.CallContract(ctx, ethereum.CallMsg{ To: &address, Data: testData, }, nil) if err == nil { successCount++ } } // If it supports 3+ ERC-20 functions, it's likely an ERC-20 token return successCount >= 3 } // CalculatePoolAddress calculates pool address using CREATE2 for known factories func (d *ABIDecoder) CalculatePoolAddress(tokenA, tokenB common.Address, fee *big.Int, protocol string) (common.Address, error) { // Arbitrum factory addresses factories := map[string]common.Address{ "uniswap_v3": common.HexToAddress("0x1F98431c8aD98523631AE4a59f267346ea31F984"), "camelot_v3": common.HexToAddress("0x1a3c9B1d2F0529D97f2afC5136Cc23e58f1FD35B"), "sushiswap": common.HexToAddress("0xc35DADB65012eC5796536bD9864eD8773aBc74C4"), "algebra": common.HexToAddress("0x411b0fAcC3489691f28ad58c47006AF5E3Ab3A28"), } factory, exists := factories[protocol] if !exists { return common.Address{}, fmt.Errorf("unknown protocol: %s", protocol) } // Ensure token addresses are not zero addresses if tokenA == (common.Address{}) || tokenB == (common.Address{}) { return common.Address{}, fmt.Errorf("token addresses cannot be zero") } // Ensure token order (tokenA < tokenB) if tokenA.Big().Cmp(tokenB.Big()) > 0 { tokenA, tokenB = tokenB, tokenA } switch protocol { case "uniswap_v3", "camelot_v3": return d.calculateUniswapV3PoolAddress(factory, tokenA, tokenB, fee) case "sushiswap": return d.calculateUniswapV2PoolAddress(factory, tokenA, tokenB) default: return d.calculateUniswapV2PoolAddress(factory, tokenA, tokenB) } } // calculateUniswapV3PoolAddress calculates Uniswap V3 pool address func (d *ABIDecoder) calculateUniswapV3PoolAddress(factory, tokenA, tokenB common.Address, fee *big.Int) (common.Address, error) { // Check if fee is nil and handle appropriately if fee == nil { fee = big.NewInt(0) // Use 0 as default fee if nil } // Ensure token addresses are not zero addresses if tokenA == (common.Address{}) || tokenB == (common.Address{}) { return common.Address{}, fmt.Errorf("token addresses cannot be zero") } // Ensure fee is never nil when calling fee.Bytes() feeBytes := common.LeftPadBytes(fee.Bytes(), 32) // Uniswap V3 CREATE2 salt: keccak256(abi.encode(token0, token1, fee)) salt := crypto.Keccak256( common.LeftPadBytes(tokenA.Bytes(), 32), common.LeftPadBytes(tokenB.Bytes(), 32), feeBytes, ) // Uniswap V3 pool init code hash initCodeHash := common.HexToHash("0xe34f199b19b2b4f47f68442619d555527d244f78a3297ea89325f843f87b8b54") // CREATE2 address calculation: keccak256(0xff ++ factory ++ salt ++ keccak256(initCode))[12:] data := make([]byte, 1+20+32+32) data[0] = 0xff copy(data[1:21], factory.Bytes()) copy(data[21:53], salt) copy(data[53:85], initCodeHash.Bytes()) hash := crypto.Keccak256(data) return common.BytesToAddress(hash[12:]), nil } // calculateUniswapV2PoolAddress calculates Uniswap V2 style pool address func (d *ABIDecoder) calculateUniswapV2PoolAddress(factory, tokenA, tokenB common.Address) (common.Address, error) { // Uniswap V2 CREATE2 salt: keccak256(abi.encodePacked(token0, token1)) salt := crypto.Keccak256(append(tokenA.Bytes(), tokenB.Bytes()...)) // SushiSwap init code hash (example) initCodeHash := common.HexToHash("0xe18a34eb0e04b04f7a0ac29a6e80748dca96319b42c54d679cb821dca90c6303") // CREATE2 address calculation data := make([]byte, 1+20+32+32) data[0] = 0xff copy(data[1:21], factory.Bytes()) copy(data[21:53], salt) copy(data[53:85], initCodeHash.Bytes()) hash := crypto.Keccak256(data) return common.BytesToAddress(hash[12:]), nil } // decodeCurveSwap decodes Curve Finance swap transactions func (d *ABIDecoder) decodeCurveSwap(data []byte, functionSig string) (*SwapParams, error) { params := &SwapParams{} data = data[4:] // Skip function selector if strings.Contains(functionSig, "exchange") { // Curve exchange(int128,int128,uint256,uint256) or exchange_underlying if len(data) >= 128 { // Skip token indices (first 64 bytes) and get amounts params.AmountIn = new(big.Int).SetBytes(data[64:96]) params.MinAmountOut = new(big.Int).SetBytes(data[96:128]) // For Curve, token addresses would need to be looked up from pool contract // For now, use placeholder logic from generic decoder return d.decodeGenericSwap(append([]byte{0, 0, 0, 0}, data...), "curve") } } return params, nil } // decodeLendingSwap decodes lending protocol transactions (Radiant, Aave, etc.) func (d *ABIDecoder) decodeLendingSwap(data []byte, functionSig string) (*SwapParams, error) { params := &SwapParams{} data = data[4:] // Skip function selector if strings.Contains(functionSig, "deposit") || strings.Contains(functionSig, "withdraw") || strings.Contains(functionSig, "borrow") || strings.Contains(functionSig, "repay") { // Standard lending operations: (asset, amount, onBehalfOf, referralCode) if len(data) >= 96 { params.TokenIn = common.BytesToAddress(data[0:32]) // asset params.AmountIn = new(big.Int).SetBytes(data[32:64]) // amount params.Recipient = common.BytesToAddress(data[64:96]) // onBehalfOf // For lending, TokenOut would be the receipt token (aToken, etc.) // This would need protocol-specific logic to determine } } else if strings.Contains(functionSig, "transfer") { // ERC-20 transfer/transferFrom if strings.Contains(functionSig, "transferFrom") && len(data) >= 96 { // transferFrom(from, to, amount) params.Recipient = common.BytesToAddress(data[32:64]) // to params.AmountIn = new(big.Int).SetBytes(data[64:96]) // amount } else if len(data) >= 64 { // transfer(to, amount) params.Recipient = common.BytesToAddress(data[0:32]) // to params.AmountIn = new(big.Int).SetBytes(data[32:64]) // amount } } return params, nil } // decodeGMXSwap decodes GMX protocol transactions func (d *ABIDecoder) decodeGMXSwap(data []byte, functionSig string) (*SwapParams, error) { params := &SwapParams{} data = data[4:] // Skip function selector if strings.Contains(functionSig, "swap") { // GMX swap(address[],uint256,uint256,address) if len(data) >= 128 { // First parameter is path array offset, skip to amount params.AmountIn = new(big.Int).SetBytes(data[32:64]) params.MinAmountOut = new(big.Int).SetBytes(data[64:96]) params.Recipient = common.BytesToAddress(data[96:128]) // Extract token path from the dynamic array // This would need more complex parsing for the path array } } // Fall back to generic decoding for complex GMX transactions return d.decodeGenericSwap(append([]byte{0, 0, 0, 0}, data...), "gmx") } // decodeMulticall decodes multicall transactions by extracting individual calls func (d *ABIDecoder) decodeMulticall(data []byte, protocol string) (*SwapParams, error) { if len(data) < 4 { return nil, fmt.Errorf("insufficient data for multicall") } // Skip function selector data = data[4:] if len(data) < 32 { return nil, fmt.Errorf("insufficient data for multicall array offset") } // Get array offset arrayOffset := new(big.Int).SetBytes(data[0:32]).Uint64() if arrayOffset >= uint64(len(data)) || arrayOffset+32 >= uint64(len(data)) { return nil, fmt.Errorf("invalid array offset in multicall") } // Get array length arrayLength := new(big.Int).SetBytes(data[arrayOffset : arrayOffset+32]).Uint64() if arrayLength == 0 { return nil, fmt.Errorf("empty multicall array") } // Parse individual calls in the multicall bestParams := &SwapParams{} callsFound := 0 for i := uint64(0); i < arrayLength; i++ { // Each call has an offset pointer offsetPos := arrayOffset + 32 + i*32 if offsetPos+32 > uint64(len(data)) { break } callOffset := arrayOffset + new(big.Int).SetBytes(data[offsetPos:offsetPos+32]).Uint64() if callOffset+32 > uint64(len(data)) { continue } // Get call data length callDataLength := new(big.Int).SetBytes(data[callOffset : callOffset+32]).Uint64() if callOffset+32+callDataLength > uint64(len(data)) { continue } // Extract call data callData := data[callOffset+32 : callOffset+32+callDataLength] if len(callData) < 4 { continue } // Try to decode this individual call params, err := d.decodeIndividualCall(callData, protocol) if err == nil && params != nil { callsFound++ // Use the first successfully decoded call or the one with most complete data if bestParams.TokenIn == (common.Address{}) || (params.TokenIn != (common.Address{}) && params.TokenOut != (common.Address{})) { bestParams = params } } } if callsFound == 0 { return nil, fmt.Errorf("no decodable calls found in multicall") } return bestParams, nil } // decodeIndividualCall decodes an individual call within a multicall func (d *ABIDecoder) decodeIndividualCall(callData []byte, protocol string) (*SwapParams, error) { if len(callData) < 4 { return nil, fmt.Errorf("insufficient call data") } // Extract function selector selector := "0x" + hex.EncodeToString(callData[:4]) // Check if this is a known swap function functionSig, exists := d.functionSignatures[selector] if !exists { // Try generic decoding for unknown functions return d.decodeGenericSwap(callData, protocol) } // Decode based on function signature if strings.Contains(functionSig, "swapExact") || strings.Contains(functionSig, "exactInput") { // This is a swap function, decode it appropriately switch { case strings.Contains(functionSig, "exactInputSingle"): return d.decodeUniswapV3Swap(callData, functionSig) case strings.Contains(functionSig, "exactInput"): return d.decodeUniswapV3Swap(callData, functionSig) case strings.Contains(functionSig, "swapExactTokensForTokens"): return d.decodeUniswapV2Swap(callData, functionSig) case strings.Contains(functionSig, "swapTokensForExactTokens"): return d.decodeUniswapV2Swap(callData, functionSig) case strings.Contains(functionSig, "swapExactETHForTokens"): return d.decodeUniswapV2Swap(callData, functionSig) case strings.Contains(functionSig, "swapExactTokensForETH"): return d.decodeUniswapV2Swap(callData, functionSig) default: return d.decodeGenericSwap(callData, protocol) } } // Not a swap function, try generic decoding return d.decodeGenericSwap(callData, protocol) }