Files
mev-beta/pkg/pricing/engine.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

280 lines
8.9 KiB
Go

package pricing
import (
"context"
"fmt"
"math/big"
"sync"
"time"
"github.com/ethereum/go-ethereum/common"
"github.com/fraktal/mev-beta/internal/logger"
oraclepkg "github.com/fraktal/mev-beta/pkg/oracle"
"github.com/fraktal/mev-beta/pkg/types"
)
// ExchangePricer handles real-time pricing across multiple DEX protocols
type ExchangePricer struct {
logger *logger.Logger
oracles map[string]*oraclepkg.PriceOracle
priceCache map[string]*PriceEntry
cacheMutex sync.RWMutex
lastUpdate time.Time
updateInterval time.Duration
}
// PriceEntry represents cached price data with timestamp
type PriceEntry struct {
Price *big.Float
Timestamp time.Time
Validity time.Duration
}
// ExchangePrice represents pricing data from a specific exchange
type ExchangePrice struct {
Exchange string
Pair string
BidPrice *big.Float
AskPrice *big.Float
Liquidity *big.Int
Timestamp time.Time
Confidence float64
}
// PricingOpportunity represents pricing-specific arbitrage data (extends canonical ArbitrageOpportunity)
type PricingOpportunity struct {
*types.ArbitrageOpportunity
BuyExchange string
SellExchange string
BuyPrice *big.Float
SellPrice *big.Float
Spread *big.Float
RequiredCapital *big.Int
Expiration time.Time
}
// NewExchangePricer creates a new cross-exchange pricer
func NewExchangePricer(logger *logger.Logger) *ExchangePricer {
return &ExchangePricer{
logger: logger,
oracles: make(map[string]*oraclepkg.PriceOracle),
priceCache: make(map[string]*PriceEntry),
updateInterval: 500 * time.Millisecond, // Update every 500ms for real-time pricing
}
}
// AddExchangeOracle adds a price oracle for a specific exchange
func (ep *ExchangePricer) AddExchangeOracle(exchange string, oracle *oraclepkg.PriceOracle) {
ep.oracles[exchange] = oracle
ep.logger.Info(fmt.Sprintf("Added price oracle for exchange: %s", exchange))
}
// GetCrossExchangePrices retrieves prices for a token pair across all exchanges
func (ep *ExchangePricer) GetCrossExchangePrices(ctx context.Context, tokenIn, tokenOut common.Address) (map[string]*ExchangePrice, error) {
prices := make(map[string]*ExchangePrice)
for exchange, oracle := range ep.oracles {
select {
case <-ctx.Done():
return nil, ctx.Err()
default:
priceReq := &oraclepkg.PriceRequest{
TokenIn: tokenIn,
TokenOut: tokenOut,
AmountIn: big.NewInt(1e18), // 1 token for reference price
Timestamp: time.Now(),
}
priceResp, err := oracle.GetPrice(ctx, priceReq)
if err != nil {
ep.logger.Debug(fmt.Sprintf("Failed to get price from %s: %v", exchange, err))
continue
}
if priceResp.Valid && priceResp.AmountOut != nil {
exchangePrice := &ExchangePrice{
Exchange: exchange,
Pair: fmt.Sprintf("%s/%s", tokenIn.Hex()[:6], tokenOut.Hex()[:6]),
BidPrice: new(big.Float).SetInt(priceResp.AmountOut),
AskPrice: new(big.Float).SetInt(priceResp.AmountOut), // Simplified - in production would have bid/ask spread
Liquidity: priceResp.Liquidity, // Estimated liquidity
Timestamp: time.Now(),
Confidence: 0.9, // High confidence for direct oracle data
}
prices[exchange] = exchangePrice
}
}
}
return prices, nil
}
// FindArbitrageOpportunities identifies cross-exchange arbitrage possibilities
func (ep *ExchangePricer) FindArbitrageOpportunities(ctx context.Context, tokenIn, tokenOut common.Address) ([]*types.ArbitrageOpportunity, error) {
prices, err := ep.GetCrossExchangePrices(ctx, tokenIn, tokenOut)
if err != nil {
return nil, fmt.Errorf("failed to get cross-exchange prices: %w", err)
}
if len(prices) < 2 {
return nil, nil // Need at least 2 exchanges to find arbitrage
}
var opportunities []*types.ArbitrageOpportunity
// Compare all exchange pairs for arbitrage opportunities
for buyExchange, buyPrice := range prices {
for sellExchange, sellPrice := range prices {
if buyExchange == sellExchange {
continue
}
// Calculate price spread
spread := new(big.Float).Sub(sellPrice.BidPrice, buyPrice.AskPrice)
if spread.Sign() <= 0 {
continue // No arbitrage opportunity
}
// Calculate spread percentage
spreadPct := new(big.Float).Quo(spread, buyPrice.AskPrice)
spreadPctFloat, _ := spreadPct.Float64()
// Only consider opportunities with > 0.3% spread (after fees)
if spreadPctFloat < 0.003 {
continue
}
// Estimate required capital (use smaller liquidity as constraint)
requiredCapital := buyPrice.Liquidity
if sellPrice.Liquidity.Cmp(requiredCapital) < 0 {
requiredCapital = sellPrice.Liquidity
}
// Estimate profit (simplified - real implementation would be more complex)
estimatedProfit := new(big.Float).Mul(spread, new(big.Float).SetInt(requiredCapital))
estimatedProfit.Quo(estimatedProfit, big.NewFloat(1e18)) // Convert to ETH terms
// Estimate gas costs (simplified)
gasEstimate := big.NewInt(300000) // ~300k gas for complex arbitrage
// Calculate net profit after gas
gasCostEth := new(big.Float).Quo(new(big.Float).SetInt(gasEstimate), big.NewFloat(1e18))
gasCostUsd := new(big.Float).Mul(gasCostEth, big.NewFloat(2000)) // Assume $2000/ETH for gas pricing
netProfit := new(big.Float).Sub(estimatedProfit, gasCostUsd)
if netProfit.Sign() <= 0 {
continue // Unprofitable after gas costs
}
// Calculate risk score (simplified)
riskScore := 0.1 // Low base risk
if spreadPctFloat > 0.1 { // > 10% spread
riskScore += 0.3 // Higher volatility risk
}
// Create 100 ETH value using string to avoid overflow
hundredETH := new(big.Int)
hundredETH.SetString("100000000000000000000", 10) // 100 * 1e18
if requiredCapital.Cmp(hundredETH) > 0 { // > 100 ETH liquidity
riskScore -= 0.05 // Lower slippage risk with deep liquidity
}
// Convert to canonical ArbitrageOpportunity
profitWei := new(big.Int)
estimatedProfit.Int(profitWei)
netProfitWei := new(big.Int)
netProfit.Int(netProfitWei)
opportunity := &types.ArbitrageOpportunity{
Path: []string{buyExchange, sellExchange},
Pools: []string{buyExchange + "-pool", sellExchange + "-pool"},
AmountIn: requiredCapital,
Profit: profitWei,
NetProfit: netProfitWei,
GasEstimate: gasEstimate,
ROI: spreadPctFloat * 100,
Protocol: "cross-exchange",
ExecutionTime: 15000, // 15 seconds in milliseconds
Confidence: (buyPrice.Confidence + sellPrice.Confidence) / 2,
PriceImpact: 0.005, // 0.5% estimated
MaxSlippage: 0.01, // 1% max slippage
TokenIn: tokenIn,
TokenOut: tokenOut,
Timestamp: time.Now().Unix(),
Risk: riskScore,
}
opportunities = append(opportunities, opportunity)
}
}
// Sort by net profit descending
ep.sortOpportunitiesByProfit(opportunities)
return opportunities, nil
}
// sortOpportunitiesByProfit sorts arbitrage opportunities by net profit
func (ep *ExchangePricer) sortOpportunitiesByProfit(opportunities []*types.ArbitrageOpportunity) {
// Simple bubble sort for small arrays
for i := 0; i < len(opportunities)-1; i++ {
for j := 0; j < len(opportunities)-i-1; j++ {
profitI := new(big.Float).SetInt(opportunities[j].NetProfit)
profitJ := new(big.Float).SetInt(opportunities[j+1].NetProfit)
profitIFloat, _ := profitI.Float64()
profitJFloat, _ := profitJ.Float64()
if profitIFloat < profitJFloat {
opportunities[j], opportunities[j+1] = opportunities[j+1], opportunities[j]
}
}
}
}
// ValidateOpportunity validates an arbitrage opportunity is still profitable
func (ep *ExchangePricer) ValidateOpportunity(ctx context.Context, opportunity *types.ArbitrageOpportunity) (bool, error) {
// Check expiration (using ExecutionTime as expiration window)
expiration := time.Unix(opportunity.Timestamp, 0).Add(time.Duration(opportunity.ExecutionTime) * time.Millisecond)
if time.Now().After(expiration) {
return false, nil
}
// Revalidate prices
prices, err := ep.GetCrossExchangePrices(ctx, opportunity.TokenIn, opportunity.TokenOut)
if err != nil {
return false, fmt.Errorf("failed to revalidate prices: %w", err)
}
// Extract buy/sell exchanges from path
if len(opportunity.Path) < 2 {
return false, nil
}
buyPrice, buyExists := prices[opportunity.Path[0]]
sellPrice, sellExists := prices[opportunity.Path[1]]
if !buyExists || !sellExists {
return false, nil
}
// Recalculate spread
spread := new(big.Float).Sub(sellPrice.BidPrice, buyPrice.AskPrice)
if spread.Sign() <= 0 {
return false, nil // No longer profitable
}
return true, nil
}
// GetPriceCacheStats returns statistics about the price cache
func (ep *ExchangePricer) GetPriceCacheStats() map[string]interface{} {
ep.cacheMutex.RLock()
defer ep.cacheMutex.RUnlock()
stats := make(map[string]interface{})
stats["cached_prices"] = len(ep.priceCache)
stats["last_update"] = ep.lastUpdate
stats["update_interval"] = ep.updateInterval
return stats
}