- 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>
482 lines
16 KiB
Go
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
|
|
}
|