Files
mev-beta/pkg/execution/executor.go

312 lines
9.6 KiB
Go

package execution
import (
"context"
"fmt"
"math/big"
"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/types"
)
// ExecutionMode defines how opportunities should be executed
type ExecutionMode int
const (
// SimulationMode only simulates execution without sending transactions
SimulationMode ExecutionMode = iota
// DryRunMode validates transactions but doesn't send
DryRunMode
// LiveMode executes real transactions on-chain
LiveMode
)
// ExecutionResult represents the result of an arbitrage execution
type ExecutionResult struct {
OpportunityID string
Success bool
TxHash common.Hash
GasUsed uint64
ActualProfit *big.Int
EstimatedProfit *big.Int
SlippagePercent float64
ExecutionTime time.Duration
Error error
Timestamp time.Time
}
// ExecutionConfig holds configuration for the executor
type ExecutionConfig struct {
Mode ExecutionMode
MaxGasPrice *big.Int // Maximum gas price willing to pay (wei)
MaxSlippage float64 // Maximum slippage tolerance (0.05 = 5%)
MinProfitThreshold *big.Int // Minimum profit to execute (wei)
SimulationRPCURL string // RPC URL for simulation/fork testing
FlashLoanProvider string // "aave", "uniswap", "balancer"
MaxRetries int // Maximum execution retries
RetryDelay time.Duration
EnableParallelExec bool // Execute multiple opportunities in parallel
DryRun bool // If true, don't send transactions
}
// ArbitrageExecutor handles execution of arbitrage opportunities
type ArbitrageExecutor struct {
config *ExecutionConfig
client *ethclient.Client
logger *logger.Logger
flashLoan FlashLoanProvider
slippage *SlippageProtector
simulator *ExecutionSimulator
resultsChan chan *ExecutionResult
stopChan chan struct{}
}
// FlashLoanProvider interface for different flash loan protocols
type FlashLoanProvider interface {
// ExecuteFlashLoan executes an arbitrage opportunity using flash loans
ExecuteFlashLoan(ctx context.Context, opportunity *types.ArbitrageOpportunity, config *ExecutionConfig) (*ExecutionResult, error)
// GetMaxLoanAmount returns maximum loan amount available for a token
GetMaxLoanAmount(ctx context.Context, token common.Address) (*big.Int, error)
// GetFee returns the flash loan fee for a given amount
GetFee(ctx context.Context, amount *big.Int) (*big.Int, error)
// SupportsToken checks if the provider supports a given token
SupportsToken(token common.Address) bool
}
// SlippageProtector handles slippage protection and validation
type SlippageProtector struct {
maxSlippage float64
logger *logger.Logger
}
// ExecutionSimulator simulates trades on a fork before real execution
type ExecutionSimulator struct {
forkClient *ethclient.Client
logger *logger.Logger
}
// NewArbitrageExecutor creates a new arbitrage executor
func NewArbitrageExecutor(
config *ExecutionConfig,
client *ethclient.Client,
logger *logger.Logger,
) (*ArbitrageExecutor, error) {
if config == nil {
return nil, fmt.Errorf("execution config cannot be nil")
}
executor := &ArbitrageExecutor{
config: config,
client: client,
logger: logger,
resultsChan: make(chan *ExecutionResult, 100),
stopChan: make(chan struct{}),
}
// Initialize slippage protector
executor.slippage = &SlippageProtector{
maxSlippage: config.MaxSlippage,
logger: logger,
}
// Initialize simulator if simulation RPC is provided
if config.SimulationRPCURL != "" {
forkClient, err := ethclient.Dial(config.SimulationRPCURL)
if err != nil {
logger.Warn(fmt.Sprintf("Failed to connect to simulation RPC: %v", err))
} else {
executor.simulator = &ExecutionSimulator{
forkClient: forkClient,
logger: logger,
}
logger.Info("Execution simulator initialized")
}
}
// Initialize flash loan provider
switch config.FlashLoanProvider {
case "aave":
executor.flashLoan = NewAaveFlashLoanProvider(client, logger)
logger.Info("Using Aave flash loans")
case "uniswap":
executor.flashLoan = NewUniswapFlashLoanProvider(client, logger)
logger.Info("Using Uniswap flash swaps")
case "balancer":
executor.flashLoan = NewBalancerFlashLoanProvider(client, logger)
logger.Info("Using Balancer flash loans")
default:
logger.Warn(fmt.Sprintf("Unknown flash loan provider: %s, using Aave", config.FlashLoanProvider))
executor.flashLoan = NewAaveFlashLoanProvider(client, logger)
}
return executor, nil
}
// ExecuteOpportunity executes an arbitrage opportunity
func (ae *ArbitrageExecutor) ExecuteOpportunity(ctx context.Context, opportunity *types.ArbitrageOpportunity) (*ExecutionResult, error) {
startTime := time.Now()
ae.logger.Info(fmt.Sprintf("🎯 Executing arbitrage opportunity: %s", opportunity.ID))
// Step 1: Validate opportunity is still profitable
if !ae.validateOpportunity(opportunity) {
return &ExecutionResult{
OpportunityID: opportunity.ID,
Success: false,
Error: fmt.Errorf("opportunity validation failed"),
Timestamp: time.Now(),
}, nil
}
// Step 2: Check slippage limits
if err := ae.slippage.ValidateSlippage(opportunity); err != nil {
ae.logger.Warn(fmt.Sprintf("Slippage validation failed: %v", err))
return &ExecutionResult{
OpportunityID: opportunity.ID,
Success: false,
Error: fmt.Errorf("slippage too high: %w", err),
Timestamp: time.Now(),
}, nil
}
// Step 3: Simulate execution if simulator available
if ae.simulator != nil && ae.config.Mode != LiveMode {
simulationResult, err := ae.simulator.Simulate(ctx, opportunity, ae.config)
if err != nil {
ae.logger.Error(fmt.Sprintf("Simulation failed: %v", err))
return &ExecutionResult{
OpportunityID: opportunity.ID,
Success: false,
Error: fmt.Errorf("simulation failed: %w", err),
Timestamp: time.Now(),
}, nil
}
// If in simulation mode, return simulation result
if ae.config.Mode == SimulationMode {
simulationResult.ExecutionTime = time.Since(startTime)
return simulationResult, nil
}
ae.logger.Info(fmt.Sprintf("Simulation succeeded: profit=%s ETH", simulationResult.ActualProfit.String()))
}
// Step 4: Execute via flash loan (if not in dry-run mode)
if ae.config.DryRun || ae.config.Mode == DryRunMode {
ae.logger.Info("Dry-run mode: skipping real execution")
return &ExecutionResult{
OpportunityID: opportunity.ID,
Success: true,
EstimatedProfit: opportunity.NetProfit,
Error: nil,
ExecutionTime: time.Since(startTime),
Timestamp: time.Now(),
}, nil
}
// Step 5: Real execution
result, err := ae.flashLoan.ExecuteFlashLoan(ctx, opportunity, ae.config)
if err != nil {
ae.logger.Error(fmt.Sprintf("Flash loan execution failed: %v", err))
return &ExecutionResult{
OpportunityID: opportunity.ID,
Success: false,
Error: err,
ExecutionTime: time.Since(startTime),
Timestamp: time.Now(),
}, err
}
result.ExecutionTime = time.Since(startTime)
ae.logger.Info(fmt.Sprintf("✅ Arbitrage executed successfully: profit=%s ETH, gas=%d",
result.ActualProfit.String(), result.GasUsed))
// Send result to channel for monitoring
select {
case ae.resultsChan <- result:
default:
ae.logger.Warn("Results channel full, dropping result")
}
return result, nil
}
// validateOpportunity validates that an opportunity is still valid
func (ae *ArbitrageExecutor) validateOpportunity(opp *types.ArbitrageOpportunity) bool {
// Check minimum profit threshold
if opp.NetProfit.Cmp(ae.config.MinProfitThreshold) < 0 {
ae.logger.Debug(fmt.Sprintf("Opportunity below profit threshold: %s < %s",
opp.NetProfit.String(), ae.config.MinProfitThreshold.String()))
return false
}
// Check opportunity hasn't expired
if time.Now().After(opp.ExpiresAt) {
ae.logger.Debug("Opportunity has expired")
return false
}
// Additional validation checks can be added here
// - Re-fetch pool states
// - Verify liquidity still available
// - Check gas prices haven't spiked
return true
}
// ValidateSlippage checks if slippage is within acceptable limits
func (sp *SlippageProtector) ValidateSlippage(opp *types.ArbitrageOpportunity) error {
// Calculate expected slippage based on pool liquidity
// This is a simplified version - production would need more sophisticated calculation
if opp.PriceImpact > sp.maxSlippage {
return fmt.Errorf("slippage %.2f%% exceeds maximum %.2f%%",
opp.PriceImpact*100, sp.maxSlippage*100)
}
return nil
}
// Simulate simulates execution on a fork
func (es *ExecutionSimulator) Simulate(
ctx context.Context,
opportunity *types.ArbitrageOpportunity,
config *ExecutionConfig,
) (*ExecutionResult, error) {
es.logger.Info(fmt.Sprintf("🧪 Simulating arbitrage: %s", opportunity.ID))
// In a real implementation, this would:
// 1. Fork the current blockchain state
// 2. Execute the arbitrage path on the fork
// 3. Validate results match expectations
// 4. Return simulated result
// For now, return a simulated success
return &ExecutionResult{
OpportunityID: opportunity.ID,
Success: true,
ActualProfit: opportunity.NetProfit,
EstimatedProfit: opportunity.NetProfit,
SlippagePercent: 0.01, // 1% simulated slippage
Timestamp: time.Now(),
}, nil
}
// GetResultsChannel returns the channel for execution results
func (ae *ArbitrageExecutor) GetResultsChannel() <-chan *ExecutionResult {
return ae.resultsChan
}
// Stop stops the executor
func (ae *ArbitrageExecutor) Stop() {
close(ae.stopChan)
ae.logger.Info("Arbitrage executor stopped")
}