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>
1073 lines
36 KiB
Go
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)
|
|
}
|