package arbitrum import ( "encoding/binary" "fmt" "math" "math/big" "time" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" "github.com/fraktal/mev-beta/internal/logger" ) // safeConvertUint32ToInt32 safely converts a uint32 to int32, capping at MaxInt32 if overflow would occur func safeConvertUint32ToInt32(v uint32) int32 { if v > math.MaxInt32 { return math.MaxInt32 } return int32(v) } // FixedSwapParser provides robust swap event parsing with proper error handling type FixedSwapParser struct { logger *logger.Logger } // NewFixedSwapParser creates a new swap parser with enhanced error handling func NewFixedSwapParser(logger *logger.Logger) *FixedSwapParser { return &FixedSwapParser{ logger: logger, } } // ParseSwapEventSafe parses swap events with comprehensive validation and error handling func (fsp *FixedSwapParser) ParseSwapEventSafe(log *types.Log, tx *types.Transaction, blockNumber uint64) (*SimpleSwapEvent, error) { // Validate input parameters if log == nil { return nil, fmt.Errorf("log cannot be nil") } if tx == nil { return nil, fmt.Errorf("transaction cannot be nil") } // Uniswap V3 Pool Swap event signature swapEventSig := common.HexToHash("0xc42079f94a6350d7e6235f29174924f928cc2ac818eb64fed8004e115fbcca67") // Uniswap V2 Pool Swap event signature swapV2EventSig := common.HexToHash("0xd78ad95fa46c994b6551d0da85fc275fe613ce37657fb8d5e3d130840159d822") if len(log.Topics) == 0 { return nil, fmt.Errorf("log has no topics") } // Determine which version of Uniswap based on event signature switch log.Topics[0] { case swapEventSig: return fsp.parseUniswapV3Swap(log, tx, blockNumber) case swapV2EventSig: return fsp.parseUniswapV2Swap(log, tx, blockNumber) default: return nil, fmt.Errorf("unknown swap event signature: %s", log.Topics[0].Hex()) } } // parseUniswapV3Swap parses Uniswap V3 swap events with proper signed integer handling func (fsp *FixedSwapParser) parseUniswapV3Swap(log *types.Log, tx *types.Transaction, blockNumber uint64) (*SimpleSwapEvent, error) { // UniswapV3 Swap event structure: // event Swap(indexed address sender, indexed address recipient, int256 amount0, int256 amount1, uint160 sqrtPriceX96, uint128 liquidity, int24 tick) // Validate log structure if len(log.Topics) < 3 { return nil, fmt.Errorf("insufficient topics for UniV3 swap: got %d, need 3", len(log.Topics)) } if len(log.Data) < 160 { // 5 * 32 bytes for amount0, amount1, sqrtPriceX96, liquidity, tick return nil, fmt.Errorf("insufficient data for UniV3 swap: got %d bytes, need 160", len(log.Data)) } // Extract indexed parameters sender := common.BytesToAddress(log.Topics[1].Bytes()) recipient := common.BytesToAddress(log.Topics[2].Bytes()) // Parse signed amounts correctly amount0, err := fsp.parseSignedInt256(log.Data[0:32]) if err != nil { return nil, fmt.Errorf("failed to parse amount0: %w", err) } amount1, err := fsp.parseSignedInt256(log.Data[32:64]) if err != nil { return nil, fmt.Errorf("failed to parse amount1: %w", err) } // Parse unsigned values sqrtPriceX96 := new(big.Int).SetBytes(log.Data[64:96]) liquidity := new(big.Int).SetBytes(log.Data[96:128]) // Parse tick as int24 (stored in int256) tick, err := fsp.parseSignedInt24(log.Data[128:160]) if err != nil { return nil, fmt.Errorf("failed to parse tick: %w", err) } // Validate parsed values if err := fsp.validateUniV3SwapData(amount0, amount1, sqrtPriceX96, liquidity, tick); err != nil { return nil, fmt.Errorf("invalid swap data: %w", err) } return &SimpleSwapEvent{ TxHash: tx.Hash(), PoolAddress: log.Address, Token0: common.Address{}, // Will be filled by caller Token1: common.Address{}, // Will be filled by caller Amount0: amount0, Amount1: amount1, SqrtPriceX96: sqrtPriceX96, Liquidity: liquidity, Tick: int32(tick), BlockNumber: blockNumber, LogIndex: log.Index, Timestamp: time.Now(), Sender: sender, Recipient: recipient, Protocol: "UniswapV3", }, nil } // parseUniswapV2Swap parses Uniswap V2 swap events func (fsp *FixedSwapParser) parseUniswapV2Swap(log *types.Log, tx *types.Transaction, blockNumber uint64) (*SimpleSwapEvent, error) { // UniswapV2 Swap event structure: // event Swap(indexed address sender, uint256 amount0In, uint256 amount1In, uint256 amount0Out, uint256 amount1Out, indexed address to) if len(log.Topics) < 3 { return nil, fmt.Errorf("insufficient topics for UniV2 swap: got %d, need 3", len(log.Topics)) } if len(log.Data) < 128 { // 4 * 32 bytes return nil, fmt.Errorf("insufficient data for UniV2 swap: got %d bytes, need 128", len(log.Data)) } // Extract indexed parameters sender := common.BytesToAddress(log.Topics[1].Bytes()) recipient := common.BytesToAddress(log.Topics[2].Bytes()) // Parse amounts (all unsigned in V2) amount0In := new(big.Int).SetBytes(log.Data[0:32]) amount1In := new(big.Int).SetBytes(log.Data[32:64]) amount0Out := new(big.Int).SetBytes(log.Data[64:96]) amount1Out := new(big.Int).SetBytes(log.Data[96:128]) // Calculate net amounts (In - Out) amount0 := new(big.Int).Sub(amount0In, amount0Out) amount1 := new(big.Int).Sub(amount1In, amount1Out) // Validate parsed values if err := fsp.validateUniV2SwapData(amount0In, amount1In, amount0Out, amount1Out); err != nil { return nil, fmt.Errorf("invalid V2 swap data: %w", err) } return &SimpleSwapEvent{ TxHash: tx.Hash(), PoolAddress: log.Address, Token0: common.Address{}, // Will be filled by caller Token1: common.Address{}, // Will be filled by caller Amount0: amount0, Amount1: amount1, BlockNumber: blockNumber, LogIndex: log.Index, Timestamp: time.Now(), Sender: sender, Recipient: recipient, Protocol: "UniswapV2", }, nil } // parseSignedInt256 correctly parses a signed 256-bit integer from bytes func (fsp *FixedSwapParser) parseSignedInt256(data []byte) (*big.Int, error) { if len(data) != 32 { return nil, fmt.Errorf("invalid data length for int256: got %d, need 32", len(data)) } value := new(big.Int).SetBytes(data) // Check if the value is negative (MSB set) if len(data) > 0 && data[0]&0x80 != 0 { // Convert from two's complement // Subtract 2^256 to get the negative value maxUint256 := new(big.Int) maxUint256.Lsh(big.NewInt(1), 256) value.Sub(value, maxUint256) } return value, nil } // parseSignedInt24 correctly parses a signed 24-bit integer stored in a 32-byte field func (fsp *FixedSwapParser) parseSignedInt24(data []byte) (int32, error) { if len(data) != 32 { return 0, fmt.Errorf("invalid data length for int24: got %d, need 32", len(data)) } signByte := data[28] if signByte != 0x00 && signByte != 0xFF { return 0, fmt.Errorf("invalid sign extension byte 0x%02x for int24", signByte) } if signByte == 0x00 && data[29]&0x80 != 0 { return 0, fmt.Errorf("value uses more than 23 bits for positive int24") } if signByte == 0xFF && data[29]&0x80 == 0 { return 0, fmt.Errorf("value uses more than 23 bits for negative int24") } // Extract the last 4 bytes (since int24 is stored as int256) value := binary.BigEndian.Uint32(data[28:32]) // Convert to int24 by masking and sign-extending int24Value := int32(safeConvertUint32ToInt32(value & 0xFFFFFF)) // Mask to 24 bits // Check if negative (bit 23 set) if int24Value&0x800000 != 0 { // Sign extend to int32 int24Value |= ^0xFFFFFF // Set all bits above bit 23 to 1 for negative numbers } // Validate range for int24 if int24Value < -8388608 || int24Value > 8388607 { return 0, fmt.Errorf("value %d out of range for int24", int24Value) } return int24Value, nil } // validateUniV3SwapData validates parsed UniswapV3 swap data func (fsp *FixedSwapParser) validateUniV3SwapData(amount0, amount1, sqrtPriceX96, liquidity *big.Int, tick int32) error { // Check that at least one amount is non-zero if amount0.Sign() == 0 && amount1.Sign() == 0 { return fmt.Errorf("both amounts cannot be zero") } // Check that amounts have opposite signs (one in, one out) if amount0.Sign() != 0 && amount1.Sign() != 0 && amount0.Sign() == amount1.Sign() { fsp.logger.Warn("Unusual swap: both amounts have same sign", "amount0", amount0.String(), "amount1", amount1.String()) } // Validate sqrtPriceX96 is positive if sqrtPriceX96.Sign() <= 0 { return fmt.Errorf("sqrtPriceX96 must be positive: %s", sqrtPriceX96.String()) } // Validate liquidity is non-negative if liquidity.Sign() < 0 { return fmt.Errorf("liquidity cannot be negative: %s", liquidity.String()) } // Validate tick range (typical UniV3 range) if tick < -887272 || tick > 887272 { return fmt.Errorf("tick %d out of valid range [-887272, 887272]", tick) } return nil } // validateUniV2SwapData validates parsed UniswapV2 swap data func (fsp *FixedSwapParser) validateUniV2SwapData(amount0In, amount1In, amount0Out, amount1Out *big.Int) error { // At least one input amount must be positive if amount0In.Sign() <= 0 && amount1In.Sign() <= 0 { return fmt.Errorf("at least one input amount must be positive") } // At least one output amount must be positive if amount0Out.Sign() <= 0 && amount1Out.Sign() <= 0 { return fmt.Errorf("at least one output amount must be positive") } // Input amounts should not equal output amounts (no zero-value swaps) if amount0In.Cmp(amount0Out) == 0 && amount1In.Cmp(amount1Out) == 0 { return fmt.Errorf("input amounts equal output amounts (zero-value swap)") } return nil } // ExtendedSwapEvent includes additional validation and error information type ExtendedSwapEvent struct { *SimpleSwapEvent ParseErrors []string `json:"parse_errors,omitempty"` Warnings []string `json:"warnings,omitempty"` Validated bool `json:"validated"` } // SimpleSwapEvent represents a parsed swap event (keeping existing structure for compatibility) type SimpleSwapEvent struct { TxHash common.Hash `json:"tx_hash"` PoolAddress common.Address `json:"pool_address"` Token0 common.Address `json:"token0"` Token1 common.Address `json:"token1"` Amount0 *big.Int `json:"amount0"` Amount1 *big.Int `json:"amount1"` SqrtPriceX96 *big.Int `json:"sqrt_price_x96,omitempty"` Liquidity *big.Int `json:"liquidity,omitempty"` Tick int32 `json:"tick,omitempty"` BlockNumber uint64 `json:"block_number"` LogIndex uint `json:"log_index"` Timestamp time.Time `json:"timestamp"` Sender common.Address `json:"sender,omitempty"` Recipient common.Address `json:"recipient,omitempty"` Protocol string `json:"protocol"` }