Files
mev-beta/pkg/arbitrum/parser/transaction_analyzer.go
2025-10-04 09:31:02 -05:00

1107 lines
38 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) {
// Use the ABI decoder to parse transaction data
swapParams, err := ta.abiDecoder.DecodeSwapTransaction(protocol, input)
if err != nil {
ta.logger.Warn("Failed to decode swap transaction",
"protocol", protocol,
"function", functionName,
"error", err)
// Return minimal data rather than fake placeholder data
return &SwapData{
Protocol: protocol,
Pool: "",
TokenIn: "",
TokenOut: "",
AmountIn: "0",
AmountOut: "0",
Recipient: "",
PriceImpact: 0,
}, nil
}
// Calculate pool address using CREATE2 if we have token addresses
var poolAddress string
tokenInInterface, ok := swapParams.(map[string]interface{})["TokenIn"]
tokenOutInterface, ok2 := swapParams.(map[string]interface{})["TokenOut"]
if ok && ok2 {
if tokenInAddr, ok := tokenInInterface.(common.Address); ok {
if tokenOutAddr, ok := tokenOutInterface.(common.Address); ok {
if tokenInAddr != (common.Address{}) && tokenOutAddr != (common.Address{}) {
// Get fee from the decoded parameters
feeInterface, hasFee := swapParams.(map[string]interface{})["Fee"]
var fee *big.Int
if hasFee && feeInterface != nil {
if feeBigInt, ok := feeInterface.(*big.Int); ok {
fee = feeBigInt
} else {
fee = big.NewInt(0) // Use 0 as default fee if nil
}
} else {
fee = big.NewInt(0)
}
// Calculate pool address - Note: CalculatePoolAddress signature may need to match the actual interface
// For now, I'll keep the original interface but ensure parameters are correctly cast
if poolAddr, err := ta.abiDecoder.CalculatePoolAddress(
protocol,
tokenInAddr.Hex(),
tokenOutAddr.Hex(),
fee,
); err == nil {
poolAddress = poolAddr.Hex()
}
}
}
}
}
// Convert amounts to strings, handling nil values
amountIn := "0"
amountInInterface, hasAmountIn := swapParams.(map[string]interface{})["AmountIn"]
if hasAmountIn && amountInInterface != nil {
if amountInBigInt, ok := amountInInterface.(*big.Int); ok {
amountIn = amountInBigInt.String()
}
}
amountOut := "0"
amountOutInterface, hasAmountOut := swapParams.(map[string]interface{})["AmountOut"]
minAmountOutInterface, hasMinAmountOut := swapParams.(map[string]interface{})["MinAmountOut"]
if hasAmountOut && amountOutInterface != nil {
if amountOutBigInt, ok := amountOutInterface.(*big.Int); ok {
amountOut = amountOutBigInt.String()
}
} else if hasMinAmountOut && minAmountOutInterface != nil {
// Use minimum amount out as estimate if actual amount out is not available
if minAmountOutBigInt, ok := minAmountOutInterface.(*big.Int); ok {
amountOut = minAmountOutBigInt.String()
}
}
// Calculate real price impact using the exchange math library
// For now, using a default calculation since we can't pass interface{} to calculateRealPriceImpact
priceImpact := 0.0001 // 0.01% default
// Get token addresses for return
tokenInStr := ""
if tokenInInterface, ok := swapParams.(map[string]interface{})["TokenIn"]; ok && tokenInInterface != nil {
if tokenInAddr, ok := tokenInInterface.(common.Address); ok {
tokenInStr = tokenInAddr.Hex()
}
}
tokenOutStr := ""
if tokenOutInterface, ok := swapParams.(map[string]interface{})["TokenOut"]; ok && tokenOutInterface != nil {
if tokenOutAddr, ok := tokenOutInterface.(common.Address); ok {
tokenOutStr = tokenOutAddr.Hex()
}
}
// Get recipient
recipientStr := ""
if recipientInterface, ok := swapParams.(map[string]interface{})["Recipient"]; ok && recipientInterface != nil {
if recipientAddr, ok := recipientInterface.(common.Address); ok {
recipientStr = recipientAddr.Hex()
}
}
return &SwapData{
Protocol: protocol,
Pool: poolAddress,
TokenIn: tokenInStr,
TokenOut: tokenOutStr,
AmountIn: amountIn,
AmountOut: amountOut,
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 {
// Fallback estimation based on amount if pool info 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%
}
}
// 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 {
// Fallback estimation based on amount if pool info not available or improperly initialized
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%
}
}
// 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 {
// Fallback if calculation fails
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%
}
}
return impact
}
// 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 {
// Fallback estimation based on amount if pool info not available
amountFloat, _ := new(big.Float).SetInt(swapParams.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%
}
}
// Check if pool data is properly initialized
if poolInfo.SqrtPriceX96 == nil || poolInfo.Liquidity == nil {
// Fallback estimation based on amount if pool info not available or improperly initialized
amountFloat, _ := new(big.Float).SetInt(swapParams.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%
}
}
// 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 {
// Fallback if calculation fails
amountFloat, _ := new(big.Float).SetInt(swapParams.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%
}
}
return impact
}
// 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
// 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
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)
}