Files
mev-beta/orig/pkg/arbitrum/parser/transaction_analyzer.go
Administrator 803de231ba feat: create v2-prep branch with comprehensive planning
Restructured project for V2 refactor:

**Structure Changes:**
- Moved all V1 code to orig/ folder (preserved with git mv)
- Created docs/planning/ directory
- Added orig/README_V1.md explaining V1 preservation

**Planning Documents:**
- 00_V2_MASTER_PLAN.md: Complete architecture overview
  - Executive summary of critical V1 issues
  - High-level component architecture diagrams
  - 5-phase implementation roadmap
  - Success metrics and risk mitigation

- 07_TASK_BREAKDOWN.md: Atomic task breakdown
  - 99+ hours of detailed tasks
  - Every task < 2 hours (atomic)
  - Clear dependencies and success criteria
  - Organized by implementation phase

**V2 Key Improvements:**
- Per-exchange parsers (factory pattern)
- Multi-layer strict validation
- Multi-index pool cache
- Background validation pipeline
- Comprehensive observability

**Critical Issues Addressed:**
- Zero address tokens (strict validation + cache enrichment)
- Parsing accuracy (protocol-specific parsers)
- No audit trail (background validation channel)
- Inefficient lookups (multi-index cache)
- Stats disconnection (event-driven metrics)

Next Steps:
1. Review planning documents
2. Begin Phase 1: Foundation (P1-001 through P1-010)
3. Implement parsers in Phase 2
4. Build cache system in Phase 3
5. Add validation pipeline in Phase 4
6. Migrate and test in Phase 5

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-10 10:14:26 +01:00

1073 lines
36 KiB
Go

