- Fixed duplicate type declarations in transport package - Removed unused variables in lifecycle and dependency injection - Fixed big.Int arithmetic operations in uniswap contracts - Added missing methods to MetricsCollector (IncrementCounter, RecordLatency, etc.) - Fixed jitter calculation in TCP transport retry logic - Updated ComponentHealth field access to use transport type - Ensured all core packages build successfully All major compilation errors resolved: ✅ Transport package builds clean ✅ Lifecycle package builds clean ✅ Main MEV bot application builds clean ✅ Fixed method signature mismatches ✅ Resolved type conflicts and duplications 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
367 lines
12 KiB
Go
367 lines
12 KiB
Go
package arbitrum
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"math/big"
|
|
"time"
|
|
|
|
"github.com/ethereum/go-ethereum/common"
|
|
"github.com/fraktal/mev-beta/internal/logger"
|
|
"github.com/fraktal/mev-beta/pkg/oracle"
|
|
)
|
|
|
|
// EventEnrichmentService provides comprehensive event data enrichment
|
|
type EventEnrichmentService struct {
|
|
priceOracle *oracle.PriceOracle
|
|
tokenMetadata *TokenMetadataService
|
|
logger *logger.Logger
|
|
|
|
// USD conversion constants
|
|
usdcAddr common.Address
|
|
wethAddr common.Address
|
|
}
|
|
|
|
// NewEventEnrichmentService creates a new event enrichment service
|
|
func NewEventEnrichmentService(
|
|
priceOracle *oracle.PriceOracle,
|
|
tokenMetadata *TokenMetadataService,
|
|
logger *logger.Logger,
|
|
) *EventEnrichmentService {
|
|
return &EventEnrichmentService{
|
|
priceOracle: priceOracle,
|
|
tokenMetadata: tokenMetadata,
|
|
logger: logger,
|
|
usdcAddr: common.HexToAddress("0xaf88d065e77c8cC2239327C5EDb3A432268e5831"), // USDC on Arbitrum
|
|
wethAddr: common.HexToAddress("0x82aF49447D8a07e3bd95BD0d56f35241523fBab1"), // WETH on Arbitrum
|
|
}
|
|
}
|
|
|
|
// EnrichEvent adds comprehensive metadata and USD values to a DEX event
|
|
func (s *EventEnrichmentService) EnrichEvent(ctx context.Context, event *EnhancedDEXEvent) error {
|
|
// Add token metadata
|
|
if err := s.addTokenMetadata(ctx, event); err != nil {
|
|
s.logger.Debug(fmt.Sprintf("Failed to add token metadata: %v", err))
|
|
}
|
|
|
|
// Calculate USD values
|
|
if err := s.calculateUSDValues(ctx, event); err != nil {
|
|
s.logger.Debug(fmt.Sprintf("Failed to calculate USD values: %v", err))
|
|
}
|
|
|
|
// Add factory and router information
|
|
s.addContractMetadata(event)
|
|
|
|
// Calculate price impact and slippage
|
|
if err := s.calculatePriceMetrics(ctx, event); err != nil {
|
|
s.logger.Debug(fmt.Sprintf("Failed to calculate price metrics: %v", err))
|
|
}
|
|
|
|
// Assess MEV potential
|
|
s.assessMEVPotential(event)
|
|
|
|
return nil
|
|
}
|
|
|
|
// addTokenMetadata enriches the event with token metadata
|
|
func (s *EventEnrichmentService) addTokenMetadata(ctx context.Context, event *EnhancedDEXEvent) error {
|
|
// Get metadata for token in
|
|
if event.TokenIn != (common.Address{}) {
|
|
if metadata, err := s.tokenMetadata.GetTokenMetadata(ctx, event.TokenIn); err == nil {
|
|
event.TokenInSymbol = metadata.Symbol
|
|
event.TokenInName = metadata.Name
|
|
event.TokenInDecimals = metadata.Decimals
|
|
event.TokenInRiskScore = metadata.RiskScore
|
|
}
|
|
}
|
|
|
|
// Get metadata for token out
|
|
if event.TokenOut != (common.Address{}) {
|
|
if metadata, err := s.tokenMetadata.GetTokenMetadata(ctx, event.TokenOut); err == nil {
|
|
event.TokenOutSymbol = metadata.Symbol
|
|
event.TokenOutName = metadata.Name
|
|
event.TokenOutDecimals = metadata.Decimals
|
|
event.TokenOutRiskScore = metadata.RiskScore
|
|
}
|
|
}
|
|
|
|
// Get metadata for token0 and token1 if available
|
|
if event.Token0 != (common.Address{}) {
|
|
if metadata, err := s.tokenMetadata.GetTokenMetadata(ctx, event.Token0); err == nil {
|
|
event.Token0Symbol = metadata.Symbol
|
|
event.Token0Decimals = metadata.Decimals
|
|
}
|
|
}
|
|
|
|
if event.Token1 != (common.Address{}) {
|
|
if metadata, err := s.tokenMetadata.GetTokenMetadata(ctx, event.Token1); err == nil {
|
|
event.Token1Symbol = metadata.Symbol
|
|
event.Token1Decimals = metadata.Decimals
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// calculateUSDValues calculates USD values for all amounts in the event
|
|
func (s *EventEnrichmentService) calculateUSDValues(ctx context.Context, event *EnhancedDEXEvent) error {
|
|
// Calculate AmountInUSD
|
|
if event.AmountIn != nil && event.TokenIn != (common.Address{}) {
|
|
if usdValue, err := s.getTokenValueInUSD(ctx, event.TokenIn, event.AmountIn); err == nil {
|
|
event.AmountInUSD = usdValue
|
|
}
|
|
}
|
|
|
|
// Calculate AmountOutUSD
|
|
if event.AmountOut != nil && event.TokenOut != (common.Address{}) {
|
|
if usdValue, err := s.getTokenValueInUSD(ctx, event.TokenOut, event.AmountOut); err == nil {
|
|
event.AmountOutUSD = usdValue
|
|
}
|
|
}
|
|
|
|
// Calculate Amount0USD and Amount1USD for V3 events
|
|
if event.Amount0 != nil && event.Token0 != (common.Address{}) {
|
|
if usdValue, err := s.getTokenValueInUSD(ctx, event.Token0, new(big.Int).Abs(event.Amount0)); err == nil {
|
|
event.Amount0USD = usdValue
|
|
}
|
|
}
|
|
|
|
if event.Amount1 != nil && event.Token1 != (common.Address{}) {
|
|
if usdValue, err := s.getTokenValueInUSD(ctx, event.Token1, new(big.Int).Abs(event.Amount1)); err == nil {
|
|
event.Amount1USD = usdValue
|
|
}
|
|
}
|
|
|
|
// Calculate fee in USD
|
|
if event.AmountInUSD > 0 && event.FeeBps > 0 {
|
|
event.FeeUSD = event.AmountInUSD * float64(event.FeeBps) / 10000.0
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// getTokenValueInUSD converts a token amount to USD value
|
|
func (s *EventEnrichmentService) getTokenValueInUSD(ctx context.Context, tokenAddr common.Address, amount *big.Int) (float64, error) {
|
|
if amount == nil || amount.Sign() == 0 {
|
|
return 0, nil
|
|
}
|
|
|
|
// Direct USDC conversion
|
|
if tokenAddr == s.usdcAddr {
|
|
// USDC has 6 decimals
|
|
amountFloat := new(big.Float).SetInt(amount)
|
|
amountFloat.Quo(amountFloat, big.NewFloat(1e6))
|
|
result, _ := amountFloat.Float64()
|
|
return result, nil
|
|
}
|
|
|
|
// Get price from oracle
|
|
priceReq := &oracle.PriceRequest{
|
|
TokenIn: tokenAddr,
|
|
TokenOut: s.usdcAddr, // Convert to USDC first
|
|
AmountIn: amount,
|
|
Timestamp: time.Now(),
|
|
}
|
|
|
|
priceResp, err := s.priceOracle.GetPrice(ctx, priceReq)
|
|
if err != nil {
|
|
// Fallback: try converting through WETH if direct conversion fails
|
|
if tokenAddr != s.wethAddr {
|
|
return s.getUSDValueThroughWETH(ctx, tokenAddr, amount)
|
|
}
|
|
return 0, fmt.Errorf("failed to get price: %w", err)
|
|
}
|
|
|
|
if !priceResp.Valid || priceResp.AmountOut == nil {
|
|
return 0, fmt.Errorf("invalid price response")
|
|
}
|
|
|
|
// Convert USDC amount to USD (USDC has 6 decimals)
|
|
usdcAmount := new(big.Float).SetInt(priceResp.AmountOut)
|
|
usdcAmount.Quo(usdcAmount, big.NewFloat(1e6))
|
|
result, _ := usdcAmount.Float64()
|
|
|
|
return result, nil
|
|
}
|
|
|
|
// getUSDValueThroughWETH converts token value to USD through WETH
|
|
func (s *EventEnrichmentService) getUSDValueThroughWETH(ctx context.Context, tokenAddr common.Address, amount *big.Int) (float64, error) {
|
|
// First convert token to WETH
|
|
wethReq := &oracle.PriceRequest{
|
|
TokenIn: tokenAddr,
|
|
TokenOut: s.wethAddr,
|
|
AmountIn: amount,
|
|
Timestamp: time.Now(),
|
|
}
|
|
|
|
wethResp, err := s.priceOracle.GetPrice(ctx, wethReq)
|
|
if err != nil || !wethResp.Valid {
|
|
return 0, fmt.Errorf("failed to convert to WETH: %w", err)
|
|
}
|
|
|
|
// Then convert WETH to USD
|
|
return s.getTokenValueInUSD(ctx, s.wethAddr, wethResp.AmountOut)
|
|
}
|
|
|
|
// addContractMetadata adds factory and router contract information
|
|
func (s *EventEnrichmentService) addContractMetadata(event *EnhancedDEXEvent) {
|
|
// Set factory addresses based on protocol
|
|
switch event.Protocol {
|
|
case ProtocolUniswapV2:
|
|
event.Factory = common.HexToAddress("0xf1D7CC64Fb4452F05c498126312eBE29f30Fbcf9")
|
|
event.Router = common.HexToAddress("0x4752ba5dbc23f44d87826276bf6fd6b1c372ad24")
|
|
case ProtocolUniswapV3:
|
|
event.Factory = common.HexToAddress("0x1F98431c8aD98523631AE4a59f267346ea31F984")
|
|
event.Router = common.HexToAddress("0xE592427A0AEce92De3Edee1F18E0157C05861564")
|
|
case ProtocolSushiSwapV2:
|
|
event.Factory = common.HexToAddress("0xc35DADB65012eC5796536bD9864eD8773aBc74C4")
|
|
event.Router = common.HexToAddress("0x1b02dA8Cb0d097eB8D57A175b88c7D8b47997506")
|
|
case ProtocolSushiSwapV3:
|
|
event.Factory = common.HexToAddress("0x7770978eED668a3ba661d51a773d3a992Fc9DDCB")
|
|
event.Router = common.HexToAddress("0x34af5256F1FC2e9F5b5c0f3d8ED82D5a15B69C88")
|
|
case ProtocolCamelotV2:
|
|
event.Factory = common.HexToAddress("0x6EcCab422D763aC031210895C81787E87B91B678")
|
|
event.Router = common.HexToAddress("0xc873fEcbd354f5A56E00E710B90EF4201db2448d")
|
|
case ProtocolCamelotV3:
|
|
event.Factory = common.HexToAddress("0x1a3c9B1d2F0529D97f2afC5136Cc23e58f1FD35B")
|
|
event.Router = common.HexToAddress("0x1F721E2E82F6676FCE4eA07A5958cF098D339e18")
|
|
}
|
|
}
|
|
|
|
// calculatePriceMetrics calculates price impact and slippage
|
|
func (s *EventEnrichmentService) calculatePriceMetrics(ctx context.Context, event *EnhancedDEXEvent) error {
|
|
// Skip if we don't have enough data
|
|
if event.AmountIn == nil || event.AmountOut == nil ||
|
|
event.TokenIn == (common.Address{}) || event.TokenOut == (common.Address{}) {
|
|
return nil
|
|
}
|
|
|
|
// Get current market price
|
|
marketReq := &oracle.PriceRequest{
|
|
TokenIn: event.TokenIn,
|
|
TokenOut: event.TokenOut,
|
|
AmountIn: big.NewInt(1e18), // 1 token for reference price
|
|
Timestamp: time.Now(),
|
|
}
|
|
|
|
marketResp, err := s.priceOracle.GetPrice(ctx, marketReq)
|
|
if err != nil || !marketResp.Valid {
|
|
return fmt.Errorf("failed to get market price: %w", err)
|
|
}
|
|
|
|
// Calculate effective price from the trade
|
|
effectivePrice := new(big.Float).Quo(
|
|
new(big.Float).SetInt(event.AmountOut),
|
|
new(big.Float).SetInt(event.AmountIn),
|
|
)
|
|
|
|
// Calculate market price
|
|
marketPrice := new(big.Float).Quo(
|
|
new(big.Float).SetInt(marketResp.AmountOut),
|
|
new(big.Float).SetInt(marketReq.AmountIn),
|
|
)
|
|
|
|
// Calculate price impact: (marketPrice - effectivePrice) / marketPrice
|
|
priceDiff := new(big.Float).Sub(marketPrice, effectivePrice)
|
|
priceImpact := new(big.Float).Quo(priceDiff, marketPrice)
|
|
|
|
impact, _ := priceImpact.Float64()
|
|
event.PriceImpact = impact
|
|
|
|
// Convert to basis points for slippage
|
|
event.SlippageBps = uint64(impact * 10000)
|
|
|
|
return nil
|
|
}
|
|
|
|
// assessMEVPotential determines if the event has MEV potential
|
|
func (s *EventEnrichmentService) assessMEVPotential(event *EnhancedDEXEvent) {
|
|
// Initialize MEV assessment
|
|
event.IsMEV = false
|
|
event.MEVType = ""
|
|
event.ProfitUSD = 0.0
|
|
|
|
// High-value transactions are more likely to be MEV
|
|
if event.AmountInUSD > 50000 { // $50k threshold
|
|
event.IsMEV = true
|
|
event.MEVType = "high_value"
|
|
event.ProfitUSD = event.AmountInUSD * 0.001 // Estimate 0.1% profit
|
|
}
|
|
|
|
// High price impact suggests potential sandwich opportunity
|
|
if event.PriceImpact > 0.02 { // 2% price impact
|
|
event.IsMEV = true
|
|
event.MEVType = "sandwich_opportunity"
|
|
event.ProfitUSD = event.AmountInUSD * event.PriceImpact * 0.5 // Estimate half the impact as profit
|
|
}
|
|
|
|
// High slippage tolerance indicates MEV potential
|
|
if event.SlippageBps > 500 { // 5% slippage tolerance
|
|
event.IsMEV = true
|
|
if event.MEVType == "" {
|
|
event.MEVType = "arbitrage"
|
|
}
|
|
event.ProfitUSD = event.AmountInUSD * 0.002 // Estimate 0.2% profit
|
|
}
|
|
|
|
// Transactions involving risky tokens
|
|
if event.TokenInRiskScore > 0.7 || event.TokenOutRiskScore > 0.7 {
|
|
event.IsMEV = true
|
|
if event.MEVType == "" {
|
|
event.MEVType = "risky_arbitrage"
|
|
}
|
|
event.ProfitUSD = event.AmountInUSD * 0.005 // Higher profit for risky trades
|
|
}
|
|
|
|
// Flash loan indicators (large amounts with no sender balance check)
|
|
if event.AmountInUSD > 100000 && event.MEVType == "" {
|
|
event.IsMEV = true
|
|
event.MEVType = "flash_loan_arbitrage"
|
|
event.ProfitUSD = event.AmountInUSD * 0.003 // Estimate 0.3% profit
|
|
}
|
|
}
|
|
|
|
// CalculatePoolTVL calculates the total value locked in a pool
|
|
func (s *EventEnrichmentService) CalculatePoolTVL(ctx context.Context, poolAddr common.Address, token0, token1 common.Address, reserve0, reserve1 *big.Int) (float64, error) {
|
|
if reserve0 == nil || reserve1 == nil {
|
|
return 0, fmt.Errorf("invalid reserves")
|
|
}
|
|
|
|
// Get USD value of both reserves
|
|
value0, err := s.getTokenValueInUSD(ctx, token0, reserve0)
|
|
if err != nil {
|
|
value0 = 0 // Continue with just one side if the other fails
|
|
}
|
|
|
|
value1, err := s.getTokenValueInUSD(ctx, token1, reserve1)
|
|
if err != nil {
|
|
value1 = 0
|
|
}
|
|
|
|
// TVL is the sum of both reserves in USD
|
|
tvl := value0 + value1
|
|
|
|
return tvl, nil
|
|
}
|
|
|
|
// EnhancedDEXEvent extensions for enriched data
|
|
type EnhancedDEXEventExtended struct {
|
|
*EnhancedDEXEvent
|
|
|
|
// Additional enriched fields
|
|
TokenInRiskScore float64 `json:"tokenInRiskScore"`
|
|
TokenOutRiskScore float64 `json:"tokenOutRiskScore"`
|
|
|
|
// Pool information
|
|
PoolTVL float64 `json:"poolTVL"`
|
|
PoolUtilization float64 `json:"poolUtilization"` // How much of the pool was used
|
|
|
|
// MEV analysis
|
|
SandwichRisk float64 `json:"sandwichRisk"` // 0.0 to 1.0
|
|
ArbitrageProfit float64 `json:"arbitrageProfit"` // Estimated profit in USD
|
|
|
|
// Market context
|
|
VolumeRank24h int `json:"volumeRank24h"` // Rank by 24h volume
|
|
PriceChange24h float64 `json:"priceChange24h"` // Price change in last 24h
|
|
}
|