package arbitrum import ( "encoding/hex" "fmt" "math/big" "strings" "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/crypto" ) // ABIDecoder handles proper ABI decoding for DEX transactions type ABIDecoder struct { // Function signatures for major DEX protocols uniswapV2ABI abi.ABI uniswapV3ABI abi.ABI sushiswapABI abi.ABI camelotABI abi.ABI balancerABI abi.ABI curveABI abi.ABI oneInchABI abi.ABI // Function signature mappings functionSignatures map[string]string } // SwapParams represents decoded swap parameters type SwapParams struct { AmountIn *big.Int AmountOut *big.Int MinAmountOut *big.Int TokenIn common.Address TokenOut common.Address Recipient common.Address Pool common.Address Path []common.Address Deadline *big.Int Fee *big.Int } // NewABIDecoder creates a new ABI decoder with protocol definitions func NewABIDecoder() (*ABIDecoder, error) { decoder := &ABIDecoder{ functionSignatures: make(map[string]string), } // Initialize known function signatures if err := decoder.initializeFunctionSignatures(); err != nil { return nil, fmt.Errorf("failed to initialize function signatures: %w", err) } return decoder, nil } // 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 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 func (d *ABIDecoder) decodeGenericSwap(data []byte, protocol string) (*SwapParams, error) { params := &SwapParams{} if len(data) < 4 { return params, nil } data = data[4:] // Skip function selector // 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 if len(data) >= 64 { 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 (common in complex calls) if params.TokenIn == (common.Address{}) && len(data) >= 96 { // Sometimes tokens are at different offsets for offset := 0; offset < 128 && offset+32 <= len(data); offset += 32 { 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 (common in path-based swaps) 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 { // 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) { // Check if there's an array length at this offset arrayLen := new(big.Int).SetBytes(data[possibleOffset : possibleOffset+32]).Uint64() if arrayLen >= 2 && arrayLen <= 10 && possibleOffset+32+arrayLen*32 <= uint64(len(data)) { // Extract first and last elements as token addresses firstToken := common.BytesToAddress(data[possibleOffset+32 : possibleOffset+64]) lastToken := common.BytesToAddress(data[possibleOffset+32+(arrayLen-1)*32 : possibleOffset+32+arrayLen*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 } } } } 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 } // 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) }