mirror of
https://github.com/luxfi/dex.git
synced 2026-09-24 00:26:31 +00:00
488 lines
12 KiB
Go
488 lines
12 KiB
Go
// Package arbitrage provides omnichain arbitrage execution
|
|
package arbitrage
|
|
|
|
import (
|
|
"context"
|
|
"crypto/ecdsa"
|
|
"fmt"
|
|
"math/big"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/shopspring/decimal"
|
|
)
|
|
|
|
// Executor executes arbitrage opportunities
|
|
type Executor struct {
|
|
mu sync.RWMutex
|
|
|
|
// Configuration
|
|
config ExecutorConfig
|
|
|
|
// Wallet for signing transactions
|
|
privateKey *ecdsa.PrivateKey
|
|
address string
|
|
|
|
// Contract addresses
|
|
flashLoanPool string
|
|
arbitrageContract string
|
|
|
|
// Chain clients
|
|
chains map[string]ChainClient
|
|
|
|
// Execution tracking
|
|
pendingExecutions map[string]*Execution
|
|
completedExecutions []Execution
|
|
|
|
// Metrics
|
|
totalExecutions int64
|
|
successfulExecutions int64
|
|
totalProfitUSD decimal.Decimal
|
|
totalGasSpent decimal.Decimal
|
|
|
|
// Running state
|
|
ctx context.Context
|
|
cancel context.CancelFunc
|
|
}
|
|
|
|
// ExecutorConfig configures the arbitrage executor
|
|
type ExecutorConfig struct {
|
|
// Maximum gas price willing to pay (gwei)
|
|
MaxGasPrice decimal.Decimal
|
|
|
|
// Slippage tolerance (basis points)
|
|
MaxSlippageBps decimal.Decimal
|
|
|
|
// Minimum confidence to execute
|
|
MinConfidence float64
|
|
|
|
// Maximum concurrent executions
|
|
MaxConcurrent int
|
|
|
|
// Use flash loans
|
|
UseFlashLoans bool
|
|
|
|
// MEV protection
|
|
UseMEVProtection bool
|
|
FlashbotsRPC string
|
|
|
|
// Execution timeout
|
|
ExecutionTimeout time.Duration
|
|
}
|
|
|
|
// ChainClient interface for interacting with different chains
|
|
type ChainClient interface {
|
|
// SendTransaction sends a transaction
|
|
SendTransaction(ctx context.Context, tx *Transaction) (string, error)
|
|
|
|
// GetBalance gets token balance
|
|
GetBalance(ctx context.Context, token, address string) (decimal.Decimal, error)
|
|
|
|
// EstimateGas estimates gas for a transaction
|
|
EstimateGas(ctx context.Context, tx *Transaction) (uint64, error)
|
|
|
|
// GetGasPrice gets current gas price
|
|
GetGasPrice(ctx context.Context) (decimal.Decimal, error)
|
|
|
|
// WaitForConfirmation waits for transaction confirmation
|
|
WaitForConfirmation(ctx context.Context, txHash string) (*Receipt, error)
|
|
}
|
|
|
|
// Transaction represents a blockchain transaction
|
|
type Transaction struct {
|
|
To string
|
|
Value *big.Int
|
|
Data []byte
|
|
GasLimit uint64
|
|
GasPrice *big.Int
|
|
Nonce uint64
|
|
}
|
|
|
|
// Receipt represents a transaction receipt
|
|
type Receipt struct {
|
|
TxHash string
|
|
BlockNumber uint64
|
|
GasUsed uint64
|
|
Status bool
|
|
Logs []Log
|
|
}
|
|
|
|
// Log represents a transaction log
|
|
type Log struct {
|
|
Address string
|
|
Topics []string
|
|
Data []byte
|
|
}
|
|
|
|
// Execution represents an arbitrage execution
|
|
type Execution struct {
|
|
ID string
|
|
Opportunity ArbitrageOpportunity
|
|
Status ExecutionStatus
|
|
StartTime time.Time
|
|
EndTime time.Time
|
|
Transactions []ExecutedTx
|
|
ActualPnL decimal.Decimal
|
|
GasSpent decimal.Decimal
|
|
Error error
|
|
}
|
|
|
|
// ExecutedTx represents an executed transaction
|
|
type ExecutedTx struct {
|
|
ChainID string
|
|
TxHash string
|
|
GasUsed uint64
|
|
Status bool
|
|
Timestamp time.Time
|
|
}
|
|
|
|
// ExecutionStatus represents execution status
|
|
type ExecutionStatus string
|
|
|
|
const (
|
|
StatusPending ExecutionStatus = "pending"
|
|
StatusExecuting ExecutionStatus = "executing"
|
|
StatusCompleted ExecutionStatus = "completed"
|
|
StatusFailed ExecutionStatus = "failed"
|
|
StatusReverted ExecutionStatus = "reverted"
|
|
)
|
|
|
|
// NewExecutor creates a new arbitrage executor
|
|
func NewExecutor(config ExecutorConfig, privateKey *ecdsa.PrivateKey) *Executor {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
|
|
return &Executor{
|
|
config: config,
|
|
privateKey: privateKey,
|
|
chains: make(map[string]ChainClient),
|
|
pendingExecutions: make(map[string]*Execution),
|
|
ctx: ctx,
|
|
cancel: cancel,
|
|
}
|
|
}
|
|
|
|
// AddChainClient adds a chain client
|
|
func (e *Executor) AddChainClient(chainID string, client ChainClient) {
|
|
e.mu.Lock()
|
|
defer e.mu.Unlock()
|
|
e.chains[chainID] = client
|
|
}
|
|
|
|
// SetContracts sets contract addresses
|
|
func (e *Executor) SetContracts(flashLoanPool, arbitrageContract string) {
|
|
e.flashLoanPool = flashLoanPool
|
|
e.arbitrageContract = arbitrageContract
|
|
}
|
|
|
|
// Execute executes an arbitrage opportunity
|
|
func (e *Executor) Execute(ctx context.Context, opp ArbitrageOpportunity) (*Execution, error) {
|
|
// Validate opportunity
|
|
if err := e.validateOpportunity(opp); err != nil {
|
|
return nil, fmt.Errorf("validation failed: %w", err)
|
|
}
|
|
|
|
// Create execution record
|
|
exec := &Execution{
|
|
ID: opp.ID,
|
|
Opportunity: opp,
|
|
Status: StatusPending,
|
|
StartTime: time.Now(),
|
|
}
|
|
|
|
e.mu.Lock()
|
|
e.pendingExecutions[opp.ID] = exec
|
|
e.mu.Unlock()
|
|
|
|
// Execute based on configuration
|
|
var err error
|
|
if e.config.UseFlashLoans {
|
|
err = e.executeWithFlashLoan(ctx, exec)
|
|
} else {
|
|
err = e.executeDirectly(ctx, exec)
|
|
}
|
|
|
|
exec.EndTime = time.Now()
|
|
|
|
if err != nil {
|
|
exec.Status = StatusFailed
|
|
exec.Error = err
|
|
return exec, err
|
|
}
|
|
|
|
exec.Status = StatusCompleted
|
|
|
|
// Update metrics
|
|
e.mu.Lock()
|
|
delete(e.pendingExecutions, opp.ID)
|
|
e.completedExecutions = append(e.completedExecutions, *exec)
|
|
e.totalExecutions++
|
|
if exec.Status == StatusCompleted {
|
|
e.successfulExecutions++
|
|
e.totalProfitUSD = e.totalProfitUSD.Add(exec.ActualPnL)
|
|
}
|
|
e.totalGasSpent = e.totalGasSpent.Add(exec.GasSpent)
|
|
e.mu.Unlock()
|
|
|
|
return exec, nil
|
|
}
|
|
|
|
// validateOpportunity validates an arbitrage opportunity before execution
|
|
func (e *Executor) validateOpportunity(opp ArbitrageOpportunity) error {
|
|
// Check expiry
|
|
if time.Now().After(opp.ExpiresAt) {
|
|
return fmt.Errorf("opportunity expired")
|
|
}
|
|
|
|
// Check confidence
|
|
if opp.Confidence < e.config.MinConfidence {
|
|
return fmt.Errorf("confidence too low: %.2f < %.2f", opp.Confidence, e.config.MinConfidence)
|
|
}
|
|
|
|
// Check profitability
|
|
if opp.NetPnL.LessThanOrEqual(decimal.Zero) {
|
|
return fmt.Errorf("negative PnL: %s", opp.NetPnL.String())
|
|
}
|
|
|
|
// Check we have clients for all chains
|
|
for _, route := range opp.Routes {
|
|
if _, ok := e.chains[route.ChainID]; !ok {
|
|
return fmt.Errorf("no client for chain: %s", route.ChainID)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// executeWithFlashLoan executes using flash loan for capital efficiency
|
|
func (e *Executor) executeWithFlashLoan(ctx context.Context, exec *Execution) error {
|
|
opp := exec.Opportunity
|
|
exec.Status = StatusExecuting
|
|
|
|
// Get Lux chain client
|
|
luxClient, ok := e.chains["lux"]
|
|
if !ok {
|
|
return fmt.Errorf("no Lux client configured")
|
|
}
|
|
|
|
// Build flash loan parameters
|
|
params := e.buildFlashLoanParams(opp)
|
|
|
|
// Encode the flash loan call
|
|
callData := e.encodeFlashLoanCall(opp, params)
|
|
|
|
// Estimate gas
|
|
tx := &Transaction{
|
|
To: e.flashLoanPool,
|
|
Data: callData,
|
|
}
|
|
|
|
gasLimit, err := luxClient.EstimateGas(ctx, tx)
|
|
if err != nil {
|
|
return fmt.Errorf("gas estimation failed: %w", err)
|
|
}
|
|
|
|
// Get gas price
|
|
gasPrice, err := luxClient.GetGasPrice(ctx)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get gas price: %w", err)
|
|
}
|
|
|
|
// Check gas price limit
|
|
if gasPrice.GreaterThan(e.config.MaxGasPrice) {
|
|
return fmt.Errorf("gas price too high: %s > %s", gasPrice.String(), e.config.MaxGasPrice.String())
|
|
}
|
|
|
|
// Build final transaction with overflow protection
|
|
const gasBuffer uint64 = 50000
|
|
if gasLimit > ^uint64(0)-gasBuffer {
|
|
return fmt.Errorf("gas limit overflow: %d", gasLimit)
|
|
}
|
|
tx.GasLimit = gasLimit + gasBuffer
|
|
tx.GasPrice = gasPrice.BigInt()
|
|
|
|
// Send transaction (with MEV protection if enabled)
|
|
var txHash string
|
|
if e.config.UseMEVProtection {
|
|
txHash, err = e.sendMEVProtected(ctx, tx)
|
|
} else {
|
|
txHash, err = luxClient.SendTransaction(ctx, tx)
|
|
}
|
|
|
|
if err != nil {
|
|
return fmt.Errorf("failed to send transaction: %w", err)
|
|
}
|
|
|
|
exec.Transactions = append(exec.Transactions, ExecutedTx{
|
|
ChainID: "lux",
|
|
TxHash: txHash,
|
|
Timestamp: time.Now(),
|
|
})
|
|
|
|
// Wait for confirmation
|
|
receipt, err := luxClient.WaitForConfirmation(ctx, txHash)
|
|
if err != nil {
|
|
return fmt.Errorf("confirmation failed: %w", err)
|
|
}
|
|
|
|
if !receipt.Status {
|
|
exec.Status = StatusReverted
|
|
return fmt.Errorf("transaction reverted")
|
|
}
|
|
|
|
// Update execution with actual results
|
|
exec.Transactions[0].GasUsed = receipt.GasUsed
|
|
exec.Transactions[0].Status = receipt.Status
|
|
exec.GasSpent = gasPrice.Mul(decimal.NewFromInt(int64(receipt.GasUsed)))
|
|
|
|
// Parse logs to get actual profit
|
|
exec.ActualPnL = e.parseProfit(receipt.Logs)
|
|
|
|
return nil
|
|
}
|
|
|
|
// executeDirectly executes without flash loan (requires capital)
|
|
func (e *Executor) executeDirectly(ctx context.Context, exec *Execution) error {
|
|
opp := exec.Opportunity
|
|
exec.Status = StatusExecuting
|
|
|
|
// Execute each route sequentially
|
|
for i, route := range opp.Routes {
|
|
client, ok := e.chains[route.ChainID]
|
|
if !ok {
|
|
return fmt.Errorf("no client for chain: %s", route.ChainID)
|
|
}
|
|
|
|
// Build swap transaction
|
|
tx := e.buildSwapTransaction(route)
|
|
|
|
// Send transaction
|
|
txHash, err := client.SendTransaction(ctx, tx)
|
|
if err != nil {
|
|
return fmt.Errorf("route %d failed: %w", i, err)
|
|
}
|
|
|
|
exec.Transactions = append(exec.Transactions, ExecutedTx{
|
|
ChainID: route.ChainID,
|
|
TxHash: txHash,
|
|
Timestamp: time.Now(),
|
|
})
|
|
|
|
// Wait for confirmation
|
|
receipt, err := client.WaitForConfirmation(ctx, txHash)
|
|
if err != nil {
|
|
return fmt.Errorf("route %d confirmation failed: %w", i, err)
|
|
}
|
|
|
|
if !receipt.Status {
|
|
exec.Status = StatusFailed
|
|
return fmt.Errorf("route %d reverted", i)
|
|
}
|
|
|
|
exec.Transactions[i].GasUsed = receipt.GasUsed
|
|
exec.Transactions[i].Status = receipt.Status
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// sendMEVProtected sends transaction via MEV-protected channel
|
|
func (e *Executor) sendMEVProtected(ctx context.Context, tx *Transaction) (string, error) {
|
|
// Implement Flashbots-style MEV protection
|
|
// This sends the transaction directly to block builders
|
|
// to avoid frontrunning/sandwich attacks
|
|
|
|
// For Lux, we can use:
|
|
// 1. Private mempool submission
|
|
// 2. Direct validator communication
|
|
// 3. Encrypted mempool (future)
|
|
|
|
// Placeholder - implement actual MEV protection
|
|
return "", fmt.Errorf("MEV protection not implemented")
|
|
}
|
|
|
|
// buildFlashLoanParams builds parameters for flash loan arbitrage
|
|
func (e *Executor) buildFlashLoanParams(opp ArbitrageOpportunity) []byte {
|
|
// ABI encode the arbitrage parameters
|
|
// This would be passed to the flash loan callback
|
|
|
|
// ArbParams struct encoding:
|
|
// - arbType (uint8)
|
|
// - routes (Route[])
|
|
// - minProfitBps (uint256)
|
|
// - maxSlippageBps (uint256)
|
|
// - deadline (uint256)
|
|
|
|
// Placeholder - implement actual ABI encoding
|
|
return nil
|
|
}
|
|
|
|
// encodeFlashLoanCall encodes the flash loan function call
|
|
func (e *Executor) encodeFlashLoanCall(opp ArbitrageOpportunity, params []byte) []byte {
|
|
// function flashLoan(address receiver, address asset, uint256 amount, bytes calldata params)
|
|
|
|
// Placeholder - implement actual ABI encoding
|
|
return nil
|
|
}
|
|
|
|
// buildSwapTransaction builds a swap transaction for a route
|
|
func (e *Executor) buildSwapTransaction(route Route) *Transaction {
|
|
// Build DEX-specific swap call based on route.Venue
|
|
|
|
// Placeholder - implement actual swap encoding
|
|
return &Transaction{
|
|
To: route.Venue,
|
|
Data: route.SwapData,
|
|
}
|
|
}
|
|
|
|
// parseProfit parses execution logs to determine actual profit
|
|
func (e *Executor) parseProfit(logs []Log) decimal.Decimal {
|
|
// Parse ArbitrageExecuted event to get actual profit
|
|
|
|
// Placeholder - implement log parsing
|
|
return decimal.Zero
|
|
}
|
|
|
|
// GetMetrics returns execution metrics
|
|
func (e *Executor) GetMetrics() ExecutorMetrics {
|
|
e.mu.RLock()
|
|
defer e.mu.RUnlock()
|
|
|
|
successRate := float64(0)
|
|
if e.totalExecutions > 0 {
|
|
successRate = float64(e.successfulExecutions) / float64(e.totalExecutions)
|
|
}
|
|
|
|
return ExecutorMetrics{
|
|
TotalExecutions: e.totalExecutions,
|
|
SuccessfulExecutions: e.successfulExecutions,
|
|
SuccessRate: successRate,
|
|
TotalProfitUSD: e.totalProfitUSD,
|
|
TotalGasSpent: e.totalGasSpent,
|
|
PendingExecutions: int64(len(e.pendingExecutions)),
|
|
}
|
|
}
|
|
|
|
// ExecutorMetrics holds executor statistics
|
|
type ExecutorMetrics struct {
|
|
TotalExecutions int64
|
|
SuccessfulExecutions int64
|
|
SuccessRate float64
|
|
TotalProfitUSD decimal.Decimal
|
|
TotalGasSpent decimal.Decimal
|
|
PendingExecutions int64
|
|
}
|
|
|
|
// DefaultExecutorConfig returns default configuration
|
|
func DefaultExecutorConfig() ExecutorConfig {
|
|
return ExecutorConfig{
|
|
MaxGasPrice: decimal.NewFromInt(100), // 100 gwei max
|
|
MaxSlippageBps: decimal.NewFromInt(50), // 0.5% max slippage
|
|
MinConfidence: 0.7, // 70% minimum confidence
|
|
MaxConcurrent: 10,
|
|
UseFlashLoans: true,
|
|
UseMEVProtection: true,
|
|
ExecutionTimeout: 30 * time.Second,
|
|
}
|
|
}
|