feat: create v2-prep branch with comprehensive planning
Restructured project for V2 refactor: **Structure Changes:** - Moved all V1 code to orig/ folder (preserved with git mv) - Created docs/planning/ directory - Added orig/README_V1.md explaining V1 preservation **Planning Documents:** - 00_V2_MASTER_PLAN.md: Complete architecture overview - Executive summary of critical V1 issues - High-level component architecture diagrams - 5-phase implementation roadmap - Success metrics and risk mitigation - 07_TASK_BREAKDOWN.md: Atomic task breakdown - 99+ hours of detailed tasks - Every task < 2 hours (atomic) - Clear dependencies and success criteria - Organized by implementation phase **V2 Key Improvements:** - Per-exchange parsers (factory pattern) - Multi-layer strict validation - Multi-index pool cache - Background validation pipeline - Comprehensive observability **Critical Issues Addressed:** - Zero address tokens (strict validation + cache enrichment) - Parsing accuracy (protocol-specific parsers) - No audit trail (background validation channel) - Inefficient lookups (multi-index cache) - Stats disconnection (event-driven metrics) Next Steps: 1. Review planning documents 2. Begin Phase 1: Foundation (P1-001 through P1-010) 3. Implement parsers in Phase 2 4. Build cache system in Phase 3 5. Add validation pipeline in Phase 4 6. Migrate and test in Phase 5 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
291
orig/pkg/execution/alerts.go
Normal file
291
orig/pkg/execution/alerts.go
Normal file
@@ -0,0 +1,291 @@
|
||||
package execution
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/big"
|
||||
"time"
|
||||
|
||||
"github.com/fraktal/mev-beta/internal/logger"
|
||||
"github.com/fraktal/mev-beta/pkg/types"
|
||||
)
|
||||
|
||||
// AlertLevel defines the severity of an alert
|
||||
type AlertLevel int
|
||||
|
||||
const (
|
||||
InfoLevel AlertLevel = iota
|
||||
WarningLevel
|
||||
CriticalLevel
|
||||
)
|
||||
|
||||
func (al AlertLevel) String() string {
|
||||
switch al {
|
||||
case InfoLevel:
|
||||
return "INFO"
|
||||
case WarningLevel:
|
||||
return "WARNING"
|
||||
case CriticalLevel:
|
||||
return "CRITICAL"
|
||||
default:
|
||||
return "UNKNOWN"
|
||||
}
|
||||
}
|
||||
|
||||
// Alert represents a system alert
|
||||
type Alert struct {
|
||||
Level AlertLevel
|
||||
Title string
|
||||
Message string
|
||||
Opportunity *types.ArbitrageOpportunity
|
||||
Timestamp time.Time
|
||||
}
|
||||
|
||||
// AlertConfig holds configuration for the alert system
|
||||
type AlertConfig struct {
|
||||
EnableConsoleAlerts bool
|
||||
EnableFileAlerts bool
|
||||
EnableWebhook bool
|
||||
WebhookURL string
|
||||
MinProfitForAlert *big.Int // Minimum profit to trigger alert (wei)
|
||||
MinROIForAlert float64 // Minimum ROI to trigger alert (0.05 = 5%)
|
||||
AlertCooldown time.Duration // Minimum time between alerts
|
||||
}
|
||||
|
||||
// AlertSystem handles opportunity alerts and notifications
|
||||
type AlertSystem struct {
|
||||
config *AlertConfig
|
||||
logger *logger.Logger
|
||||
lastAlertTime time.Time
|
||||
alertCount uint64
|
||||
}
|
||||
|
||||
// NewAlertSystem creates a new alert system
|
||||
func NewAlertSystem(config *AlertConfig, logger *logger.Logger) *AlertSystem {
|
||||
return &AlertSystem{
|
||||
config: config,
|
||||
logger: logger,
|
||||
lastAlertTime: time.Time{},
|
||||
alertCount: 0,
|
||||
}
|
||||
}
|
||||
|
||||
// SendOpportunityAlert sends an alert for a profitable opportunity
|
||||
func (as *AlertSystem) SendOpportunityAlert(opp *types.ArbitrageOpportunity) {
|
||||
// Check cooldown
|
||||
if time.Since(as.lastAlertTime) < as.config.AlertCooldown {
|
||||
as.logger.Debug("Alert cooldown active, skipping alert")
|
||||
return
|
||||
}
|
||||
|
||||
// Check minimum thresholds
|
||||
if opp.NetProfit.Cmp(as.config.MinProfitForAlert) < 0 {
|
||||
return
|
||||
}
|
||||
|
||||
if opp.ROI < as.config.MinROIForAlert {
|
||||
return
|
||||
}
|
||||
|
||||
// Determine alert level
|
||||
level := as.determineAlertLevel(opp)
|
||||
|
||||
// Create alert
|
||||
alert := &Alert{
|
||||
Level: level,
|
||||
Title: fmt.Sprintf("Profitable Arbitrage Opportunity Detected"),
|
||||
Message: as.formatOpportunityMessage(opp),
|
||||
Opportunity: opp,
|
||||
Timestamp: time.Now(),
|
||||
}
|
||||
|
||||
// Send alert via configured channels
|
||||
as.sendAlert(alert)
|
||||
|
||||
as.lastAlertTime = time.Now()
|
||||
as.alertCount++
|
||||
}
|
||||
|
||||
// SendExecutionAlert sends an alert for execution results
|
||||
func (as *AlertSystem) SendExecutionAlert(result *ExecutionResult) {
|
||||
var level AlertLevel
|
||||
var title string
|
||||
|
||||
if result.Success {
|
||||
level = InfoLevel
|
||||
title = "Arbitrage Executed Successfully"
|
||||
} else {
|
||||
level = WarningLevel
|
||||
title = "Arbitrage Execution Failed"
|
||||
}
|
||||
|
||||
alert := &Alert{
|
||||
Level: level,
|
||||
Title: title,
|
||||
Message: as.formatExecutionMessage(result),
|
||||
Timestamp: time.Now(),
|
||||
}
|
||||
|
||||
as.sendAlert(alert)
|
||||
}
|
||||
|
||||
// SendSystemAlert sends a system-level alert
|
||||
func (as *AlertSystem) SendSystemAlert(level AlertLevel, title, message string) {
|
||||
alert := &Alert{
|
||||
Level: level,
|
||||
Title: title,
|
||||
Message: message,
|
||||
Timestamp: time.Now(),
|
||||
}
|
||||
|
||||
as.sendAlert(alert)
|
||||
}
|
||||
|
||||
// determineAlertLevel determines the appropriate alert level
|
||||
func (as *AlertSystem) determineAlertLevel(opp *types.ArbitrageOpportunity) AlertLevel {
|
||||
// Critical if ROI > 10% or profit > 1 ETH
|
||||
oneETH := new(big.Int).Mul(big.NewInt(1), big.NewInt(1e18))
|
||||
if opp.ROI > 0.10 || opp.NetProfit.Cmp(oneETH) > 0 {
|
||||
return CriticalLevel
|
||||
}
|
||||
|
||||
// Warning if ROI > 5% or profit > 0.1 ETH
|
||||
pointOneETH := new(big.Int).Mul(big.NewInt(1), big.NewInt(1e17))
|
||||
if opp.ROI > 0.05 || opp.NetProfit.Cmp(pointOneETH) > 0 {
|
||||
return WarningLevel
|
||||
}
|
||||
|
||||
return InfoLevel
|
||||
}
|
||||
|
||||
// sendAlert sends an alert via all configured channels
|
||||
func (as *AlertSystem) sendAlert(alert *Alert) {
|
||||
// Console alert
|
||||
if as.config.EnableConsoleAlerts {
|
||||
as.sendConsoleAlert(alert)
|
||||
}
|
||||
|
||||
// File alert
|
||||
if as.config.EnableFileAlerts {
|
||||
as.sendFileAlert(alert)
|
||||
}
|
||||
|
||||
// Webhook alert
|
||||
if as.config.EnableWebhook && as.config.WebhookURL != "" {
|
||||
as.sendWebhookAlert(alert)
|
||||
}
|
||||
}
|
||||
|
||||
// sendConsoleAlert prints alert to console
|
||||
func (as *AlertSystem) sendConsoleAlert(alert *Alert) {
|
||||
emoji := "ℹ️"
|
||||
switch alert.Level {
|
||||
case WarningLevel:
|
||||
emoji = "⚠️"
|
||||
case CriticalLevel:
|
||||
emoji = "🚨"
|
||||
}
|
||||
|
||||
as.logger.Info(fmt.Sprintf("%s [%s] %s", emoji, alert.Level, alert.Title))
|
||||
as.logger.Info(alert.Message)
|
||||
}
|
||||
|
||||
// sendFileAlert writes alert to file
|
||||
func (as *AlertSystem) sendFileAlert(alert *Alert) {
|
||||
// TODO: Implement file-based alerts
|
||||
// Write to logs/alerts/alert_YYYYMMDD_HHMMSS.json
|
||||
}
|
||||
|
||||
// sendWebhookAlert sends alert to webhook (Slack, Discord, etc.)
|
||||
func (as *AlertSystem) sendWebhookAlert(alert *Alert) {
|
||||
// TODO: Implement webhook alerts
|
||||
// POST JSON to configured webhook URL
|
||||
as.logger.Debug(fmt.Sprintf("Would send webhook alert to: %s", as.config.WebhookURL))
|
||||
}
|
||||
|
||||
// formatOpportunityMessage formats an opportunity alert message
|
||||
func (as *AlertSystem) formatOpportunityMessage(opp *types.ArbitrageOpportunity) string {
|
||||
profitETH := new(big.Float).Quo(
|
||||
new(big.Float).SetInt(opp.NetProfit),
|
||||
big.NewFloat(1e18),
|
||||
)
|
||||
|
||||
gasEstimate := "N/A"
|
||||
if opp.GasEstimate != nil {
|
||||
gasEstimate = opp.GasEstimate.String()
|
||||
}
|
||||
|
||||
return fmt.Sprintf(`
|
||||
🎯 Arbitrage Opportunity Details:
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
• ID: %s
|
||||
• Path: %v
|
||||
• Protocol: %s
|
||||
• Amount In: %s wei
|
||||
• Estimated Profit: %.6f ETH
|
||||
• ROI: %.2f%%
|
||||
• Gas Estimate: %s wei
|
||||
• Confidence: %.1f%%
|
||||
• Price Impact: %.2f%%
|
||||
• Expires: %s
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
`,
|
||||
opp.ID,
|
||||
opp.Path,
|
||||
opp.Protocol,
|
||||
opp.AmountIn.String(),
|
||||
profitETH,
|
||||
opp.ROI*100,
|
||||
gasEstimate,
|
||||
opp.Confidence*100,
|
||||
opp.PriceImpact*100,
|
||||
opp.ExpiresAt.Format("15:04:05"),
|
||||
)
|
||||
}
|
||||
|
||||
// formatExecutionMessage formats an execution result message
|
||||
func (as *AlertSystem) formatExecutionMessage(result *ExecutionResult) string {
|
||||
status := "✅ SUCCESS"
|
||||
if !result.Success {
|
||||
status = "❌ FAILED"
|
||||
}
|
||||
|
||||
profitETH := "N/A"
|
||||
if result.ActualProfit != nil {
|
||||
p := new(big.Float).Quo(
|
||||
new(big.Float).SetInt(result.ActualProfit),
|
||||
big.NewFloat(1e18),
|
||||
)
|
||||
profitETH = fmt.Sprintf("%.6f ETH", p)
|
||||
}
|
||||
|
||||
errorMsg := ""
|
||||
if result.Error != nil {
|
||||
errorMsg = fmt.Sprintf("\n• Error: %v", result.Error)
|
||||
}
|
||||
|
||||
return fmt.Sprintf(`
|
||||
%s Arbitrage Execution
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
• Opportunity ID: %s
|
||||
• Tx Hash: %s
|
||||
• Actual Profit: %s
|
||||
• Gas Used: %d
|
||||
• Slippage: %.2f%%
|
||||
• Execution Time: %v%s
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
`,
|
||||
status,
|
||||
result.OpportunityID,
|
||||
result.TxHash.Hex(),
|
||||
profitETH,
|
||||
result.GasUsed,
|
||||
result.SlippagePercent*100,
|
||||
result.ExecutionTime,
|
||||
errorMsg,
|
||||
)
|
||||
}
|
||||
|
||||
// GetAlertCount returns the total number of alerts sent
|
||||
func (as *AlertSystem) GetAlertCount() uint64 {
|
||||
return as.alertCount
|
||||
}
|
||||
270
orig/pkg/execution/execution_test.go
Normal file
270
orig/pkg/execution/execution_test.go
Normal file
@@ -0,0 +1,270 @@
|
||||
package execution
|
||||
|
||||
import (
|
||||
"context"
|
||||
"math/big"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestExecutionModes(t *testing.T) {
|
||||
// Test that execution modes are properly defined
|
||||
assert.Equal(t, ExecutionMode(0), SimulationMode)
|
||||
assert.Equal(t, ExecutionMode(1), DryRunMode)
|
||||
assert.Equal(t, ExecutionMode(2), LiveMode)
|
||||
}
|
||||
|
||||
func TestExecutionConfigDefaults(t *testing.T) {
|
||||
config := &ExecutionConfig{
|
||||
Mode: SimulationMode,
|
||||
MaxSlippage: 0.05,
|
||||
MaxRetries: 3,
|
||||
RetryDelay: 1 * time.Second,
|
||||
DryRun: true,
|
||||
}
|
||||
|
||||
assert.Equal(t, SimulationMode, config.Mode)
|
||||
assert.Equal(t, 0.05, config.MaxSlippage)
|
||||
assert.Equal(t, 3, config.MaxRetries)
|
||||
assert.Equal(t, true, config.DryRun)
|
||||
}
|
||||
|
||||
func TestExecutionResultCreation(t *testing.T) {
|
||||
result := &ExecutionResult{
|
||||
OpportunityID: "test_opp_001",
|
||||
Success: true,
|
||||
TxHash: common.HexToHash("0x1234567890abcdef"),
|
||||
GasUsed: 100000,
|
||||
ActualProfit: big.NewInt(1000),
|
||||
EstimatedProfit: big.NewInt(1100),
|
||||
SlippagePercent: 0.5,
|
||||
ExecutionTime: 1500 * time.Millisecond,
|
||||
Timestamp: time.Now(),
|
||||
}
|
||||
|
||||
assert.NotNil(t, result)
|
||||
assert.Equal(t, "test_opp_001", result.OpportunityID)
|
||||
assert.True(t, result.Success)
|
||||
assert.NotNil(t, result.TxHash)
|
||||
assert.Equal(t, uint64(100000), result.GasUsed)
|
||||
assert.NotNil(t, result.ActualProfit)
|
||||
assert.NotNil(t, result.EstimatedProfit)
|
||||
}
|
||||
|
||||
func TestExecutionResultWithError(t *testing.T) {
|
||||
result := &ExecutionResult{
|
||||
OpportunityID: "test_opp_002",
|
||||
Success: false,
|
||||
Error: assert.AnError,
|
||||
Timestamp: time.Now(),
|
||||
}
|
||||
|
||||
assert.NotNil(t, result)
|
||||
assert.False(t, result.Success)
|
||||
assert.NotNil(t, result.Error)
|
||||
}
|
||||
|
||||
func TestSimulationMode(t *testing.T) {
|
||||
config := &ExecutionConfig{
|
||||
Mode: SimulationMode,
|
||||
DryRun: true,
|
||||
}
|
||||
|
||||
// In simulation mode, no transactions should be sent
|
||||
assert.Equal(t, SimulationMode, config.Mode)
|
||||
assert.True(t, config.DryRun)
|
||||
}
|
||||
|
||||
func TestDryRunMode(t *testing.T) {
|
||||
config := &ExecutionConfig{
|
||||
Mode: DryRunMode,
|
||||
DryRun: true,
|
||||
}
|
||||
|
||||
// In dry run mode, validate but don't execute
|
||||
assert.Equal(t, DryRunMode, config.Mode)
|
||||
assert.True(t, config.DryRun)
|
||||
}
|
||||
|
||||
func TestLiveMode(t *testing.T) {
|
||||
config := &ExecutionConfig{
|
||||
Mode: LiveMode,
|
||||
DryRun: false,
|
||||
}
|
||||
|
||||
// In live mode, execute real transactions
|
||||
assert.Equal(t, LiveMode, config.Mode)
|
||||
assert.False(t, config.DryRun)
|
||||
}
|
||||
|
||||
func TestExecutionConfigWithGasPrice(t *testing.T) {
|
||||
maxGasPrice := big.NewInt(100000000) // 0.1 gwei
|
||||
|
||||
config := &ExecutionConfig{
|
||||
Mode: DryRunMode,
|
||||
MaxGasPrice: maxGasPrice,
|
||||
MaxSlippage: 0.03,
|
||||
}
|
||||
|
||||
assert.NotNil(t, config.MaxGasPrice)
|
||||
assert.Equal(t, maxGasPrice, config.MaxGasPrice)
|
||||
assert.Equal(t, 0.03, config.MaxSlippage)
|
||||
}
|
||||
|
||||
func TestExecutionConfigWithMinProfit(t *testing.T) {
|
||||
minProfit := big.NewInt(1000000000000000) // 0.001 ETH
|
||||
|
||||
config := &ExecutionConfig{
|
||||
Mode: SimulationMode,
|
||||
MinProfitThreshold: minProfit,
|
||||
}
|
||||
|
||||
assert.NotNil(t, config.MinProfitThreshold)
|
||||
assert.Equal(t, minProfit, config.MinProfitThreshold)
|
||||
}
|
||||
|
||||
func TestExecutionFlashLoanConfig(t *testing.T) {
|
||||
config := &ExecutionConfig{
|
||||
Mode: LiveMode,
|
||||
FlashLoanProvider: "balancer",
|
||||
MaxRetries: 5,
|
||||
RetryDelay: 500 * time.Millisecond,
|
||||
}
|
||||
|
||||
assert.Equal(t, "balancer", config.FlashLoanProvider)
|
||||
assert.Equal(t, 5, config.MaxRetries)
|
||||
assert.Equal(t, 500*time.Millisecond, config.RetryDelay)
|
||||
}
|
||||
|
||||
func TestExecutionParallelConfig(t *testing.T) {
|
||||
config := &ExecutionConfig{
|
||||
Mode: DryRunMode,
|
||||
EnableParallelExec: true,
|
||||
MaxRetries: 3,
|
||||
}
|
||||
|
||||
assert.True(t, config.EnableParallelExec)
|
||||
assert.Equal(t, 3, config.MaxRetries)
|
||||
}
|
||||
|
||||
func TestExecutionTimestamp(t *testing.T) {
|
||||
before := time.Now()
|
||||
result := &ExecutionResult{
|
||||
OpportunityID: "test_opp_003",
|
||||
Success: true,
|
||||
Timestamp: time.Now(),
|
||||
}
|
||||
after := time.Now()
|
||||
|
||||
assert.True(t, result.Timestamp.After(before) || result.Timestamp.Equal(before))
|
||||
assert.True(t, result.Timestamp.Before(after) || result.Timestamp.Equal(after))
|
||||
}
|
||||
|
||||
func TestMultipleExecutionResults(t *testing.T) {
|
||||
results := make([]*ExecutionResult, 5)
|
||||
|
||||
for i := 0; i < 5; i++ {
|
||||
results[i] = &ExecutionResult{
|
||||
OpportunityID: "opp_" + string(rune(i)),
|
||||
Success: i%2 == 0,
|
||||
GasUsed: uint64(100000 + i*1000),
|
||||
ActualProfit: big.NewInt(int64(1000 * (i + 1))),
|
||||
ExecutionTime: time.Duration(1000*(i+1)) * time.Millisecond,
|
||||
Timestamp: time.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
assert.Equal(t, 5, len(results))
|
||||
for i, result := range results {
|
||||
assert.NotNil(t, result)
|
||||
assert.NotEmpty(t, result.OpportunityID)
|
||||
assert.Equal(t, i%2 == 0, result.Success)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecutionResultWithZeroProfit(t *testing.T) {
|
||||
result := &ExecutionResult{
|
||||
OpportunityID: "zero_profit_opp",
|
||||
Success: true,
|
||||
ActualProfit: big.NewInt(0),
|
||||
EstimatedProfit: big.NewInt(100),
|
||||
Timestamp: time.Now(),
|
||||
}
|
||||
|
||||
assert.NotNil(t, result)
|
||||
assert.True(t, result.Success)
|
||||
assert.Equal(t, int64(0), result.ActualProfit.Int64())
|
||||
}
|
||||
|
||||
func TestExecutionResultWithNegativeProfit(t *testing.T) {
|
||||
result := &ExecutionResult{
|
||||
OpportunityID: "loss_opp",
|
||||
Success: false,
|
||||
ActualProfit: big.NewInt(-500),
|
||||
EstimatedProfit: big.NewInt(100),
|
||||
Timestamp: time.Now(),
|
||||
}
|
||||
|
||||
assert.NotNil(t, result)
|
||||
assert.False(t, result.Success)
|
||||
assert.True(t, result.ActualProfit.Sign() < 0)
|
||||
}
|
||||
|
||||
func TestContextTimeout(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
|
||||
defer cancel()
|
||||
|
||||
config := &ExecutionConfig{
|
||||
Mode: SimulationMode,
|
||||
DryRun: true,
|
||||
}
|
||||
|
||||
// Should handle context timeout gracefully
|
||||
assert.NotNil(t, config)
|
||||
<-ctx.Done()
|
||||
assert.Error(t, ctx.Err())
|
||||
}
|
||||
|
||||
func TestExecutionConfigValidation(t *testing.T) {
|
||||
configs := []struct {
|
||||
name string
|
||||
config *ExecutionConfig
|
||||
valid bool
|
||||
}{
|
||||
{
|
||||
name: "Valid simulation config",
|
||||
config: &ExecutionConfig{Mode: SimulationMode, DryRun: true},
|
||||
valid: true,
|
||||
},
|
||||
{
|
||||
name: "Valid dry run config",
|
||||
config: &ExecutionConfig{Mode: DryRunMode, DryRun: true},
|
||||
valid: true,
|
||||
},
|
||||
{
|
||||
name: "Valid live config",
|
||||
config: &ExecutionConfig{Mode: LiveMode, DryRun: false},
|
||||
valid: true,
|
||||
},
|
||||
{
|
||||
name: "Config with max gas price",
|
||||
config: &ExecutionConfig{MaxGasPrice: big.NewInt(100000000)},
|
||||
valid: true,
|
||||
},
|
||||
{
|
||||
name: "Config with min profit",
|
||||
config: &ExecutionConfig{MinProfitThreshold: big.NewInt(1000000000000000)},
|
||||
valid: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range configs {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
assert.NotNil(t, tc.config)
|
||||
assert.Equal(t, tc.valid, tc.valid)
|
||||
})
|
||||
}
|
||||
}
|
||||
311
orig/pkg/execution/executor.go
Normal file
311
orig/pkg/execution/executor.go
Normal file
@@ -0,0 +1,311 @@
|
||||
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")
|
||||
}
|
||||
326
orig/pkg/execution/flashloan_providers.go
Normal file
326
orig/pkg/execution/flashloan_providers.go
Normal file
@@ -0,0 +1,326 @@
|
||||
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"
|
||||
)
|
||||
|
||||
// AaveFlashLoanProvider implements flash loans using Aave Protocol
|
||||
type AaveFlashLoanProvider struct {
|
||||
client *ethclient.Client
|
||||
logger *logger.Logger
|
||||
|
||||
// Aave V3 Pool contract on Arbitrum
|
||||
poolAddress common.Address
|
||||
fee *big.Int // 0.09% fee = 9 basis points
|
||||
}
|
||||
|
||||
// NewAaveFlashLoanProvider creates a new Aave flash loan provider
|
||||
func NewAaveFlashLoanProvider(client *ethclient.Client, logger *logger.Logger) *AaveFlashLoanProvider {
|
||||
return &AaveFlashLoanProvider{
|
||||
client: client,
|
||||
logger: logger,
|
||||
// Aave V3 Pool on Arbitrum
|
||||
poolAddress: common.HexToAddress("0x794a61358D6845594F94dc1DB02A252b5b4814aD"),
|
||||
fee: big.NewInt(9), // 0.09% = 9 basis points
|
||||
}
|
||||
}
|
||||
|
||||
// ExecuteFlashLoan executes arbitrage using Aave flash loan
|
||||
func (a *AaveFlashLoanProvider) ExecuteFlashLoan(
|
||||
ctx context.Context,
|
||||
opportunity *types.ArbitrageOpportunity,
|
||||
config *ExecutionConfig,
|
||||
) (*ExecutionResult, error) {
|
||||
a.logger.Info(fmt.Sprintf("⚡ Executing Aave flash loan for %s ETH", opportunity.AmountIn.String()))
|
||||
|
||||
// TODO: Implement actual Aave flash loan execution
|
||||
// Steps:
|
||||
// 1. Build flashLoan() calldata with:
|
||||
// - Assets to borrow
|
||||
// - Amounts
|
||||
// - Modes (0 for no debt)
|
||||
// - OnBehalfOf address
|
||||
// - Params (encoded arbitrage path)
|
||||
// - ReferralCode
|
||||
// 2. Send transaction to Aave Pool
|
||||
// 3. Wait for receipt
|
||||
// 4. Parse events and calculate actual profit
|
||||
|
||||
return &ExecutionResult{
|
||||
OpportunityID: opportunity.ID,
|
||||
Success: false,
|
||||
Error: fmt.Errorf("Aave flash loan execution not yet implemented"),
|
||||
EstimatedProfit: opportunity.NetProfit,
|
||||
}, fmt.Errorf("not implemented")
|
||||
}
|
||||
|
||||
// GetMaxLoanAmount returns maximum borrowable amount from Aave
|
||||
func (a *AaveFlashLoanProvider) GetMaxLoanAmount(ctx context.Context, token common.Address) (*big.Int, error) {
|
||||
// TODO: Query Aave reserves to get available liquidity
|
||||
// For now, return a large amount
|
||||
return new(big.Int).Mul(big.NewInt(1000), big.NewInt(1e18)), nil // 1000 ETH
|
||||
}
|
||||
|
||||
// GetFee calculates Aave flash loan fee
|
||||
func (a *AaveFlashLoanProvider) GetFee(ctx context.Context, amount *big.Int) (*big.Int, error) {
|
||||
// Aave V3 fee is 0.09% (9 basis points)
|
||||
fee := new(big.Int).Mul(amount, a.fee)
|
||||
fee = fee.Div(fee, big.NewInt(10000))
|
||||
return fee, nil
|
||||
}
|
||||
|
||||
// SupportsToken checks if Aave supports the token
|
||||
func (a *AaveFlashLoanProvider) SupportsToken(token common.Address) bool {
|
||||
// TODO: Query Aave reserves to check token support
|
||||
// For now, support common tokens
|
||||
supportedTokens := map[common.Address]bool{
|
||||
common.HexToAddress("0x82aF49447D8a07e3bd95BD0d56f35241523fBab1"): true, // WETH
|
||||
common.HexToAddress("0xaf88d065e77c8cC2239327C5EDb3A432268e5831"): true, // USDC
|
||||
common.HexToAddress("0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9"): true, // USDT
|
||||
common.HexToAddress("0x2f2a2543B76A4166549F7aaB2e75Bef0aefC5B0f"): true, // WBTC
|
||||
common.HexToAddress("0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1"): true, // DAI
|
||||
}
|
||||
return supportedTokens[token]
|
||||
}
|
||||
|
||||
// UniswapFlashLoanProvider implements flash swaps using Uniswap V2/V3
|
||||
type UniswapFlashLoanProvider struct {
|
||||
client *ethclient.Client
|
||||
logger *logger.Logger
|
||||
}
|
||||
|
||||
// NewUniswapFlashLoanProvider creates a new Uniswap flash swap provider
|
||||
func NewUniswapFlashLoanProvider(client *ethclient.Client, logger *logger.Logger) *UniswapFlashLoanProvider {
|
||||
return &UniswapFlashLoanProvider{
|
||||
client: client,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// ExecuteFlashLoan executes arbitrage using Uniswap flash swap
|
||||
func (u *UniswapFlashLoanProvider) ExecuteFlashLoan(
|
||||
ctx context.Context,
|
||||
opportunity *types.ArbitrageOpportunity,
|
||||
config *ExecutionConfig,
|
||||
) (*ExecutionResult, error) {
|
||||
u.logger.Info(fmt.Sprintf("⚡ Executing Uniswap flash swap for %s ETH", opportunity.AmountIn.String()))
|
||||
|
||||
// TODO: Implement Uniswap V2/V3 flash swap
|
||||
// V2 Flash Swap:
|
||||
// 1. Call swap() on pair with amount0Out/amount1Out
|
||||
// 2. Implement uniswapV2Call callback
|
||||
// 3. Execute arbitrage in callback
|
||||
// 4. Repay loan + fee (0.3%)
|
||||
//
|
||||
// V3 Flash:
|
||||
// 1. Call flash() on pool
|
||||
// 2. Implement uniswapV3FlashCallback
|
||||
// 3. Execute arbitrage
|
||||
// 4. Repay exact amount
|
||||
|
||||
return &ExecutionResult{
|
||||
OpportunityID: opportunity.ID,
|
||||
Success: false,
|
||||
Error: fmt.Errorf("Uniswap flash swap execution not yet implemented"),
|
||||
EstimatedProfit: opportunity.NetProfit,
|
||||
}, fmt.Errorf("not implemented")
|
||||
}
|
||||
|
||||
// GetMaxLoanAmount returns maximum borrowable from Uniswap pools
|
||||
func (u *UniswapFlashLoanProvider) GetMaxLoanAmount(ctx context.Context, token common.Address) (*big.Int, error) {
|
||||
// TODO: Find pool with most liquidity for the token
|
||||
return new(big.Int).Mul(big.NewInt(100), big.NewInt(1e18)), nil // 100 ETH
|
||||
}
|
||||
|
||||
// GetFee calculates Uniswap flash swap fee
|
||||
func (u *UniswapFlashLoanProvider) GetFee(ctx context.Context, amount *big.Int) (*big.Int, error) {
|
||||
// V2 flash swap fee is same as trading fee (0.3%)
|
||||
// V3 fee depends on pool tier (0.05%, 0.3%, 1%)
|
||||
// Use 0.3% as default
|
||||
fee := new(big.Int).Mul(amount, big.NewInt(3))
|
||||
fee = fee.Div(fee, big.NewInt(1000))
|
||||
return fee, nil
|
||||
}
|
||||
|
||||
// SupportsToken checks if Uniswap has pools for the token
|
||||
func (u *UniswapFlashLoanProvider) SupportsToken(token common.Address) bool {
|
||||
// Uniswap supports most tokens via pools
|
||||
return true
|
||||
}
|
||||
|
||||
// BalancerFlashLoanProvider implements flash loans using Balancer Vault
|
||||
type BalancerFlashLoanProvider struct {
|
||||
client *ethclient.Client
|
||||
logger *logger.Logger
|
||||
|
||||
// Balancer Vault on Arbitrum
|
||||
vaultAddress common.Address
|
||||
|
||||
// Flash loan receiver contract address (must be deployed first)
|
||||
receiverAddress common.Address
|
||||
}
|
||||
|
||||
// NewBalancerFlashLoanProvider creates a new Balancer flash loan provider
|
||||
func NewBalancerFlashLoanProvider(client *ethclient.Client, logger *logger.Logger) *BalancerFlashLoanProvider {
|
||||
return &BalancerFlashLoanProvider{
|
||||
client: client,
|
||||
logger: logger,
|
||||
// Balancer Vault on Arbitrum
|
||||
vaultAddress: common.HexToAddress("0xBA12222222228d8Ba445958a75a0704d566BF2C8"),
|
||||
// Flash loan receiver contract (TODO: Set this after deployment)
|
||||
receiverAddress: common.Address{}, // Zero address means not deployed yet
|
||||
}
|
||||
}
|
||||
|
||||
// ExecuteFlashLoan executes arbitrage using Balancer flash loan
|
||||
func (b *BalancerFlashLoanProvider) ExecuteFlashLoan(
|
||||
ctx context.Context,
|
||||
opportunity *types.ArbitrageOpportunity,
|
||||
config *ExecutionConfig,
|
||||
) (*ExecutionResult, error) {
|
||||
startTime := time.Now()
|
||||
b.logger.Info(fmt.Sprintf("⚡ Executing Balancer flash loan for opportunity %s", opportunity.ID))
|
||||
|
||||
// Check if receiver contract is deployed
|
||||
if b.receiverAddress == (common.Address{}) {
|
||||
return &ExecutionResult{
|
||||
OpportunityID: opportunity.ID,
|
||||
Success: false,
|
||||
Error: fmt.Errorf("flash loan receiver contract not deployed"),
|
||||
EstimatedProfit: opportunity.NetProfit,
|
||||
ExecutionTime: time.Since(startTime),
|
||||
Timestamp: time.Now(),
|
||||
}, fmt.Errorf("receiver contract not deployed")
|
||||
}
|
||||
|
||||
// Step 1: Prepare flash loan parameters
|
||||
tokens := []common.Address{opportunity.TokenIn} // Borrow input token
|
||||
amounts := []*big.Int{opportunity.AmountIn}
|
||||
|
||||
// Step 2: Encode arbitrage path as userData
|
||||
userData, err := b.encodeArbitragePath(opportunity, config)
|
||||
if err != nil {
|
||||
b.logger.Error(fmt.Sprintf("Failed to encode arbitrage path: %v", err))
|
||||
return &ExecutionResult{
|
||||
OpportunityID: opportunity.ID,
|
||||
Success: false,
|
||||
Error: fmt.Errorf("failed to encode path: %w", err),
|
||||
EstimatedProfit: opportunity.NetProfit,
|
||||
ExecutionTime: time.Since(startTime),
|
||||
Timestamp: time.Now(),
|
||||
}, err
|
||||
}
|
||||
|
||||
// Step 3: Build flash loan transaction
|
||||
// This would require:
|
||||
// - ABI for FlashLoanReceiver.executeArbitrage()
|
||||
// - Transaction signing
|
||||
// - Gas estimation
|
||||
// - Transaction submission
|
||||
// - Receipt waiting
|
||||
|
||||
b.logger.Info(fmt.Sprintf("Flash loan parameters prepared: tokens=%d, amount=%s", len(tokens), amounts[0].String()))
|
||||
b.logger.Info(fmt.Sprintf("UserData size: %d bytes", len(userData)))
|
||||
|
||||
// For now, return a detailed "not fully implemented" result
|
||||
// In production, this would call the FlashLoanReceiver.executeArbitrage() function
|
||||
return &ExecutionResult{
|
||||
OpportunityID: opportunity.ID,
|
||||
Success: false,
|
||||
Error: fmt.Errorf("transaction signing and submission not yet implemented (calldata encoding complete)"),
|
||||
EstimatedProfit: opportunity.NetProfit,
|
||||
ExecutionTime: time.Since(startTime),
|
||||
Timestamp: time.Now(),
|
||||
}, fmt.Errorf("not fully implemented")
|
||||
}
|
||||
|
||||
// encodeArbitragePath encodes an arbitrage path for the FlashLoanReceiver contract
|
||||
func (b *BalancerFlashLoanProvider) encodeArbitragePath(
|
||||
opportunity *types.ArbitrageOpportunity,
|
||||
config *ExecutionConfig,
|
||||
) ([]byte, error) {
|
||||
// Prepare path data for Solidity struct
|
||||
// struct ArbitragePath {
|
||||
// address[] tokens;
|
||||
// address[] exchanges;
|
||||
// uint24[] fees;
|
||||
// bool[] isV3;
|
||||
// uint256 minProfit;
|
||||
// }
|
||||
|
||||
numHops := len(opportunity.Path) - 1
|
||||
|
||||
// Extract exchange addresses and determine protocol versions
|
||||
exchanges := make([]common.Address, numHops)
|
||||
poolAddresses := make([]common.Address, 0)
|
||||
for _, poolStr := range opportunity.Pools {
|
||||
poolAddresses = append(poolAddresses, common.HexToAddress(poolStr))
|
||||
}
|
||||
fees := make([]*big.Int, numHops)
|
||||
isV3 := make([]bool, numHops)
|
||||
|
||||
for i := 0; i < numHops; i++ {
|
||||
// Use pool address from opportunity
|
||||
if i < len(poolAddresses) {
|
||||
exchanges[i] = poolAddresses[i]
|
||||
} else {
|
||||
exchanges[i] = common.Address{}
|
||||
}
|
||||
|
||||
// Check if Uniswap V3 based on protocol
|
||||
if opportunity.Protocol == "uniswap_v3" {
|
||||
isV3[i] = true
|
||||
fees[i] = big.NewInt(3000) // 0.3% fee tier
|
||||
} else {
|
||||
isV3[i] = false
|
||||
fees[i] = big.NewInt(0) // V2 doesn't use fee parameter
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate minimum acceptable profit (with slippage)
|
||||
minProfit := new(big.Int).Set(opportunity.NetProfit)
|
||||
slippageMultiplier := big.NewInt(int64((1.0 - config.MaxSlippage) * 10000))
|
||||
minProfit.Mul(minProfit, slippageMultiplier)
|
||||
minProfit.Div(minProfit, big.NewInt(10000))
|
||||
|
||||
// Pack the struct using ABI encoding
|
||||
// This is a simplified version - production would use go-ethereum's abi package
|
||||
b.logger.Info(fmt.Sprintf("Encoded path: %d hops, minProfit=%s", numHops, minProfit.String()))
|
||||
|
||||
// Return empty bytes for now - full ABI encoding implementation needed
|
||||
return []byte{}, nil
|
||||
}
|
||||
|
||||
// GetMaxLoanAmount returns maximum borrowable from Balancer
|
||||
func (b *BalancerFlashLoanProvider) GetMaxLoanAmount(ctx context.Context, token common.Address) (*big.Int, error) {
|
||||
// TODO: Query Balancer Vault reserves
|
||||
return new(big.Int).Mul(big.NewInt(500), big.NewInt(1e18)), nil // 500 ETH
|
||||
}
|
||||
|
||||
// GetFee calculates Balancer flash loan fee
|
||||
func (b *BalancerFlashLoanProvider) GetFee(ctx context.Context, amount *big.Int) (*big.Int, error) {
|
||||
// Balancer flash loans are FREE (0% fee)!
|
||||
return big.NewInt(0), nil
|
||||
}
|
||||
|
||||
// SupportsToken checks if Balancer Vault has the token
|
||||
func (b *BalancerFlashLoanProvider) SupportsToken(token common.Address) bool {
|
||||
// Balancer supports many tokens
|
||||
supportedTokens := map[common.Address]bool{
|
||||
common.HexToAddress("0x82aF49447D8a07e3bd95BD0d56f35241523fBab1"): true, // WETH
|
||||
common.HexToAddress("0xaf88d065e77c8cC2239327C5EDb3A432268e5831"): true, // USDC
|
||||
common.HexToAddress("0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9"): true, // USDT
|
||||
common.HexToAddress("0x2f2a2543B76A4166549F7aaB2e75Bef0aefC5B0f"): true, // WBTC
|
||||
common.HexToAddress("0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1"): true, // DAI
|
||||
}
|
||||
return supportedTokens[token]
|
||||
}
|
||||
367
orig/pkg/execution/queue.go
Normal file
367
orig/pkg/execution/queue.go
Normal file
@@ -0,0 +1,367 @@
|
||||
// Package execution implements the execution layer for the MEV bot.
|
||||
// It manages prioritized execution of arbitrage opportunities with features like
|
||||
// priority-based queues, circuit breakers, rate limiting, and retry logic.
|
||||
package execution
|
||||
|
||||
import (
|
||||
"container/heap"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/fraktal/mev-beta/internal/logger"
|
||||
pkgtypes "github.com/fraktal/mev-beta/pkg/types"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrQueueFullLowerPriority indicates that the queue is full and the new opportunity has lower priority than existing items
|
||||
ErrQueueFullLowerPriority = errors.New("queue full and new opportunity has lower priority")
|
||||
)
|
||||
|
||||
const (
|
||||
// Default queue configuration
|
||||
DefaultMaxQueueSize = 100
|
||||
DefaultExecutionRate = 500 * time.Millisecond
|
||||
DefaultMaxRetries = 3
|
||||
|
||||
// Circuit breaker configuration
|
||||
CircuitBreakerMaxFailures = 5
|
||||
CircuitBreakerTimeWindow = 5 * time.Minute
|
||||
|
||||
// Priority calculation factors
|
||||
ProfitScoreScaleFactor = 10.0
|
||||
ConfidenceBoostFactor = 20.0
|
||||
MarginBoostFactor = 10.0
|
||||
DefaultTimeDecayPriority = 100.0
|
||||
|
||||
// Execution simulation parameters
|
||||
ConfidenceThresholdForSuccess = 0.7
|
||||
SimulatedExecutionTime = 200 * time.Millisecond
|
||||
)
|
||||
|
||||
// ExecutionQueue manages prioritized execution of arbitrage opportunities
|
||||
type ExecutionQueue struct {
|
||||
logger *logger.Logger
|
||||
queue *PriorityQueue
|
||||
mu sync.RWMutex
|
||||
maxQueueSize int
|
||||
executionRate time.Duration
|
||||
circuitBreaker *CircuitBreaker
|
||||
|
||||
// Execution stats
|
||||
totalExecuted int64
|
||||
successCount int64
|
||||
failureCount int64
|
||||
totalProfitUSD float64
|
||||
}
|
||||
|
||||
// ExecutionItem represents an arbitrage opportunity in the execution queue
|
||||
type ExecutionItem struct {
|
||||
Opportunity *pkgtypes.ArbitrageOpportunity
|
||||
Priority float64 // Higher = more urgent
|
||||
Timestamp time.Time
|
||||
Retries int
|
||||
MaxRetries int
|
||||
}
|
||||
|
||||
// PriorityQueue implements a priority queue for execution items
|
||||
type PriorityQueue []*ExecutionItem
|
||||
|
||||
func (pq PriorityQueue) Len() int { return len(pq) }
|
||||
|
||||
func (pq PriorityQueue) Less(i, j int) bool {
|
||||
// Higher priority first, then by timestamp for tie-breaking
|
||||
if pq[i].Priority != pq[j].Priority {
|
||||
return pq[i].Priority > pq[j].Priority
|
||||
}
|
||||
return pq[i].Timestamp.Before(pq[j].Timestamp)
|
||||
}
|
||||
|
||||
func (pq PriorityQueue) Swap(i, j int) {
|
||||
pq[i], pq[j] = pq[j], pq[i]
|
||||
}
|
||||
|
||||
func (pq *PriorityQueue) Push(x interface{}) {
|
||||
*pq = append(*pq, x.(*ExecutionItem))
|
||||
}
|
||||
|
||||
func (pq *PriorityQueue) Pop() interface{} {
|
||||
old := *pq
|
||||
n := len(old)
|
||||
item := old[n-1]
|
||||
*pq = old[0 : n-1]
|
||||
return item
|
||||
}
|
||||
|
||||
// CircuitBreaker prevents execution when too many failures occur
|
||||
type CircuitBreaker struct {
|
||||
maxFailures int
|
||||
timeWindow time.Duration
|
||||
failures []time.Time
|
||||
isOpen bool
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
// NewExecutionQueue creates a new execution queue
|
||||
func NewExecutionQueue(logger *logger.Logger) *ExecutionQueue {
|
||||
pq := &PriorityQueue{}
|
||||
heap.Init(pq)
|
||||
|
||||
return &ExecutionQueue{
|
||||
logger: logger,
|
||||
queue: pq,
|
||||
maxQueueSize: DefaultMaxQueueSize,
|
||||
executionRate: DefaultExecutionRate, // Execute every 500ms max
|
||||
circuitBreaker: &CircuitBreaker{
|
||||
maxFailures: CircuitBreakerMaxFailures,
|
||||
timeWindow: CircuitBreakerTimeWindow,
|
||||
failures: make([]time.Time, 0),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// AddOpportunity adds an arbitrage opportunity to the execution queue
|
||||
func (eq *ExecutionQueue) AddOpportunity(opportunity *pkgtypes.ArbitrageOpportunity) error {
|
||||
eq.mu.Lock()
|
||||
defer eq.mu.Unlock()
|
||||
|
||||
// Check if queue is full
|
||||
if eq.queue.Len() >= eq.maxQueueSize {
|
||||
// Remove lowest priority item
|
||||
if eq.queue.Len() > 0 {
|
||||
lowestPriorityItem := (*eq.queue)[eq.queue.Len()-1]
|
||||
if eq.calculatePriority(opportunity) > lowestPriorityItem.Priority {
|
||||
heap.Pop(eq.queue) // Remove lowest priority
|
||||
eq.logger.Info("Queue full, replaced lower priority opportunity")
|
||||
} else {
|
||||
return ErrQueueFullLowerPriority
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create execution item
|
||||
item := &ExecutionItem{
|
||||
Opportunity: opportunity,
|
||||
Priority: eq.calculatePriority(opportunity),
|
||||
Timestamp: time.Now(),
|
||||
MaxRetries: DefaultMaxRetries,
|
||||
}
|
||||
|
||||
heap.Push(eq.queue, item)
|
||||
|
||||
// Convert UniversalDecimal to float64 for display
|
||||
profitFloat := 0.0
|
||||
if opportunity.NetProfit != nil {
|
||||
profitEth := new(big.Float).Quo(new(big.Float).SetInt(opportunity.NetProfit), big.NewFloat(1e18))
|
||||
var accuracy big.Accuracy
|
||||
profitFloat, accuracy = profitEth.Float64()
|
||||
// Check if the conversion was exact (accuracy == 0) or if there was a loss of precision
|
||||
if accuracy != big.Exact {
|
||||
eq.logger.Warn(fmt.Sprintf("NetProfit conversion to float64 may have lost precision, accuracy: %v", accuracy))
|
||||
}
|
||||
}
|
||||
eq.logger.Info(fmt.Sprintf("📋 Added arbitrage opportunity to queue: %.6f ETH profit, priority: %.2f",
|
||||
profitFloat, item.Priority))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// calculatePriority calculates execution priority based on profit, confidence, and time sensitivity
|
||||
func (eq *ExecutionQueue) calculatePriority(opp *pkgtypes.ArbitrageOpportunity) float64 {
|
||||
// Base priority on profit potential
|
||||
// Convert NetProfit to float64 for priority calculation
|
||||
profitScore := 0.0
|
||||
if opp.NetProfit != nil {
|
||||
profitEth := new(big.Float).Quo(new(big.Float).SetInt(opp.NetProfit), big.NewFloat(1e18))
|
||||
var accuracy big.Accuracy
|
||||
profitScore, accuracy = profitEth.Float64()
|
||||
// Check if the conversion was exact (accuracy == 0) or if there was a loss of precision
|
||||
if accuracy != big.Exact {
|
||||
eq.logger.Warn(fmt.Sprintf("NetProfit conversion to float64 may have lost precision, accuracy: %v", accuracy))
|
||||
}
|
||||
profitScore *= ProfitScoreScaleFactor // Scale by factor
|
||||
}
|
||||
|
||||
// Boost for high confidence
|
||||
confidenceBoost := opp.Confidence * ConfidenceBoostFactor
|
||||
|
||||
// Boost for large profit margins (indicates stable opportunity)
|
||||
marginBoost := opp.ROI * MarginBoostFactor // ROI is already a float64 percentage
|
||||
|
||||
// Time decay - use current time as opportunities don't have timestamps
|
||||
// This could be enhanced by adding creation timestamp to ArbitrageOpportunity
|
||||
timeDecay := DefaultTimeDecayPriority // Default high priority for new opportunities
|
||||
|
||||
priority := profitScore + confidenceBoost + marginBoost + timeDecay
|
||||
|
||||
return priority
|
||||
}
|
||||
|
||||
// Start begins processing the execution queue
|
||||
func (eq *ExecutionQueue) Start(ctx context.Context) {
|
||||
eq.logger.Info("🚀 Starting execution queue processor")
|
||||
|
||||
ticker := time.NewTicker(eq.executionRate)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
eq.logger.Info("⏹️ Execution queue stopped")
|
||||
return
|
||||
case <-ticker.C:
|
||||
eq.processNext()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// processNext processes the next item in the queue
|
||||
func (eq *ExecutionQueue) processNext() {
|
||||
eq.mu.Lock()
|
||||
|
||||
// Check circuit breaker
|
||||
if eq.circuitBreaker.IsOpen() {
|
||||
eq.mu.Unlock()
|
||||
eq.logger.Warn("⚠️ Circuit breaker open, skipping execution")
|
||||
return
|
||||
}
|
||||
|
||||
if eq.queue.Len() == 0 {
|
||||
eq.mu.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
item := heap.Pop(eq.queue).(*ExecutionItem)
|
||||
eq.mu.Unlock()
|
||||
|
||||
// Execute the opportunity
|
||||
success := eq.executeOpportunity(item)
|
||||
|
||||
if success {
|
||||
eq.successCount++
|
||||
// Convert NetProfit to float64 for tracking
|
||||
profitFloat := 0.0
|
||||
if item.Opportunity.NetProfit != nil {
|
||||
profitEth := new(big.Float).Quo(new(big.Float).SetInt(item.Opportunity.NetProfit), big.NewFloat(1e18))
|
||||
var accuracy big.Accuracy
|
||||
profitFloat, accuracy = profitEth.Float64()
|
||||
// Check if the conversion was exact (accuracy == 0) or if there was a loss of precision
|
||||
if accuracy != big.Exact {
|
||||
eq.logger.Warn(fmt.Sprintf("NetProfit conversion to float64 may have lost precision, accuracy: %v", accuracy))
|
||||
}
|
||||
}
|
||||
eq.totalProfitUSD += profitFloat
|
||||
eq.logger.Info(fmt.Sprintf("✅ Execution successful: %.6f ETH profit", profitFloat))
|
||||
} else {
|
||||
eq.failureCount++
|
||||
eq.circuitBreaker.RecordFailure()
|
||||
|
||||
// Retry if not exceeded max retries
|
||||
if item.Retries < item.MaxRetries {
|
||||
item.Retries++
|
||||
item.Priority *= 0.9 // Slightly lower priority for retries
|
||||
|
||||
eq.mu.Lock()
|
||||
heap.Push(eq.queue, item)
|
||||
eq.mu.Unlock()
|
||||
|
||||
eq.logger.Warn(fmt.Sprintf("🔄 Execution failed, retrying (%d/%d)", item.Retries, item.MaxRetries))
|
||||
} else {
|
||||
eq.logger.Error("❌ Execution failed after max retries")
|
||||
}
|
||||
}
|
||||
|
||||
eq.totalExecuted++
|
||||
}
|
||||
|
||||
// executeOpportunity executes a single arbitrage opportunity
|
||||
func (eq *ExecutionQueue) executeOpportunity(item *ExecutionItem) bool {
|
||||
opp := item.Opportunity
|
||||
|
||||
// Convert NetProfit to float64 for logging
|
||||
profitFloat := 0.0
|
||||
if opp.NetProfit != nil {
|
||||
profitEth := new(big.Float).Quo(new(big.Float).SetInt(opp.NetProfit), big.NewFloat(1e18))
|
||||
var accuracy big.Accuracy
|
||||
profitFloat, accuracy = profitEth.Float64()
|
||||
// Check if the conversion was exact (accuracy == 0) or if there was a loss of precision
|
||||
if accuracy != big.Exact {
|
||||
eq.logger.Warn(fmt.Sprintf("NetProfit conversion to float64 may have lost precision, accuracy: %v", accuracy))
|
||||
}
|
||||
}
|
||||
|
||||
// Get exchange info from path if available
|
||||
exchangeInfo := "multi-DEX"
|
||||
if len(opp.Path) > 0 {
|
||||
exchangeInfo = fmt.Sprintf("%d-hop", len(opp.Path))
|
||||
}
|
||||
|
||||
eq.logger.Info(fmt.Sprintf("⚡ Executing arbitrage: %s path, %.6f ETH profit",
|
||||
exchangeInfo, profitFloat))
|
||||
|
||||
// TODO: Implement actual execution logic
|
||||
// For now, simulate execution with success probability based on confidence
|
||||
simulatedSuccess := opp.Confidence > ConfidenceThresholdForSuccess // 70% confidence threshold
|
||||
|
||||
// Simulate execution time
|
||||
time.Sleep(SimulatedExecutionTime)
|
||||
|
||||
return simulatedSuccess
|
||||
}
|
||||
|
||||
// IsOpen checks if the circuit breaker is open
|
||||
func (cb *CircuitBreaker) IsOpen() bool {
|
||||
cb.mu.Lock()
|
||||
defer cb.mu.Unlock()
|
||||
|
||||
// Clean old failures
|
||||
now := time.Now()
|
||||
validFailures := make([]time.Time, 0)
|
||||
for _, failure := range cb.failures {
|
||||
if now.Sub(failure) < cb.timeWindow {
|
||||
validFailures = append(validFailures, failure)
|
||||
}
|
||||
}
|
||||
cb.failures = validFailures
|
||||
|
||||
// Check if we should open the circuit breaker
|
||||
if len(cb.failures) >= cb.maxFailures {
|
||||
cb.isOpen = true
|
||||
} else {
|
||||
cb.isOpen = false
|
||||
}
|
||||
|
||||
return cb.isOpen
|
||||
}
|
||||
|
||||
// RecordFailure records a failed execution
|
||||
func (cb *CircuitBreaker) RecordFailure() {
|
||||
cb.mu.Lock()
|
||||
defer cb.mu.Unlock()
|
||||
|
||||
cb.failures = append(cb.failures, time.Now())
|
||||
}
|
||||
|
||||
// GetStats returns execution queue statistics
|
||||
func (eq *ExecutionQueue) GetStats() map[string]interface{} {
|
||||
eq.mu.RLock()
|
||||
defer eq.mu.RUnlock()
|
||||
|
||||
successRate := 0.0
|
||||
if eq.totalExecuted > 0 {
|
||||
successRate = float64(eq.successCount) / float64(eq.totalExecuted) * 100
|
||||
}
|
||||
|
||||
return map[string]interface{}{
|
||||
"queue_size": eq.queue.Len(),
|
||||
"total_executed": eq.totalExecuted,
|
||||
"success_count": eq.successCount,
|
||||
"failure_count": eq.failureCount,
|
||||
"success_rate": successRate,
|
||||
"total_profit_usd": eq.totalProfitUSD,
|
||||
"circuit_breaker_open": eq.circuitBreaker.IsOpen(),
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user