Files
mev-beta/pkg/exchanges/arbitrage_finder.go
Krypto Kajun 850223a953 fix(multicall): resolve critical multicall parsing corruption issues
- Added comprehensive bounds checking to prevent buffer overruns in multicall parsing
- Implemented graduated validation system (Strict/Moderate/Permissive) to reduce false positives
- Added LRU caching system for address validation with 10-minute TTL
- Enhanced ABI decoder with missing Universal Router and Arbitrum-specific DEX signatures
- Fixed duplicate function declarations and import conflicts across multiple files
- Added error recovery mechanisms with multiple fallback strategies
- Updated tests to handle new validation behavior for suspicious addresses
- Fixed parser test expectations for improved validation system
- Applied gofmt formatting fixes to ensure code style compliance
- Fixed mutex copying issues in monitoring package by introducing MetricsSnapshot
- Resolved critical security vulnerabilities in heuristic address extraction
- Progress: Updated TODO audit from 10% to 35% complete

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-17 00:12:55 -05:00

482 lines
16 KiB
Go

package exchanges
import (
"context"
"fmt"
"math/big"
"sort"
"time"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/fraktal/mev-beta/internal/logger"
"github.com/fraktal/mev-beta/pkg/math"
"github.com/fraktal/mev-beta/pkg/types"
)
// ArbitrageOpportunity represents a cross-exchange arbitrage opportunity
type ArbitrageOpportunity struct {
TokenIn common.Address
TokenOut common.Address
BuyExchange math.ExchangeType
SellExchange math.ExchangeType
BuyAmount *big.Int
SellAmount *big.Int
BuyPrice *big.Float
SellPrice *big.Float
Spread *big.Float // Price difference (SellPrice - BuyPrice)
SpreadPercent *big.Float // Spread as percentage
Profit *big.Int // Estimated profit in wei
GasCost *big.Int // Estimated gas cost
NetProfit *big.Int // Net profit after gas
ROI float64 // Return on investment percentage
Confidence float64 // Confidence score (0.0 to 1.0)
MaxSlippage float64 // Maximum acceptable slippage
ExecutionTime time.Duration // Estimated execution time
Path []string // Execution path description
Timestamp time.Time // When the opportunity was discovered
}
// CrossExchangeArbitrageFinder finds arbitrage opportunities between exchanges
type CrossExchangeArbitrageFinder struct {
client *ethclient.Client
logger *logger.Logger
registry *ExchangeRegistry
engine *math.ExchangePricingEngine
minSpread *big.Float // Minimum required spread percentage
minProfit *big.Int // Minimum required profit in wei
maxSlippage float64 // Maximum acceptable slippage
}
// NewCrossExchangeArbitrageFinder creates a new cross-exchange arbitrage finder
func NewCrossExchangeArbitrageFinder(
client *ethclient.Client,
logger *logger.Logger,
registry *ExchangeRegistry,
engine *math.ExchangePricingEngine,
) *CrossExchangeArbitrageFinder {
return &CrossExchangeArbitrageFinder{
client: client,
logger: logger,
registry: registry,
engine: engine,
minSpread: big.NewFloat(0.003), // 0.3% minimum spread after fees
minProfit: big.NewInt(10000000000000000), // 0.01 ETH minimum profit
maxSlippage: 0.01, // 1% maximum slippage
}
}
// FindArbitrageOpportunities finds cross-exchange arbitrage opportunities for a token pair
func (a *CrossExchangeArbitrageFinder) FindArbitrageOpportunities(ctx context.Context, tokenIn, tokenOut common.Address) ([]*ArbitrageOpportunity, error) {
// Get all exchanges that support this token pair
exchanges := a.registry.GetExchangesForPair(tokenIn, tokenOut)
if len(exchanges) < 2 {
return nil, nil // Need at least 2 exchanges to find arbitrage
}
var opportunities []*ArbitrageOpportunity
// Compare all exchange pairs for arbitrage opportunities
for i, buyExchangeConfig := range exchanges {
for j, sellExchangeConfig := range exchanges {
if i == j {
continue // Skip same exchange
}
// Check if we can buy on buyExchange and sell on sellExchange
opportunity, err := a.findDirectArbitrage(ctx, tokenIn, tokenOut, buyExchangeConfig.Type, sellExchangeConfig.Type)
if err != nil {
continue
}
if opportunity != nil {
// Validate the opportunity
if a.isValidOpportunity(opportunity) {
opportunities = append(opportunities, opportunity)
}
}
}
}
// Sort opportunities by net profit descending
sort.Slice(opportunities, func(i, j int) bool {
return opportunities[i].NetProfit.Cmp(opportunities[j].NetProfit) > 0
})
return opportunities, nil
}
// findDirectArbitrage finds a direct arbitrage opportunity between two exchanges
func (a *CrossExchangeArbitrageFinder) findDirectArbitrage(
ctx context.Context,
tokenIn, tokenOut common.Address,
buyExchangeType, sellExchangeType math.ExchangeType,
) (*ArbitrageOpportunity, error) {
// Get swap routers for both exchanges
buyRouter := a.registry.GetSwapRouter(buyExchangeType)
sellRouter := a.registry.GetSwapRouter(sellExchangeType)
if buyRouter == nil || sellRouter == nil {
return nil, fmt.Errorf("missing swap router for one of the exchanges")
}
// Use a standard amount for comparison (1 ETH equivalent)
standardAmount := big.NewInt(1000000000000000000) // 1 ETH
// Calculate how much we'd get if we buy on buyExchange
amountAfterBuy, err := buyRouter.CalculateSwap(tokenIn, tokenOut, standardAmount)
if err != nil {
return nil, fmt.Errorf("error calculating buy swap: %w", err)
}
// Calculate how much we'd get if we sell the result on sellExchange
amountAfterSell, err := sellRouter.CalculateSwap(tokenOut, tokenIn, amountAfterBuy)
if err != nil {
return nil, fmt.Errorf("error calculating sell swap: %w", err)
}
// Calculate the spread and profit
spread := new(big.Float).Sub(
new(big.Float).SetInt(amountAfterSell),
new(big.Float).SetInt(standardAmount),
)
// Calculate spread percentage
spreadPercent := new(big.Float).Quo(
spread,
new(big.Float).SetInt(standardAmount),
)
// If the spread is positive, we have an opportunity
if spread.Sign() > 0 {
// Calculate profit in terms of the original token
profit := new(big.Int).Sub(amountAfterSell, standardAmount)
// Calculate estimated gas cost (this would be more sophisticated in production)
gasCost := big.NewInt(500000000000000) // 0.0005 ETH in wei as estimate
// Calculate net profit
netProfit := new(big.Int).Sub(profit, gasCost)
// Calculate ROI
roi := new(big.Float).Quo(
new(big.Float).SetInt(netProfit),
new(big.Float).SetInt(standardAmount),
)
roiFloat, _ := roi.Float64()
// Get exchange config names for the path
buyConfig := a.registry.GetExchangeByType(buyExchangeType)
sellConfig := a.registry.GetExchangeByType(sellExchangeType)
buyExchangeName := "Unknown"
sellExchangeName := "Unknown"
if buyConfig != nil {
buyExchangeName = buyConfig.Name
}
if sellConfig != nil {
sellExchangeName = sellConfig.Name
}
// Create opportunity
opportunity := &ArbitrageOpportunity{
TokenIn: tokenIn,
TokenOut: tokenOut,
BuyExchange: buyExchangeType,
SellExchange: sellExchangeType,
BuyAmount: standardAmount,
SellAmount: amountAfterSell,
BuyPrice: new(big.Float).Quo(new(big.Float).SetInt(amountAfterBuy), new(big.Float).SetInt(standardAmount)),
SellPrice: new(big.Float).Quo(new(big.Float).SetInt(amountAfterSell), new(big.Float).SetInt(amountAfterBuy)),
Spread: spread,
SpreadPercent: spreadPercent,
Profit: profit,
GasCost: gasCost,
NetProfit: netProfit,
ROI: roiFloat * 100, // Convert to percentage
Confidence: 0.9, // High confidence for basic calculation
MaxSlippage: a.maxSlippage,
Path: []string{buyExchangeName, sellExchangeName},
Timestamp: time.Now(),
}
return opportunity, nil
}
return nil, nil // No arbitrage opportunity
}
// isValidOpportunity checks if an arbitrage opportunity meets our criteria
func (a *CrossExchangeArbitrageFinder) isValidOpportunity(opportunity *ArbitrageOpportunity) bool {
// Check if spread is above minimum
spreadPercentFloat, _ := opportunity.SpreadPercent.Float64()
minSpreadFloat, _ := a.minSpread.Float64()
if spreadPercentFloat < minSpreadFloat {
return false
}
// Check if net profit is above minimum
if opportunity.NetProfit.Cmp(a.minProfit) < 0 {
return false
}
// Check if ROI is reasonable
if opportunity.ROI < 0.1 { // 0.1% minimum ROI
return false
}
// Additional validations could go here
return true
}
// FindTriangleArbitrage finds triangular arbitrage opportunities across exchanges
func (a *CrossExchangeArbitrageFinder) FindTriangleArbitrage(ctx context.Context, tokens []common.Address) ([]*ArbitrageOpportunity, error) {
if len(tokens) < 3 {
return nil, fmt.Errorf("need at least 3 tokens for triangular arbitrage")
}
var opportunities []*ArbitrageOpportunity
// For each combination of 3 tokens
for i := 0; i < len(tokens)-2; i++ {
for j := i + 1; j < len(tokens)-1; j++ {
for k := j + 1; k < len(tokens); k++ {
// Try A -> B -> C -> A cycle
cycleOpportunities, err := a.findTriangleCycle(ctx, tokens[i], tokens[j], tokens[k])
if err == nil && cycleOpportunities != nil {
opportunities = append(opportunities, cycleOpportunities...)
}
// Try A -> C -> B -> A cycle (reverse)
cycleOpportunities, err = a.findTriangleCycle(ctx, tokens[i], tokens[k], tokens[j])
if err == nil && cycleOpportunities != nil {
opportunities = append(opportunities, cycleOpportunities...)
}
}
}
}
// Sort by net profit descending
sort.Slice(opportunities, func(i, j int) bool {
return opportunities[i].NetProfit.Cmp(opportunities[j].NetProfit) > 0
})
return opportunities, nil
}
// findTriangleCycle finds opportunities for a specific 3-token cycle
func (a *CrossExchangeArbitrageFinder) findTriangleCycle(
ctx context.Context,
tokenA, tokenB, tokenC common.Address,
) ([]*ArbitrageOpportunity, error) {
// Find if there are exchanges supporting A->B, B->C, C->A
// This would be complex to implement completely, so here's a simplified approach
var opportunities []*ArbitrageOpportunity
// Get exchanges for each pair
exchangesAB := a.registry.GetExchangesForPair(tokenA, tokenB)
exchangesBC := a.registry.GetExchangesForPair(tokenB, tokenC)
exchangesCA := a.registry.GetExchangesForPair(tokenC, tokenA)
if len(exchangesAB) == 0 || len(exchangesBC) == 0 || len(exchangesCA) == 0 {
return nil, nil // Can't form a cycle without all pairs
}
// Use first available exchange for each leg of the cycle
// In a real implementation, we'd try all combinations
if len(exchangesAB) > 0 && len(exchangesBC) > 0 && len(exchangesCA) > 0 {
// For this simplified version, we'll just use the first available exchanges
// More complex implementations would iterate through all combinations
// Start with 1 of tokenA
startAmount := big.NewInt(1000000000000000000) // 1 tokenA equivalent
// Get the three exchanges we'll use
exchangeAB := exchangesAB[0].Type
exchangeBC := exchangesBC[0].Type
exchangeCA := exchangesCA[0].Type
// Get swap routers
routerAB := a.registry.GetSwapRouter(exchangeAB)
routerBC := a.registry.GetSwapRouter(exchangeBC)
routerCA := a.registry.GetSwapRouter(exchangeCA)
if routerAB == nil || routerBC == nil || routerCA == nil {
return nil, nil
}
// Execute the cycle: A -> B -> C -> A
amountB, err := routerAB.CalculateSwap(tokenA, tokenB, startAmount)
if err != nil {
return nil, nil
}
amountC, err := routerBC.CalculateSwap(tokenB, tokenC, amountB)
if err != nil {
return nil, nil
}
finalAmount, err := routerCA.CalculateSwap(tokenC, tokenA, amountC)
if err != nil {
return nil, nil
}
// Calculate profit
profit := new(big.Int).Sub(finalAmount, startAmount)
// If we made a profit, we have an opportunity
if profit.Sign() > 0 {
// Calculate spread percentage
spreadPercent := new(big.Float).Quo(
new(big.Float).SetInt(profit),
new(big.Float).SetInt(startAmount),
)
// Calculate ROI
roi := new(big.Float).Quo(
new(big.Float).SetInt(profit),
new(big.Float).SetInt(startAmount),
)
roiFloat, _ := roi.Float64()
// Calculate estimated gas cost for the complex multi-swap
gasCost := big.NewInt(1500000000000000) // 0.0015 ETH in wei for 3 swaps
// Calculate net profit
netProfit := new(big.Int).Sub(profit, gasCost)
// Create opportunity
opportunity := &ArbitrageOpportunity{
TokenIn: tokenA,
TokenOut: tokenA, // We start and end with the same token
BuyExchange: exchangeAB,
SellExchange: exchangeCA, // This is somewhat of a convention for this type of arbitrage
BuyAmount: startAmount,
SellAmount: finalAmount,
Spread: new(big.Float).SetInt(profit),
SpreadPercent: spreadPercent,
Profit: profit,
GasCost: gasCost,
NetProfit: netProfit,
ROI: roiFloat * 100,
Confidence: 0.7, // Lower confidence for complex triangle arbitrage
MaxSlippage: a.maxSlippage,
Path: []string{exchangesAB[0].Name, exchangesBC[0].Name, exchangesCA[0].Name},
Timestamp: time.Now(),
}
opportunities = append(opportunities, opportunity)
}
}
return opportunities, nil
}
// ValidateOpportunity checks if an arbitrage opportunity is still valid
func (a *CrossExchangeArbitrageFinder) ValidateOpportunity(ctx context.Context, opportunity *ArbitrageOpportunity) (bool, error) {
// Check if the opportunity is still fresh (not too old)
if time.Since(opportunity.Timestamp) > 100*time.Millisecond { // 100ms expiration
return false, nil
}
// Recalculate the opportunity to see if it's still profitable
recalculated, err := a.findDirectArbitrage(ctx, opportunity.TokenIn, opportunity.TokenOut, opportunity.BuyExchange, opportunity.SellExchange)
if err != nil || recalculated == nil {
return false, err
}
// Check if the recalculated profit is still above our minimum
if recalculated.NetProfit.Cmp(a.minProfit) < 0 {
return false, nil
}
return true, nil
}
// EstimateExecutionGas estimates the gas cost for executing an arbitrage opportunity
func (a *CrossExchangeArbitrageFinder) EstimateExecutionGas(opportunity *ArbitrageOpportunity) (*big.Int, error) {
// In a real implementation, this would estimate gas based on complexity
// For this implementation, we'll use a basic estimation based on the type of arbitrage
baseGas := big.NewInt(100000) // Base gas for a single swap
// For each exchange in the path, add more gas
gasPerSwap := big.NewInt(100000)
totalGas := new(big.Int).Add(baseGas, new(big.Int).Mul(gasPerSwap, big.NewInt(int64(len(opportunity.Path)))))
return totalGas, nil
}
// ConvertToCanonicalOpportunity converts our internal opportunity to the canonical type
func (a *CrossExchangeArbitrageFinder) ConvertToCanonicalOpportunity(opportunity *ArbitrageOpportunity) *types.ArbitrageOpportunity {
// Convert our internal opportunity struct to the canonical ArbitrageOpportunity type
roiFloat, _ := opportunity.ROI, opportunity.ROI
gasEstimate := big.NewInt(200000) // Default gas estimate
return &types.ArbitrageOpportunity{
Path: opportunity.Path,
Pools: []string{}, // Would be populated with actual pool addresses
AmountIn: opportunity.BuyAmount,
Profit: opportunity.Profit,
NetProfit: opportunity.NetProfit,
GasEstimate: gasEstimate,
ROI: roiFloat,
Protocol: "cross-exchange",
ExecutionTime: int64(opportunity.ExecutionTime.Milliseconds()),
Confidence: opportunity.Confidence,
PriceImpact: 0.005, // 0.5% estimated price impact
MaxSlippage: opportunity.MaxSlippage,
TokenIn: opportunity.TokenIn,
TokenOut: opportunity.TokenOut,
Timestamp: opportunity.Timestamp.Unix(),
Risk: 0.1, // Low risk for simple arbitrage
}
}
// FindHighPriorityArbitrage looks for opportunities with high probability and profit
func (a *CrossExchangeArbitrageFinder) FindHighPriorityArbitrage(ctx context.Context) ([]*ArbitrageOpportunity, error) {
// Get high priority tokens from the registry
highPriorityTokens := a.registry.GetHighPriorityTokens(10) // Top 10 tokens
var allOpportunities []*ArbitrageOpportunity
// Create token address array from high priority tokens
var tokenAddresses []common.Address
for _, token := range highPriorityTokens {
tokenAddresses = append(tokenAddresses, common.HexToAddress(token.Address))
}
// Find arbitrage opportunities between high priority tokens
for i, tokenA := range tokenAddresses {
for j, tokenB := range tokenAddresses {
if i < j { // Only check each pair once
opportunities, err := a.FindArbitrageOpportunities(ctx, tokenA, tokenB)
if err != nil {
continue
}
allOpportunities = append(allOpportunities, opportunities...)
}
}
}
// Sort by profitability and confidence
sort.Slice(allOpportunities, func(i, j int) bool {
// Sort by net profit first, then by confidence
if allOpportunities[i].NetProfit.Cmp(allOpportunities[j].NetProfit) == 0 {
return allOpportunities[i].Confidence > allOpportunities[j].Confidence
}
return allOpportunities[i].NetProfit.Cmp(allOpportunities[j].NetProfit) > 0
})
// Return only the top opportunities (top 20)
if len(allOpportunities) > 20 {
allOpportunities = allOpportunities[:20]
}
return allOpportunities, nil
}