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>
313 lines
10 KiB
Go
313 lines
10 KiB
Go
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"`
|
|
}
|