Files
mev-beta/orig/pkg/arbitrum/l2_parser.go
Administrator 803de231ba 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>
2025-11-10 10:14:26 +01:00

1986 lines
69 KiB
Go

package arbitrum
import (
"context"
"encoding/hex"
"fmt"
"math/big"
"strings"
"time"
"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/rpc"
"github.com/fraktal/mev-beta/internal/logger"
"github.com/fraktal/mev-beta/pkg/oracle"
"github.com/fraktal/mev-beta/pkg/pools"
"github.com/fraktal/mev-beta/pkg/security"
)
// RawL2Transaction represents a raw Arbitrum L2 transaction
type RawL2Transaction struct {
Hash string `json:"hash"`
From string `json:"from"`
To string `json:"to"`
Value string `json:"value"`
Gas string `json:"gas"`
GasPrice string `json:"gasPrice"`
Input string `json:"input"`
Nonce string `json:"nonce"`
TransactionIndex string `json:"transactionIndex"`
Type string `json:"type"`
ChainID string `json:"chainId,omitempty"`
V string `json:"v,omitempty"`
R string `json:"r,omitempty"`
S string `json:"s,omitempty"`
BlockNumber string `json:"blockNumber,omitempty"`
}
// RawL2Block represents a raw Arbitrum L2 block
type RawL2Block struct {
Hash string `json:"hash"`
Number string `json:"number"`
Timestamp string `json:"timestamp"`
Transactions []RawL2Transaction `json:"transactions"`
}
// RawL2BlockWithLogs represents a raw Arbitrum L2 block with logs
type RawL2BlockWithLogs struct {
Hash string `json:"hash"`
Number string `json:"number"`
Timestamp string `json:"timestamp"`
Transactions []RawL2TransactionWithLogs `json:"transactions"`
}
// RawL2TransactionWithLogs includes transaction logs for pool discovery
type RawL2TransactionWithLogs struct {
Hash string `json:"hash"`
From string `json:"from"`
To string `json:"to"`
Value string `json:"value"`
Gas string `json:"gas"`
GasPrice string `json:"gasPrice"`
Input string `json:"input"`
Logs []interface{} `json:"logs"`
TransactionIndex string `json:"transactionIndex"`
Type string `json:"type"`
}
// DEXFunctionSignature represents a DEX function signature
type DEXFunctionSignature struct {
Signature string
Name string
Protocol string
Description string
}
// ArbitrumL2Parser handles parsing of Arbitrum L2 transactions
type ArbitrumL2Parser struct {
client *rpc.Client
logger *logger.Logger
oracle *oracle.PriceOracle
// DEX contract addresses on Arbitrum
dexContracts map[common.Address]string
// DEX function signatures
dexFunctions map[string]DEXFunctionSignature
// Pool discovery system
poolDiscovery *pools.PoolDiscovery
// ABI decoders for sophisticated parameter parsing
uniswapV2ABI abi.ABI
uniswapV3ABI abi.ABI
sushiSwapABI abi.ABI
}
// NewArbitrumL2Parser creates a new Arbitrum L2 transaction parser
func NewArbitrumL2Parser(rpcEndpoint string, logger *logger.Logger, priceOracle *oracle.PriceOracle) (*ArbitrumL2Parser, error) {
client, err := rpc.Dial(rpcEndpoint)
if err != nil {
return nil, fmt.Errorf("failed to connect to Arbitrum RPC: %v", err)
}
parser := &ArbitrumL2Parser{
client: client,
logger: logger,
oracle: priceOracle,
dexContracts: make(map[common.Address]string),
dexFunctions: make(map[string]DEXFunctionSignature),
}
// Initialize DEX contracts and functions
parser.initializeDEXData()
// Initialize ABI decoders for sophisticated parsing
if err := parser.initializeABIs(); err != nil {
logger.Warn(fmt.Sprintf("Failed to initialize ABI decoders: %v", err))
}
// Initialize pool discovery system
parser.poolDiscovery = pools.NewPoolDiscovery(client, logger)
logger.Info(fmt.Sprintf("Pool discovery system initialized - %d pools, %d exchanges loaded",
parser.poolDiscovery.GetPoolCount(), parser.poolDiscovery.GetExchangeCount()))
return parser, nil
}
// initializeDEXData initializes known DEX contracts and function signatures
func (p *ArbitrumL2Parser) initializeDEXData() {
// Official Arbitrum DEX contracts
p.dexContracts[common.HexToAddress("0xf1D7CC64Fb4452F05c498126312eBE29f30Fbcf9")] = "UniswapV2Factory"
p.dexContracts[common.HexToAddress("0x1F98431c8aD98523631AE4a59f267346ea31F984")] = "UniswapV3Factory"
p.dexContracts[common.HexToAddress("0xc35DADB65012eC5796536bD9864eD8773aBc74C4")] = "SushiSwapFactory"
p.dexContracts[common.HexToAddress("0x4752ba5dbc23f44d87826276bf6fd6b1c372ad24")] = "UniswapV2Router02"
p.dexContracts[common.HexToAddress("0xE592427A0AEce92De3Edee1F18E0157C05861564")] = "UniswapV3Router"
p.dexContracts[common.HexToAddress("0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45")] = "UniswapV3Router02"
p.dexContracts[common.HexToAddress("0x1b02dA8Cb0d097eB8D57A175b88c7D8b47997506")] = "SushiSwapRouter"
p.dexContracts[common.HexToAddress("0xC36442b4a4522E871399CD717aBDD847Ab11FE88")] = "UniswapV3PositionManager"
// MISSING HIGH-ACTIVITY DEX CONTRACTS (based on log analysis)
p.dexContracts[common.HexToAddress("0xaa78afc926d0df40458ad7b1f7eed37251bd2b5f")] = "SushiSwapRouter_Arbitrum" // 44 transactions
p.dexContracts[common.HexToAddress("0x87d66368cd08a7ca42252f5ab44b2fb6d1fb8d15")] = "TraderJoeRouter" // 50 transactions
p.dexContracts[common.HexToAddress("0x16e71b13fe6079b4312063f7e81f76d165ad32ad")] = "SushiSwapRouter_V2" // Frequent
p.dexContracts[common.HexToAddress("0xaa277cb7914b7e5514946da92cb9de332ce610ef")] = "RamsesExchange" // Multi calls
p.dexContracts[common.HexToAddress("0xc873fEcbd354f5A56E00E710B90EF4201db2448d")] = "CamelotRouter" // Camelot DEX
p.dexContracts[common.HexToAddress("0x5ffe7FB82894076ECB99A30D6A32e969e6e35E98")] = "CurveAddressProvider" // Curve
p.dexContracts[common.HexToAddress("0xba12222222228d8ba445958a75a0704d566bf2c8")] = "BalancerVault" // Balancer V2
// HIGH-ACTIVITY UNISWAP V3 POOLS (detected from logs)
p.dexContracts[common.HexToAddress("0xC6962004f452bE9203591991D15f6b388e09E8D0")] = "UniswapV3Pool_WETH_USDC" // 381 occurrences
p.dexContracts[common.HexToAddress("0x641C00A822e8b671738d32a431a4Fb6074E5c79d")] = "UniswapV3Pool_WETH_USDT" // 168 occurrences
p.dexContracts[common.HexToAddress("0x2f5e87C9312fa29aed5c179E456625D79015299c")] = "UniswapV3Pool_ARB_ETH" // 169 occurrences
// 1INCH AGGREGATOR (major MEV source)
p.dexContracts[common.HexToAddress("0x1111111254eeb25477b68fb85ed929f73a960582")] = "1InchAggregatorV5"
p.dexContracts[common.HexToAddress("0x1111111254fb6c44bac0bed2854e76f90643097d")] = "1InchAggregatorV4"
// CORRECT DEX function signatures verified for Arbitrum (first 4 bytes of keccak256(function_signature))
// Uniswap V2 swap functions
p.dexFunctions["0x38ed1739"] = DEXFunctionSignature{
Signature: "0x38ed1739",
Name: "swapExactTokensForTokens",
Protocol: "UniswapV2",
Description: "Swap exact tokens for tokens",
}
p.dexFunctions["0x8803dbee"] = DEXFunctionSignature{
Signature: "0x8803dbee",
Name: "swapTokensForExactTokens",
Protocol: "UniswapV2",
Description: "Swap tokens for exact tokens",
}
p.dexFunctions["0x7ff36ab5"] = DEXFunctionSignature{
Signature: "0x7ff36ab5",
Name: "swapExactETHForTokens",
Protocol: "UniswapV2",
Description: "Swap exact ETH for tokens",
}
p.dexFunctions["0x4a25d94a"] = DEXFunctionSignature{
Signature: "0x4a25d94a",
Name: "swapTokensForExactETH",
Protocol: "UniswapV2",
Description: "Swap tokens for exact ETH",
}
p.dexFunctions["0x18cbafe5"] = DEXFunctionSignature{
Signature: "0x18cbafe5",
Name: "swapExactTokensForETH",
Protocol: "UniswapV2",
Description: "Swap exact tokens for ETH",
}
p.dexFunctions["0x791ac947"] = DEXFunctionSignature{
Signature: "0x791ac947",
Name: "swapExactTokensForETHSupportingFeeOnTransferTokens",
Protocol: "UniswapV2",
Description: "Swap exact tokens for ETH supporting fee-on-transfer tokens",
}
p.dexFunctions["0xb6f9de95"] = DEXFunctionSignature{
Signature: "0xb6f9de95",
Name: "swapExactETHForTokensSupportingFeeOnTransferTokens",
Protocol: "UniswapV2",
Description: "Swap exact ETH for tokens supporting fee-on-transfer tokens",
}
p.dexFunctions["0x5c11d795"] = DEXFunctionSignature{
Signature: "0x5c11d795",
Name: "swapExactTokensForTokensSupportingFeeOnTransferTokens",
Protocol: "UniswapV2",
Description: "Swap exact tokens for tokens supporting fee-on-transfer tokens",
}
// Uniswap V2 liquidity functions
p.dexFunctions["0xe8e33700"] = DEXFunctionSignature{
Signature: "0xe8e33700",
Name: "addLiquidity",
Protocol: "UniswapV2",
Description: "Add liquidity to pool",
}
p.dexFunctions["0xf305d719"] = DEXFunctionSignature{
Signature: "0xf305d719",
Name: "addLiquidityETH",
Protocol: "UniswapV2",
Description: "Add liquidity with ETH",
}
p.dexFunctions["0xbaa2abde"] = DEXFunctionSignature{
Signature: "0xbaa2abde",
Name: "removeLiquidity",
Protocol: "UniswapV2",
Description: "Remove liquidity from pool",
}
p.dexFunctions["0x02751cec"] = DEXFunctionSignature{
Signature: "0x02751cec",
Name: "removeLiquidityETH",
Protocol: "UniswapV2",
Description: "Remove liquidity with ETH",
}
// Uniswap V3 swap functions
p.dexFunctions["0x414bf389"] = DEXFunctionSignature{
Signature: "0x414bf389",
Name: "exactInputSingle",
Protocol: "UniswapV3",
Description: "Exact input single swap",
}
p.dexFunctions["0xc04b8d59"] = DEXFunctionSignature{
Signature: "0xc04b8d59",
Name: "exactInput",
Protocol: "UniswapV3",
Description: "Exact input multi-hop swap",
}
p.dexFunctions["0xdb3e2198"] = DEXFunctionSignature{
Signature: "0xdb3e2198",
Name: "exactOutputSingle",
Protocol: "UniswapV3",
Description: "Exact output single swap",
}
p.dexFunctions["0xf28c0498"] = DEXFunctionSignature{
Signature: "0xf28c0498",
Name: "exactOutput",
Protocol: "UniswapV3",
Description: "Exact output multi-hop swap",
}
p.dexFunctions["0xac9650d8"] = DEXFunctionSignature{
Signature: "0xac9650d8",
Name: "multicall",
Protocol: "UniswapV3",
Description: "Batch multiple function calls",
}
// Uniswap V3 position management functions
p.dexFunctions["0x88316456"] = DEXFunctionSignature{
Signature: "0x88316456",
Name: "mint",
Protocol: "UniswapV3",
Description: "Mint new liquidity position",
}
p.dexFunctions["0xfc6f7865"] = DEXFunctionSignature{
Signature: "0xfc6f7865",
Name: "collect",
Protocol: "UniswapV3",
Description: "Collect fees from position",
}
p.dexFunctions["0x219f5d17"] = DEXFunctionSignature{
Signature: "0x219f5d17",
Name: "increaseLiquidity",
Protocol: "UniswapV3",
Description: "Increase liquidity in position",
}
p.dexFunctions["0x0c49ccbe"] = DEXFunctionSignature{
Signature: "0x0c49ccbe",
Name: "decreaseLiquidity",
Protocol: "UniswapV3",
Description: "Decrease liquidity in position",
}
// MISSING CRITICAL FUNCTION SIGNATURES (major MEV sources)
// Multicall functions (used heavily in V3 and aggregators)
p.dexFunctions["0xac9650d8"] = DEXFunctionSignature{
Signature: "0xac9650d8",
Name: "multicall",
Protocol: "Multicall",
Description: "Execute multiple function calls in single transaction",
}
p.dexFunctions["0x5ae401dc"] = DEXFunctionSignature{
Signature: "0x5ae401dc",
Name: "multicall",
Protocol: "MultiV2",
Description: "Multicall with deadline",
}
p.dexFunctions["0x1f0464d1"] = DEXFunctionSignature{
Signature: "0x1f0464d1",
Name: "multicall",
Protocol: "MultiV3",
Description: "Multicall with previous blockhash guard",
}
// 1INCH Aggregator functions (major arbitrage source)
p.dexFunctions["0x7c025200"] = DEXFunctionSignature{
Signature: "0x7c025200",
Name: "swap",
Protocol: "1Inch",
Description: "1inch aggregator swap",
}
p.dexFunctions["0xe449022e"] = DEXFunctionSignature{
Signature: "0xe449022e",
Name: "uniswapV3Swap",
Protocol: "1Inch",
Description: "1inch uniswap v3 swap",
}
p.dexFunctions["0x12aa3caf"] = DEXFunctionSignature{
Signature: "0x12aa3caf",
Name: "ethUnoswap",
Protocol: "1Inch",
Description: "1inch ETH unoswap",
}
p.dexFunctions["0x0502b1c5"] = DEXFunctionSignature{
Signature: "0x0502b1c5",
Name: "swapMulti",
Protocol: "1Inch",
Description: "1inch multi-hop swap",
}
p.dexFunctions["0x2e95b6c8"] = DEXFunctionSignature{
Signature: "0x2e95b6c8",
Name: "unoswapTo",
Protocol: "1Inch",
Description: "1inch unoswap to recipient",
}
p.dexFunctions["0xbabe3335"] = DEXFunctionSignature{
Signature: "0xbabe3335",
Name: "clipperSwap",
Protocol: "1Inch",
Description: "1inch clipper swap",
}
// Balancer V2 functions
p.dexFunctions["0x52bbbe29"] = DEXFunctionSignature{
Signature: "0x52bbbe29",
Name: "swap",
Protocol: "BalancerV2",
Description: "Balancer V2 single swap",
}
p.dexFunctions["0x945bcec9"] = DEXFunctionSignature{
Signature: "0x945bcec9",
Name: "batchSwap",
Protocol: "BalancerV2",
Description: "Balancer V2 batch swap",
}
// Curve functions
p.dexFunctions["0x3df02124"] = DEXFunctionSignature{
Signature: "0x3df02124",
Name: "exchange",
Protocol: "Curve",
Description: "Curve token exchange",
}
p.dexFunctions["0xa6417ed6"] = DEXFunctionSignature{
Signature: "0xa6417ed6",
Name: "exchange_underlying",
Protocol: "Curve",
Description: "Curve exchange underlying tokens",
}
// SushiSwap specific functions
p.dexFunctions["0x02751cec"] = DEXFunctionSignature{
Signature: "0x02751cec",
Name: "removeLiquidityETH",
Protocol: "SushiSwap",
Description: "Remove liquidity with ETH",
}
// TraderJoe functions
p.dexFunctions["0x18cbafe5"] = DEXFunctionSignature{
Signature: "0x18cbafe5",
Name: "swapExactTokensForETH",
Protocol: "TraderJoe",
Description: "TraderJoe exact tokens for ETH",
}
// Universal Router functions (Uniswap's new router)
p.dexFunctions["0x3593564c"] = DEXFunctionSignature{
Signature: "0x3593564c",
Name: "execute",
Protocol: "UniversalRouter",
Description: "Universal router execute",
}
// Generic DEX functions that appear frequently
p.dexFunctions["0x022c0d9f"] = DEXFunctionSignature{
Signature: "0x022c0d9f",
Name: "swap",
Protocol: "Generic",
Description: "Generic swap function",
}
p.dexFunctions["0x128acb08"] = DEXFunctionSignature{
Signature: "0x128acb08",
Name: "swapTokensForTokens",
Protocol: "Generic",
Description: "Generic token to token swap",
}
}
// AddDiscoveredPoolsToDEXContracts adds discovered pools to the DEX contracts map for swap detection
// This is CRITICAL for detecting swaps on discovered pools, not just hardcoded high-activity pools
func (p *ArbitrumL2Parser) AddDiscoveredPoolsToDEXContracts() {
if p.poolDiscovery == nil {
p.logger.Warn("Pool discovery not initialized, cannot add discovered pools to DEX contracts")
return
}
allPools := p.poolDiscovery.GetAllPools()
addedCount := 0
for _, pool := range allPools {
poolAddr := common.HexToAddress(pool.Address)
// Create descriptive name for the pool contract using token address prefixes
token0Prefix := pool.Token0
token1Prefix := pool.Token1
if len(token0Prefix) > 10 {
token0Prefix = token0Prefix[:10] // Use first 10 chars of address
}
if len(token1Prefix) > 10 {
token1Prefix = token1Prefix[:10]
}
contractName := fmt.Sprintf("%s_Pool_%s_%s", pool.Protocol, token0Prefix, token1Prefix)
// Only add if not already in the map (preserve hardcoded high-activity pools)
if _, exists := p.dexContracts[poolAddr]; !exists {
p.dexContracts[poolAddr] = contractName
addedCount++
}
}
p.logger.Info(fmt.Sprintf("✅ Added %d discovered pools to DEX contract filter (total: %d DEX contracts monitored)",
addedCount, len(p.dexContracts)))
}
// GetBlockByNumber fetches a block with full transaction details using raw RPC
func (p *ArbitrumL2Parser) GetBlockByNumber(ctx context.Context, blockNumber uint64) (*RawL2Block, error) {
var block RawL2Block
blockNumHex := fmt.Sprintf("0x%x", blockNumber)
err := p.client.CallContext(ctx, &block, "eth_getBlockByNumber", blockNumHex, true)
if err != nil {
return nil, fmt.Errorf("failed to get block %d: %v", blockNumber, err)
}
p.logger.Debug(fmt.Sprintf("Retrieved L2 block %d with %d transactions", blockNumber, len(block.Transactions)))
return &block, nil
}
// ParseDEXTransactions analyzes transactions in a block for DEX interactions
func (p *ArbitrumL2Parser) ParseDEXTransactions(ctx context.Context, block *RawL2Block) []DEXTransaction {
var dexTransactions []DEXTransaction
for _, tx := range block.Transactions {
if dexTx := p.parseDEXTransaction(tx); dexTx != nil {
if tx.BlockNumber != "" {
dexTx.BlockNumber = tx.BlockNumber
} else if block.Number != "" {
dexTx.BlockNumber = block.Number
}
dexTransactions = append(dexTransactions, *dexTx)
}
}
if len(dexTransactions) > 0 {
p.logger.Info(fmt.Sprintf("Block %s: Found %d DEX transactions", block.Number, len(dexTransactions)))
}
return dexTransactions
}
// ParseDEXTransaction analyzes a single raw transaction for DEX interaction details.
func (p *ArbitrumL2Parser) ParseDEXTransaction(tx RawL2Transaction) (*DEXTransaction, error) {
dexTx := p.parseDEXTransaction(tx)
if dexTx == nil {
return nil, fmt.Errorf("transaction %s is not a recognized DEX interaction", tx.Hash)
}
if tx.BlockNumber != "" {
dexTx.BlockNumber = tx.BlockNumber
}
return dexTx, nil
}
// SwapDetails contains detailed information about a DEX swap
type SwapDetails struct {
AmountIn *big.Int
AmountOut *big.Int
AmountMin *big.Int
TokenIn string
TokenOut string
TokenInAddress common.Address
TokenOutAddress common.Address
Fee uint32
Deadline uint64
Recipient string
IsValid bool
}
// DEXTransaction represents a parsed DEX transaction
type DEXTransaction struct {
Hash string
From string
To string
Value *big.Int
FunctionSig string
FunctionName string
Protocol string
InputData []byte
ContractName string
BlockNumber string
SwapDetails *SwapDetails // Detailed swap information
}
// parseDEXTransaction checks if a transaction is a DEX interaction
func (p *ArbitrumL2Parser) parseDEXTransaction(tx RawL2Transaction) *DEXTransaction {
// Skip transactions without recipient (contract creation)
if tx.To == "" || tx.To == "0x" {
return nil
}
// Skip transactions without input data
if tx.Input == "" || tx.Input == "0x" || len(tx.Input) < 10 {
return nil
}
toAddr := common.HexToAddress(tx.To)
// Check if transaction is to a known DEX contract
contractName, isDEXContract := p.dexContracts[toAddr]
// Extract function signature (first 4 bytes of input data)
functionSig := tx.Input[:10] // "0x" + 8 hex chars = 10 chars
// Check if function signature matches known DEX functions
if funcInfo, isDEXFunction := p.dexFunctions[functionSig]; isDEXFunction {
// Parse value
value := big.NewInt(0)
if tx.Value != "" && tx.Value != "0x" && tx.Value != "0x0" {
value.SetString(strings.TrimPrefix(tx.Value, "0x"), 16)
}
// Parse input data
inputData, err := hex.DecodeString(strings.TrimPrefix(tx.Input, "0x"))
if err != nil {
p.logger.Debug(fmt.Sprintf("Failed to decode input data for transaction %s: %v", tx.Hash, err))
inputData = []byte{}
}
// Decode function parameters based on function type
swapDetails := p.decodeFunctionDataStructured(funcInfo, inputData)
// Log basic transaction detection (opportunities logged later with actual amounts from events)
if swapDetails != nil && swapDetails.IsValid {
p.logger.Info(fmt.Sprintf("DEX Transaction detected: %s -> %s (%s) calling %s (%s) - TokenIn: %s, TokenOut: %s",
tx.From, tx.To, contractName, funcInfo.Name, funcInfo.Protocol, swapDetails.TokenIn, swapDetails.TokenOut))
} else {
// Fallback to simple logging
swapDetailsStr := p.decodeFunctionData(funcInfo, inputData)
p.logger.Info(fmt.Sprintf("DEX Transaction detected: %s -> %s (%s) calling %s (%s), Value: %s ETH%s",
tx.From, tx.To, contractName, funcInfo.Name, funcInfo.Protocol,
new(big.Float).Quo(new(big.Float).SetInt(value), big.NewFloat(1e18)).String(), swapDetailsStr))
}
// CRITICAL FIX: Only include SwapDetails if valid, otherwise set to nil
// This prevents zero address corruption from invalid swap details
var validSwapDetails *SwapDetails
if swapDetails != nil && swapDetails.IsValid {
// EDGE CASE DETECTION: Check if IsValid=true but tokens are still zero
zeroAddr := common.Address{}
if swapDetails.TokenInAddress == zeroAddr && swapDetails.TokenOutAddress == zeroAddr {
inputPreview := ""
if len(inputData) > 64 {
inputPreview = fmt.Sprintf("0x%x...", inputData[:64])
} else {
inputPreview = fmt.Sprintf("0x%x", inputData)
}
p.logger.Warn(fmt.Sprintf("🔍 EDGE CASE DETECTED: SwapDetails marked IsValid=true but has zero addresses! TxHash: %s, Function: %s (%s), Protocol: %s, InputData: %s",
tx.Hash, funcInfo.Name, functionSig, funcInfo.Protocol, inputPreview))
// Don't include this SwapDetails - it's corrupted despite IsValid flag
validSwapDetails = nil
} else {
validSwapDetails = swapDetails
}
}
return &DEXTransaction{
Hash: tx.Hash,
From: tx.From,
To: tx.To,
Value: value,
FunctionSig: functionSig,
FunctionName: funcInfo.Name,
Protocol: funcInfo.Protocol,
InputData: inputData,
ContractName: contractName,
BlockNumber: "", // Will be set by caller
SwapDetails: validSwapDetails,
}
}
// Check if it's to a known DEX contract but unknown function
if isDEXContract {
p.logger.Debug(fmt.Sprintf("Unknown DEX function call: %s -> %s (%s), Function: %s",
tx.From, tx.To, contractName, functionSig))
}
return nil
}
// decodeFunctionData extracts parameters from transaction input data
func (p *ArbitrumL2Parser) decodeFunctionData(funcInfo DEXFunctionSignature, inputData []byte) string {
if len(inputData) < 4 {
return ""
}
// Skip the 4-byte function selector
params := inputData[4:]
switch funcInfo.Name {
case "swapExactTokensForTokens":
return p.decodeSwapExactTokensForTokens(params)
case "swapTokensForExactTokens":
return p.decodeSwapTokensForExactTokens(params)
case "swapExactETHForTokens":
return p.decodeSwapExactETHForTokens(params)
case "swapExactTokensForETH":
return p.decodeSwapExactTokensForETH(params)
case "exactInputSingle":
return p.decodeExactInputSingle(params)
case "exactInput":
return p.decodeExactInput(params)
case "exactOutputSingle":
return p.decodeExactOutputSingle(params)
case "multicall":
return p.decodeMulticall(params)
default:
return fmt.Sprintf(", Raw input: %d bytes", len(inputData))
}
}
// decodeSwapExactTokensForTokens decodes UniswapV2 swapExactTokensForTokens parameters
// function swapExactTokensForTokens(uint amountIn, uint amountOutMin, address[] calldata path, address to, uint deadline)
func (p *ArbitrumL2Parser) decodeSwapExactTokensForTokens(params []byte) string {
if len(params) < 160 { // 5 parameters * 32 bytes each
return ", Invalid parameters"
}
// Use sophisticated ABI decoding instead of basic byte slicing
fullInputData := append([]byte{0x38, 0xed, 0x17, 0x39}, params...) // Add function selector
decoded, err := p.decodeWithABI("UniswapV2", "swapExactTokensForTokens", fullInputData)
if err != nil {
// Fallback to basic decoding
if len(params) >= 64 {
amountIn := new(big.Int).SetBytes(params[0:32])
amountOutMin := new(big.Int).SetBytes(params[32:64])
amountInEth := new(big.Float).Quo(new(big.Float).SetInt(amountIn), big.NewFloat(1e18))
amountOutMinEth := new(big.Float).Quo(new(big.Float).SetInt(amountOutMin), big.NewFloat(1e18))
return fmt.Sprintf(", AmountIn: %s tokens, MinOut: %s tokens (fallback)",
amountInEth.Text('f', 6), amountOutMinEth.Text('f', 6))
}
return ", Invalid parameters"
}
// Extract values from ABI decoded parameters
amountIn, ok1 := decoded["amountIn"].(*big.Int)
amountOutMin, ok2 := decoded["amountOutMin"].(*big.Int)
path, ok3 := decoded["path"].([]common.Address)
deadline, ok4 := decoded["deadline"].(*big.Int)
if !ok1 || !ok2 || !ok3 || !ok4 {
return ", Failed to decode parameters"
}
// Convert to readable format with enhanced details
amountInEth := new(big.Float).Quo(new(big.Float).SetInt(amountIn), big.NewFloat(1e18))
amountOutMinEth := new(big.Float).Quo(new(big.Float).SetInt(amountOutMin), big.NewFloat(1e18))
// Extract token addresses from path
tokenIn := "0x0000000000000000000000000000000000000000"
tokenOut := "0x0000000000000000000000000000000000000000"
if len(path) >= 2 {
tokenIn = path[0].Hex()
tokenOut = path[len(path)-1].Hex()
}
return fmt.Sprintf(", AmountIn: %s (%s), MinOut: %s (%s), Hops: %d, Deadline: %s",
amountInEth.Text('f', 6), tokenIn,
amountOutMinEth.Text('f', 6), tokenOut,
len(path)-1,
time.Unix(deadline.Int64(), 0).Format("15:04:05"))
}
// decodeSwapTokensForExactTokens decodes UniswapV2 swapTokensForExactTokens parameters
func (p *ArbitrumL2Parser) decodeSwapTokensForExactTokens(params []byte) string {
if len(params) < 160 {
return ", Invalid parameters"
}
amountOut := new(big.Int).SetBytes(params[0:32])
amountInMax := new(big.Int).SetBytes(params[32:64])
amountOutEth := new(big.Float).Quo(new(big.Float).SetInt(amountOut), big.NewFloat(1e18))
amountInMaxEth := new(big.Float).Quo(new(big.Float).SetInt(amountInMax), big.NewFloat(1e18))
return fmt.Sprintf(", AmountOut: %s tokens, MaxIn: %s tokens",
amountOutEth.Text('f', 6), amountInMaxEth.Text('f', 6))
}
// decodeSwapExactETHForTokens decodes UniswapV2 swapExactETHForTokens parameters
func (p *ArbitrumL2Parser) decodeSwapExactETHForTokens(params []byte) string {
if len(params) < 32 {
return ", Invalid parameters"
}
amountOutMin := new(big.Int).SetBytes(params[0:32])
amountOutMinEth := new(big.Float).Quo(new(big.Float).SetInt(amountOutMin), big.NewFloat(1e18))
return fmt.Sprintf(", MinOut: %s tokens", amountOutMinEth.Text('f', 6))
}
// decodeSwapExactTokensForETH decodes UniswapV2 swapExactTokensForETH parameters
func (p *ArbitrumL2Parser) decodeSwapExactTokensForETH(params []byte) string {
if len(params) < 64 {
return ", Invalid parameters"
}
amountIn := new(big.Int).SetBytes(params[0:32])
amountOutMin := new(big.Int).SetBytes(params[32:64])
amountInEth := new(big.Float).Quo(new(big.Float).SetInt(amountIn), big.NewFloat(1e18))
amountOutMinEth := new(big.Float).Quo(new(big.Float).SetInt(amountOutMin), big.NewFloat(1e18))
return fmt.Sprintf(", AmountIn: %s tokens, MinETH: %s",
amountInEth.Text('f', 6), amountOutMinEth.Text('f', 6))
}
// decodeExactInputSingle decodes UniswapV3 exactInputSingle parameters
func (p *ArbitrumL2Parser) decodeExactInputSingle(params []byte) string {
if len(params) < 160 { // ExactInputSingleParams struct
return ", Invalid parameters"
}
// Simplified decoding - real implementation would parse the struct properly
amountIn := new(big.Int).SetBytes(params[128:160]) // approximation
amountInEth := new(big.Float).Quo(new(big.Float).SetInt(amountIn), big.NewFloat(1e18))
return fmt.Sprintf(", AmountIn: %s tokens", amountInEth.Text('f', 6))
}
// decodeExactInput decodes UniswapV3 exactInput parameters
func (p *ArbitrumL2Parser) decodeExactInput(params []byte) string {
if len(params) < 128 {
return ", Invalid parameters"
}
amountIn := new(big.Int).SetBytes(params[64:96]) // approximation
amountInEth := new(big.Float).Quo(new(big.Float).SetInt(amountIn), big.NewFloat(1e18))
return fmt.Sprintf(", AmountIn: %s tokens (multi-hop)", amountInEth.Text('f', 6))
}
// decodeExactOutputSingle decodes UniswapV3 exactOutputSingle parameters
func (p *ArbitrumL2Parser) decodeExactOutputSingle(params []byte) string {
if len(params) < 160 {
return ", Invalid parameters"
}
amountOut := new(big.Int).SetBytes(params[160:192]) // approximation
amountOutEth := new(big.Float).Quo(new(big.Float).SetInt(amountOut), big.NewFloat(1e18))
return fmt.Sprintf(", AmountOut: %s tokens", amountOutEth.Text('f', 6))
}
// decodeMulticall decodes UniswapV3 multicall parameters
func (p *ArbitrumL2Parser) decodeMulticall(params []byte) string {
if len(params) < 32 {
return ", Invalid parameters"
}
// Multicall contains an array of encoded function calls
// This is complex to decode without full ABI parsing
return fmt.Sprintf(", Multicall with %d bytes of data", len(params))
}
// decodeFunctionDataStructured extracts structured parameters from transaction input data
func (p *ArbitrumL2Parser) decodeFunctionDataStructured(funcInfo DEXFunctionSignature, inputData []byte) *SwapDetails {
if len(inputData) < 4 {
return &SwapDetails{IsValid: false}
}
// Skip the 4-byte function selector
params := inputData[4:]
switch funcInfo.Name {
case "swapExactTokensForTokens":
return p.decodeSwapExactTokensForTokensStructured(params)
case "swapTokensForExactTokens":
return p.decodeSwapTokensForExactTokensStructured(params)
case "swapExactETHForTokens":
return p.decodeSwapExactETHForTokensStructured(params)
case "swapExactTokensForETH":
return p.decodeSwapExactTokensForETHStructured(params)
case "exactInputSingle":
return p.decodeExactInputSingleStructured(params)
case "exactInput":
return p.decodeExactInputStructured(params)
case "exactOutputSingle":
return p.decodeExactOutputSingleStructured(params)
case "multicall":
return p.decodeMulticallStructured(params)
default:
return &SwapDetails{IsValid: false}
}
}
// decodeSwapExactTokensForTokensStructured decodes UniswapV2 swapExactTokensForTokens parameters
func (p *ArbitrumL2Parser) decodeSwapExactTokensForTokensStructured(params []byte) *SwapDetails {
if len(params) < 160 { // 5 parameters * 32 bytes each
return &SwapDetails{IsValid: false}
}
// Extract amounts directly
amountIn := new(big.Int).SetBytes(params[0:32])
amountMin := new(big.Int).SetBytes(params[32:64])
// CRITICAL FIX: Use the working extraction method instead of broken inline extraction
// Build full calldata with function signature
fullCalldata := make([]byte, len(params)+4)
// swapExactTokensForTokens signature: 0x38ed1739
fullCalldata[0] = 0x38
fullCalldata[1] = 0xed
fullCalldata[2] = 0x17
fullCalldata[3] = 0x39
copy(fullCalldata[4:], params)
tokenInAddr, tokenOutAddr, err := p.ExtractTokensFromCalldata(fullCalldata)
var (
tokenIn string
tokenOut string
)
if err == nil && tokenInAddr != (common.Address{}) && tokenOutAddr != (common.Address{}) {
tokenIn = p.resolveTokenSymbol(tokenInAddr.Hex())
tokenOut = p.resolveTokenSymbol(tokenOutAddr.Hex())
} else {
// Enhanced error logging for parsing failures
if err != nil {
p.logger.Debug(fmt.Sprintf("Token extraction failed in swapExactTokensForTokens: %v, input length: %d", err, len(params)))
}
if tokenInAddr == (common.Address{}) || tokenOutAddr == (common.Address{}) {
p.logger.Debug(fmt.Sprintf("Zero address detected in swapExactTokensForTokens: tokenIn=%s, tokenOut=%s", tokenInAddr.Hex(), tokenOutAddr.Hex()))
}
// Fallback to zero addresses if extraction fails
tokenIn = "0x0000000000000000000000000000000000000000"
tokenOut = "0x0000000000000000000000000000000000000000"
}
// CRITICAL FIX: Validate token addresses before marking as valid
zeroAddr := common.Address{}
if tokenInAddr == zeroAddr || tokenOutAddr == zeroAddr {
p.logger.Debug(fmt.Sprintf("Rejecting swapExactTokensForTokens due to zero address: tokenIn=%s, tokenOut=%s", tokenInAddr.Hex(), tokenOutAddr.Hex()))
return &SwapDetails{IsValid: false}
}
return &SwapDetails{
AmountIn: amountIn,
AmountOut: amountMin, // For UniswapV2, this is actually AmountMin but we display it as expected output
AmountMin: amountMin,
TokenIn: tokenIn,
TokenOut: tokenOut,
TokenInAddress: tokenInAddr,
TokenOutAddress: tokenOutAddr,
Deadline: new(big.Int).SetBytes(params[128:160]).Uint64(),
Recipient: fmt.Sprintf("0x%x", params[96:128]), // address is last 20 bytes
IsValid: true,
}
}
// decodeSwapExactTokensForETHStructured decodes UniswapV2 swapExactTokensForETH parameters
func (p *ArbitrumL2Parser) decodeSwapExactTokensForETHStructured(params []byte) *SwapDetails {
if len(params) < 64 {
return &SwapDetails{IsValid: false}
}
// CRITICAL FIX: Use the working extraction method to get actual token addresses
// Get signature from dexFunctions map instead of hardcoding
funcSig := p.dexFunctions["0x18cbafe5"] // swapExactTokensForETH
fullCalldata, createErr := p.createCalldataWithSignature(funcSig.Signature, params)
if createErr != nil {
p.logger.Debug(fmt.Sprintf("Failed to create calldata for %s: %v", funcSig.Name, createErr))
return &SwapDetails{IsValid: false}
}
tokenInAddr, tokenOutAddr, err := p.ExtractTokensFromCalldata(fullCalldata)
var (
tokenIn string
tokenOut string
)
if err == nil && tokenInAddr != (common.Address{}) {
tokenIn = p.resolveTokenSymbol(tokenInAddr.Hex())
tokenOut = "ETH"
} else {
// Failed to extract tokens - log for debugging
p.logger.Debug(fmt.Sprintf("Token extraction failed in swapExactTokensForETH: %v, input length: %d", err, len(params)))
tokenIn = "0x0000000000000000000000000000000000000000"
tokenOut = "ETH"
}
// CRITICAL FIX: Validate token addresses before marking as valid
// For swapping tokens to ETH, tokenInAddr must be valid (tokenOutAddr can be zero for native ETH)
zeroAddr := common.Address{}
if tokenInAddr == zeroAddr {
p.logger.Debug(fmt.Sprintf("Rejecting swapExactTokensForETH due to zero tokenIn address: tokenIn=%s, err=%v",
tokenInAddr.Hex(), err))
return &SwapDetails{IsValid: false}
}
return &SwapDetails{
AmountIn: new(big.Int).SetBytes(params[0:32]),
AmountOut: new(big.Int).SetBytes(params[32:64]), // For UniswapV2, this is actually AmountMin but we display it as expected output
AmountMin: new(big.Int).SetBytes(params[32:64]),
TokenIn: tokenIn,
TokenOut: tokenOut,
TokenInAddress: tokenInAddr,
TokenOutAddress: tokenOutAddr, // Will be zero for native ETH (expected)
IsValid: true,
}
}
// decodeExactInputSingleStructured decodes UniswapV3 exactInputSingle parameters
func (p *ArbitrumL2Parser) decodeExactInputSingleStructured(params []byte) *SwapDetails {
if len(params) < 160 { // ExactInputSingleParams struct
return &SwapDetails{IsValid: false}
}
// ExactInputSingleParams structure:
// struct ExactInputSingleParams {
// address tokenIn; // offset 0, 32 bytes
// address tokenOut; // offset 32, 32 bytes
// uint24 fee; // offset 64, 32 bytes (padded)
// address recipient; // offset 96, 32 bytes
// uint256 deadline; // offset 128, 32 bytes
// uint256 amountIn; // offset 160, 32 bytes
// uint256 amountOutMinimum; // offset 192, 32 bytes
// uint160 sqrtPriceLimitX96; // offset 224, 32 bytes
// }
// Properly extract token addresses (last 20 bytes of each 32-byte slot)
tokenInAddr := common.BytesToAddress(params[12:32]) // Skip first 12 bytes, take last 20
tokenOutAddr := common.BytesToAddress(params[44:64]) // Skip first 12 bytes, take last 20
recipient := common.BytesToAddress(params[108:128])
// Extract amounts and other values
rawFee := new(big.Int).SetBytes(params[64:96]).Uint64()
fee, err := security.SafeUint64ToUint32(rawFee)
if err != nil {
p.logger.Error("Fee value exceeds uint32 maximum", "rawFee", rawFee, "error", err)
return &SwapDetails{IsValid: false} // Return invalid if fee is invalid
}
deadline := new(big.Int).SetBytes(params[128:160]).Uint64()
amountIn := new(big.Int).SetBytes(params[160:192])
amountOutMin := new(big.Int).SetBytes(params[192:224])
// CRITICAL FIX: Validate token addresses before marking as valid
zeroAddr := common.Address{}
if tokenInAddr == zeroAddr || tokenOutAddr == zeroAddr {
p.logger.Debug(fmt.Sprintf("Rejecting exactInputSingle due to zero address: tokenIn=%s, tokenOut=%s, amountIn=%s, fee=%d",
tokenInAddr.Hex(), tokenOutAddr.Hex(), amountIn.String(), fee))
return &SwapDetails{IsValid: false}
}
return &SwapDetails{
AmountIn: amountIn,
AmountOut: amountOutMin, // For exactInputSingle, we display amountOutMinimum as expected output
AmountMin: amountOutMin,
TokenIn: p.resolveTokenSymbol(tokenInAddr.Hex()),
TokenOut: p.resolveTokenSymbol(tokenOutAddr.Hex()),
TokenInAddress: tokenInAddr,
TokenOutAddress: tokenOutAddr,
Fee: fee,
Deadline: deadline,
Recipient: recipient.Hex(),
IsValid: true,
}
}
// decodeSwapTokensForExactTokensStructured decodes UniswapV2 swapTokensForExactTokens parameters
func (p *ArbitrumL2Parser) decodeSwapTokensForExactTokensStructured(params []byte) *SwapDetails {
if len(params) < 160 {
return &SwapDetails{IsValid: false}
}
// CRITICAL FIX: Use the working extraction method
fullCalldata := make([]byte, len(params)+4)
// swapTokensForExactTokens signature: 0x8803dbee
fullCalldata[0] = 0x88
fullCalldata[1] = 0x03
fullCalldata[2] = 0xdb
fullCalldata[3] = 0xee
copy(fullCalldata[4:], params)
tokenInAddr, tokenOutAddr, err := p.ExtractTokensFromCalldata(fullCalldata)
var (
tokenIn string
tokenOut string
)
if err == nil && tokenInAddr != (common.Address{}) && tokenOutAddr != (common.Address{}) {
tokenIn = p.resolveTokenSymbol(tokenInAddr.Hex())
tokenOut = p.resolveTokenSymbol(tokenOutAddr.Hex())
} else {
tokenIn = "0x0000000000000000000000000000000000000000"
tokenOut = "0x0000000000000000000000000000000000000000"
}
// CRITICAL FIX: Validate token addresses before marking as valid
zeroAddr := common.Address{}
if tokenInAddr == zeroAddr || tokenOutAddr == zeroAddr {
p.logger.Debug(fmt.Sprintf("Rejecting swapTokensForExactTokens due to zero address: tokenIn=%s, tokenOut=%s, err=%v",
tokenInAddr.Hex(), tokenOutAddr.Hex(), err))
return &SwapDetails{IsValid: false}
}
return &SwapDetails{
AmountOut: new(big.Int).SetBytes(params[0:32]),
AmountIn: new(big.Int).SetBytes(params[32:64]), // Max amount in
TokenIn: tokenIn,
TokenOut: tokenOut,
TokenInAddress: tokenInAddr,
TokenOutAddress: tokenOutAddr,
IsValid: true,
}
}
// decodeSwapExactETHForTokensStructured decodes UniswapV2 swapExactETHForTokens parameters
func (p *ArbitrumL2Parser) decodeSwapExactETHForTokensStructured(params []byte) *SwapDetails {
if len(params) < 32 {
return &SwapDetails{IsValid: false}
}
// CRITICAL FIX: Use the working extraction method
fullCalldata := make([]byte, len(params)+4)
// swapExactETHForTokens signature: 0x7ff36ab5
fullCalldata[0] = 0x7f
fullCalldata[1] = 0xf3
fullCalldata[2] = 0x6a
fullCalldata[3] = 0xb5
copy(fullCalldata[4:], params)
tokenInAddr, tokenOutAddr, err := p.ExtractTokensFromCalldata(fullCalldata)
var (
tokenIn string
tokenOut string
)
if err == nil && tokenOutAddr != (common.Address{}) {
tokenIn = "ETH"
tokenOut = p.resolveTokenSymbol(tokenOutAddr.Hex())
} else {
tokenIn = "ETH"
tokenOut = "0x0000000000000000000000000000000000000000"
}
// CRITICAL FIX: Validate token addresses before marking as valid
// For ETH swaps, tokenOutAddr must be valid (tokenInAddr can be zero for native ETH)
zeroAddr := common.Address{}
if tokenOutAddr == zeroAddr {
p.logger.Debug(fmt.Sprintf("Rejecting swapExactETHForTokens due to zero tokenOut address: tokenOut=%s, err=%v",
tokenOutAddr.Hex(), err))
return &SwapDetails{IsValid: false}
}
return &SwapDetails{
AmountMin: new(big.Int).SetBytes(params[0:32]),
TokenIn: tokenIn,
TokenOut: tokenOut,
TokenInAddress: tokenInAddr, // Will be WETH
TokenOutAddress: tokenOutAddr,
IsValid: true,
}
}
// decodeExactInputStructured decodes UniswapV3 exactInput parameters
func (p *ArbitrumL2Parser) decodeExactInputStructured(params []byte) *SwapDetails {
if len(params) < 128 {
return &SwapDetails{IsValid: false}
}
// ExactInputParams struct:
// struct ExactInputParams {
// bytes path; // offset 0: pointer to path data
// address recipient; // offset 32: 32 bytes
// uint256 deadline; // offset 64: 32 bytes
// uint256 amountIn; // offset 96: 32 bytes
// uint256 amountOutMinimum; // offset 128: 32 bytes
// }
recipient := common.BytesToAddress(params[44:64]) // Skip padding, take last 20 bytes
deadline := new(big.Int).SetBytes(params[64:96]).Uint64()
amountIn := new(big.Int).SetBytes(params[96:128])
var amountOutMin *big.Int
if len(params) >= 160 {
amountOutMin = new(big.Int).SetBytes(params[128:160])
} else {
amountOutMin = big.NewInt(0)
}
// CRITICAL FIX: Extract actual token addresses from calldata
// Get signature from dexFunctions map instead of hardcoding
funcSig := p.dexFunctions["0xc04b8d59"] // exactInput
fullCalldata, createErr := p.createCalldataWithSignature(funcSig.Signature, params)
if createErr != nil {
p.logger.Debug(fmt.Sprintf("Failed to create calldata for %s: %v", funcSig.Name, createErr))
return &SwapDetails{IsValid: false}
}
tokenInAddr, tokenOutAddr, err := p.ExtractTokensFromCalldata(fullCalldata)
var (
tokenIn string
tokenOut string
)
if err == nil && tokenInAddr != (common.Address{}) && tokenOutAddr != (common.Address{}) {
tokenIn = p.resolveTokenSymbol(tokenInAddr.Hex())
tokenOut = p.resolveTokenSymbol(tokenOutAddr.Hex())
} else {
// Failed to extract tokens - log for debugging
p.logger.Debug(fmt.Sprintf("Token extraction failed in exactInput: %v, input length: %d", err, len(params)))
tokenIn = "0x0000000000000000000000000000000000000000"
tokenOut = "0x0000000000000000000000000000000000000000"
}
// CRITICAL FIX: Validate token addresses before marking as valid
// For exactInput, both tokenIn and tokenOut must be valid
zeroAddr := common.Address{}
if tokenInAddr == zeroAddr || tokenOutAddr == zeroAddr {
p.logger.Debug(fmt.Sprintf("Rejecting exactInput due to zero addresses: tokenIn=%s, tokenOut=%s, err=%v",
tokenInAddr.Hex(), tokenOutAddr.Hex(), err))
return &SwapDetails{IsValid: false}
}
return &SwapDetails{
AmountIn: amountIn,
AmountOut: amountOutMin, // For exactInput, we display amountOutMinimum as expected output
AmountMin: amountOutMin,
TokenIn: tokenIn,
TokenOut: tokenOut,
TokenInAddress: tokenInAddr,
TokenOutAddress: tokenOutAddr,
Deadline: deadline,
Recipient: recipient.Hex(),
IsValid: true,
}
}
// decodeExactOutputSingleStructured decodes UniswapV3 exactOutputSingle parameters
func (p *ArbitrumL2Parser) decodeExactOutputSingleStructured(params []byte) *SwapDetails {
if len(params) < 160 {
return &SwapDetails{IsValid: false}
}
tokenInAddr := common.BytesToAddress(params[12:32])
tokenOutAddr := common.BytesToAddress(params[44:64])
// CRITICAL FIX: Validate token addresses before marking as valid
zeroAddr := common.Address{}
if tokenInAddr == zeroAddr || tokenOutAddr == zeroAddr {
p.logger.Debug(fmt.Sprintf("Rejecting exactOutputSingle due to zero address: tokenIn=%s, tokenOut=%s, params length=%d",
tokenInAddr.Hex(), tokenOutAddr.Hex(), len(params)))
return &SwapDetails{IsValid: false}
}
return &SwapDetails{
AmountOut: new(big.Int).SetBytes(params[160:192]),
TokenIn: p.resolveTokenSymbol(tokenInAddr.Hex()),
TokenOut: p.resolveTokenSymbol(tokenOutAddr.Hex()),
TokenInAddress: tokenInAddr,
TokenOutAddress: tokenOutAddr,
IsValid: true,
}
}
// decodeMulticallStructured decodes UniswapV3 multicall parameters
func (p *ArbitrumL2Parser) decodeMulticallStructured(params []byte) *SwapDetails {
if len(params) < 64 {
return &SwapDetails{IsValid: false}
}
// Multicall contains an array of encoded function calls
// First 32 bytes is the offset to the array
// Next 32 bytes is the array length
if len(params) < 64 {
return &SwapDetails{IsValid: false}
}
// Get array length (skip offset, get length)
if len(params) < 64+32 {
return &SwapDetails{IsValid: false}
}
arrayLength := new(big.Int).SetBytes(params[32:64])
// Validate array length
if arrayLength.Sign() <= 0 || arrayLength.Cmp(big.NewInt(100)) > 0 {
return &SwapDetails{IsValid: false}
}
// Enhanced multicall parsing to handle various router patterns
if arrayLength.Cmp(big.NewInt(0)) > 0 && len(params) > 96 {
// Try to extract tokens from any function call in the multicall
token0, token1 := p.extractTokensFromMulticallData(params)
if token0 != "" && token1 != "" {
token0Addr := common.HexToAddress(token0)
token1Addr := common.HexToAddress(token1)
// CRITICAL FIX: Validate token addresses before marking as valid
zeroAddr := common.Address{}
if token0Addr == zeroAddr || token1Addr == zeroAddr {
p.logger.Debug(fmt.Sprintf("Rejecting multicall due to zero address in extracted tokens: token0=%s, token1=%s, arrayLength=%d",
token0Addr.Hex(), token1Addr.Hex(), arrayLength))
return &SwapDetails{IsValid: false}
}
return &SwapDetails{
TokenIn: p.resolveTokenSymbol(token0),
TokenOut: p.resolveTokenSymbol(token1),
TokenInAddress: token0Addr,
TokenOutAddress: token1Addr,
IsValid: true,
}
}
}
// If we can't decode specific parameters, mark as invalid rather than returning zeros
// This will trigger fallback processing
return &SwapDetails{
TokenIn: "0x0000000000000000000000000000000000000000",
TokenOut: "0x0000000000000000000000000000000000000000",
TokenInAddress: common.Address{},
TokenOutAddress: common.Address{},
IsValid: false, // Mark as invalid so fallback processing can handle it
}
}
// calculateProfitWithOracle calculates profit estimation using the price oracle
func (p *ArbitrumL2Parser) calculateProfitWithOracle(swapDetails *SwapDetails) (float64, error) {
if p.oracle == nil {
return 0.0, fmt.Errorf("price oracle not available")
}
// Skip calculation for invalid swaps
if !swapDetails.IsValid || swapDetails.AmountIn == nil || swapDetails.AmountIn.Sign() == 0 {
return 0.0, nil
}
// Convert token addresses from string to common.Address
tokenIn := swapDetails.TokenInAddress
tokenOut := swapDetails.TokenOutAddress
// Fall back to decoding from string if address fields are empty
if tokenIn == (common.Address{}) {
if !common.IsHexAddress(swapDetails.TokenIn) {
return 0.0, fmt.Errorf("invalid tokenIn address: %s", swapDetails.TokenIn)
}
tokenIn = common.HexToAddress(swapDetails.TokenIn)
}
if tokenOut == (common.Address{}) {
if !common.IsHexAddress(swapDetails.TokenOut) {
return 0.0, fmt.Errorf("invalid tokenOut address: %s", swapDetails.TokenOut)
}
tokenOut = common.HexToAddress(swapDetails.TokenOut)
}
// Create price request
priceReq := &oracle.PriceRequest{
TokenIn: tokenIn,
TokenOut: tokenOut,
AmountIn: swapDetails.AmountIn,
Timestamp: time.Now(),
}
// Get price from oracle with timeout
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
priceResp, err := p.oracle.GetPrice(ctx, priceReq)
if err != nil {
return 0.0, fmt.Errorf("oracle price lookup failed: %w", err)
}
if !priceResp.Valid {
return 0.0, fmt.Errorf("oracle returned invalid price")
}
// Convert amounts to float for USD calculation (assuming 18 decimals)
amountInFloat := new(big.Float).Quo(new(big.Float).SetInt(swapDetails.AmountIn), big.NewFloat(1e18))
amountOutFloat := new(big.Float).Quo(new(big.Float).SetInt(priceResp.AmountOut), big.NewFloat(1e18))
amountInVal, _ := amountInFloat.Float64()
amountOutVal, _ := amountOutFloat.Float64()
// Estimate profit based on price difference
// This is a simplified calculation - in reality you'd need:
// 1. USD prices for both tokens
// 2. Gas cost estimation
// 3. Market impact analysis
// 4. Arbitrage opportunity assessment
// For now, calculate potential arbitrage profit as percentage of swap value
profitPercentage := 0.0
slippageBps := int64(0) // Initialize slippage variable
if amountInVal > 0 {
// Simple profit estimation based on price impact
slippageBps = priceResp.SlippageBps.Int64()
if slippageBps > 0 {
// Higher slippage = higher potential arbitrage profit
profitPercentage = float64(slippageBps) / 10000.0 * 0.1 // 10% of slippage as profit estimate
}
}
// Convert to USD estimate (simplified - assumes token has $1 base value)
estimatedProfitUSD := amountInVal * profitPercentage
p.logger.Debug(fmt.Sprintf("Calculated profit for swap %s->%s: amountIn=%.6f, amountOut=%.6f, slippage=%d bps, profit=$%.2f",
tokenIn.Hex()[:8], tokenOut.Hex()[:8], amountInVal, amountOutVal, slippageBps, estimatedProfitUSD))
return estimatedProfitUSD, nil
}
// initializeABIs initializes the ABI decoders for sophisticated parameter parsing
func (p *ArbitrumL2Parser) initializeABIs() error {
// Uniswap V2 Router ABI (essential functions)
uniswapV2JSON := `[
{
"name": "swapExactTokensForTokens",
"type": "function",
"inputs": [
{"name": "amountIn", "type": "uint256"},
{"name": "amountOutMin", "type": "uint256"},
{"name": "path", "type": "address[]"},
{"name": "to", "type": "address"},
{"name": "deadline", "type": "uint256"}
]
},
{
"name": "swapTokensForExactTokens",
"type": "function",
"inputs": [
{"name": "amountOut", "type": "uint256"},
{"name": "amountInMax", "type": "uint256"},
{"name": "path", "type": "address[]"},
{"name": "to", "type": "address"},
{"name": "deadline", "type": "uint256"}
]
},
{
"name": "swapExactETHForTokens",
"type": "function",
"inputs": [
{"name": "amountOutMin", "type": "uint256"},
{"name": "path", "type": "address[]"},
{"name": "to", "type": "address"},
{"name": "deadline", "type": "uint256"}
]
},
{
"name": "swapExactTokensForETH",
"type": "function",
"inputs": [
{"name": "amountIn", "type": "uint256"},
{"name": "amountOutMin", "type": "uint256"},
{"name": "path", "type": "address[]"},
{"name": "to", "type": "address"},
{"name": "deadline", "type": "uint256"}
]
}
]`
// Uniswap V3 Router ABI (essential functions)
uniswapV3JSON := `[
{
"name": "exactInputSingle",
"type": "function",
"inputs": [
{
"name": "params",
"type": "tuple",
"components": [
{"name": "tokenIn", "type": "address"},
{"name": "tokenOut", "type": "address"},
{"name": "fee", "type": "uint24"},
{"name": "recipient", "type": "address"},
{"name": "deadline", "type": "uint256"},
{"name": "amountIn", "type": "uint256"},
{"name": "amountOutMinimum", "type": "uint256"},
{"name": "sqrtPriceLimitX96", "type": "uint160"}
]
}
]
},
{
"name": "exactInput",
"type": "function",
"inputs": [
{
"name": "params",
"type": "tuple",
"components": [
{"name": "path", "type": "bytes"},
{"name": "recipient", "type": "address"},
{"name": "deadline", "type": "uint256"},
{"name": "amountIn", "type": "uint256"},
{"name": "amountOutMinimum", "type": "uint256"}
]
}
]
},
{
"name": "exactOutputSingle",
"type": "function",
"inputs": [
{
"name": "params",
"type": "tuple",
"components": [
{"name": "tokenIn", "type": "address"},
{"name": "tokenOut", "type": "address"},
{"name": "fee", "type": "uint24"},
{"name": "recipient", "type": "address"},
{"name": "deadline", "type": "uint256"},
{"name": "amountOut", "type": "uint256"},
{"name": "amountInMaximum", "type": "uint256"},
{"name": "sqrtPriceLimitX96", "type": "uint160"}
]
}
]
},
{
"name": "multicall",
"type": "function",
"inputs": [
{"name": "data", "type": "bytes[]"}
]
}
]`
var err error
// Parse Uniswap V2 ABI
p.uniswapV2ABI, err = abi.JSON(strings.NewReader(uniswapV2JSON))
if err != nil {
return fmt.Errorf("failed to parse Uniswap V2 ABI: %w", err)
}
// Parse Uniswap V3 ABI
p.uniswapV3ABI, err = abi.JSON(strings.NewReader(uniswapV3JSON))
if err != nil {
return fmt.Errorf("failed to parse Uniswap V3 ABI: %w", err)
}
// Use same ABI for SushiSwap (same interface as Uniswap V2)
p.sushiSwapABI = p.uniswapV2ABI
p.logger.Info("ABI decoders initialized successfully for sophisticated transaction parsing")
return nil
}
// decodeWithABI uses proper ABI decoding instead of basic byte slicing
func (p *ArbitrumL2Parser) decodeWithABI(protocol, functionName string, inputData []byte) (map[string]interface{}, error) {
if len(inputData) < 4 {
return nil, fmt.Errorf("input data too short")
}
// Remove function selector (first 4 bytes)
params := inputData[4:]
var targetABI abi.ABI
switch protocol {
case "UniswapV2":
targetABI = p.uniswapV2ABI
case "UniswapV3":
targetABI = p.uniswapV3ABI
case "SushiSwap":
targetABI = p.sushiSwapABI
default:
return nil, fmt.Errorf("unsupported protocol: %s", protocol)
}
// Get the method from ABI
method, exists := targetABI.Methods[functionName]
if !exists {
return nil, fmt.Errorf("method %s not found in %s ABI", functionName, protocol)
}
// Decode the parameters
values, err := method.Inputs.Unpack(params)
if err != nil {
return nil, fmt.Errorf("failed to unpack parameters: %w", err)
}
// Convert to map for easier access
result := make(map[string]interface{})
for i, input := range method.Inputs {
if i < len(values) {
result[input.Name] = values[i]
}
}
p.logger.Debug(fmt.Sprintf("Successfully decoded %s.%s with %d parameters", protocol, functionName, len(values)))
return result, nil
}
// GetDetailedSwapInfo extracts detailed swap information from DEXTransaction
func (p *ArbitrumL2Parser) GetDetailedSwapInfo(dexTx *DEXTransaction) *DetailedSwapInfo {
if dexTx == nil || dexTx.SwapDetails == nil || !dexTx.SwapDetails.IsValid {
return &DetailedSwapInfo{IsValid: false}
}
return &DetailedSwapInfo{
TxHash: dexTx.Hash,
From: dexTx.From,
To: dexTx.To,
MethodName: dexTx.FunctionName,
Protocol: dexTx.Protocol,
AmountIn: dexTx.SwapDetails.AmountIn,
AmountOut: dexTx.SwapDetails.AmountOut,
AmountMin: dexTx.SwapDetails.AmountMin,
TokenIn: dexTx.SwapDetails.TokenIn,
TokenOut: dexTx.SwapDetails.TokenOut,
TokenInAddress: dexTx.SwapDetails.TokenInAddress,
TokenOutAddress: dexTx.SwapDetails.TokenOutAddress,
Fee: dexTx.SwapDetails.Fee,
Recipient: dexTx.SwapDetails.Recipient,
IsValid: true,
}
}
// DetailedSwapInfo represents enhanced swap information for external processing
type DetailedSwapInfo struct {
TxHash string
From string
To string
MethodName string
Protocol string
AmountIn *big.Int
AmountOut *big.Int
AmountMin *big.Int
TokenIn string
TokenOut string
TokenInAddress common.Address
TokenOutAddress common.Address
Fee uint32
Recipient string
IsValid bool
}
// Close closes the RPC connection
// resolveTokenSymbol converts token address to human-readable symbol
func (p *ArbitrumL2Parser) resolveTokenSymbol(tokenAddress string) string {
// Convert to lowercase for consistent lookup
addr := strings.ToLower(tokenAddress)
// Known Arbitrum token mappings
tokenMap := map[string]string{
"0x82af49447d8a07e3bd95bd0d56f35241523fbab1": "WETH",
"0xaf88d065e77c8cc2239327c5edb3a432268e5831": "USDC",
"0xff970a61a04b1ca14834a43f5de4533ebddb5cc8": "USDC.e",
"0xfd086bc7cd5c481dcc9c85ebe478a1c0b69fcbb9": "USDT",
"0x2f2a2543b76a4166549f7aab2e75bef0aefc5b0f": "WBTC",
"0x912ce59144191c1204e64559fe8253a0e49e6548": "ARB",
"0xfc5a1a6eb076a2c7ad06ed22c90d7e710e35ad0a": "GMX",
"0xf97f4df75117a78c1a5a0dbb814af92458539fb4": "LINK",
"0xfa7f8980b0f1e64a2062791cc3b0871572f1f7f0": "UNI",
"0xba5ddd1f9d7f570dc94a51479a000e3bce967196": "AAVE",
"0x0de59c86c306b9fead9fb67e65551e2b6897c3f6": "KUMA",
"0x6efa9b8883dfb78fd75cd89d8474c44c3cbda469": "DIA",
"0x440017a1b021006d556d7fc06a54c32e42eb745b": "G@ARB",
"0x11cdb42b0eb46d95f990bedd4695a6e3fa034978": "CRV",
"0x040d1edc9569d4bab2d15287dc5a4f10f56a56b8": "BAL",
"0x354a6da3fcde098f8389cad84b0182725c6c91de": "COMP",
"0x2e9a6df78e42c50b0cefcf9000d0c3a4d34e1dd5": "MKR",
"0x539bde0d7dbd336b79148aa742883198bbf60342": "MAGIC",
"0x3d9907f9a368ad0a51be60f7da3b97cf940982d8": "GRAIL",
"0x6c2c06790b3e3e3c38e12ee22f8183b37a13ee55": "DPX",
"0x3082cc23568ea640225c2467653db90e9250aaa0": "RDNT",
"0xaaa6c1e32c55a7bfa8066a6fae9b42650f262418": "RAM",
"0x0c880f6761f1af8d9aa9c466984b80dab9a8c9e8": "PENDLE",
}
if symbol, exists := tokenMap[addr]; exists {
return symbol
}
// Return shortened address if not found
if len(tokenAddress) > 10 {
return tokenAddress[:6] + "..." + tokenAddress[len(tokenAddress)-4:]
}
return tokenAddress
}
// extractTokensFromMulticallData extracts token addresses from multicall transaction data
// CRITICAL FIX: Decode multicall structure and route to working extraction methods
// instead of calling broken multicall.go heuristics
func (p *ArbitrumL2Parser) extractTokensFromMulticallData(params []byte) (token0, token1 string) {
if len(params) < 32 {
return "", ""
}
// Multicall format: offset (32 bytes) + length (32 bytes) + data array
offset := new(big.Int).SetBytes(params[0:32]).Uint64()
if offset >= uint64(len(params)) {
return "", ""
}
// Read array length
arrayLength := new(big.Int).SetBytes(params[offset : offset+32]).Uint64()
if arrayLength == 0 {
return "", ""
}
// Process each call in the multicall
currentOffset := offset + 32
for i := uint64(0); i < arrayLength && i < 10; i++ { // Limit to first 10 calls
if currentOffset+32 > uint64(len(params)) {
break
}
// Read call data offset (this is a relative offset from the array start)
callOffsetRaw := new(big.Int).SetBytes(params[currentOffset : currentOffset+32]).Uint64()
currentOffset += 32
// Calculate absolute offset (relative to params start + array offset)
callOffset := offset + callOffsetRaw
// Bounds check for callOffset
if callOffset+32 > uint64(len(params)) {
continue
}
// Read call data length
callLength := new(big.Int).SetBytes(params[callOffset : callOffset+32]).Uint64()
callStart := callOffset + 32
callEnd := callStart + callLength
// Bounds check for call data
if callEnd > uint64(len(params)) || callEnd < callStart {
continue
}
// Extract the actual call data
callData := params[callStart:callEnd]
if len(callData) < 4 {
continue
}
// Try to extract tokens using our WORKING signature-based methods
t0, t1, err := p.ExtractTokensFromCalldata(callData)
if err == nil && t0 != (common.Address{}) && t1 != (common.Address{}) {
return t0.Hex(), t1.Hex()
}
}
return "", ""
}
// isValidTokenAddress checks if an address looks like a valid token address
func (p *ArbitrumL2Parser) isValidTokenAddress(hexAddr string) bool {
// Basic validation - more sophisticated validation could be added
if len(hexAddr) != 40 {
return false
}
// Check if it's all hex
for _, r := range hexAddr {
if !((r >= '0' && r <= '9') || (r >= 'a' && r <= 'f') || (r >= 'A' && r <= 'F')) {
return false
}
}
// Filter out common non-token addresses (routers, common addresses, etc.)
commonNonTokens := []string{
"e592427a0aece92de3edee1f18e0157c05861564", // Uniswap V3 Router
"a51afafe0263b40edaef0df8781ea9aa03e381a3", // Universal Router
"4752ba5dbc23f44d87826276bf6fd6b1c372ad24", // Uniswap V2 Router
"0000000000000000000000000000000000000000", // Zero address
"ffffffffffffffffffffffffffffffffffffffff", // Max address
"0000000000000000000000000000000000000001", // Common system addresses
}
for _, nonToken := range commonNonTokens {
if strings.EqualFold(hexAddr, nonToken) {
return false
}
}
return true
}
func (p *ArbitrumL2Parser) Close() {
if p.client != nil {
p.client.Close()
}
}
// getSignatureBytes converts a hex signature string (e.g., "0x18cbafe5") to 4-byte array
func (p *ArbitrumL2Parser) getSignatureBytes(sig string) ([]byte, error) {
// Remove "0x" prefix if present
sig = strings.TrimPrefix(sig, "0x")
// Decode hex string to bytes
bytes, err := hex.DecodeString(sig)
if err != nil {
return nil, fmt.Errorf("failed to decode signature %s: %w", sig, err)
}
if len(bytes) != 4 {
return nil, fmt.Errorf("signature must be 4 bytes, got %d", len(bytes))
}
return bytes, nil
}
// createCalldataWithSignature creates full calldata by prepending function signature to params
func (p *ArbitrumL2Parser) createCalldataWithSignature(signatureHex string, params []byte) ([]byte, error) {
sigBytes, err := p.getSignatureBytes(signatureHex)
if err != nil {
return nil, err
}
fullCalldata := make([]byte, len(params)+4)
copy(fullCalldata[0:4], sigBytes)
copy(fullCalldata[4:], params)
return fullCalldata, nil
}
// CRITICAL FIX: Public wrapper for token extraction - exposed for events parser integration
func (p *ArbitrumL2Parser) ExtractTokensFromMulticallData(params []byte) (token0, token1 string) {
return p.extractTokensFromMulticallData(params)
}
// ExtractTokensFromCalldata implements interfaces.TokenExtractor for direct calldata parsing
func (p *ArbitrumL2Parser) ExtractTokensFromCalldata(calldata []byte) (token0, token1 common.Address, err error) {
if len(calldata) < 4 {
return common.Address{}, common.Address{}, fmt.Errorf("calldata too short")
}
// Try to parse using known function signatures
functionSignature := hex.EncodeToString(calldata[:4])
switch functionSignature {
case "3593564c": // execute (UniversalRouter)
return p.extractTokensFromUniversalRouter(calldata[4:])
case "38ed1739": // swapExactTokensForTokens
return p.extractTokensFromSwapExactTokensForTokens(calldata[4:])
case "8803dbee": // swapTokensForExactTokens
return p.extractTokensFromSwapTokensForExactTokens(calldata[4:])
case "7ff36ab5": // swapExactETHForTokens
return p.extractTokensFromSwapExactETHForTokens(calldata[4:])
case "18cbafe5": // swapExactTokensForETH
return p.extractTokensFromSwapExactTokensForETH(calldata[4:])
case "414bf389": // exactInputSingle (Uniswap V3)
return p.extractTokensFromExactInputSingle(calldata[4:])
case "ac9650d8": // multicall
// For multicall, extract tokens from first successful call
stringToken0, stringToken1 := p.extractTokensFromMulticallData(calldata[4:])
if stringToken0 != "" && stringToken1 != "" {
return common.HexToAddress(stringToken0), common.HexToAddress(stringToken1), nil
}
return common.Address{}, common.Address{}, fmt.Errorf("no tokens found in multicall")
default:
return common.Address{}, common.Address{}, fmt.Errorf("unknown function signature: %s", functionSignature)
}
}
// Helper methods for specific function signature parsing
func (p *ArbitrumL2Parser) extractTokensFromSwapExactTokensForTokens(params []byte) (token0, token1 common.Address, err error) {
if len(params) < 160 {
return common.Address{}, common.Address{}, fmt.Errorf("invalid parameters length")
}
// Extract path offset (3rd parameter)
pathOffset := new(big.Int).SetBytes(params[64:96]).Uint64()
if pathOffset >= uint64(len(params)) {
return common.Address{}, common.Address{}, fmt.Errorf("invalid path offset")
}
// Extract path length
pathLengthBytes := params[pathOffset : pathOffset+32]
pathLength := new(big.Int).SetBytes(pathLengthBytes).Uint64()
if pathLength < 2 || pathOffset+32+pathLength*32 > uint64(len(params)) {
return common.Address{}, common.Address{}, fmt.Errorf("invalid path length")
}
// Extract first and last addresses from path
token0 = common.BytesToAddress(params[pathOffset+32 : pathOffset+64])
token1 = common.BytesToAddress(params[pathOffset+32+(pathLength-1)*32 : pathOffset+32+pathLength*32])
return token0, token1, nil
}
func (p *ArbitrumL2Parser) extractTokensFromSwapTokensForExactTokens(params []byte) (token0, token1 common.Address, err error) {
// Similar to swapExactTokensForTokens but with different parameter order
return p.extractTokensFromSwapExactTokensForTokens(params)
}
func (p *ArbitrumL2Parser) extractTokensFromSwapExactETHForTokens(params []byte) (token0, token1 common.Address, err error) {
if len(params) < 96 {
return common.Address{}, common.Address{}, fmt.Errorf("invalid parameters length")
}
// ETH is typically represented as WETH
token0 = common.HexToAddress("0x82aF49447D8a07e3bd95BD0d56f35241523fBab1") // WETH on Arbitrum
// Extract path offset (2nd parameter)
pathOffset := new(big.Int).SetBytes(params[32:64]).Uint64()
if pathOffset >= uint64(len(params)) {
return common.Address{}, common.Address{}, fmt.Errorf("invalid path offset")
}
// Extract path length and last token
pathLengthBytes := params[pathOffset : pathOffset+32]
pathLength := new(big.Int).SetBytes(pathLengthBytes).Uint64()
if pathLength < 2 || pathOffset+32+pathLength*32 > uint64(len(params)) {
return common.Address{}, common.Address{}, fmt.Errorf("invalid path length")
}
token1 = common.BytesToAddress(params[pathOffset+32+(pathLength-1)*32 : pathOffset+32+pathLength*32])
return token0, token1, nil
}
func (p *ArbitrumL2Parser) extractTokensFromSwapExactTokensForETH(params []byte) (token0, token1 common.Address, err error) {
token0, token1, err = p.extractTokensFromSwapExactETHForTokens(params)
// Swap the order since this is tokens -> ETH
return token1, token0, err
}
func (p *ArbitrumL2Parser) extractTokensFromExactInputSingle(params []byte) (token0, token1 common.Address, err error) {
if len(params) < 64 {
return common.Address{}, common.Address{}, fmt.Errorf("invalid parameters length")
}
// Extract tokenIn and tokenOut from exactInputSingle struct
token0 = common.BytesToAddress(params[0:32])
token1 = common.BytesToAddress(params[32:64])
return token0, token1, nil
}
// extractTokensFromUniversalRouter decodes UniversalRouter execute() commands
func (p *ArbitrumL2Parser) extractTokensFromUniversalRouter(params []byte) (token0, token1 common.Address, err error) {
// UniversalRouter execute format:
// bytes commands, bytes[] inputs, uint256 deadline
if len(params) < 96 {
return common.Address{}, common.Address{}, fmt.Errorf("params too short for universal router")
}
// Parse commands offset (first 32 bytes)
commandsOffset := new(big.Int).SetBytes(params[0:32]).Uint64()
// Parse inputs offset (second 32 bytes)
inputsOffset := new(big.Int).SetBytes(params[32:64]).Uint64()
if commandsOffset >= uint64(len(params)) || inputsOffset >= uint64(len(params)) {
return common.Address{}, common.Address{}, fmt.Errorf("invalid offsets")
}
// Read commands length
commandsLength := new(big.Int).SetBytes(params[commandsOffset : commandsOffset+32]).Uint64()
commandsStart := commandsOffset + 32
// Read first command (V3_SWAP_EXACT_IN = 0x00, V2_SWAP_EXACT_IN = 0x08)
if commandsStart >= uint64(len(params)) || commandsLength == 0 {
return common.Address{}, common.Address{}, fmt.Errorf("no commands")
}
firstCommand := params[commandsStart]
// Read inputs array
inputsLength := new(big.Int).SetBytes(params[inputsOffset : inputsOffset+32]).Uint64()
if inputsLength == 0 {
return common.Address{}, common.Address{}, fmt.Errorf("no inputs")
}
// Read first input offset and data
firstInputOffset := inputsOffset + 32
inputDataOffset := new(big.Int).SetBytes(params[firstInputOffset : firstInputOffset+32]).Uint64()
if inputDataOffset >= uint64(len(params)) {
return common.Address{}, common.Address{}, fmt.Errorf("invalid input offset")
}
inputDataLength := new(big.Int).SetBytes(params[inputDataOffset : inputDataOffset+32]).Uint64()
inputDataStart := inputDataOffset + 32
inputDataEnd := inputDataStart + inputDataLength
if inputDataEnd > uint64(len(params)) {
return common.Address{}, common.Address{}, fmt.Errorf("input data out of bounds")
}
inputData := params[inputDataStart:inputDataEnd]
// Decode based on command type
switch firstCommand {
case 0x00: // V3_SWAP_EXACT_IN
// Format: recipient(addr), amountIn(uint256), amountOutMin(uint256), path(bytes), payerIsUser(bool)
if len(inputData) >= 160 {
// Path starts at offset 128 (4th parameter)
pathOffset := new(big.Int).SetBytes(inputData[96:128]).Uint64()
if pathOffset < uint64(len(inputData)) {
pathLength := new(big.Int).SetBytes(inputData[pathOffset : pathOffset+32]).Uint64()
pathStart := pathOffset + 32
// V3 path format: token0(20 bytes) + fee(3 bytes) + token1(20 bytes)
if pathLength >= 43 && pathStart+43 <= uint64(len(inputData)) {
token0 = common.BytesToAddress(inputData[pathStart : pathStart+20])
token1 = common.BytesToAddress(inputData[pathStart+23 : pathStart+43])
return token0, token1, nil
}
}
}
case 0x08: // V2_SWAP_EXACT_IN
// Format: recipient(addr), amountIn(uint256), amountOutMin(uint256), path(addr[]), payerIsUser(bool)
if len(inputData) >= 128 {
// Path array offset is at position 96 (4th parameter)
pathOffset := new(big.Int).SetBytes(inputData[96:128]).Uint64()
if pathOffset < uint64(len(inputData)) {
pathArrayLength := new(big.Int).SetBytes(inputData[pathOffset : pathOffset+32]).Uint64()
if pathArrayLength >= 2 {
// First token
token0 = common.BytesToAddress(inputData[pathOffset+32 : pathOffset+64])
// Last token
lastTokenOffset := pathOffset + 32 + (pathArrayLength-1)*32
if lastTokenOffset+32 <= uint64(len(inputData)) {
token1 = common.BytesToAddress(inputData[lastTokenOffset : lastTokenOffset+32])
return token0, token1, nil
}
}
}
}
}
return common.Address{}, common.Address{}, fmt.Errorf("unsupported universal router command: 0x%02x", firstCommand)
}