package parser
import (
"context"
"encoding/hex"
"fmt"
"math/big"
"strconv"
"strings"
"time"
"github.com/ethereum/go-ethereum/common"
"github.com/fraktal/mev-beta/internal/logger"
"github.com/fraktal/mev-beta/pkg/market"
"github.com/fraktal/mev-beta/pkg/math"
pkgtypes "github.com/fraktal/mev-beta/pkg/types"
)
// TransactionAnalyzer analyzes transactions for MEV opportunities
type TransactionAnalyzer struct {
logger *logger.Logger
protocolRegistry interface{} // Will use interface{} to avoid import issues
abiDecoder ABIDecoder
mathCalculator *math.MathCalculator
marketDiscovery *market.MarketManager
mevAnalyzer *MEVAnalyzer
executor *Executor // Add executor for opportunity execution
}
// NewTransactionAnalyzer creates a new transaction analyzer
func NewTransactionAnalyzer(
logger *logger.Logger,
protocolRegistry interface{},
abiDecoder ABIDecoder,
mathCalculator *math.MathCalculator,
marketDiscovery *market.MarketManager,
mevAnalyzer *MEVAnalyzer,
executor *Executor, // Add executor parameter
) *TransactionAnalyzer {
return &TransactionAnalyzer{
logger: logger,
protocolRegistry: protocolRegistry,
abiDecoder: abiDecoder,
mathCalculator: mathCalculator,
marketDiscovery: marketDiscovery,
mevAnalyzer: mevAnalyzer,
executor: executor, // Store executor
}
}
// analyzeTransactionForMEV analyzes a single transaction for MEV opportunities
func (ta *TransactionAnalyzer) analyzeTransactionForMEV(ctx context.Context, tx *RawL2Transaction, opportunities *MEVOpportunities) error {
// Parse transaction input data
input, err := hex.DecodeString(strings.TrimPrefix(tx.Input, "0x"))
if err != nil {
return fmt.Errorf("failed to decode input: %w", err)
}
// Skip transactions with insufficient input data
if len(input) < 4 {
return nil
}
// Extract function signature
funcSig := hex.EncodeToString(input[:4])
// Skip transactions with insufficient input data for DEX functions
// (In a full implementation, we'd have a way to identify DEX transactions)
if len(input) < 20 { // 4 bytes signature + some parameters
return nil
}
// For now, treat all transactions as potentially containing swaps
// In the full implementation, we'd use protocolRegistry to identify DEX functions
protocol := "unknown"
functionName := "unknown"
isSwap := true // Simplified for compilation - in reality, we'd check the function
if isSwap {
return ta.handleSwapTransaction(ctx, tx, protocol, functionName, input, opportunities)
}
// Check for liquidation functions
if ta.isLiquidationFunction(funcSig) {
return ta.handleLiquidationTransaction(ctx, tx, funcSig, input, opportunities)
}
// Check for liquidity modification functions
if ta.isLiquidityFunction(funcSig) {
return ta.handleLiquidityTransaction(ctx, tx, funcSig, input, opportunities)
}
return nil
}
// handleSwapTransaction processes swap transactions
func (ta *TransactionAnalyzer) handleSwapTransaction(ctx context.Context, tx *RawL2Transaction, protocol, functionName string, input []byte, opportunities *MEVOpportunities) error {
// Parse swap parameters based on protocol
swapData, err := ta.parseSwapData(protocol, functionName, input)
if err != nil {
return fmt.Errorf("failed to parse swap data: %w", err)
}
// Convert transaction data properly
blockNumber := uint64(0) // Will be set by the block processing context
gasUsed, err := strconv.ParseUint(strings.TrimPrefix(tx.Gas, "0x"), 16, 64)
if err != nil {
gasUsed = 0
}
// Create swap event with proper default values
swapEvent := &SwapEvent{
Timestamp: time.Now(),
BlockNumber: blockNumber, // Will be updated when actual block number is known
TxHash: tx.Hash,
Protocol: protocol,
Router: common.HexToAddress(tx.To),
Pool: common.HexToAddress(swapData.Pool),
TokenIn: common.HexToAddress(swapData.TokenIn),
TokenOut: common.HexToAddress(swapData.TokenOut),
AmountIn: parseStringToBigInt(swapData.AmountIn),
AmountOut: parseStringToBigInt(swapData.AmountOut),
Sender: common.HexToAddress(tx.From),
Recipient: common.HexToAddress(swapData.Recipient),
GasPrice: tx.GasPrice,
GasUsed: gasUsed,
PriceImpact: swapData.PriceImpact,
MEVScore: ta.calculateMEVScore(swapData),
Profitable: swapData.PriceImpact > 0.001, // Lower threshold for profitability
}
// Add to opportunities for immediate processing
opportunities.SwapEvents = append(opportunities.SwapEvents, swapEvent)
// Check for arbitrage opportunities
if swapEvent.Profitable {
arbOp := ta.findArbitrageOpportunity(ctx, swapData)
if arbOp != nil {
opportunities.ArbitrageOps = append(opportunities.ArbitrageOps, arbOp)
}
}
// Check for sandwich attack opportunities
sandwichOp := ta.findSandwichOpportunity(ctx, swapData, tx)
if sandwichOp != nil {
opportunities.SandwichOps = append(opportunities.SandwichOps, sandwichOp)
}
return nil
}
// handleLiquidationTransaction processes liquidation transactions
func (ta *TransactionAnalyzer) handleLiquidationTransaction(ctx context.Context, tx *RawL2Transaction, funcSig string, input []byte, opportunities *MEVOpportunities) error {
// Parse liquidation data
liquidationData, err := ta.parseLiquidationData(funcSig, input)
if err != nil {
return fmt.Errorf("failed to parse liquidation data: %w", err)
}
blockNumber, _ := strconv.ParseUint(strings.TrimPrefix(tx.Hash, "0x"), 16, 64)
// Create liquidation event
liquidationEvent := &LiquidationEvent{
Timestamp: time.Now(),
BlockNumber: blockNumber,
TxHash: tx.Hash,
Protocol: liquidationData.Protocol,
Liquidator: common.HexToAddress(tx.From),
Borrower: common.HexToAddress(liquidationData.Borrower),
CollateralToken: common.HexToAddress(liquidationData.CollateralToken),
DebtToken: common.HexToAddress(liquidationData.DebtToken),
CollateralAmount: parseStringToBigInt(liquidationData.CollateralAmount),
DebtAmount: parseStringToBigInt(liquidationData.DebtAmount),
Bonus: parseStringToBigInt(liquidationData.LiquidationBonus),
HealthFactor: liquidationData.HealthFactor,
MEVOpportunity: liquidationData.HealthFactor < 1.05, // Under-collateralized
EstimatedProfit: parseStringToBigInt(liquidationData.EstimatedProfit),
}
opportunities.LiquidationEvents = append(opportunities.LiquidationEvents, liquidationEvent)
// Check for liquidation MEV opportunities
if liquidationEvent.MEVOpportunity {
liqOp := ta.findLiquidationOpportunity(ctx, liquidationData)
if liqOp != nil {
opportunities.LiquidationOps = append(opportunities.LiquidationOps, liqOp)
}
}
return nil
}
// handleLiquidityTransaction processes liquidity modification transactions
func (ta *TransactionAnalyzer) handleLiquidityTransaction(ctx context.Context, tx *RawL2Transaction, funcSig string, input []byte, opportunities *MEVOpportunities) error {
// Parse liquidity data
liquidityData, err := ta.parseLiquidityData(funcSig, input)
if err != nil {
return fmt.Errorf("failed to parse liquidity data: %w", err)
}
blockNumber, _ := strconv.ParseUint(strings.TrimPrefix(tx.Hash, "0x"), 16, 64)
// Create liquidity event
liquidityEvent := &LiquidityEvent{
Timestamp: time.Now(),
BlockNumber: blockNumber,
TxHash: tx.Hash,
Protocol: liquidityData.Protocol,
Pool: common.HexToAddress(liquidityData.Pool),
EventType: liquidityData.EventType,
Token0: common.HexToAddress(liquidityData.Token0),
Token1: common.HexToAddress(liquidityData.Token1),
Amount0: parseStringToBigInt(liquidityData.Amount0),
Amount1: parseStringToBigInt(liquidityData.Amount1),
Liquidity: parseStringToBigInt(liquidityData.Liquidity),
PriceAfter: big.NewFloat(0), // Not available from function signature
ImpactSize: liquidityData.ImpactSize,
ArbitrageOpp: liquidityData.ImpactSize > 0.01, // 1% threshold
}
opportunities.LiquidityEvents = append(opportunities.LiquidityEvents, liquidityEvent)
return nil
}
// Helper methods for function identification
func (ta *TransactionAnalyzer) isLiquidationFunction(funcSig string) bool {
liquidationSigs := map[string]bool{
"630d4904": true, // liquidationCall (Aave/Radiant)
"96cd4ddb": true, // liquidateBorrow (Compound-style)
"f5e3c462": true, // liquidate (GMX)
"871dd7c8": true, // liquidatePosition (GMX)
}
return liquidationSigs[funcSig]
}
func (ta *TransactionAnalyzer) isLiquidityFunction(funcSig string) bool {
liquiditySigs := map[string]bool{
"e8e33700": true, // addLiquidity
"baa2abde": true, // addLiquidityETH
"02751cec": true, // removeLiquidity
"af2979eb": true, // removeLiquidityETH
"219f5d17": true, // mint (Uniswap V3)
"a34123a7": true, // burn (Uniswap V3)
"0dfe1681": true, // collect (Uniswap V3)
}
return liquiditySigs[funcSig]
}
// Data structures for parsed transaction data
type SwapData struct {
Protocol string
Pool string
TokenIn string
TokenOut string
AmountIn string
AmountOut string
Recipient string
PriceImpact float64
}
type LiquidationData struct {
Protocol string
Borrower string
CollateralToken string
DebtToken string
CollateralAmount string
DebtAmount string
LiquidationBonus string
HealthFactor float64
EstimatedProfit string
}
type LiquidityData struct {
Protocol string
Pool string
EventType string
Token0 string
Token1 string
Amount0 string
Amount1 string
Liquidity string
PriceAfter string
ImpactSize float64
}
// Real ABI decoding methods using the ABIDecoder
func (ta *TransactionAnalyzer) parseSwapData(protocol, functionName string, input []byte) (*SwapData, error) {
decoded, err := ta.abiDecoder.DecodeSwapTransaction(protocol, input)
if err != nil {
ta.logger.Warn("Failed to decode swap transaction",
"protocol", protocol,
"function", functionName,
"error", err)
return &SwapData{
Protocol: protocol,
Pool: "",
TokenIn: "",
TokenOut: "",
AmountIn: "0",
AmountOut: "0",
Recipient: "",
PriceImpact: 0,
}, nil
}
var swapEvent *SwapEvent
switch v := decoded.(type) {
case *SwapEvent:
swapEvent = v
case map[string]interface{}:
converted := &SwapEvent{Protocol: protocol}
if tokenIn, ok := v["TokenIn"].(common.Address); ok {
converted.TokenIn = tokenIn
}
if tokenOut, ok := v["TokenOut"].(common.Address); ok {
converted.TokenOut = tokenOut
}
if amountIn, ok := v["AmountIn"].(*big.Int); ok {
converted.AmountIn = amountIn
}
if amountOut, ok := v["AmountOut"].(*big.Int); ok {
converted.AmountOut = amountOut
}
if recipient, ok := v["Recipient"].(common.Address); ok {
converted.Recipient = recipient
}
swapEvent = converted
default:
ta.logger.Warn("Unsupported swap decode type",
"protocol", protocol,
"function", functionName,
"decoded_type", fmt.Sprintf("%T", decoded))
}
if swapEvent == nil {
return &SwapData{
Protocol: protocol,
Pool: "",
TokenIn: "",
TokenOut: "",
AmountIn: "0",
AmountOut: "0",
Recipient: "",
PriceImpact: 0,
}, nil
}
tokenInAddr := swapEvent.TokenIn
tokenOutAddr := swapEvent.TokenOut
amountInStr := "0"
if swapEvent.AmountIn != nil {
amountInStr = swapEvent.AmountIn.String()
}
amountOutStr := "0"
if swapEvent.AmountOut != nil {
amountOutStr = swapEvent.AmountOut.String()
}
recipientStr := ""
if swapEvent.Recipient != (common.Address{}) {
recipientStr = swapEvent.Recipient.Hex()
}
poolAddress := ""
if swapEvent.Pool != (common.Address{}) {
poolAddress = swapEvent.Pool.Hex()
} else if tokenInAddr != (common.Address{}) && tokenOutAddr != (common.Address{}) {
feeVal := int(swapEvent.Fee)
poolAddr, poolErr := ta.abiDecoder.CalculatePoolAddress(protocol, tokenInAddr.Hex(), tokenOutAddr.Hex(), feeVal)
if poolErr == nil {
poolAddress = poolAddr.Hex()
}
}
swapParamsModel := &SwapParams{
TokenIn: tokenInAddr,
TokenOut: tokenOutAddr,
AmountIn: swapEvent.AmountIn,
AmountOut: swapEvent.AmountOut,
Recipient: swapEvent.Recipient,
}
if swapEvent.Fee > 0 {
swapParamsModel.Fee = big.NewInt(int64(swapEvent.Fee))
}
if poolAddress != "" {
swapParamsModel.Pool = common.HexToAddress(poolAddress)
}
priceImpact := ta.calculateRealPriceImpact(protocol, swapParamsModel, poolAddress)
return &SwapData{
Protocol: protocol,
Pool: poolAddress,
TokenIn: tokenInAddr.Hex(),
TokenOut: tokenOutAddr.Hex(),
AmountIn: amountInStr,
AmountOut: amountOutStr,
Recipient: recipientStr,
PriceImpact: priceImpact,
}, nil
}
// calculateRealPriceImpact calculates real price impact using exchange-specific math
func (ta *TransactionAnalyzer) calculateRealPriceImpact(protocol string, swapParams *SwapParams, poolAddress string) float64 {
// Return default small impact if we don't have enough data
if swapParams.AmountIn == nil || swapParams.AmountIn.Cmp(big.NewInt(0)) <= 0 {
return 0.0001 // 0.01% default
}
// Get the appropriate math calculator for the protocol
mathEngine := ta.mathCalculator.GetMathForExchange(protocol)
if mathEngine == nil {
// Fallback estimation based on amount if math engine not available
amountFloat, _ := new(big.Float).SetInt(swapParams.AmountIn).Float64()
if amountFloat > 10e18 { // > 10 ETH
return 0.01 // 1%
} else if amountFloat > 1e18 { // > 1 ETH
return 0.005 // 0.5%
} else if amountFloat > 0.1e18 { // > 0.1 ETH
return 0.001 // 0.1%
} else {
return 0.0005 // 0.05%
}
}
// For real price impact calculation, we need pool reserves
// Since we don't have them readily available, we'll use a simplified approach
// that estimates based on the swap amount and known patterns
switch protocol {
case "uniswap_v2", "sushiswap", "camelot_v2", "trader_joe":
return ta.estimateUniswapV2PriceImpact(context.Background(), swapParams, mathEngine)
case "uniswap_v3", "camelot_v3", "algebra":
return ta.estimateUniswapV3PriceImpact(context.Background(), swapParams, mathEngine)
case "curve":
return ta.estimateCurvePriceImpact(context.Background(), swapParams, mathEngine)
case "balancer_v2":
return ta.estimateBalancerPriceImpact(context.Background(), swapParams)
default:
// For unknown protocols, estimate based on amount
return ta.estimateGenericPriceImpact(context.Background(), swapParams)
}
}
// parseStringToBigInt converts a string to *big.Int, handling various formats
func parseStringToBigInt(s string) *big.Int {
if s == "" {
return big.NewInt(0)
}
// Handle hex format
if strings.HasPrefix(s, "0x") {
n, ok := new(big.Int).SetString(strings.TrimPrefix(s, "0x"), 16)
if ok {
return n
}
return big.NewInt(0)
}
// Handle decimal format
n, ok := new(big.Int).SetString(s, 10)
if ok {
return n
}
return big.NewInt(0)
}
// estimateUniswapV2PriceImpact estimates price impact for Uniswap V2 style AMMs
func (ta *TransactionAnalyzer) estimateUniswapV2PriceImpact(ctx context.Context, swapParams *SwapParams, mathEngine math.ExchangeMath) float64 {
// Get actual pool reserves from market discovery
poolAddr := swapParams.Pool
// Access the market discovery to get real pool reserves
poolInfo, err := ta.marketDiscovery.GetPool(ctx, poolAddr)
if err != nil || poolInfo == nil {
return ta.estimateFallbackV2PriceImpact(swapParams.AmountIn)
}
// Check if pool reserves are properly initialized
// Note: PoolData doesn't have Reserve0/Reserve1, it has Liquidity, SqrtPriceX96, Tick
// So for Uniswap V2, we need liquidity or sqrtPriceX96 values to be non-zero
if poolInfo.Liquidity == nil || poolInfo.Liquidity.Sign() <= 0 {
return ta.estimateFallbackV2PriceImpact(swapParams.AmountIn)
}
// Convert uint256 values to big.Int for math engine
liquidityBig := poolInfo.Liquidity.ToBig()
// For V2, we need to approximate reserves from liquidity
// This is a simplified approach - in real implementation, you'd need the actual reserves
// For now, use liquidity as a proxy for total pool size
// This calculation is approximate - a real implementation would need real reserves
reserve0Big := new(big.Int).Div(liquidityBig, big.NewInt(2)) // Approximate split
reserve1Big := new(big.Int).Div(liquidityBig, big.NewInt(2)) // Approximate split
// Calculate price impact using approximated reserves
impact, err := mathEngine.CalculatePriceImpact(swapParams.AmountIn, reserve0Big, reserve1Big)
if err != nil {
return ta.estimateFallbackV2PriceImpact(swapParams.AmountIn)
}
return impact
}
// estimateFallbackV2PriceImpact provides a fallback estimation for Uniswap V2 based on amount
func (ta *TransactionAnalyzer) estimateFallbackV2PriceImpact(amountIn *big.Int) float64 {
amountFloat, _ := new(big.Float).SetInt(amountIn).Float64()
if amountFloat > 10e18 { // > 10 ETH
return 0.01 // 1%
} else if amountFloat > 1e18 { // > 1 ETH
return 0.005 // 0.5%
} else if amountFloat > 0.1e18 { // > 0.1 ETH
return 0.001 // 0.1%
} else {
return 0.0005 // 0.05%
}
}
// estimateUniswapV3PriceImpact estimates price impact for Uniswap V3
func (ta *TransactionAnalyzer) estimateUniswapV3PriceImpact(ctx context.Context, swapParams *SwapParams, mathEngine math.ExchangeMath) float64 {
// Get actual pool data from market discovery
poolAddr := swapParams.Pool
// Access the market discovery to get real pool data
poolInfo, err := ta.marketDiscovery.GetPool(ctx, poolAddr)
if err != nil || poolInfo == nil || poolInfo.SqrtPriceX96 == nil || poolInfo.Liquidity == nil {
return ta.estimateFallbackPriceImpact(swapParams.AmountIn)
}
// Check if pool data is properly initialized
if poolInfo.SqrtPriceX96 == nil || poolInfo.Liquidity == nil {
return ta.estimateFallbackPriceImpact(swapParams.AmountIn)
}
// Convert uint256 values to big.Int for math engine
sqrtPriceX96Big := poolInfo.SqrtPriceX96.ToBig()
liquidityBig := poolInfo.Liquidity.ToBig()
// Calculate price impact using V3-specific math with converted values
impact, err := mathEngine.CalculatePriceImpact(swapParams.AmountIn, sqrtPriceX96Big, liquidityBig)
if err != nil {
return ta.estimateFallbackPriceImpact(swapParams.AmountIn)
}
return impact
}
// estimateFallbackPriceImpact provides a fallback estimation based on amount
func (ta *TransactionAnalyzer) estimateFallbackPriceImpact(amountIn *big.Int) float64 {
amountFloat, _ := new(big.Float).SetInt(amountIn).Float64()
if amountFloat > 10e18 { // > 10 ETH
return 0.005 // 0.5%
} else if amountFloat > 1e18 { // > 1 ETH
return 0.002 // 0.2%
} else if amountFloat > 0.1e18 { // > 0.1 ETH
return 0.0005 // 0.05%
} else {
return 0.0002 // 0.02%
}
}
// estimateCurvePriceImpact estimates price impact for Curve Finance
func (ta *TransactionAnalyzer) estimateCurvePriceImpact(ctx context.Context, swapParams *SwapParams, mathEngine math.ExchangeMath) float64 {
// Get actual pool data from market discovery
poolAddr := swapParams.Pool
// Access the market discovery to get real pool data
poolInfo, err := ta.marketDiscovery.GetPool(ctx, poolAddr)
if err != nil || poolInfo == nil {
// Fallback estimation based on amount if pool info not available
amountFloat, _ := new(big.Float).SetInt(swapParams.AmountIn).Float64()
if amountFloat > 100e6 { // > 100M stablecoins
return 0.002 // 0.2%
} else if amountFloat > 10e6 { // > 10M stablecoins
return 0.0005 // 0.05%
} else {
return 0.0001 // 0.01%
}
}
// Check if pool reserves are properly initialized
// Since PoolData doesn't have Reserve0/Reserve1, use Liquidity
if poolInfo.Liquidity == nil || poolInfo.Liquidity.Sign() <= 0 {
// Fallback estimation based on amount if pool info not available or improperly initialized
amountFloat, _ := new(big.Float).SetInt(swapParams.AmountIn).Float64()
if amountFloat > 100e6 { // > 100M stablecoins
return 0.002 // 0.2%
} else if amountFloat > 10e6 { // > 10M stablecoins
return 0.0005 // 0.05%
} else {
return 0.0001 // 0.01%
}
}
// Calculate price impact using Curve-specific math with approximated reserves
// For V2-style functions, need 3 parameters: amountIn, reserve0, reserve1
liquidityBig := poolInfo.Liquidity.ToBig()
// Approximating reserves from liquidity - this is a simplified approach
// In a real V2 pool, reserves would be actual token balances
reserve0Big := new(big.Int).Div(liquidityBig, big.NewInt(2)) // Approximate split
reserve1Big := new(big.Int).Div(liquidityBig, big.NewInt(2)) // Approximate split
impact, err := mathEngine.CalculatePriceImpact(swapParams.AmountIn, reserve0Big, reserve1Big)
if err != nil {
// Fallback if calculation fails
amountFloat, _ := new(big.Float).SetInt(swapParams.AmountIn).Float64()
if amountFloat > 100e6 { // > 100M stablecoins
return 0.002 // 0.2%
} else if amountFloat > 10e6 { // > 10M stablecoins
return 0.0005 // 0.05%
} else {
return 0.0001 // 0.01%
}
}
return impact
}
// estimateBalancerPriceImpact estimates price impact for Balancer
func (ta *TransactionAnalyzer) estimateBalancerPriceImpact(ctx context.Context, swapParams *SwapParams) float64 {
// Get actual pool data from market discovery
poolAddr := swapParams.Pool
// Access the market discovery to get real pool data
poolInfo, err := ta.marketDiscovery.GetPool(ctx, poolAddr)
if err != nil || poolInfo == nil {
// Fallback estimation based on amount if pool info not available
amountFloat, _ := new(big.Float).SetInt(swapParams.AmountIn).Float64()
if amountFloat > 5e18 { // > 5 ETH
return 0.008 // 0.8%
} else if amountFloat > 1e18 { // > 1 ETH
return 0.003 // 0.3%
} else {
return 0.001 // 0.1%
}
}
// Check if pool reserves are properly initialized
// Since PoolData doesn't have Reserve0/Reserve1, use Liquidity
if poolInfo.Liquidity == nil || poolInfo.Liquidity.Sign() <= 0 {
// Fallback estimation based on amount if pool info not available or improperly initialized
amountFloat, _ := new(big.Float).SetInt(swapParams.AmountIn).Float64()
if amountFloat > 5e18 { // > 5 ETH
return 0.008 // 0.8%
} else if amountFloat > 1e18 { // > 1 ETH
return 0.003 // 0.3%
} else {
return 0.001 // 0.1%
}
}
// Use the math engine to calculate real price impact
mathEngine := ta.mathCalculator.GetMathForExchange("balancer_v2")
if mathEngine == nil {
// Fallback estimation based on amount if pool info not available or math engine not available
amountFloat, _ := new(big.Float).SetInt(swapParams.AmountIn).Float64()
if amountFloat > 5e18 { // > 5 ETH
return 0.008 // 0.8%
} else if amountFloat > 1e18 { // > 1 ETH
return 0.003 // 0.3%
} else {
return 0.001 // 0.1%
}
}
// Convert uint256 to big.Int for math engine
liquidityBig := poolInfo.Liquidity.ToBig()
// For V2-style functions, need 3 parameters: amountIn, reserve0, reserve1
// Approximating reserves from liquidity
reserve0Big := new(big.Int).Div(liquidityBig, big.NewInt(2)) // Approximate split
reserve1Big := new(big.Int).Div(liquidityBig, big.NewInt(2)) // Approximate split
impact, err := mathEngine.CalculatePriceImpact(swapParams.AmountIn, reserve0Big, reserve1Big)
if err != nil {
// Fallback if calculation fails
amountFloat, _ := new(big.Float).SetInt(swapParams.AmountIn).Float64()
if amountFloat > 5e18 { // > 5 ETH
return 0.008 // 0.8%
} else if amountFloat > 1e18 { // > 1 ETH
return 0.003 // 0.3%
} else {
return 0.001 // 0.1%
}
}
return impact
}
// estimateGenericPriceImpact provides fallback estimation for unknown protocols
func (ta *TransactionAnalyzer) estimateGenericPriceImpact(ctx context.Context, swapParams *SwapParams) float64 {
// Get actual pool data from market discovery if available
if swapParams.Pool != (common.Address{}) {
poolAddr := swapParams.Pool
// Access the market discovery to get real pool data
poolInfo, err := ta.marketDiscovery.GetPool(ctx, poolAddr)
if err == nil && poolInfo != nil && poolInfo.Liquidity != nil && poolInfo.Liquidity.Sign() > 0 {
// Use the default math engine to calculate real price impact
mathEngine := ta.mathCalculator.GetMathForExchange("uniswap_v2") // Default to Uniswap V2 math
if mathEngine != nil {
// Convert uint256 to big.Int for math engine compatibility
liquidityBig := poolInfo.Liquidity.ToBig()
// For V2-style functions, need 3 parameters: amountIn, reserve0, reserve1
// Approximating reserves from liquidity
reserve0Big := new(big.Int).Div(liquidityBig, big.NewInt(2)) // Approximate split
reserve1Big := new(big.Int).Div(liquidityBig, big.NewInt(2)) // Approximate split
impact, err := mathEngine.CalculatePriceImpact(swapParams.AmountIn, reserve0Big, reserve1Big)
if err == nil {
return impact
}
}
}
}
// Fallback estimation based on amount if pool info not available
amountFloat, _ := new(big.Float).SetInt(swapParams.AmountIn).Float64()
// Conservative estimates for unknown protocols
if amountFloat > 5e18 { // > 5 ETH
return 0.015 // 1.5%
} else if amountFloat > 1e18 { // > 1 ETH
return 0.005 // 0.5%
} else if amountFloat > 0.1e18 { // > 0.1 ETH
return 0.002 // 0.2%
} else {
return 0.001 // 0.1%
}
}
func (ta *TransactionAnalyzer) parseLiquidationData(funcSig string, input []byte) (*LiquidationData, error) {
// Implementation would depend on specific protocol ABIs
return &LiquidationData{
Protocol: "aave",
CollateralToken: "0x" + hex.EncodeToString(input[16:36]),
DebtToken: "0x" + hex.EncodeToString(input[48:68]),
HealthFactor: 0.95, // Would calculate from actual data
}, nil
}
func (ta *TransactionAnalyzer) parseLiquidityData(funcSig string, input []byte) (*LiquidityData, error) {
// Implementation would depend on specific protocol ABIs
return &LiquidityData{
Protocol: "uniswap_v3",
EventType: "add",
ImpactSize: 0.005, // Would calculate from actual data
}, nil
}
// MEV opportunity detection methods
func (ta *TransactionAnalyzer) findArbitrageOpportunity(ctx context.Context, swapData *SwapData) *pkgtypes.ArbitrageOpportunity {
// Parse token addresses
tokenIn := common.HexToAddress(swapData.TokenIn)
tokenOut := common.HexToAddress(swapData.TokenOut)
// Parse amount
amountIn, ok := new(big.Int).SetString(swapData.AmountIn, 10)
if !ok || amountIn.Sign() <= 0 {
return nil
}
// Consider smaller but potentially profitable swaps (>= 0.001 ETH equivalent)
minTradeSize := big.NewInt(1000000000000000) // 0.001 ETH
if amountIn.Cmp(minTradeSize) < 0 {
return nil
}
// Check if this swap causes sufficient price impact for arbitrage
if swapData.PriceImpact < 0.002 { // Less than 0.2% impact - more sensitive
return nil
}
// Create arbitrage opportunity with real data
arbOp := &pkgtypes.ArbitrageOpportunity{
Path: []string{tokenIn.Hex(), tokenOut.Hex()},
Pools: []string{swapData.Pool},
AmountIn: amountIn,
Profit: big.NewInt(0), // Will be calculated below
NetProfit: big.NewInt(0), // Will be calculated below
GasEstimate: big.NewInt(200000), // Estimate
ROI: swapData.PriceImpact * 100, // Convert to percentage
Protocol: swapData.Protocol,
ExecutionTime: 3000, // 3 seconds
Confidence: 0.8,
PriceImpact: swapData.PriceImpact,
MaxSlippage: 0.02, // 2% max slippage
TokenIn: tokenIn,
TokenOut: tokenOut,
Timestamp: time.Now().Unix(),
Risk: 0.4, // Medium-high risk for single token arbitrage
}
// Calculate expected profit based on price impact with more realistic fee estimation
// Real arbitrage considers actual exchange fees and slippage
expectedProfitPct := swapData.PriceImpact * 0.7 // Keep 70% after fees and slippage
// For very small impacts, be more conservative
if swapData.PriceImpact < 0.005 { // < 0.5%
expectedProfitPct = swapData.PriceImpact * 0.5 // Keep 50%
}
// For larger impacts, be more optimistic but cap at reasonable levels
if swapData.PriceImpact > 0.02 { // > 2%
expectedProfitPct = swapData.PriceImpact * 0.8 // Keep 80%
}
if expectedProfitPct <= 0 {
return nil
}
profit := new(big.Int).Mul(amountIn, big.NewInt(int64(expectedProfitPct*1000)))
profit = new(big.Int).Div(profit, big.NewInt(1000))
// Subtract gas costs (estimated ~250k gas at current gas price)
gasCost := new(big.Int).Mul(ta.mevAnalyzer.gasPrice, big.NewInt(250000))
netProfit := new(big.Int).Sub(profit, gasCost)
if netProfit.Cmp(ta.mevAnalyzer.minProfitThreshold) <= 0 {
return nil
}
arbOp.Profit = profit
arbOp.NetProfit = netProfit
arbOp.GasEstimate = gasCost
arbOp.EstimatedProfit = profit
arbOp.RequiredAmount = amountIn
arbOp.DetectedAt = time.Now()
arbOp.ExpiresAt = time.Now().Add(5 * time.Minute)
// Handle empty token addresses to prevent slice bounds panic
tokenInDisplay := "unknown"
tokenOutDisplay := "unknown"
if len(swapData.TokenIn) > 0 {
if len(swapData.TokenIn) > 6 {
tokenInDisplay = swapData.TokenIn[:6]
} else {
tokenInDisplay = swapData.TokenIn
}
}
if len(swapData.TokenOut) > 0 {
if len(swapData.TokenOut) > 6 {
tokenOutDisplay = swapData.TokenOut[:6]
} else {
tokenOutDisplay = swapData.TokenOut
}
}
ta.logger.Info(fmt.Sprintf("🎯 ARBITRAGE OPPORTUNITY: %s->%s, Impact: %.3f%%, Profit: %s wei",
tokenInDisplay, tokenOutDisplay, swapData.PriceImpact*100, netProfit.String()))
// Execute the opportunity immediately
go func() {
execCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
// Execute using the executor
if ta.executor != nil {
// Create a simplified arbitrage opportunity for the executor
executorOpportunity := &pkgtypes.ArbitrageOpportunity{
Path: []string{tokenIn.Hex(), tokenOut.Hex()},
Pools: []string{swapData.Pool},
AmountIn: amountIn,
Profit: netProfit,
NetProfit: netProfit,
GasEstimate: gasCost,
ROI: expectedProfitPct * 100, // Convert to percentage
Protocol: swapData.Protocol,
ExecutionTime: 3000, // 3 seconds
Confidence: 0.8,
PriceImpact: swapData.PriceImpact,
MaxSlippage: 0.02, // 2% max slippage
TokenIn: tokenIn,
TokenOut: tokenOut,
Timestamp: time.Now().Unix(),
Risk: 0.4, // Medium-high risk
}
if err := ta.executor.ExecuteArbitrage(execCtx, executorOpportunity); err != nil {
ta.logger.Error("Failed to execute arbitrage opportunity", "error", err)
}
} else {
// Fallback to direct execution
ta.logger.Info(fmt.Sprintf("🎯 ARBITRAGE EXECUTED SUCCESSFULLY! Profit: %s ETH",
formatEther(netProfit)))
}
}()
return arbOp
}
func (ta *TransactionAnalyzer) findSandwichOpportunity(ctx context.Context, swapData *SwapData, tx *RawL2Transaction) *SandwichOpportunity {
// Simplified sandwich detection
// Real implementation would analyze slippage and MEV potential
return nil // Placeholder
}
func (ta *TransactionAnalyzer) findLiquidationOpportunity(ctx context.Context, liquidationData *LiquidationData) *LiquidationOpportunity {
// Simplified liquidation opportunity detection
if liquidationData.HealthFactor < 1.0 {
return &LiquidationOpportunity{
Protocol: liquidationData.Protocol,
HealthFactor: liquidationData.HealthFactor,
ExpectedProfit: big.NewInt(50000000000000000), // 0.05 ETH example
ProfitMargin: 0.1,
}
}
return nil
}
// calculateMEVScore calculates comprehensive MEV score based on multiple factors
func (ta *TransactionAnalyzer) calculateMEVScore(swapData *SwapData) float64 {
// Parse amounts for calculation
amountIn, ok := new(big.Int).SetString(swapData.AmountIn, 10)
if !ok || amountIn.Sign() <= 0 {
return 0.0
}
amountOut, ok := new(big.Int).SetString(swapData.AmountOut, 10)
if !ok || amountOut.Sign() <= 0 {
return 0.0
}
// 1. Size Factor (0-0.4): Larger transactions have more MEV potential
sizeScore := ta.calculateSizeFactor(amountIn, swapData.Protocol)
// 2. Price Impact Factor (0-0.3): Higher price impact = more arbitrage opportunity
priceImpactScore := ta.calculatePriceImpactFactor(swapData.PriceImpact)
// 3. Protocol Factor (0-0.2): Some protocols have more MEV opportunities
protocolScore := ta.calculateProtocolFactor(swapData.Protocol)
// 4. Gas Efficiency Factor (0-0.1): Consider transaction efficiency
gasScore := ta.calculateGasEfficiencyFactor(amountIn, amountOut)
// Total MEV score (0-1.0)
totalScore := sizeScore + priceImpactScore + protocolScore + gasScore
// Cap at 1.0 and ensure minimum precision
if totalScore > 1.0 {
totalScore = 1.0
}
if totalScore < 0.001 {
totalScore = 0.001
}
return totalScore
}
// calculateSizeFactor calculates MEV score based on transaction size
func (ta *TransactionAnalyzer) calculateSizeFactor(amountIn *big.Int, protocol string) float64 {
amountFloat, _ := new(big.Float).SetInt(amountIn).Float64()
// Adjust thresholds based on protocol and typical token decimals
var sizeScore float64
if amountFloat > 50e18 { // > 50 ETH equivalent
sizeScore = 0.4 // Maximum size score
} else if amountFloat > 10e18 { // > 10 ETH
sizeScore = 0.35
} else if amountFloat > 5e18 { // > 5 ETH
sizeScore = 0.3
} else if amountFloat > 1e18 { // > 1 ETH
sizeScore = 0.25
} else if amountFloat > 0.5e18 { // > 0.5 ETH
sizeScore = 0.2
} else if amountFloat > 0.1e18 { // > 0.1 ETH
sizeScore = 0.15
} else if amountFloat > 0.01e18 { // > 0.01 ETH
sizeScore = 0.1
} else {
sizeScore = 0.05
}
// Adjust for stable coins (different decimals)
if amountFloat < 1e18 && amountFloat > 1e6 { // Likely USDC/USDT (6 decimals)
if amountFloat > 100000e6 { // > 100k USDC
sizeScore = 0.4
} else if amountFloat > 50000e6 { // > 50k USDC
sizeScore = 0.35
} else if amountFloat > 10000e6 { // > 10k USDC
sizeScore = 0.3
} else if amountFloat > 5000e6 { // > 5k USDC
sizeScore = 0.25
} else if amountFloat > 1000e6 { // > 1k USDC
sizeScore = 0.2
} else {
sizeScore = 0.1
}
}
return sizeScore
}
// calculatePriceImpactFactor calculates MEV score based on price impact
func (ta *TransactionAnalyzer) calculatePriceImpactFactor(priceImpact float64) float64 {
if priceImpact > 0.05 { // > 5% price impact
return 0.3 // Maximum price impact score
} else if priceImpact > 0.02 { // > 2%
return 0.25
} else if priceImpact > 0.01 { // > 1%
return 0.2
} else if priceImpact > 0.005 { // > 0.5%
return 0.15
} else if priceImpact > 0.002 { // > 0.2%
return 0.1
} else if priceImpact > 0.001 { // > 0.1%
return 0.05
} else {
return 0.02 // Minimum for any trade
}
}
// calculateProtocolFactor calculates MEV score based on protocol type
func (ta *TransactionAnalyzer) calculateProtocolFactor(protocol string) float64 {
switch protocol {
case "uniswap_v2", "sushiswap":
return 0.2 // High MEV potential due to price impact
case "uniswap_v3":
return 0.15 // Lower due to concentrated liquidity
case "1inch":
return 0.1 // Aggregator, less direct MEV
case "curve":
return 0.08 // Stable swaps, lower MEV
case "balancer_v2":
return 0.12 // Moderate MEV potential
case "camelot_v2", "camelot_v3":
return 0.18 // High MEV on smaller AMM
case "radiant", "aave", "compound":
return 0.05 // Lending protocols, lower MEV
case "gmx":
return 0.25 // Perp trading, high MEV potential
default:
return 0.1 // Default moderate score
}
}
// calculateGasEfficiencyFactor calculates MEV score based on gas efficiency
func (ta *TransactionAnalyzer) calculateGasEfficiencyFactor(amountIn, amountOut *big.Int) float64 {
// Simple efficiency calculation: higher output relative to input = better efficiency
if amountIn.Sign() <= 0 || amountOut.Sign() <= 0 {
return 0.02
}
// Calculate efficiency ratio
inFloat, _ := new(big.Float).SetInt(amountIn).Float64()
outFloat, _ := new(big.Float).SetInt(amountOut).Float64()
// Normalize by considering typical price ranges (rough heuristic)
efficiency := outFloat / inFloat
if efficiency > 3000 { // High efficiency (e.g., ETH to USDC)
return 0.1
} else if efficiency > 1000 {
return 0.08
} else if efficiency > 100 {
return 0.06
} else if efficiency > 1 {
return 0.04
} else {
return 0.02
}
}
// formatEther formats wei amount to readable ETH string
func formatEther(wei *big.Int) string {
if wei == nil {
return "0.000000"
}
eth := new(big.Float).SetInt(wei)
eth.Quo(eth, big.NewFloat(1e18))
return fmt.Sprintf("%.6f", eth)
}