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>
This commit is contained in:
967
orig/pkg/arbitrum/parser.go
Normal file
967
orig/pkg/arbitrum/parser.go
Normal file
@@ -0,0 +1,967 @@
|
||||
package arbitrum
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/accounts/abi"
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/core/types"
|
||||
"github.com/ethereum/go-ethereum/crypto"
|
||||
|
||||
"github.com/fraktal/mev-beta/internal/logger"
|
||||
)
|
||||
|
||||
// L2MessageParser parses Arbitrum L2 messages and transactions
|
||||
type L2MessageParser struct {
|
||||
logger *logger.Logger
|
||||
uniswapV2RouterABI abi.ABI
|
||||
uniswapV3RouterABI abi.ABI
|
||||
|
||||
// Known DEX contract addresses on Arbitrum
|
||||
knownRouters map[common.Address]string
|
||||
knownPools map[common.Address]string
|
||||
}
|
||||
|
||||
// NewL2MessageParser creates a new L2 message parser
|
||||
func NewL2MessageParser(logger *logger.Logger) *L2MessageParser {
|
||||
parser := &L2MessageParser{
|
||||
logger: logger,
|
||||
knownRouters: make(map[common.Address]string),
|
||||
knownPools: make(map[common.Address]string),
|
||||
}
|
||||
|
||||
// Initialize known Arbitrum DEX addresses
|
||||
parser.initializeKnownAddresses()
|
||||
|
||||
// Load ABIs for parsing
|
||||
parser.loadABIs()
|
||||
|
||||
return parser
|
||||
}
|
||||
|
||||
// KnownPools returns the known pools map for debugging
|
||||
func (p *L2MessageParser) KnownPools() map[common.Address]string {
|
||||
return p.knownPools
|
||||
}
|
||||
|
||||
// initializeKnownAddresses sets up known DEX addresses on Arbitrum
|
||||
func (p *L2MessageParser) initializeKnownAddresses() {
|
||||
// Uniswap V3 on Arbitrum
|
||||
p.knownRouters[common.HexToAddress("0xE592427A0AEce92De3Edee1F18E0157C05861564")] = "UniswapV3"
|
||||
p.knownRouters[common.HexToAddress("0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45")] = "UniswapV3Router2"
|
||||
|
||||
// Uniswap V2 on Arbitrum
|
||||
p.knownRouters[common.HexToAddress("0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D")] = "UniswapV2"
|
||||
|
||||
// SushiSwap on Arbitrum
|
||||
p.knownRouters[common.HexToAddress("0x1b02dA8Cb0d097eB8D57A175b88c7D8b47997506")] = "SushiSwap"
|
||||
|
||||
// Camelot DEX (Arbitrum native)
|
||||
p.knownRouters[common.HexToAddress("0xc873fEcbd354f5A56E00E710B90EF4201db2448d")] = "Camelot"
|
||||
|
||||
// GMX
|
||||
p.knownRouters[common.HexToAddress("0x327df1e6de05895d2ab08513aadd9317845f20d9")] = "GMX"
|
||||
|
||||
// Balancer V2
|
||||
p.knownRouters[common.HexToAddress("0xBA12222222228d8Ba445958a75a0704d566BF2C8")] = "BalancerV2"
|
||||
|
||||
// Curve
|
||||
p.knownRouters[common.HexToAddress("0x98EE8517825C0bd778a57471a27555614F97F48D")] = "Curve"
|
||||
|
||||
// Popular pools on Arbitrum - map to protocol names instead of pool descriptions
|
||||
p.knownPools[common.HexToAddress("0xC31E54c7a869B9FcBEcc14363CF510d1c41fa443")] = "UniswapV3"
|
||||
p.knownPools[common.HexToAddress("0x17c14D2c404D167802b16C450d3c99F88F2c4F4d")] = "UniswapV3"
|
||||
p.knownPools[common.HexToAddress("0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640")] = "UniswapV3"
|
||||
p.knownPools[common.HexToAddress("0xB4e16d0168e52d35CaCD2c6185b44281Ec28C9Dc")] = "UniswapV2"
|
||||
|
||||
// SushiSwap pools
|
||||
p.knownPools[common.HexToAddress("0x905dfCD5649217c42684f23958568e533C711Aa3")] = "SushiSwap"
|
||||
|
||||
// Camelot pools
|
||||
p.knownPools[common.HexToAddress("0x84652bb2539513BAf36e225c930Fdd8eaa63CE27")] = "Camelot"
|
||||
|
||||
// Balancer pools
|
||||
p.knownPools[common.HexToAddress("0x32dF62dc3aEd2cD6224193052Ce665DC18165841")] = "Balancer"
|
||||
|
||||
// Curve pools
|
||||
p.knownPools[common.HexToAddress("0x7f90122BF0700F9E7e1F688fe926940E8839F353")] = "Curve"
|
||||
}
|
||||
|
||||
// loadABIs loads the required ABI definitions
|
||||
func (p *L2MessageParser) loadABIs() {
|
||||
// Simplified ABI loading - in production, load from files
|
||||
uniswapV2RouterABI := `[
|
||||
{
|
||||
"inputs": [
|
||||
{"internalType": "uint256", "name": "amountIn", "type": "uint256"},
|
||||
{"internalType": "uint256", "name": "amountOutMin", "type": "uint256"},
|
||||
{"internalType": "address[]", "name": "path", "type": "address[]"},
|
||||
{"internalType": "address", "name": "to", "type": "address"},
|
||||
{"internalType": "uint256", "name": "deadline", "type": "uint256"}
|
||||
],
|
||||
"name": "swapExactTokensForTokens",
|
||||
"outputs": [{"internalType": "uint256[]", "name": "amounts", "type": "uint256[]"}],
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "function"
|
||||
}
|
||||
]`
|
||||
|
||||
var err error
|
||||
p.uniswapV2RouterABI, err = abi.JSON(bytes.NewReader([]byte(uniswapV2RouterABI)))
|
||||
if err != nil {
|
||||
p.logger.Error(fmt.Sprintf("Failed to load Uniswap V2 Router ABI: %v", err))
|
||||
}
|
||||
}
|
||||
|
||||
// ParseL2Message parses an L2 message and extracts relevant information
|
||||
func (p *L2MessageParser) ParseL2Message(messageData []byte, messageNumber *big.Int, timestamp uint64) (*L2Message, error) {
|
||||
// Validate inputs
|
||||
if messageData == nil {
|
||||
return nil, fmt.Errorf("message data is nil")
|
||||
}
|
||||
|
||||
if len(messageData) < 4 {
|
||||
return nil, fmt.Errorf("message data too short: %d bytes", len(messageData))
|
||||
}
|
||||
|
||||
// Validate message number
|
||||
if messageNumber == nil {
|
||||
return nil, fmt.Errorf("message number is nil")
|
||||
}
|
||||
|
||||
// Validate timestamp (should be a reasonable Unix timestamp)
|
||||
if timestamp > uint64(time.Now().Unix()+86400) || timestamp < 1609459200 { // 1609459200 = 2021-01-01
|
||||
p.logger.Warn(fmt.Sprintf("Suspicious timestamp: %d", timestamp))
|
||||
// We'll still process it but log the warning
|
||||
}
|
||||
|
||||
l2Message := &L2Message{
|
||||
MessageNumber: messageNumber,
|
||||
Data: messageData,
|
||||
Timestamp: timestamp,
|
||||
Type: L2Unknown,
|
||||
}
|
||||
|
||||
// Parse message type from first bytes
|
||||
msgType := binary.BigEndian.Uint32(messageData[:4])
|
||||
|
||||
// Validate message type
|
||||
if msgType != 3 && msgType != 7 {
|
||||
p.logger.Debug(fmt.Sprintf("Unknown L2 message type: %d", msgType))
|
||||
// We'll still return the message but mark it as unknown
|
||||
return l2Message, nil
|
||||
}
|
||||
|
||||
switch msgType {
|
||||
case 3: // L2 Transaction
|
||||
return p.parseL2Transaction(l2Message, messageData[4:])
|
||||
case 7: // Batch submission
|
||||
return p.parseL2Batch(l2Message, messageData[4:])
|
||||
default:
|
||||
p.logger.Debug(fmt.Sprintf("Unknown L2 message type: %d", msgType))
|
||||
return l2Message, nil
|
||||
}
|
||||
}
|
||||
|
||||
// parseL2Transaction parses an L2 transaction message
|
||||
func (p *L2MessageParser) parseL2Transaction(l2Message *L2Message, data []byte) (*L2Message, error) {
|
||||
// Validate inputs
|
||||
if l2Message == nil {
|
||||
return nil, fmt.Errorf("l2Message is nil")
|
||||
}
|
||||
|
||||
if data == nil {
|
||||
return nil, fmt.Errorf("transaction data is nil")
|
||||
}
|
||||
|
||||
// Validate data length
|
||||
if len(data) == 0 {
|
||||
return nil, fmt.Errorf("transaction data is empty")
|
||||
}
|
||||
|
||||
l2Message.Type = L2Transaction
|
||||
|
||||
// Parse RLP-encoded transaction
|
||||
tx := &types.Transaction{}
|
||||
if err := tx.UnmarshalBinary(data); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal transaction: %v", err)
|
||||
}
|
||||
|
||||
// Validate the parsed transaction
|
||||
if tx == nil {
|
||||
return nil, fmt.Errorf("parsed transaction is nil")
|
||||
}
|
||||
|
||||
// Additional validation for transaction fields
|
||||
if tx.Gas() == 0 && len(tx.Data()) == 0 {
|
||||
p.logger.Warn("Transaction has zero gas and no data")
|
||||
}
|
||||
|
||||
l2Message.ParsedTx = tx
|
||||
|
||||
// Extract sender (this might require signature recovery)
|
||||
if tx.To() != nil {
|
||||
// For now, we'll extract what we can without signature recovery
|
||||
l2Message.Sender = common.HexToAddress("0x0") // Placeholder
|
||||
}
|
||||
|
||||
return l2Message, nil
|
||||
}
|
||||
|
||||
// parseL2Batch parses a batch submission message
|
||||
func (p *L2MessageParser) parseL2Batch(l2Message *L2Message, data []byte) (*L2Message, error) {
|
||||
// Validate inputs
|
||||
if l2Message == nil {
|
||||
return nil, fmt.Errorf("l2Message is nil")
|
||||
}
|
||||
|
||||
if data == nil {
|
||||
return nil, fmt.Errorf("batch data is nil")
|
||||
}
|
||||
|
||||
l2Message.Type = L2BatchSubmission
|
||||
|
||||
// Parse batch data structure
|
||||
if len(data) < 32 {
|
||||
return nil, fmt.Errorf("batch data too short: %d bytes", len(data))
|
||||
}
|
||||
|
||||
// Extract batch index
|
||||
batchIndex := new(big.Int).SetBytes(data[:32])
|
||||
|
||||
// Validate batch index
|
||||
if batchIndex == nil || batchIndex.Sign() < 0 {
|
||||
return nil, fmt.Errorf("invalid batch index")
|
||||
}
|
||||
|
||||
l2Message.BatchIndex = batchIndex
|
||||
|
||||
// Parse individual transactions in the batch
|
||||
remainingData := data[32:]
|
||||
|
||||
// Validate remaining data
|
||||
if remainingData == nil {
|
||||
// No transactions in the batch, which is valid
|
||||
l2Message.InnerTxs = []*types.Transaction{}
|
||||
return l2Message, nil
|
||||
}
|
||||
|
||||
var innerTxs []*types.Transaction
|
||||
|
||||
for len(remainingData) > 0 {
|
||||
// Each transaction is prefixed with its length
|
||||
if len(remainingData) < 4 {
|
||||
// Incomplete data, log warning but continue with what we have
|
||||
p.logger.Warn("Incomplete transaction length prefix in batch")
|
||||
break
|
||||
}
|
||||
|
||||
txLength := binary.BigEndian.Uint32(remainingData[:4])
|
||||
|
||||
// Validate transaction length
|
||||
if txLength == 0 {
|
||||
p.logger.Warn("Zero-length transaction in batch")
|
||||
remainingData = remainingData[4:]
|
||||
continue
|
||||
}
|
||||
|
||||
if uint32(len(remainingData)) < 4+txLength {
|
||||
// Incomplete transaction data, log warning but continue with what we have
|
||||
p.logger.Warn(fmt.Sprintf("Incomplete transaction data in batch: expected %d bytes, got %d", txLength, len(remainingData)-4))
|
||||
break
|
||||
}
|
||||
|
||||
txData := remainingData[4 : 4+txLength]
|
||||
tx := &types.Transaction{}
|
||||
|
||||
if err := tx.UnmarshalBinary(txData); err == nil {
|
||||
// Validate the parsed transaction
|
||||
if tx != nil {
|
||||
innerTxs = append(innerTxs, tx)
|
||||
} else {
|
||||
p.logger.Warn("Parsed nil transaction in batch")
|
||||
}
|
||||
} else {
|
||||
// Log the error but continue processing other transactions
|
||||
p.logger.Warn(fmt.Sprintf("Failed to unmarshal transaction in batch: %v", err))
|
||||
}
|
||||
|
||||
remainingData = remainingData[4+txLength:]
|
||||
}
|
||||
|
||||
l2Message.InnerTxs = innerTxs
|
||||
return l2Message, nil
|
||||
}
|
||||
|
||||
// ParseDEXInteraction extracts DEX interaction details from a transaction
|
||||
func (p *L2MessageParser) ParseDEXInteraction(tx *types.Transaction) (*DEXInteraction, error) {
|
||||
// Validate inputs
|
||||
if tx == nil {
|
||||
return nil, fmt.Errorf("transaction is nil")
|
||||
}
|
||||
|
||||
if tx.To() == nil {
|
||||
return nil, fmt.Errorf("contract creation transaction")
|
||||
}
|
||||
|
||||
to := *tx.To()
|
||||
|
||||
// Validate address
|
||||
if to == (common.Address{}) {
|
||||
return nil, fmt.Errorf("invalid contract address")
|
||||
}
|
||||
|
||||
protocol, isDEX := p.knownRouters[to]
|
||||
if !isDEX {
|
||||
// Also check if this might be a direct pool interaction
|
||||
if _, isPool := p.knownPools[to]; isPool {
|
||||
// For pool interactions, we should identify the protocol that owns the pool
|
||||
// For now, we'll map common pools to their protocols
|
||||
// In a more sophisticated implementation, we would look up the pool's factory
|
||||
if strings.Contains(strings.ToLower(p.knownPools[to]), "uniswap") {
|
||||
protocol = "UniswapV3"
|
||||
} else if strings.Contains(strings.ToLower(p.knownPools[to]), "sushi") {
|
||||
protocol = "SushiSwap"
|
||||
} else if strings.Contains(strings.ToLower(p.knownPools[to]), "camelot") {
|
||||
protocol = "Camelot"
|
||||
} else if strings.Contains(strings.ToLower(p.knownPools[to]), "balancer") {
|
||||
protocol = "Balancer"
|
||||
} else if strings.Contains(strings.ToLower(p.knownPools[to]), "curve") {
|
||||
protocol = "Curve"
|
||||
} else {
|
||||
// Default to the pool name if we can't identify the protocol
|
||||
protocol = p.knownPools[to]
|
||||
}
|
||||
} else {
|
||||
return nil, fmt.Errorf("not a known DEX router or pool")
|
||||
}
|
||||
}
|
||||
|
||||
data := tx.Data()
|
||||
|
||||
// Validate transaction data
|
||||
if data == nil {
|
||||
return nil, fmt.Errorf("transaction data is nil")
|
||||
}
|
||||
|
||||
if len(data) < 4 {
|
||||
return nil, fmt.Errorf("transaction data too short: %d bytes", len(data))
|
||||
}
|
||||
|
||||
// Validate function selector (first 4 bytes)
|
||||
selector := data[:4]
|
||||
if len(selector) != 4 {
|
||||
return nil, fmt.Errorf("invalid function selector length: %d", len(selector))
|
||||
}
|
||||
|
||||
interaction := &DEXInteraction{
|
||||
Protocol: protocol,
|
||||
Router: to,
|
||||
Timestamp: uint64(time.Now().Unix()), // Use current time as default
|
||||
MessageNumber: big.NewInt(0), // Will be set by caller
|
||||
}
|
||||
|
||||
// Parse based on function selector
|
||||
switch common.Bytes2Hex(selector) {
|
||||
case "38ed1739": // swapExactTokensForTokens (Uniswap V2)
|
||||
return p.parseSwapExactTokensForTokens(interaction, data[4:])
|
||||
case "8803dbee": // swapTokensForExactTokens (Uniswap V2)
|
||||
return p.parseSwapTokensForExactTokens(interaction, data[4:])
|
||||
case "18cbafe5": // swapExactTokensForTokensSupportingFeeOnTransferTokens (Uniswap V2)
|
||||
return p.parseSwapExactTokensForTokens(interaction, data[4:])
|
||||
case "414bf389": // exactInputSingle (Uniswap V3)
|
||||
return p.parseExactInputSingle(interaction, data[4:])
|
||||
case "db3e2198": // exactInput (Uniswap V3)
|
||||
return p.parseExactInput(interaction, data[4:])
|
||||
case "f305d719": // exactOutputSingle (Uniswap V3)
|
||||
return p.parseExactOutputSingle(interaction, data[4:])
|
||||
case "04e45aaf": // exactOutput (Uniswap V3)
|
||||
return p.parseExactOutput(interaction, data[4:])
|
||||
case "7ff36ab5": // swapExactETHForTokens (Uniswap V2)
|
||||
return p.parseSwapExactETHForTokens(interaction, data[4:])
|
||||
case "18cffa1c": // swapExactETHForTokensSupportingFeeOnTransferTokens (Uniswap V2)
|
||||
return p.parseSwapExactETHForTokens(interaction, data[4:])
|
||||
case "b6f9de95": // swapExactTokensForETH (Uniswap V2)
|
||||
return p.parseSwapExactTokensForETH(interaction, data[4:])
|
||||
case "791ac947": // swapExactTokensForETHSupportingFeeOnTransferTokens (Uniswap V2)
|
||||
return p.parseSwapExactTokensForETH(interaction, data[4:])
|
||||
case "5ae401dc": // multicall (Uniswap V3)
|
||||
return p.parseMulticall(interaction, data[4:])
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown DEX function selector: %s", common.Bytes2Hex(selector))
|
||||
}
|
||||
}
|
||||
|
||||
// parseSwapExactTokensForTokens parses Uniswap V2 style swap
|
||||
func (p *L2MessageParser) parseSwapExactTokensForTokens(interaction *DEXInteraction, data []byte) (*DEXInteraction, error) {
|
||||
// Validate inputs
|
||||
if interaction == nil {
|
||||
return nil, fmt.Errorf("interaction is nil")
|
||||
}
|
||||
|
||||
if data == nil {
|
||||
return nil, fmt.Errorf("data is nil")
|
||||
}
|
||||
|
||||
// Decode ABI data
|
||||
method, err := p.uniswapV2RouterABI.MethodById(crypto.Keccak256([]byte("swapExactTokensForTokens(uint256,uint256,address[],address,uint256)"))[:4])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get ABI method: %v", err)
|
||||
}
|
||||
|
||||
// Validate data length before unpacking
|
||||
if len(data) == 0 {
|
||||
return nil, fmt.Errorf("data is empty")
|
||||
}
|
||||
|
||||
inputs, err := method.Inputs.Unpack(data)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to unpack ABI data: %v", err)
|
||||
}
|
||||
|
||||
if len(inputs) < 5 {
|
||||
return nil, fmt.Errorf("insufficient swap parameters: got %d, expected 5", len(inputs))
|
||||
}
|
||||
|
||||
// Extract parameters with validation
|
||||
amountIn, ok := inputs[0].(*big.Int)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("amountIn is not a *big.Int")
|
||||
}
|
||||
|
||||
// Validate amountIn is not negative
|
||||
if amountIn.Sign() < 0 {
|
||||
return nil, fmt.Errorf("negative amountIn")
|
||||
}
|
||||
|
||||
interaction.AmountIn = amountIn
|
||||
|
||||
// amountOutMin := inputs[1].(*big.Int)
|
||||
path, ok := inputs[2].([]common.Address)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("path is not []common.Address")
|
||||
}
|
||||
|
||||
// Validate path
|
||||
if len(path) < 2 {
|
||||
return nil, fmt.Errorf("path must contain at least 2 tokens, got %d", len(path))
|
||||
}
|
||||
|
||||
// Validate addresses in path are not zero
|
||||
for i, addr := range path {
|
||||
if addr == (common.Address{}) {
|
||||
return nil, fmt.Errorf("zero address in path at index %d", i)
|
||||
}
|
||||
}
|
||||
|
||||
recipient, ok := inputs[3].(common.Address)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("recipient is not common.Address")
|
||||
}
|
||||
|
||||
// Validate recipient is not zero
|
||||
if recipient == (common.Address{}) {
|
||||
return nil, fmt.Errorf("recipient address is zero")
|
||||
}
|
||||
|
||||
interaction.Recipient = recipient
|
||||
interaction.Deadline = inputs[4].(*big.Int).Uint64()
|
||||
|
||||
interaction.TokenIn = path[0]
|
||||
interaction.TokenOut = path[len(path)-1]
|
||||
|
||||
return interaction, nil
|
||||
}
|
||||
|
||||
// parseSwapTokensForExactTokens parses exact output swaps
|
||||
func (p *L2MessageParser) parseSwapTokensForExactTokens(interaction *DEXInteraction, data []byte) (*DEXInteraction, error) {
|
||||
// Validate inputs
|
||||
if interaction == nil {
|
||||
return nil, fmt.Errorf("interaction is nil")
|
||||
}
|
||||
|
||||
if data == nil {
|
||||
return nil, fmt.Errorf("data is nil")
|
||||
}
|
||||
|
||||
// Uniswap V2 swapTokensForExactTokens structure:
|
||||
// function swapTokensForExactTokens(
|
||||
// uint256 amountOut,
|
||||
// uint256 amountInMax,
|
||||
// address[] calldata path,
|
||||
// address to,
|
||||
// uint256 deadline
|
||||
// )
|
||||
|
||||
// Validate minimum data length (at least 5 parameters * 32 bytes each)
|
||||
if len(data) < 160 {
|
||||
return nil, fmt.Errorf("insufficient data for swapTokensForExactTokens: %d bytes", len(data))
|
||||
}
|
||||
|
||||
// Parse parameters with bounds checking
|
||||
// amountOut (first parameter) - bytes 0-31
|
||||
if len(data) >= 32 {
|
||||
amountOut := new(big.Int).SetBytes(data[0:32])
|
||||
// Validate amount is reasonable (not negative)
|
||||
if amountOut.Sign() < 0 {
|
||||
return nil, fmt.Errorf("negative amountOut")
|
||||
}
|
||||
interaction.AmountOut = amountOut
|
||||
}
|
||||
|
||||
// amountInMax (second parameter) - bytes 32-63
|
||||
if len(data) >= 64 {
|
||||
amountInMax := new(big.Int).SetBytes(data[32:64])
|
||||
// Validate amount is reasonable (not negative)
|
||||
if amountInMax.Sign() < 0 {
|
||||
return nil, fmt.Errorf("negative amountInMax")
|
||||
}
|
||||
interaction.AmountIn = amountInMax
|
||||
}
|
||||
|
||||
// path offset (third parameter) - bytes 64-95
|
||||
// For now, we'll extract the first and last tokens from path if possible
|
||||
// In a full implementation, we'd parse the entire path array
|
||||
|
||||
// recipient (fourth parameter) - bytes 96-127, address is in last 20 bytes (108-127)
|
||||
if len(data) >= 128 {
|
||||
interaction.Recipient = common.BytesToAddress(data[108:128])
|
||||
}
|
||||
|
||||
// deadline (fifth parameter) - bytes 128-159, uint64 is in last 8 bytes (152-159)
|
||||
if len(data) >= 160 {
|
||||
interaction.Deadline = binary.BigEndian.Uint64(data[152:160])
|
||||
}
|
||||
|
||||
// Set default values for fields that might not be parsed
|
||||
if interaction.AmountIn == nil {
|
||||
interaction.AmountIn = big.NewInt(0)
|
||||
}
|
||||
|
||||
return interaction, nil
|
||||
}
|
||||
|
||||
// parseSwapExactETHForTokens parses ETH to token swaps
|
||||
func (p *L2MessageParser) parseSwapExactETHForTokens(interaction *DEXInteraction, data []byte) (*DEXInteraction, error) {
|
||||
// Validate inputs
|
||||
if interaction == nil {
|
||||
return nil, fmt.Errorf("interaction is nil")
|
||||
}
|
||||
|
||||
if data == nil {
|
||||
return nil, fmt.Errorf("data is nil")
|
||||
}
|
||||
|
||||
// Uniswap V2 swapExactETHForTokens structure:
|
||||
// function swapExactETHForTokens(
|
||||
// uint256 amountOutMin,
|
||||
// address[] calldata path,
|
||||
// address to,
|
||||
// uint256 deadline
|
||||
// )
|
||||
|
||||
// Validate minimum data length (at least 4 parameters * 32 bytes each)
|
||||
if len(data) < 128 {
|
||||
return nil, fmt.Errorf("insufficient data for swapExactETHForTokens: %d bytes", len(data))
|
||||
}
|
||||
|
||||
// Parse parameters with bounds checking
|
||||
// amountOutMin (first parameter) - bytes 0-31
|
||||
if len(data) >= 32 {
|
||||
amountOutMin := new(big.Int).SetBytes(data[0:32])
|
||||
// Validate amount is reasonable (not negative)
|
||||
if amountOutMin.Sign() < 0 {
|
||||
return nil, fmt.Errorf("negative amountOutMin")
|
||||
}
|
||||
interaction.AmountOut = amountOutMin
|
||||
}
|
||||
|
||||
// ETH is always tokenIn for this function
|
||||
interaction.TokenIn = common.HexToAddress("0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE") // Special address for ETH
|
||||
|
||||
// path offset (second parameter) - bytes 32-63
|
||||
// For now, we'll extract the last token from path if possible
|
||||
// In a full implementation, we'd parse the entire path array
|
||||
|
||||
// recipient (third parameter) - bytes 64-95, address is in last 20 bytes (76-95)
|
||||
if len(data) >= 96 {
|
||||
interaction.Recipient = common.BytesToAddress(data[76:96])
|
||||
}
|
||||
|
||||
// deadline (fourth parameter) - bytes 96-127, uint64 is in last 8 bytes (120-127)
|
||||
if len(data) >= 128 {
|
||||
interaction.Deadline = binary.BigEndian.Uint64(data[120:128])
|
||||
}
|
||||
|
||||
return interaction, nil
|
||||
}
|
||||
|
||||
// parseSwapExactTokensForETH parses token to ETH swaps
|
||||
func (p *L2MessageParser) parseSwapExactTokensForETH(interaction *DEXInteraction, data []byte) (*DEXInteraction, error) {
|
||||
// Validate inputs
|
||||
if interaction == nil {
|
||||
return nil, fmt.Errorf("interaction is nil")
|
||||
}
|
||||
|
||||
if data == nil {
|
||||
return nil, fmt.Errorf("data is nil")
|
||||
}
|
||||
|
||||
// Uniswap V2 swapExactTokensForETH structure:
|
||||
// function swapExactTokensForETH(
|
||||
// uint256 amountIn,
|
||||
// uint256 amountOutMin,
|
||||
// address[] calldata path,
|
||||
// address to,
|
||||
// uint256 deadline
|
||||
// )
|
||||
|
||||
// Validate minimum data length (at least 5 parameters * 32 bytes each)
|
||||
if len(data) < 160 {
|
||||
return nil, fmt.Errorf("insufficient data for swapExactTokensForETH: %d bytes", len(data))
|
||||
}
|
||||
|
||||
// Parse parameters with bounds checking
|
||||
// amountIn (first parameter) - bytes 0-31
|
||||
if len(data) >= 32 {
|
||||
amountIn := new(big.Int).SetBytes(data[0:32])
|
||||
// Validate amount is reasonable (not negative)
|
||||
if amountIn.Sign() < 0 {
|
||||
return nil, fmt.Errorf("negative amountIn")
|
||||
}
|
||||
interaction.AmountIn = amountIn
|
||||
}
|
||||
|
||||
// amountOutMin (second parameter) - bytes 32-63
|
||||
if len(data) >= 64 {
|
||||
amountOutMin := new(big.Int).SetBytes(data[32:64])
|
||||
// Validate amount is reasonable (not negative)
|
||||
if amountOutMin.Sign() < 0 {
|
||||
return nil, fmt.Errorf("negative amountOutMin")
|
||||
}
|
||||
interaction.AmountOut = amountOutMin
|
||||
}
|
||||
|
||||
// ETH is always tokenOut for this function
|
||||
interaction.TokenOut = common.HexToAddress("0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE") // Special address for ETH
|
||||
|
||||
// path offset (third parameter) - bytes 64-95
|
||||
// For now, we'll extract the first token from path if possible
|
||||
// In a full implementation, we'd parse the entire path array
|
||||
|
||||
// recipient (fourth parameter) - bytes 96-127, address is in last 20 bytes (108-127)
|
||||
if len(data) >= 128 {
|
||||
interaction.Recipient = common.BytesToAddress(data[108:128])
|
||||
}
|
||||
|
||||
// deadline (fifth parameter) - bytes 128-159, uint64 is in last 8 bytes (152-159)
|
||||
if len(data) >= 160 {
|
||||
interaction.Deadline = binary.BigEndian.Uint64(data[152:160])
|
||||
}
|
||||
|
||||
return interaction, nil
|
||||
}
|
||||
|
||||
func (p *L2MessageParser) parseUniswapV3SingleSwap(interaction *DEXInteraction, data []byte, isExactInput bool) (*DEXInteraction, error) {
|
||||
// Validate inputs
|
||||
if interaction == nil {
|
||||
return nil, fmt.Errorf("interaction is nil")
|
||||
}
|
||||
|
||||
if data == nil {
|
||||
return nil, fmt.Errorf("data is nil")
|
||||
}
|
||||
|
||||
// Validate minimum data length (at least 8 parameters * 32 bytes each)
|
||||
if len(data) < 256 {
|
||||
return nil, fmt.Errorf("insufficient data for single swap: %d bytes", len(data))
|
||||
}
|
||||
|
||||
// Parse parameters with bounds checking
|
||||
if len(data) >= 32 {
|
||||
interaction.TokenIn = common.BytesToAddress(data[12:32])
|
||||
}
|
||||
if len(data) >= 64 {
|
||||
interaction.TokenOut = common.BytesToAddress(data[44:64])
|
||||
}
|
||||
if len(data) >= 128 {
|
||||
interaction.Recipient = common.BytesToAddress(data[108:128])
|
||||
}
|
||||
if len(data) >= 160 {
|
||||
interaction.Deadline = binary.BigEndian.Uint64(data[152:160])
|
||||
}
|
||||
|
||||
if isExactInput {
|
||||
if len(data) >= 192 {
|
||||
amountIn := new(big.Int).SetBytes(data[160:192])
|
||||
if amountIn.Sign() < 0 {
|
||||
return nil, fmt.Errorf("negative amountIn")
|
||||
}
|
||||
interaction.AmountIn = amountIn
|
||||
}
|
||||
if len(data) >= 224 {
|
||||
amountOutMin := new(big.Int).SetBytes(data[192:224])
|
||||
if amountOutMin.Sign() < 0 {
|
||||
return nil, fmt.Errorf("negative amountOutMinimum")
|
||||
}
|
||||
interaction.AmountOut = amountOutMin
|
||||
}
|
||||
} else { // exact output
|
||||
if len(data) >= 192 {
|
||||
amountOut := new(big.Int).SetBytes(data[160:192])
|
||||
if amountOut.Sign() < 0 {
|
||||
return nil, fmt.Errorf("negative amountOut")
|
||||
}
|
||||
interaction.AmountOut = amountOut
|
||||
}
|
||||
if len(data) >= 224 {
|
||||
amountInMax := new(big.Int).SetBytes(data[192:224])
|
||||
if amountInMax.Sign() < 0 {
|
||||
return nil, fmt.Errorf("negative amountInMaximum")
|
||||
}
|
||||
interaction.AmountIn = amountInMax
|
||||
}
|
||||
}
|
||||
|
||||
if interaction.AmountOut == nil {
|
||||
interaction.AmountOut = big.NewInt(0)
|
||||
}
|
||||
if interaction.AmountIn == nil {
|
||||
interaction.AmountIn = big.NewInt(0)
|
||||
}
|
||||
|
||||
if interaction.TokenIn == (common.Address{}) && interaction.TokenOut == (common.Address{}) {
|
||||
return nil, fmt.Errorf("unable to parse token addresses from data")
|
||||
}
|
||||
|
||||
return interaction, nil
|
||||
}
|
||||
|
||||
// parseExactOutputSingle parses Uniswap V3 exact output single pool swap
|
||||
func (p *L2MessageParser) parseExactOutputSingle(interaction *DEXInteraction, data []byte) (*DEXInteraction, error) {
|
||||
return p.parseUniswapV3SingleSwap(interaction, data, false)
|
||||
}
|
||||
|
||||
// parseExactOutput parses Uniswap V3 exact output multi-hop swap
|
||||
func (p *L2MessageParser) parseExactOutput(interaction *DEXInteraction, data []byte) (*DEXInteraction, error) {
|
||||
// Validate inputs
|
||||
if interaction == nil {
|
||||
return nil, fmt.Errorf("interaction is nil")
|
||||
}
|
||||
|
||||
if data == nil {
|
||||
return nil, fmt.Errorf("data is nil")
|
||||
}
|
||||
|
||||
// Uniswap V3 exactOutput structure:
|
||||
// function exactOutput(ExactOutputParams calldata params)
|
||||
// struct ExactOutputParams {
|
||||
// bytes path;
|
||||
// address recipient;
|
||||
// uint256 deadline;
|
||||
// uint256 amountOut;
|
||||
// uint256 amountInMaximum;
|
||||
// }
|
||||
|
||||
// Validate minimum data length (at least 5 parameters * 32 bytes each)
|
||||
if len(data) < 160 {
|
||||
return nil, fmt.Errorf("insufficient data for exactOutput: %d bytes", len(data))
|
||||
}
|
||||
|
||||
// Parse parameters with bounds checking
|
||||
// path offset (first parameter) - bytes 0-31
|
||||
// For now, we'll extract tokens from path if possible
|
||||
// In a full implementation, we'd parse the entire path bytes
|
||||
|
||||
// recipient (second parameter) - bytes 32-63, address is in last 20 bytes (44-63)
|
||||
if len(data) >= 64 {
|
||||
interaction.Recipient = common.BytesToAddress(data[44:64])
|
||||
}
|
||||
|
||||
// deadline (third parameter) - bytes 64-95, uint64 is in last 8 bytes (88-95)
|
||||
if len(data) >= 96 {
|
||||
interaction.Deadline = binary.BigEndian.Uint64(data[88:96])
|
||||
}
|
||||
|
||||
// amountOut (fourth parameter) - bytes 96-127
|
||||
if len(data) >= 128 {
|
||||
amountOut := new(big.Int).SetBytes(data[96:128])
|
||||
// Validate amount is reasonable (not negative)
|
||||
if amountOut.Sign() < 0 {
|
||||
return nil, fmt.Errorf("negative amountOut")
|
||||
}
|
||||
interaction.AmountOut = amountOut
|
||||
}
|
||||
|
||||
// amountInMaximum (fifth parameter) - bytes 128-159
|
||||
if len(data) >= 160 {
|
||||
amountInMax := new(big.Int).SetBytes(data[128:160])
|
||||
// Validate amount is reasonable (not negative)
|
||||
if amountInMax.Sign() < 0 {
|
||||
return nil, fmt.Errorf("negative amountInMaximum")
|
||||
}
|
||||
interaction.AmountIn = amountInMax
|
||||
}
|
||||
|
||||
// Set default values for fields that might not be parsed
|
||||
if interaction.AmountOut == nil {
|
||||
interaction.AmountOut = big.NewInt(0)
|
||||
}
|
||||
|
||||
return interaction, nil
|
||||
}
|
||||
|
||||
// parseMulticall parses Uniswap V3 multicall transactions
|
||||
func (p *L2MessageParser) parseMulticall(interaction *DEXInteraction, data []byte) (*DEXInteraction, error) {
|
||||
// Validate inputs
|
||||
if interaction == nil {
|
||||
return nil, fmt.Errorf("interaction is nil")
|
||||
}
|
||||
|
||||
if data == nil {
|
||||
return nil, fmt.Errorf("data is nil")
|
||||
}
|
||||
|
||||
// Uniswap V3 multicall structure:
|
||||
// function multicall(uint256 deadline, bytes[] calldata data)
|
||||
// or
|
||||
// function multicall(bytes[] calldata data)
|
||||
|
||||
// For simplicity, we'll handle the more common version with just bytes[] parameter
|
||||
// bytes[] calldata data - this is a dynamic array
|
||||
// TODO: Implement comprehensive multicall parameter parsing for full DEX support
|
||||
// Current simplified implementation may miss profitable MEV opportunities
|
||||
|
||||
// Validate minimum data length (at least 1 parameter * 32 bytes for array offset)
|
||||
if len(data) < 32 {
|
||||
return nil, fmt.Errorf("insufficient data for multicall: %d bytes", len(data))
|
||||
}
|
||||
|
||||
// Parse array offset (first parameter) - bytes 0-31
|
||||
// For now, we'll just acknowledge this is a multicall transaction
|
||||
// A full implementation would parse each call in the data array
|
||||
|
||||
// Set a flag to indicate this is a multicall transaction
|
||||
// This would typically be handled differently in a full implementation
|
||||
|
||||
return interaction, nil
|
||||
}
|
||||
|
||||
// parseExactInputSingle parses Uniswap V3 single pool swap
|
||||
func (p *L2MessageParser) parseExactInputSingle(interaction *DEXInteraction, data []byte) (*DEXInteraction, error) {
|
||||
return p.parseUniswapV3SingleSwap(interaction, data, true)
|
||||
}
|
||||
|
||||
// parseExactInput parses Uniswap V3 multi-hop swap
|
||||
func (p *L2MessageParser) parseExactInput(interaction *DEXInteraction, data []byte) (*DEXInteraction, error) {
|
||||
// Validate inputs
|
||||
if interaction == nil {
|
||||
return nil, fmt.Errorf("interaction is nil")
|
||||
}
|
||||
|
||||
if data == nil {
|
||||
return nil, fmt.Errorf("data is nil")
|
||||
}
|
||||
|
||||
// Uniswap V3 exactInput structure:
|
||||
// function exactInput(ExactInputParams calldata params)
|
||||
// struct ExactInputParams {
|
||||
// bytes path;
|
||||
// address recipient;
|
||||
// uint256 deadline;
|
||||
// uint256 amountIn;
|
||||
// uint256 amountOutMinimum;
|
||||
// }
|
||||
|
||||
// Validate minimum data length (at least 5 parameters * 32 bytes each)
|
||||
if len(data) < 160 {
|
||||
return nil, fmt.Errorf("insufficient data for exactInput: %d bytes", len(data))
|
||||
}
|
||||
|
||||
// Parse parameters with bounds checking
|
||||
// path offset (first parameter) - bytes 0-31
|
||||
// For now, we'll extract tokens from path if possible
|
||||
// In a full implementation, we'd parse the entire path bytes
|
||||
|
||||
// recipient (second parameter) - bytes 32-63, address is in last 20 bytes (44-63)
|
||||
if len(data) >= 64 {
|
||||
interaction.Recipient = common.BytesToAddress(data[44:64])
|
||||
}
|
||||
|
||||
// deadline (third parameter) - bytes 64-95, uint64 is in last 8 bytes (88-95)
|
||||
if len(data) >= 96 {
|
||||
interaction.Deadline = binary.BigEndian.Uint64(data[88:96])
|
||||
}
|
||||
|
||||
// amountIn (fourth parameter) - bytes 96-127
|
||||
if len(data) >= 128 {
|
||||
amountIn := new(big.Int).SetBytes(data[96:128])
|
||||
// Validate amount is reasonable (not negative)
|
||||
if amountIn.Sign() < 0 {
|
||||
return nil, fmt.Errorf("negative amountIn")
|
||||
}
|
||||
interaction.AmountIn = amountIn
|
||||
}
|
||||
|
||||
// amountOutMinimum (fifth parameter) - bytes 128-159
|
||||
if len(data) >= 160 {
|
||||
amountOutMin := new(big.Int).SetBytes(data[128:160])
|
||||
// Validate amount is reasonable (not negative)
|
||||
if amountOutMin.Sign() < 0 {
|
||||
return nil, fmt.Errorf("negative amountOutMinimum")
|
||||
}
|
||||
interaction.AmountOut = amountOutMin
|
||||
}
|
||||
|
||||
// Set default values for fields that might not be parsed
|
||||
if interaction.AmountIn == nil {
|
||||
interaction.AmountIn = big.NewInt(0)
|
||||
}
|
||||
|
||||
return interaction, nil
|
||||
}
|
||||
|
||||
// IsSignificantSwap determines if a DEX interaction is significant enough to monitor
|
||||
func (p *L2MessageParser) IsSignificantSwap(interaction *DEXInteraction, minAmountUSD float64) bool {
|
||||
// Validate inputs
|
||||
if interaction == nil {
|
||||
p.logger.Warn("IsSignificantSwap called with nil interaction")
|
||||
return false
|
||||
}
|
||||
|
||||
// Validate minAmountUSD
|
||||
if minAmountUSD < 0 {
|
||||
p.logger.Warn(fmt.Sprintf("Negative minAmountUSD: %f", minAmountUSD))
|
||||
return false
|
||||
}
|
||||
|
||||
// This would implement logic to determine if the swap is large enough
|
||||
// to be worth monitoring for arbitrage opportunities
|
||||
|
||||
// For now, check if amount is above a threshold
|
||||
if interaction.AmountIn == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// Validate AmountIn is not negative
|
||||
if interaction.AmountIn.Sign() < 0 {
|
||||
p.logger.Warn("Negative AmountIn in DEX interaction")
|
||||
return false
|
||||
}
|
||||
|
||||
// Simplified check - in practice, you'd convert to USD value
|
||||
threshold := new(big.Int).Exp(big.NewInt(10), big.NewInt(18), nil) // 1 ETH worth
|
||||
|
||||
// Validate threshold
|
||||
if threshold == nil || threshold.Sign() <= 0 {
|
||||
p.logger.Error("Invalid threshold calculation")
|
||||
return false
|
||||
}
|
||||
|
||||
return interaction.AmountIn.Cmp(threshold) >= 0
|
||||
}
|
||||
Reference in New Issue
Block a user