- Fixed duplicate package declarations in arbitrum parser - Resolved missing methods in events parser (ParseTransaction, AddKnownPool) - Fixed logger test assertion failures by updating expected log format - Updated NewPipeline constructor calls to include ethClient parameter - Fixed nil pointer dereference in pipeline processing - Corrected known pool mappings for protocol identification - Removed duplicate entries in parser initialization - Added proper error handling and validation in parsers These changes resolve the build failures and integration test crashes that were preventing proper testing of the MEV bot functionality. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
1050 lines
32 KiB
Go
1050 lines
32 KiB
Go
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
|
|
}
|
|
|
|
// parseExactOutputSingle parses Uniswap V3 exact output single pool swap
|
|
func (p *L2MessageParser) parseExactOutputSingle(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 exactOutputSingle structure:
|
|
// struct ExactOutputSingleParams {
|
|
// address tokenIn;
|
|
// address tokenOut;
|
|
// uint24 fee;
|
|
// address recipient;
|
|
// uint256 deadline;
|
|
// uint256 amountOut;
|
|
// uint256 amountInMaximum;
|
|
// uint160 sqrtPriceLimitX96;
|
|
// }
|
|
|
|
// Validate minimum data length (at least 8 parameters * 32 bytes each)
|
|
if len(data) < 256 {
|
|
return nil, fmt.Errorf("insufficient data for exactOutputSingle: %d bytes", len(data))
|
|
}
|
|
|
|
// Parse parameters with bounds checking
|
|
// tokenIn (first parameter) - bytes 0-31, address is in last 20 bytes (12-31)
|
|
if len(data) >= 32 {
|
|
interaction.TokenIn = common.BytesToAddress(data[12:32])
|
|
}
|
|
|
|
// tokenOut (second parameter) - bytes 32-63, address is in last 20 bytes (44-63)
|
|
if len(data) >= 64 {
|
|
interaction.TokenOut = common.BytesToAddress(data[44:64])
|
|
}
|
|
|
|
// 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])
|
|
}
|
|
|
|
// amountOut (sixth parameter) - bytes 160-191
|
|
if len(data) >= 192 {
|
|
amountOut := new(big.Int).SetBytes(data[160:192])
|
|
// Validate amount is reasonable (not negative)
|
|
if amountOut.Sign() < 0 {
|
|
return nil, fmt.Errorf("negative amountOut")
|
|
}
|
|
interaction.AmountOut = amountOut
|
|
}
|
|
|
|
// amountInMaximum (seventh parameter) - bytes 192-223
|
|
if len(data) >= 224 {
|
|
amountInMax := new(big.Int).SetBytes(data[192:224])
|
|
// 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)
|
|
}
|
|
|
|
// Validate that we have required fields
|
|
if interaction.TokenIn == (common.Address{}) && interaction.TokenOut == (common.Address{}) {
|
|
// If both are zero, we likely don't have valid data
|
|
return nil, fmt.Errorf("unable to parse token addresses from data")
|
|
}
|
|
|
|
return interaction, nil
|
|
}
|
|
|
|
// 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
|
|
|
|
// 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) {
|
|
// Validate inputs
|
|
if interaction == nil {
|
|
return nil, fmt.Errorf("interaction is nil")
|
|
}
|
|
|
|
if data == nil {
|
|
return nil, fmt.Errorf("data is nil")
|
|
}
|
|
|
|
// Uniswap V3 exactInputSingle structure:
|
|
// struct ExactInputSingleParams {
|
|
// address tokenIn;
|
|
// address tokenOut;
|
|
// uint24 fee;
|
|
// address recipient;
|
|
// uint256 deadline;
|
|
// uint256 amountIn;
|
|
// uint256 amountOutMinimum;
|
|
// uint160 sqrtPriceLimitX96;
|
|
// }
|
|
|
|
// Validate minimum data length (at least 8 parameters * 32 bytes each)
|
|
if len(data) < 256 {
|
|
return nil, fmt.Errorf("insufficient data for exactInputSingle: %d bytes", len(data))
|
|
}
|
|
|
|
// Parse parameters with bounds checking
|
|
// tokenIn (first parameter) - bytes 0-31, address is in last 20 bytes (12-31)
|
|
if len(data) >= 32 {
|
|
interaction.TokenIn = common.BytesToAddress(data[12:32])
|
|
}
|
|
|
|
// tokenOut (second parameter) - bytes 32-63, address is in last 20 bytes (44-63)
|
|
if len(data) >= 64 {
|
|
interaction.TokenOut = common.BytesToAddress(data[44:64])
|
|
}
|
|
|
|
// 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])
|
|
}
|
|
|
|
// amountIn (sixth parameter) - bytes 160-191
|
|
if len(data) >= 192 {
|
|
amountIn := new(big.Int).SetBytes(data[160:192])
|
|
// Validate amount is reasonable (not negative)
|
|
if amountIn.Sign() < 0 {
|
|
return nil, fmt.Errorf("negative amountIn")
|
|
}
|
|
interaction.AmountIn = amountIn
|
|
}
|
|
|
|
// amountOutMinimum (seventh parameter) - bytes 192-223
|
|
if len(data) >= 224 {
|
|
amountOutMin := new(big.Int).SetBytes(data[192:224])
|
|
// 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.AmountOut == nil {
|
|
interaction.AmountOut = big.NewInt(0)
|
|
}
|
|
|
|
// Validate that we have required fields
|
|
if interaction.TokenIn == (common.Address{}) && interaction.TokenOut == (common.Address{}) {
|
|
// If both are zero, we likely don't have valid data
|
|
return nil, fmt.Errorf("unable to parse token addresses from data")
|
|
}
|
|
|
|
// Note: We're not strictly validating that addresses are non-zero since some
|
|
// transactions might legitimately use zero addresses in certain contexts
|
|
// The calling code should validate addresses as appropriate for their use case
|
|
|
|
return interaction, nil
|
|
}
|
|
|
|
// 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
|
|
}
|