- 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>
280 lines
8.9 KiB
Go
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
|
|
}
|