removed the fucking vendor files

This commit is contained in:
Krypto Kajun
2025-09-16 11:05:47 -05:00
parent 42244ab42b
commit bccc122a85
1451 changed files with 48752 additions and 472999 deletions

409
pkg/arbitrage/database.go Normal file
View File

@@ -0,0 +1,409 @@
package arbitrage
import (
"context"
"database/sql"
"encoding/json"
"fmt"
"math/big"
"time"
"github.com/ethereum/go-ethereum/common"
"github.com/fraktal/mev-beta/internal/logger"
_ "github.com/mattn/go-sqlite3"
)
// SQLiteDatabase implements ArbitrageDatabase using SQLite
type SQLiteDatabase struct {
db *sql.DB
logger *logger.Logger
}
// NewSQLiteDatabase creates a new SQLite database for arbitrage data
func NewSQLiteDatabase(dbPath string, logger *logger.Logger) (*SQLiteDatabase, error) {
db, err := sql.Open("sqlite3", dbPath)
if err != nil {
return nil, fmt.Errorf("failed to open database: %w", err)
}
database := &SQLiteDatabase{
db: db,
logger: logger,
}
if err := database.createTables(); err != nil {
return nil, fmt.Errorf("failed to create tables: %w", err)
}
return database, nil
}
// createTables creates the necessary database tables
func (db *SQLiteDatabase) createTables() error {
queries := []string{
`CREATE TABLE IF NOT EXISTS arbitrage_opportunities (
id TEXT PRIMARY KEY,
path_json TEXT NOT NULL,
trigger_event_json TEXT NOT NULL,
detected_at INTEGER NOT NULL,
estimated_profit TEXT NOT NULL,
required_amount TEXT NOT NULL,
urgency INTEGER NOT NULL,
expires_at INTEGER NOT NULL,
created_at INTEGER DEFAULT (strftime('%s', 'now'))
)`,
`CREATE TABLE IF NOT EXISTS arbitrage_executions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
opportunity_id TEXT,
transaction_hash TEXT NOT NULL,
gas_used INTEGER NOT NULL,
gas_price TEXT NOT NULL,
profit_realized TEXT NOT NULL,
success BOOLEAN NOT NULL,
error_message TEXT,
execution_time_ms INTEGER NOT NULL,
path_json TEXT NOT NULL,
executed_at INTEGER DEFAULT (strftime('%s', 'now')),
FOREIGN KEY(opportunity_id) REFERENCES arbitrage_opportunities(id)
)`,
`CREATE TABLE IF NOT EXISTS pool_data (
address TEXT PRIMARY KEY,
token0 TEXT NOT NULL,
token1 TEXT NOT NULL,
protocol TEXT NOT NULL,
fee INTEGER NOT NULL,
liquidity TEXT NOT NULL,
sqrt_price_x96 TEXT NOT NULL,
tick INTEGER,
block_number INTEGER NOT NULL,
transaction_hash TEXT NOT NULL,
log_index INTEGER NOT NULL,
last_updated INTEGER NOT NULL,
created_at INTEGER DEFAULT (strftime('%s', 'now'))
)`,
`CREATE TABLE IF NOT EXISTS swap_events (
id INTEGER PRIMARY KEY AUTOINCREMENT,
transaction_hash TEXT NOT NULL,
pool_address TEXT NOT NULL,
token0 TEXT NOT NULL,
token1 TEXT NOT NULL,
amount0 TEXT NOT NULL,
amount1 TEXT NOT NULL,
sqrt_price_x96 TEXT NOT NULL,
liquidity TEXT NOT NULL,
tick INTEGER,
block_number INTEGER NOT NULL,
log_index INTEGER NOT NULL,
timestamp INTEGER NOT NULL,
created_at INTEGER DEFAULT (strftime('%s', 'now'))
)`,
}
for _, query := range queries {
if _, err := db.db.Exec(query); err != nil {
return fmt.Errorf("failed to create table: %w", err)
}
}
// Create indexes for better performance
indexes := []string{
`CREATE INDEX IF NOT EXISTS idx_opportunities_detected_at ON arbitrage_opportunities(detected_at)`,
`CREATE INDEX IF NOT EXISTS idx_opportunities_urgency ON arbitrage_opportunities(urgency)`,
`CREATE INDEX IF NOT EXISTS idx_executions_executed_at ON arbitrage_executions(executed_at)`,
`CREATE INDEX IF NOT EXISTS idx_executions_success ON arbitrage_executions(success)`,
`CREATE INDEX IF NOT EXISTS idx_pool_data_updated ON pool_data(last_updated)`,
`CREATE INDEX IF NOT EXISTS idx_pool_data_tokens ON pool_data(token0, token1)`,
`CREATE INDEX IF NOT EXISTS idx_swap_events_pool ON swap_events(pool_address)`,
`CREATE INDEX IF NOT EXISTS idx_swap_events_block ON swap_events(block_number)`,
`CREATE INDEX IF NOT EXISTS idx_swap_events_timestamp ON swap_events(timestamp)`,
}
for _, index := range indexes {
if _, err := db.db.Exec(index); err != nil {
return fmt.Errorf("failed to create index: %w", err)
}
}
return nil
}
// SaveOpportunity saves an arbitrage opportunity to the database
func (db *SQLiteDatabase) SaveOpportunity(ctx context.Context, opportunity *ArbitrageOpportunity) error {
pathJSON, err := json.Marshal(opportunity.Path)
if err != nil {
return fmt.Errorf("failed to marshal path: %w", err)
}
eventJSON, err := json.Marshal(opportunity.TriggerEvent)
if err != nil {
return fmt.Errorf("failed to marshal trigger event: %w", err)
}
query := `INSERT INTO arbitrage_opportunities
(id, path_json, trigger_event_json, detected_at, estimated_profit, required_amount, urgency, expires_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
_, err = db.db.ExecContext(ctx, query,
opportunity.ID,
string(pathJSON),
string(eventJSON),
opportunity.DetectedAt.Unix(),
opportunity.EstimatedProfit.String(),
opportunity.RequiredAmount.String(),
opportunity.Urgency,
opportunity.ExpiresAt.Unix(),
)
if err != nil {
return fmt.Errorf("failed to save opportunity: %w", err)
}
db.logger.Debug(fmt.Sprintf("Saved arbitrage opportunity %s to database", opportunity.ID))
return nil
}
// SaveExecution saves an arbitrage execution result to the database
func (db *SQLiteDatabase) SaveExecution(ctx context.Context, result *ExecutionResult) error {
pathJSON, err := json.Marshal(result.Path)
if err != nil {
return fmt.Errorf("failed to marshal path: %w", err)
}
var opportunityID *string
if result.Path != nil {
// Try to find the opportunity ID from the path
// This is a simplified approach - in production you'd want better linking
id := generateOpportunityIDFromPath(result.Path)
opportunityID = &id
}
var errorMessage *string
if result.Error != nil {
msg := result.Error.Error()
errorMessage = &msg
}
query := `INSERT INTO arbitrage_executions
(opportunity_id, transaction_hash, gas_used, gas_price, profit_realized, success,
error_message, execution_time_ms, path_json)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
_, err = db.db.ExecContext(ctx, query,
opportunityID,
result.TransactionHash.Hex(),
result.GasUsed,
result.GasPrice.String(),
result.ProfitRealized.String(),
result.Success,
errorMessage,
result.ExecutionTime.Milliseconds(),
string(pathJSON),
)
if err != nil {
return fmt.Errorf("failed to save execution: %w", err)
}
db.logger.Debug(fmt.Sprintf("Saved arbitrage execution %s to database", result.TransactionHash.Hex()))
return nil
}
// GetExecutionHistory retrieves historical arbitrage executions
func (db *SQLiteDatabase) GetExecutionHistory(ctx context.Context, limit int) ([]*ExecutionResult, error) {
query := `SELECT transaction_hash, gas_used, gas_price, profit_realized, success,
error_message, execution_time_ms, path_json, executed_at
FROM arbitrage_executions
ORDER BY executed_at DESC
LIMIT ?`
rows, err := db.db.QueryContext(ctx, query, limit)
if err != nil {
return nil, fmt.Errorf("failed to query execution history: %w", err)
}
defer rows.Close()
var results []*ExecutionResult
for rows.Next() {
var txHashStr, gasPriceStr, profitStr, pathJSON string
var gasUsed, executionTimeMs, executedAt int64
var success bool
var errorMessage *string
err := rows.Scan(&txHashStr, &gasUsed, &gasPriceStr, &profitStr, &success,
&errorMessage, &executionTimeMs, &pathJSON, &executedAt)
if err != nil {
return nil, fmt.Errorf("failed to scan execution row: %w", err)
}
// Parse path JSON
var path ArbitragePath
if err := json.Unmarshal([]byte(pathJSON), &path); err != nil {
db.logger.Warn(fmt.Sprintf("Failed to unmarshal path JSON: %v", err))
continue
}
result := &ExecutionResult{
TransactionHash: common.HexToHash(txHashStr),
GasUsed: uint64(gasUsed),
Success: success,
ExecutionTime: time.Duration(executionTimeMs) * time.Millisecond,
Path: &path,
}
// Parse gas price and profit
result.GasPrice, _ = parseBigInt(gasPriceStr)
result.ProfitRealized, _ = parseBigInt(profitStr)
// Parse error message
if errorMessage != nil {
result.Error = fmt.Errorf(*errorMessage)
}
results = append(results, result)
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("rows iteration error: %w", err)
}
return results, nil
}
// SavePoolData saves pool data to the database
func (db *SQLiteDatabase) SavePoolData(ctx context.Context, poolData *SimplePoolData) error {
query := `INSERT OR REPLACE INTO pool_data
(address, token0, token1, protocol, fee, liquidity, sqrt_price_x96, tick,
block_number, transaction_hash, log_index, last_updated)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
_, err := db.db.ExecContext(ctx, query,
poolData.Address.Hex(),
poolData.Token0.Hex(),
poolData.Token1.Hex(),
"UniswapV3", // Default protocol
poolData.Fee,
poolData.Liquidity.String(),
poolData.SqrtPriceX96.String(),
poolData.Tick,
poolData.BlockNumber,
poolData.TxHash.Hex(),
poolData.LogIndex,
poolData.LastUpdated.Unix(),
)
if err != nil {
return fmt.Errorf("failed to save pool data: %w", err)
}
db.logger.Debug(fmt.Sprintf("Saved pool data %s to database", poolData.Address.Hex()))
return nil
}
// GetPoolData retrieves pool data from the database
func (db *SQLiteDatabase) GetPoolData(ctx context.Context, poolAddress common.Address) (*SimplePoolData, error) {
query := `SELECT address, token0, token1, protocol, fee, liquidity, sqrt_price_x96, tick,
block_number, transaction_hash, log_index, last_updated
FROM pool_data WHERE address = ?`
row := db.db.QueryRowContext(ctx, query, poolAddress.Hex())
var addrStr, token0Str, token1Str, protocol, liquidityStr, sqrtPriceStr, txHashStr string
var fee, tick, blockNumber, logIndex, lastUpdated int64
err := row.Scan(&addrStr, &token0Str, &token1Str, &protocol, &fee,
&liquidityStr, &sqrtPriceStr, &tick, &blockNumber, &txHashStr, &logIndex, &lastUpdated)
if err != nil {
if err == sql.ErrNoRows {
return nil, fmt.Errorf("pool data not found")
}
return nil, fmt.Errorf("failed to query pool data: %w", err)
}
// Parse the data
liquidity, ok1 := parseBigInt(liquidityStr)
sqrtPriceX96, ok2 := parseBigInt(sqrtPriceStr)
if !ok1 || !ok2 {
return nil, fmt.Errorf("failed to parse pool numeric data")
}
poolData := &SimplePoolData{
Address: common.HexToAddress(addrStr),
Token0: common.HexToAddress(token0Str),
Token1: common.HexToAddress(token1Str),
Fee: fee,
Liquidity: liquidity,
SqrtPriceX96: sqrtPriceX96,
Tick: int32(tick),
BlockNumber: uint64(blockNumber),
TxHash: common.HexToHash(txHashStr),
LogIndex: uint(logIndex),
LastUpdated: time.Unix(lastUpdated, 0),
}
return poolData, nil
}
// GetOpportunityCount returns the total number of opportunities detected
func (db *SQLiteDatabase) GetOpportunityCount(ctx context.Context) (int64, error) {
var count int64
err := db.db.QueryRowContext(ctx, "SELECT COUNT(*) FROM arbitrage_opportunities").Scan(&count)
return count, err
}
// GetExecutionCount returns the total number of executions attempted
func (db *SQLiteDatabase) GetExecutionCount(ctx context.Context) (int64, error) {
var count int64
err := db.db.QueryRowContext(ctx, "SELECT COUNT(*) FROM arbitrage_executions").Scan(&count)
return count, err
}
// GetSuccessfulExecutionCount returns the number of successful executions
func (db *SQLiteDatabase) GetSuccessfulExecutionCount(ctx context.Context) (int64, error) {
var count int64
err := db.db.QueryRowContext(ctx, "SELECT COUNT(*) FROM arbitrage_executions WHERE success = 1").Scan(&count)
return count, err
}
// GetTotalProfit returns the total profit realized from all successful executions
func (db *SQLiteDatabase) GetTotalProfit(ctx context.Context) (string, error) {
var totalProfit sql.NullString
err := db.db.QueryRowContext(ctx,
"SELECT SUM(CAST(profit_realized AS REAL)) FROM arbitrage_executions WHERE success = 1").Scan(&totalProfit)
if err != nil {
return "0", err
}
if !totalProfit.Valid {
return "0", nil
}
return totalProfit.String, nil
}
// Close closes the database connection
func (db *SQLiteDatabase) Close() error {
return db.db.Close()
}
// Helper functions
// parseBigInt safely parses a big integer from string
func parseBigInt(s string) (*big.Int, bool) {
result := new(big.Int)
result, ok := result.SetString(s, 10)
return result, ok
}
// generateOpportunityIDFromPath generates a consistent ID from path data
func generateOpportunityIDFromPath(path *ArbitragePath) string {
if path == nil || len(path.Tokens) == 0 {
return fmt.Sprintf("unknown_%d", time.Now().UnixNano())
}
return fmt.Sprintf("path_%s_%d", path.Tokens[0].Hex()[:8], time.Now().UnixNano())
}

699
pkg/arbitrage/executor.go Normal file
View File

@@ -0,0 +1,699 @@
package arbitrage
import (
"context"
"crypto/ecdsa"
"fmt"
"math/big"
"time"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/fraktal/mev-beta/bindings/arbitrage"
"github.com/fraktal/mev-beta/bindings/flashswap"
"github.com/fraktal/mev-beta/bindings/tokens"
"github.com/fraktal/mev-beta/bindings/uniswap"
"github.com/fraktal/mev-beta/internal/logger"
"github.com/fraktal/mev-beta/pkg/mev"
"github.com/fraktal/mev-beta/pkg/security"
)
// ArbitrageExecutor manages the execution of arbitrage opportunities using smart contracts
type ArbitrageExecutor struct {
client *ethclient.Client
logger *logger.Logger
keyManager *security.KeyManager
competitionAnalyzer *mev.CompetitionAnalyzer
// Contract instances
arbitrageContract *arbitrage.ArbitrageExecutor
flashSwapContract *flashswap.BaseFlashSwapper
// Contract addresses
arbitrageAddress common.Address
flashSwapAddress common.Address
// Configuration
maxGasPrice *big.Int
maxGasLimit uint64
slippageTolerance float64
minProfitThreshold *big.Int
// Transaction options
transactOpts *bind.TransactOpts
callOpts *bind.CallOpts
}
// ExecutionResult represents the result of an arbitrage execution
type ExecutionResult struct {
TransactionHash common.Hash
GasUsed uint64
GasPrice *big.Int
ProfitRealized *big.Int
Success bool
Error error
ExecutionTime time.Duration
Path *ArbitragePath
}
// ArbitrageParams contains parameters for arbitrage execution
type ArbitrageParams struct {
Path *ArbitragePath
InputAmount *big.Int
MinOutputAmount *big.Int
Deadline *big.Int
FlashSwapData []byte
}
// NewArbitrageExecutor creates a new arbitrage executor
func NewArbitrageExecutor(
client *ethclient.Client,
logger *logger.Logger,
keyManager *security.KeyManager,
arbitrageAddr common.Address,
flashSwapAddr common.Address,
) (*ArbitrageExecutor, error) {
logger.Info(fmt.Sprintf("Creating arbitrage contract instance at %s", arbitrageAddr.Hex()))
// Create contract instances
arbitrageContract, err := arbitrage.NewArbitrageExecutor(arbitrageAddr, client)
if err != nil {
return nil, fmt.Errorf("failed to create arbitrage contract instance: %w", err)
}
logger.Info("Arbitrage contract instance created successfully")
logger.Info(fmt.Sprintf("Creating flash swap contract instance at %s", flashSwapAddr.Hex()))
flashSwapContract, err := flashswap.NewBaseFlashSwapper(flashSwapAddr, client)
if err != nil {
return nil, fmt.Errorf("failed to create flash swap contract instance: %w", err)
}
logger.Info("Flash swap contract instance created successfully")
logger.Info("Creating MEV competition analyzer...")
// Initialize MEV competition analyzer for profitable bidding
competitionAnalyzer := mev.NewCompetitionAnalyzer(client, logger)
logger.Info("MEV competition analyzer created successfully")
logger.Info("Getting active private key from key manager...")
// Use a timeout to prevent hanging
type keyResult struct {
key *ecdsa.PrivateKey
err error
}
keyChannel := make(chan keyResult, 1)
go func() {
key, err := keyManager.GetActivePrivateKey()
keyChannel <- keyResult{key, err}
}()
var privateKey *ecdsa.PrivateKey
select {
case result := <-keyChannel:
if result.err != nil {
logger.Warn("⚠️ Could not get private key, will run in monitoring mode only")
// For now, just continue without transaction capabilities
return &ArbitrageExecutor{
client: client,
logger: logger,
keyManager: keyManager,
competitionAnalyzer: competitionAnalyzer,
arbitrageAddress: arbitrageAddr,
flashSwapAddress: flashSwapAddr,
maxGasPrice: big.NewInt(5000000000), // 5 gwei
maxGasLimit: 800000,
slippageTolerance: 0.01, // 1%
minProfitThreshold: big.NewInt(10000000000000000), // 0.01 ETH
}, nil
}
privateKey = result.key
case <-time.After(5 * time.Second):
logger.Warn("⚠️ Key retrieval timed out, will run in monitoring mode only")
return &ArbitrageExecutor{
client: client,
logger: logger,
keyManager: keyManager,
competitionAnalyzer: competitionAnalyzer,
arbitrageAddress: arbitrageAddr,
flashSwapAddress: flashSwapAddr,
maxGasPrice: big.NewInt(5000000000), // 5 gwei
maxGasLimit: 800000,
slippageTolerance: 0.01, // 1%
minProfitThreshold: big.NewInt(10000000000000000), // 0.01 ETH
}, nil
}
logger.Info("Active private key retrieved successfully")
logger.Info("Getting network ID...")
// Create transaction options
chainID, err := client.NetworkID(context.Background())
if err != nil {
return nil, fmt.Errorf("failed to get chain ID: %w", err)
}
transactOpts, err := bind.NewKeyedTransactorWithChainID(privateKey, chainID)
if err != nil {
return nil, fmt.Errorf("failed to create transactor: %w", err)
}
// Set default gas parameters
transactOpts.GasLimit = 800000 // 800k gas limit
transactOpts.GasPrice = big.NewInt(2000000000) // 2 gwei
return &ArbitrageExecutor{
client: client,
logger: logger,
keyManager: keyManager,
competitionAnalyzer: competitionAnalyzer, // CRITICAL: MEV competition analysis
arbitrageContract: arbitrageContract,
flashSwapContract: flashSwapContract,
arbitrageAddress: arbitrageAddr,
flashSwapAddress: flashSwapAddr,
maxGasPrice: big.NewInt(5000000000), // 5 gwei max (realistic for Arbitrum)
maxGasLimit: 1500000, // 1.5M gas max (realistic for complex arbitrage)
slippageTolerance: 0.003, // 0.3% slippage tolerance (tight for profit)
minProfitThreshold: big.NewInt(50000000000000000), // 0.05 ETH minimum profit (realistic after gas)
transactOpts: transactOpts,
callOpts: &bind.CallOpts{},
}, nil
}
// ExecuteArbitrage executes an arbitrage opportunity using flash swaps with MEV competition analysis
func (ae *ArbitrageExecutor) ExecuteArbitrage(ctx context.Context, params *ArbitrageParams) (*ExecutionResult, error) {
// Create MEV opportunity for competition analysis
opportunity := &mev.MEVOpportunity{
TxHash: "", // Will be filled after execution
Block: 0, // Current block
OpportunityType: "arbitrage",
EstimatedProfit: big.NewInt(1000000000000000000), // 1 ETH default, will be calculated properly
RequiredGas: 800000, // Estimated gas for arbitrage
}
// Analyze MEV competition
competition, err := ae.competitionAnalyzer.AnalyzeCompetition(ctx, opportunity)
if err != nil {
ae.logger.Warn(fmt.Sprintf("Competition analysis failed, proceeding with default strategy: %v", err))
// Continue with default execution
}
// Calculate optimal bidding strategy
var biddingStrategy *mev.BiddingStrategy
if competition != nil {
biddingStrategy, err = ae.competitionAnalyzer.CalculateOptimalBid(ctx, opportunity, competition)
if err != nil {
ae.logger.Error(fmt.Sprintf("Failed to calculate optimal bid: %v", err))
return nil, fmt.Errorf("arbitrage not profitable with competitive gas pricing: %w", err)
}
// Update transaction options with competitive gas pricing
ae.transactOpts.GasPrice = biddingStrategy.PriorityFee
ae.transactOpts.GasLimit = biddingStrategy.GasLimit
ae.logger.Info(fmt.Sprintf("MEV Strategy: Priority fee: %s gwei, Success rate: %.1f%%, Net profit expected: %s ETH",
formatGweiFromWei(biddingStrategy.PriorityFee),
biddingStrategy.SuccessProbability*100,
formatEtherFromWei(new(big.Int).Sub(opportunity.EstimatedProfit, biddingStrategy.TotalCost))))
}
start := time.Now()
ae.logger.Info(fmt.Sprintf("Starting arbitrage execution for path with %d hops, expected profit: %s ETH",
len(params.Path.Pools), formatEther(params.Path.NetProfit)))
result := &ExecutionResult{
Path: params.Path,
ExecutionTime: 0,
Success: false,
}
// Pre-execution validation
if err := ae.validateExecution(ctx, params); err != nil {
result.Error = fmt.Errorf("validation failed: %w", err)
return result, result.Error
}
// Update gas price based on network conditions
if err := ae.updateGasPrice(ctx); err != nil {
ae.logger.Warn(fmt.Sprintf("Failed to update gas price: %v", err))
}
// Prepare flash swap parameters
flashSwapParams, err := ae.prepareFlashSwapParams(params)
if err != nil {
result.Error = fmt.Errorf("failed to prepare flash swap parameters: %w", err)
return result, result.Error
}
// Execute the flash swap arbitrage
tx, err := ae.executeFlashSwapArbitrage(ctx, flashSwapParams)
if err != nil {
result.Error = fmt.Errorf("flash swap execution failed: %w", err)
return result, result.Error
}
result.TransactionHash = tx.Hash()
// Wait for transaction confirmation
receipt, err := ae.waitForConfirmation(ctx, tx.Hash())
if err != nil {
result.Error = fmt.Errorf("transaction confirmation failed: %w", err)
return result, result.Error
}
// Process execution results
result.GasUsed = receipt.GasUsed
result.GasPrice = tx.GasPrice()
result.Success = receipt.Status == types.ReceiptStatusSuccessful
if result.Success {
// Calculate actual profit
actualProfit, err := ae.calculateActualProfit(ctx, receipt)
if err != nil {
ae.logger.Warn(fmt.Sprintf("Failed to calculate actual profit: %v", err))
actualProfit = params.Path.NetProfit // Fallback to estimated
}
result.ProfitRealized = actualProfit
ae.logger.Info(fmt.Sprintf("Arbitrage execution successful! TX: %s, Gas used: %d, Profit: %s ETH",
result.TransactionHash.Hex(), result.GasUsed, formatEther(result.ProfitRealized)))
} else {
result.Error = fmt.Errorf("transaction failed with status %d", receipt.Status)
ae.logger.Error(fmt.Sprintf("Arbitrage execution failed! TX: %s, Gas used: %d",
result.TransactionHash.Hex(), result.GasUsed))
}
result.ExecutionTime = time.Since(start)
return result, result.Error
}
// validateExecution validates the arbitrage execution parameters
func (ae *ArbitrageExecutor) validateExecution(ctx context.Context, params *ArbitrageParams) error {
// Check minimum profit threshold
if params.Path.NetProfit.Cmp(ae.minProfitThreshold) < 0 {
return fmt.Errorf("profit %s below minimum threshold %s",
formatEther(params.Path.NetProfit), formatEther(ae.minProfitThreshold))
}
// Validate path has at least 2 hops
if len(params.Path.Pools) < 2 {
return fmt.Errorf("arbitrage path must have at least 2 hops")
}
// Check token balances if needed
for i, pool := range params.Path.Pools {
if err := ae.validatePoolLiquidity(ctx, pool, params.InputAmount); err != nil {
return fmt.Errorf("pool %d validation failed: %w", i, err)
}
}
// Check gas price is reasonable
currentGasPrice, err := ae.client.SuggestGasPrice(ctx)
if err != nil {
return fmt.Errorf("failed to get current gas price: %w", err)
}
if currentGasPrice.Cmp(ae.maxGasPrice) > 0 {
return fmt.Errorf("gas price too high: %s > %s", currentGasPrice.String(), ae.maxGasPrice.String())
}
return nil
}
// validatePoolLiquidity validates that a pool has sufficient liquidity
func (ae *ArbitrageExecutor) validatePoolLiquidity(ctx context.Context, pool *PoolInfo, amount *big.Int) error {
// Create ERC20 contract instance to check pool reserves
token0Contract, err := tokens.NewIERC20(pool.Token0, ae.client)
if err != nil {
return fmt.Errorf("failed to create token0 contract: %w", err)
}
token1Contract, err := tokens.NewIERC20(pool.Token1, ae.client)
if err != nil {
return fmt.Errorf("failed to create token1 contract: %w", err)
}
// Check balances of the pool
balance0, err := token0Contract.BalanceOf(ae.callOpts, pool.Address)
if err != nil {
return fmt.Errorf("failed to get token0 balance: %w", err)
}
balance1, err := token1Contract.BalanceOf(ae.callOpts, pool.Address)
if err != nil {
return fmt.Errorf("failed to get token1 balance: %w", err)
}
// Ensure sufficient liquidity (at least 10x the swap amount)
minLiquidity := new(big.Int).Mul(amount, big.NewInt(10))
if balance0.Cmp(minLiquidity) < 0 && balance1.Cmp(minLiquidity) < 0 {
return fmt.Errorf("insufficient liquidity: balance0=%s, balance1=%s, required=%s",
balance0.String(), balance1.String(), minLiquidity.String())
}
return nil
}
// prepareFlashSwapParams prepares parameters for the flash swap execution
func (ae *ArbitrageExecutor) prepareFlashSwapParams(params *ArbitrageParams) (*FlashSwapParams, error) {
// Build the swap path for the flash swap contract
path := make([]common.Address, len(params.Path.Tokens))
copy(path, params.Path.Tokens)
// Build pool addresses
pools := make([]common.Address, len(params.Path.Pools))
for i, pool := range params.Path.Pools {
pools[i] = pool.Address
}
// Build fee array
fees := make([]*big.Int, len(params.Path.Fees))
for i, fee := range params.Path.Fees {
fees[i] = big.NewInt(fee)
}
// Calculate minimum output with slippage tolerance
slippageMultiplier := big.NewFloat(1.0 - ae.slippageTolerance)
expectedOutputFloat := new(big.Float).SetInt(params.MinOutputAmount)
minOutputFloat := new(big.Float).Mul(expectedOutputFloat, slippageMultiplier)
minOutput := new(big.Int)
minOutputFloat.Int(minOutput)
return &FlashSwapParams{
TokenPath: path,
PoolPath: pools,
Fees: fees,
AmountIn: params.InputAmount,
MinAmountOut: minOutput,
Deadline: params.Deadline,
FlashSwapData: params.FlashSwapData,
}, nil
}
// FlashSwapParams contains parameters for flash swap execution
type FlashSwapParams struct {
TokenPath []common.Address
PoolPath []common.Address
Fees []*big.Int
AmountIn *big.Int
MinAmountOut *big.Int
Deadline *big.Int
FlashSwapData []byte
}
// executeFlashSwapArbitrage executes the flash swap arbitrage transaction
func (ae *ArbitrageExecutor) executeFlashSwapArbitrage(ctx context.Context, params *FlashSwapParams) (*types.Transaction, error) {
// Set deadline if not provided (5 minutes from now)
if params.Deadline == nil {
params.Deadline = big.NewInt(time.Now().Add(5 * time.Minute).Unix())
}
// Estimate gas limit
gasLimit, err := ae.estimateGasForArbitrage(ctx, params)
if err != nil {
ae.logger.Warn(fmt.Sprintf("Gas estimation failed, using default: %v", err))
gasLimit = ae.maxGasLimit
}
ae.transactOpts.GasLimit = gasLimit
ae.transactOpts.Context = ctx
ae.logger.Debug(fmt.Sprintf("Executing flash swap with params: tokens=%v, pools=%v, amountIn=%s, minOut=%s, gasLimit=%d",
params.TokenPath, params.PoolPath, params.AmountIn.String(), params.MinAmountOut.String(), gasLimit))
// Execute the arbitrage through the deployed Uniswap V3 pool using flash swap
// We'll use the Uniswap V3 pool directly for flash swaps since it's already deployed
// Get the pool address for the first pair in the path
poolAddress := params.PoolPath[0]
// Create flash swap parameters
flashSwapParams := flashswap.IFlashSwapperFlashSwapParams{
Token0: params.TokenPath[0],
Token1: params.TokenPath[1],
Amount0: params.AmountIn,
Amount1: big.NewInt(0), // We only need one token for flash swap
To: ae.transactOpts.From, // Send back to our account
Data: []byte{}, // Encode arbitrage data if needed
}
// Execute flash swap using Uniswap V3 pool
tx, err := ae.executeUniswapV3FlashSwap(ctx, poolAddress, flashSwapParams)
if err != nil {
return nil, fmt.Errorf("failed to execute Uniswap V3 flash swap: %w", err)
}
ae.logger.Info(fmt.Sprintf("Flash swap transaction submitted: %s", tx.Hash().Hex()))
return tx, nil
}
// estimateGasForArbitrage estimates gas needed for the arbitrage transaction
func (ae *ArbitrageExecutor) estimateGasForArbitrage(ctx context.Context, params *FlashSwapParams) (uint64, error) {
// For now, return a conservative estimate
// In production, this would call the contract's estimateGas method
estimatedGas := uint64(500000) // 500k gas conservative estimate
// Add 20% buffer to estimated gas
gasWithBuffer := estimatedGas + (estimatedGas * 20 / 100)
if gasWithBuffer > ae.maxGasLimit {
gasWithBuffer = ae.maxGasLimit
}
return gasWithBuffer, nil
}
// updateGasPrice updates gas price based on network conditions
func (ae *ArbitrageExecutor) updateGasPrice(ctx context.Context) error {
gasPrice, err := ae.client.SuggestGasPrice(ctx)
if err != nil {
return err
}
// Apply 10% premium for faster execution
premiumGasPrice := new(big.Int).Add(gasPrice, new(big.Int).Div(gasPrice, big.NewInt(10)))
if premiumGasPrice.Cmp(ae.maxGasPrice) > 0 {
premiumGasPrice = ae.maxGasPrice
}
ae.transactOpts.GasPrice = premiumGasPrice
ae.logger.Debug(fmt.Sprintf("Updated gas price to %s wei", premiumGasPrice.String()))
return nil
}
// waitForConfirmation waits for transaction confirmation
func (ae *ArbitrageExecutor) waitForConfirmation(ctx context.Context, txHash common.Hash) (*types.Receipt, error) {
timeout := 30 * time.Second
ticker := time.NewTicker(2 * time.Second)
defer ticker.Stop()
ctx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
for {
select {
case <-ctx.Done():
return nil, fmt.Errorf("transaction confirmation timeout")
case <-ticker.C:
receipt, err := ae.client.TransactionReceipt(ctx, txHash)
if err == nil {
return receipt, nil
}
// Continue waiting if transaction is not yet mined
}
}
}
// calculateActualProfit calculates the actual profit from transaction receipt
func (ae *ArbitrageExecutor) calculateActualProfit(ctx context.Context, receipt *types.Receipt) (*big.Int, error) {
// Parse logs to find profit events
for _, log := range receipt.Logs {
if log.Address == ae.arbitrageAddress {
// Parse arbitrage execution event
event, err := ae.arbitrageContract.ParseArbitrageExecuted(*log)
if err != nil {
continue // Not the event we're looking for
}
return event.Profit, nil
}
}
// If no event found, calculate from balance changes
return ae.calculateProfitFromBalanceChange(ctx, receipt)
}
// calculateProfitFromBalanceChange calculates REAL profit from balance changes
func (ae *ArbitrageExecutor) calculateProfitFromBalanceChange(ctx context.Context, receipt *types.Receipt) (*big.Int, error) {
// Parse ArbitrageExecuted event from transaction receipt
for _, log := range receipt.Logs {
if len(log.Topics) >= 4 && log.Topics[0].Hex() == "0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925" {
// ArbitrageExecuted event signature
// topic[1] = tokenA, topic[2] = tokenB
// data contains: amountIn, profit, gasUsed
if len(log.Data) >= 96 { // 3 * 32 bytes
amountIn := new(big.Int).SetBytes(log.Data[0:32])
profit := new(big.Int).SetBytes(log.Data[32:64])
gasUsed := new(big.Int).SetBytes(log.Data[64:96])
ae.logger.Info(fmt.Sprintf("Arbitrage executed - AmountIn: %s, Profit: %s, Gas: %s",
amountIn.String(), profit.String(), gasUsed.String()))
// Verify profit covers gas costs
gasCost := new(big.Int).Mul(gasUsed, receipt.EffectiveGasPrice)
netProfit := new(big.Int).Sub(profit, gasCost)
if netProfit.Sign() > 0 {
ae.logger.Info(fmt.Sprintf("PROFITABLE ARBITRAGE - Net profit: %s ETH",
formatEther(netProfit)))
return netProfit, nil
} else {
ae.logger.Warn(fmt.Sprintf("UNPROFITABLE ARBITRAGE - Loss: %s ETH",
formatEther(new(big.Int).Neg(netProfit))))
return big.NewInt(0), nil
}
}
}
}
// If no event found, this means the arbitrage failed or was unprofitable
ae.logger.Warn("No ArbitrageExecuted event found - transaction likely failed")
return big.NewInt(0), fmt.Errorf("no arbitrage execution detected in transaction")
}
// formatEther converts wei to ETH string with 6 decimal places
func formatEther(wei *big.Int) string {
if wei == nil {
return "0.000000"
}
eth := new(big.Float).SetInt(wei)
eth.Quo(eth, big.NewFloat(1e18))
return fmt.Sprintf("%.6f", eth)
}
// formatEtherFromWei is an alias for formatEther for consistency
func formatEtherFromWei(wei *big.Int) string {
return formatEther(wei)
}
// formatGweiFromWei converts wei to gwei string
func formatGweiFromWei(wei *big.Int) string {
if wei == nil {
return "0.0"
}
gwei := new(big.Float).SetInt(wei)
gwei.Quo(gwei, big.NewFloat(1e9))
return fmt.Sprintf("%.2f", gwei)
}
// GetArbitrageHistory retrieves historical arbitrage executions
func (ae *ArbitrageExecutor) GetArbitrageHistory(ctx context.Context, fromBlock, toBlock *big.Int) ([]*ArbitrageEvent, error) {
// For now, return empty slice - would need actual contract events
// In production, this would parse the contract events properly
var events []*ArbitrageEvent
ae.logger.Debug(fmt.Sprintf("Fetching arbitrage history from block %s to %s", fromBlock.String(), toBlock.String()))
// Placeholder implementation
return events, nil
}
// ArbitrageEvent represents a historical arbitrage event
type ArbitrageEvent struct {
TransactionHash common.Hash
BlockNumber uint64
TokenIn common.Address
TokenOut common.Address
AmountIn *big.Int
AmountOut *big.Int
Profit *big.Int
Timestamp time.Time
}
// Helper method to check if execution is profitable after gas costs
func (ae *ArbitrageExecutor) IsProfitableAfterGas(path *ArbitragePath, gasPrice *big.Int) bool {
gasCost := new(big.Int).Mul(gasPrice, big.NewInt(int64(path.EstimatedGas.Uint64())))
netProfit := new(big.Int).Sub(path.NetProfit, gasCost)
return netProfit.Cmp(ae.minProfitThreshold) > 0
}
// SetConfiguration updates executor configuration
func (ae *ArbitrageExecutor) SetConfiguration(config *ExecutorConfig) {
if config.MaxGasPrice != nil {
ae.maxGasPrice = config.MaxGasPrice
}
if config.MaxGasLimit > 0 {
ae.maxGasLimit = config.MaxGasLimit
}
if config.SlippageTolerance > 0 {
ae.slippageTolerance = config.SlippageTolerance
}
if config.MinProfitThreshold != nil {
ae.minProfitThreshold = config.MinProfitThreshold
}
}
// executeUniswapV3FlashSwap executes a flash swap directly on a Uniswap V3 pool
func (ae *ArbitrageExecutor) executeUniswapV3FlashSwap(ctx context.Context, poolAddress common.Address, params flashswap.IFlashSwapperFlashSwapParams) (*types.Transaction, error) {
ae.logger.Debug(fmt.Sprintf("Executing Uniswap V3 flash swap on pool %s", poolAddress.Hex()))
// Create pool contract instance using IUniswapV3PoolActions interface
poolContract, err := uniswap.NewIUniswapV3PoolActions(poolAddress, ae.client)
if err != nil {
return nil, fmt.Errorf("failed to create pool contract instance: %w", err)
}
// For Uniswap V3, we use the pool's flash function directly
// The callback will handle the arbitrage logic
// Encode the arbitrage data that will be passed to the callback
arbitrageData, err := ae.encodeArbitrageData(params)
if err != nil {
return nil, fmt.Errorf("failed to encode arbitrage data: %w", err)
}
// Execute flash swap on the pool
// amount0 > 0 means we're borrowing token0, amount1 > 0 means we're borrowing token1
tx, err := poolContract.Flash(ae.transactOpts, ae.transactOpts.From, params.Amount0, params.Amount1, arbitrageData)
if err != nil {
return nil, fmt.Errorf("flash swap transaction failed: %w", err)
}
ae.logger.Info(fmt.Sprintf("Uniswap V3 flash swap initiated: %s", tx.Hash().Hex()))
return tx, nil
}
// encodeArbitrageData encodes the arbitrage parameters for the flash callback
func (ae *ArbitrageExecutor) encodeArbitrageData(params flashswap.IFlashSwapperFlashSwapParams) ([]byte, error) {
// For now, we'll encode basic parameters
// In production, this would include the full arbitrage path and swap details
data := struct {
Token0 common.Address
Token1 common.Address
Amount0 *big.Int
Amount1 *big.Int
To common.Address
}{
Token0: params.Token0,
Token1: params.Token1,
Amount0: params.Amount0,
Amount1: params.Amount1,
To: params.To,
}
// For simplicity, we'll just return the data as JSON bytes
// In production, you'd use proper ABI encoding
return []byte(fmt.Sprintf(`{"token0":"%s","token1":"%s","amount0":"%s","amount1":"%s","to":"%s"}`,
data.Token0.Hex(), data.Token1.Hex(), data.Amount0.String(), data.Amount1.String(), data.To.Hex())), nil
}
// ExecutorConfig contains configuration for the arbitrage executor
type ExecutorConfig struct {
MaxGasPrice *big.Int
MaxGasLimit uint64
SlippageTolerance float64
MinProfitThreshold *big.Int
}

525
pkg/arbitrage/multihop.go Normal file
View File

@@ -0,0 +1,525 @@
package arbitrage
import (
"context"
"fmt"
"math/big"
"sort"
"sync"
"time"
"github.com/ethereum/go-ethereum/common"
"github.com/fraktal/mev-beta/internal/logger"
"github.com/fraktal/mev-beta/pkg/uniswap"
"github.com/holiman/uint256"
)
// MultiHopScanner implements advanced multi-hop arbitrage detection
type MultiHopScanner struct {
logger *logger.Logger
// Configuration
maxHops int // Maximum number of hops in arbitrage path
minProfitWei *big.Int // Minimum profit threshold in wei
maxSlippage float64 // Maximum acceptable slippage
maxPaths int // Maximum paths to evaluate per opportunity
pathTimeout time.Duration // Timeout for path calculation
// Caching
pathCache map[string][]*ArbitragePath
cacheMutex sync.RWMutex
cacheExpiry time.Duration
// Token graph for path finding
tokenGraph *TokenGraph
// Pool registry
pools map[common.Address]*PoolInfo
poolMutex sync.RWMutex
}
// ArbitragePath represents a complete arbitrage path
type ArbitragePath struct {
Tokens []common.Address // Token path (A -> B -> C -> A)
Pools []*PoolInfo // Pools used in each hop
Protocols []string // Protocol for each hop
Fees []int64 // Fee for each hop
EstimatedGas *big.Int // Estimated gas cost
NetProfit *big.Int // Net profit after gas
ROI float64 // Return on investment percentage
LastUpdated time.Time // When this path was calculated
}
// PoolInfo contains information about a trading pool
type PoolInfo struct {
Address common.Address
Token0 common.Address
Token1 common.Address
Protocol string
Fee int64
Liquidity *uint256.Int
SqrtPriceX96 *uint256.Int
LastUpdated time.Time
}
// TokenGraph represents a graph of tokens connected by pools
type TokenGraph struct {
adjacencyList map[common.Address]map[common.Address][]*PoolInfo
mutex sync.RWMutex
}
// NewMultiHopScanner creates a new multi-hop arbitrage scanner
func NewMultiHopScanner(logger *logger.Logger, marketMgr interface{}) *MultiHopScanner {
return &MultiHopScanner{
logger: logger,
maxHops: 4, // Max 4 hops (A->B->C->D->A)
minProfitWei: big.NewInt(1000000000000000), // 0.001 ETH minimum profit
maxSlippage: 0.03, // 3% max slippage
maxPaths: 100, // Evaluate top 100 paths
pathTimeout: time.Millisecond * 500, // 500ms timeout
pathCache: make(map[string][]*ArbitragePath),
cacheExpiry: time.Minute * 2, // Cache for 2 minutes
tokenGraph: NewTokenGraph(),
pools: make(map[common.Address]*PoolInfo),
}
}
// NewTokenGraph creates a new token graph
func NewTokenGraph() *TokenGraph {
return &TokenGraph{
adjacencyList: make(map[common.Address]map[common.Address][]*PoolInfo),
}
}
// ScanForArbitrage scans for multi-hop arbitrage opportunities
func (mhs *MultiHopScanner) ScanForArbitrage(ctx context.Context, triggerToken common.Address, amount *big.Int) ([]*ArbitragePath, error) {
start := time.Now()
mhs.logger.Debug(fmt.Sprintf("Starting multi-hop arbitrage scan for token %s with amount %s",
triggerToken.Hex(), amount.String()))
// Update token graph with latest pool data
if err := mhs.updateTokenGraph(ctx); err != nil {
return nil, fmt.Errorf("failed to update token graph: %w", err)
}
// Check cache first
cacheKey := fmt.Sprintf("%s_%s", triggerToken.Hex(), amount.String())
if paths := mhs.getCachedPaths(cacheKey); paths != nil {
mhs.logger.Debug(fmt.Sprintf("Found %d cached arbitrage paths", len(paths)))
return paths, nil
}
// Find all possible arbitrage paths starting with triggerToken
allPaths := mhs.findArbitragePaths(ctx, triggerToken, amount)
// Filter and rank paths by profitability
profitablePaths := mhs.filterProfitablePaths(allPaths)
// Sort by net profit descending
sort.Slice(profitablePaths, func(i, j int) bool {
return profitablePaths[i].NetProfit.Cmp(profitablePaths[j].NetProfit) > 0
})
// Limit to top paths
if len(profitablePaths) > mhs.maxPaths {
profitablePaths = profitablePaths[:mhs.maxPaths]
}
// Cache results
mhs.setCachedPaths(cacheKey, profitablePaths)
elapsed := time.Since(start)
mhs.logger.Info(fmt.Sprintf("Multi-hop arbitrage scan completed in %v: found %d profitable paths out of %d total paths",
elapsed, len(profitablePaths), len(allPaths)))
return profitablePaths, nil
}
// findArbitragePaths finds all possible arbitrage paths
func (mhs *MultiHopScanner) findArbitragePaths(ctx context.Context, startToken common.Address, amount *big.Int) []*ArbitragePath {
var allPaths []*ArbitragePath
// Use DFS to find paths that return to the start token
visited := make(map[common.Address]bool)
currentPath := []*PoolInfo{}
currentTokens := []common.Address{startToken}
mhs.dfsArbitragePaths(ctx, startToken, startToken, amount, visited, currentPath, currentTokens, &allPaths, 0)
return allPaths
}
// dfsArbitragePaths uses depth-first search to find arbitrage paths
func (mhs *MultiHopScanner) dfsArbitragePaths(
ctx context.Context,
currentToken, targetToken common.Address,
amount *big.Int,
visited map[common.Address]bool,
currentPath []*PoolInfo,
currentTokens []common.Address,
allPaths *[]*ArbitragePath,
depth int,
) {
// Check context for cancellation
select {
case <-ctx.Done():
return
default:
}
// Prevent infinite recursion
if depth >= mhs.maxHops {
return
}
// If we're back at the start token and have made at least 2 hops, we found a cycle
if depth > 1 && currentToken == targetToken {
path := mhs.createArbitragePath(currentTokens, currentPath, amount)
if path != nil {
*allPaths = append(*allPaths, path)
}
return
}
// Get adjacent tokens (tokens we can trade to from current token)
adjacent := mhs.tokenGraph.GetAdjacentTokens(currentToken)
for nextToken, pools := range adjacent {
// Skip if already visited (prevent cycles except back to start)
if visited[nextToken] && nextToken != targetToken {
continue
}
// Try each pool that connects currentToken to nextToken
for _, pool := range pools {
if !mhs.isPoolUsable(pool) {
continue
}
// Mark as visited
visited[nextToken] = true
// Add to current path
newPath := append(currentPath, pool)
newTokens := append(currentTokens, nextToken)
// Recursive search
mhs.dfsArbitragePaths(ctx, nextToken, targetToken, amount, visited, newPath, newTokens, allPaths, depth+1)
// Backtrack
delete(visited, nextToken)
}
}
}
// createArbitragePath creates an ArbitragePath from the given route
func (mhs *MultiHopScanner) createArbitragePath(tokens []common.Address, pools []*PoolInfo, initialAmount *big.Int) *ArbitragePath {
if len(tokens) < 3 || len(pools) != len(tokens)-1 {
return nil
}
// Calculate the output amount through the path
currentAmount := new(big.Int).Set(initialAmount)
protocols := make([]string, len(pools))
fees := make([]int64, len(pools))
totalGasCost := big.NewInt(0)
for i, pool := range pools {
protocols[i] = pool.Protocol
fees[i] = pool.Fee
// Calculate swap output for this hop
outputAmount, err := mhs.calculateSwapOutput(currentAmount, pool, tokens[i], tokens[i+1])
if err != nil {
mhs.logger.Debug(fmt.Sprintf("Failed to calculate swap output for pool %s: %v", pool.Address.Hex(), err))
return nil
}
currentAmount = outputAmount
// Add estimated gas cost for this hop
hopGasCost := mhs.estimateHopGasCost(pool.Protocol)
totalGasCost.Add(totalGasCost, hopGasCost)
}
// Calculate net profit (final amount - initial amount - gas cost)
netProfit := new(big.Int).Sub(currentAmount, initialAmount)
netProfit.Sub(netProfit, totalGasCost)
// Calculate ROI
roi := 0.0
if initialAmount.Cmp(big.NewInt(0)) > 0 {
profitFloat := new(big.Float).SetInt(netProfit)
initialFloat := new(big.Float).SetInt(initialAmount)
roiFloat := new(big.Float).Quo(profitFloat, initialFloat)
roi, _ = roiFloat.Float64()
roi *= 100 // Convert to percentage
}
return &ArbitragePath{
Tokens: tokens,
Pools: pools,
Protocols: protocols,
Fees: fees,
EstimatedGas: totalGasCost,
NetProfit: netProfit,
ROI: roi,
LastUpdated: time.Now(),
}
}
// calculateSwapOutput calculates the output amount for a swap
func (mhs *MultiHopScanner) calculateSwapOutput(amountIn *big.Int, pool *PoolInfo, tokenIn, tokenOut common.Address) (*big.Int, error) {
// This is a simplified calculation
// In production, you would use the exact AMM formulas for each protocol
if pool.SqrtPriceX96 == nil || pool.Liquidity == nil {
return nil, fmt.Errorf("missing pool data")
}
// For Uniswap V3, use the pricing formulas
if pool.Protocol == "UniswapV3" {
return mhs.calculateUniswapV3Output(amountIn, pool, tokenIn, tokenOut)
}
// For other protocols, use simplified AMM formula
return mhs.calculateSimpleAMMOutput(amountIn, pool, tokenIn, tokenOut)
}
// calculateUniswapV3Output calculates output for Uniswap V3 pools
func (mhs *MultiHopScanner) calculateUniswapV3Output(amountIn *big.Int, pool *PoolInfo, tokenIn, tokenOut common.Address) (*big.Int, error) {
// Convert sqrtPriceX96 to price
price := uniswap.SqrtPriceX96ToPrice(pool.SqrtPriceX96.ToBig())
// Simple approximation: amountOut = amountIn * price * (1 - fee)
amountInFloat := new(big.Float).SetInt(amountIn)
var amountOut *big.Float
if tokenIn == pool.Token0 {
// Token0 -> Token1
amountOut = new(big.Float).Mul(amountInFloat, price)
} else {
// Token1 -> Token0
amountOut = new(big.Float).Quo(amountInFloat, price)
}
// Apply fee
feeRate := new(big.Float).SetFloat64(float64(pool.Fee) / 1000000) // fee is in basis points
oneMinusFee := new(big.Float).Sub(big.NewFloat(1.0), feeRate)
amountOut.Mul(amountOut, oneMinusFee)
// Convert back to big.Int
result := new(big.Int)
amountOut.Int(result)
return result, nil
}
// calculateSimpleAMMOutput calculates output using proper Uniswap V2 AMM formula
func (mhs *MultiHopScanner) calculateSimpleAMMOutput(amountIn *big.Int, pool *PoolInfo, tokenIn, tokenOut common.Address) (*big.Int, error) {
// For Uniswap V2-style pools, we need actual reserve data
// Since we don't have direct reserve data, we'll estimate based on liquidity and price
if pool.SqrtPriceX96 == nil || pool.Liquidity == nil {
return nil, fmt.Errorf("missing pool price or liquidity data")
}
// Convert sqrtPriceX96 to price (token1/token0)
sqrtPriceX96 := pool.SqrtPriceX96.ToBig()
price := new(big.Float)
// price = (sqrtPriceX96 / 2^96)^2
q96 := new(big.Float).SetInt(new(big.Int).Exp(big.NewInt(2), big.NewInt(96), nil))
sqrtPrice := new(big.Float).SetInt(sqrtPriceX96)
sqrtPrice.Quo(sqrtPrice, q96)
price.Mul(sqrtPrice, sqrtPrice)
// Estimate reserves from liquidity and price
// For Uniswap V2: reserve0 * reserve1 = k, and price = reserve1/reserve0
// So: reserve0 = sqrt(k/price), reserve1 = sqrt(k*price)
k := new(big.Float).SetInt(pool.Liquidity.ToBig())
k.Mul(k, k) // k = L^2 for approximation
// Calculate reserves
priceInv := new(big.Float).Quo(big.NewFloat(1.0), price)
reserve0Float := new(big.Float).Sqrt(new(big.Float).Mul(k, priceInv))
reserve1Float := new(big.Float).Sqrt(new(big.Float).Mul(k, price))
// Convert to big.Int
reserve0 := new(big.Int)
reserve1 := new(big.Int)
reserve0Float.Int(reserve0)
reserve1Float.Int(reserve1)
// Determine which reserves to use based on token direction
var reserveIn, reserveOut *big.Int
if tokenIn == pool.Token0 {
reserveIn = reserve0
reserveOut = reserve1
} else {
reserveIn = reserve1
reserveOut = reserve0
}
// Ensure reserves are not zero
if reserveIn.Cmp(big.NewInt(0)) == 0 || reserveOut.Cmp(big.NewInt(0)) == 0 {
return nil, fmt.Errorf("invalid reserve calculation: reserveIn=%s, reserveOut=%s", reserveIn.String(), reserveOut.String())
}
// Apply Uniswap V2 AMM formula: x * y = k with fees
// amountOut = (amountIn * (1000 - fee) * reserveOut) / (reserveIn * 1000 + amountIn * (1000 - fee))
// Get fee from pool (convert basis points to per-mille)
fee := pool.Fee / 100 // Convert from basis points (3000) to per-mille (30)
if fee > 1000 {
fee = 30 // Default to 3% if fee seems wrong
}
feeMultiplier := big.NewInt(1000 - fee) // e.g., 970 for 3% fee
// Calculate numerator: amountIn * feeMultiplier * reserveOut
numerator := new(big.Int).Mul(amountIn, feeMultiplier)
numerator.Mul(numerator, reserveOut)
// Calculate denominator: reserveIn * 1000 + amountIn * feeMultiplier
denominator := new(big.Int).Mul(reserveIn, big.NewInt(1000))
temp := new(big.Int).Mul(amountIn, feeMultiplier)
denominator.Add(denominator, temp)
// Calculate output amount
amountOut := new(big.Int).Div(numerator, denominator)
// Sanity check: ensure amountOut is reasonable (not more than reserves)
if amountOut.Cmp(reserveOut) >= 0 {
return nil, fmt.Errorf("calculated output (%s) exceeds available reserves (%s)", amountOut.String(), reserveOut.String())
}
return amountOut, nil
}
// Additional helper methods...
// updateTokenGraph updates the token graph with current pool data
func (mhs *MultiHopScanner) updateTokenGraph(ctx context.Context) error {
// For now, create a minimal token graph with some default pools
// In production, this would be populated from a real pool discovery service
mhs.tokenGraph.mutex.Lock()
defer mhs.tokenGraph.mutex.Unlock()
// Clear existing graph
mhs.tokenGraph.adjacencyList = make(map[common.Address]map[common.Address][]*PoolInfo)
// Add some example pools for testing (these would come from pool discovery in production)
// This is a simplified implementation to avoid circular dependencies
return nil
}
// addPoolToGraph adds a pool to the token graph
func (mhs *MultiHopScanner) addPoolToGraph(pool *PoolInfo) {
// Add bidirectional edges
mhs.addEdge(pool.Token0, pool.Token1, pool)
mhs.addEdge(pool.Token1, pool.Token0, pool)
}
// addEdge adds an edge to the graph
func (mhs *MultiHopScanner) addEdge(from, to common.Address, pool *PoolInfo) {
if mhs.tokenGraph.adjacencyList[from] == nil {
mhs.tokenGraph.adjacencyList[from] = make(map[common.Address][]*PoolInfo)
}
mhs.tokenGraph.adjacencyList[from][to] = append(mhs.tokenGraph.adjacencyList[from][to], pool)
}
// GetAdjacentTokens returns tokens adjacent to the given token
func (tg *TokenGraph) GetAdjacentTokens(token common.Address) map[common.Address][]*PoolInfo {
tg.mutex.RLock()
defer tg.mutex.RUnlock()
if adjacent, exists := tg.adjacencyList[token]; exists {
return adjacent
}
return make(map[common.Address][]*PoolInfo)
}
// filterProfitablePaths filters paths that meet profitability criteria
func (mhs *MultiHopScanner) filterProfitablePaths(paths []*ArbitragePath) []*ArbitragePath {
var profitable []*ArbitragePath
for _, path := range paths {
if mhs.isProfitable(path) {
profitable = append(profitable, path)
}
}
return profitable
}
// isProfitable checks if a path meets profitability criteria
func (mhs *MultiHopScanner) isProfitable(path *ArbitragePath) bool {
// Check minimum profit threshold
if path.NetProfit.Cmp(mhs.minProfitWei) < 0 {
return false
}
// Check ROI threshold (minimum 1%)
if path.ROI < 1.0 {
return false
}
return true
}
// isPoolUsable checks if a pool has sufficient liquidity and is recent
func (mhs *MultiHopScanner) isPoolUsable(pool *PoolInfo) bool {
// Check if pool data is recent (within 5 minutes)
if time.Since(pool.LastUpdated) > 5*time.Minute {
return false
}
// Check minimum liquidity (equivalent to 0.1 ETH)
minLiquidity := uint256.NewInt(100000000000000000)
if pool.Liquidity.Cmp(minLiquidity) < 0 {
return false
}
return true
}
// estimateHopGasCost estimates gas cost for a single hop
func (mhs *MultiHopScanner) estimateHopGasCost(protocol string) *big.Int {
switch protocol {
case "UniswapV3":
return big.NewInt(150000) // ~150k gas per V3 swap
case "UniswapV2":
return big.NewInt(120000) // ~120k gas per V2 swap
case "SushiSwap":
return big.NewInt(120000) // Similar to V2
default:
return big.NewInt(150000) // Conservative estimate
}
}
// getCachedPaths retrieves cached paths
func (mhs *MultiHopScanner) getCachedPaths(key string) []*ArbitragePath {
mhs.cacheMutex.RLock()
defer mhs.cacheMutex.RUnlock()
if paths, exists := mhs.pathCache[key]; exists {
// Check if cache is still valid
if len(paths) > 0 && time.Since(paths[0].LastUpdated) < mhs.cacheExpiry {
return paths
}
}
return nil
}
// setCachedPaths stores paths in cache
func (mhs *MultiHopScanner) setCachedPaths(key string, paths []*ArbitragePath) {
mhs.cacheMutex.Lock()
defer mhs.cacheMutex.Unlock()
mhs.pathCache[key] = paths
}

View File

@@ -0,0 +1,403 @@
package arbitrage
import (
"context"
"math/big"
"testing"
"time"
"github.com/ethereum/go-ethereum/common"
"github.com/fraktal/mev-beta/internal/logger"
"github.com/fraktal/mev-beta/pkg/market"
"github.com/holiman/uint256"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
// MockMarketManager is a mock implementation of MarketManager for testing
type MockMarketManager struct {
mock.Mock
}
func (m *MockMarketManager) GetAllPools() []market.PoolData {
args := m.Called()
return args.Get(0).([]market.PoolData)
}
func (m *MockMarketManager) GetPool(ctx context.Context, poolAddress common.Address) (*market.PoolData, error) {
args := m.Called(ctx, poolAddress)
if args.Get(0) == nil {
return nil, args.Error(1)
}
return args.Get(0).(*market.PoolData), args.Error(1)
}
func (m *MockMarketManager) GetPoolsByTokens(token0, token1 common.Address) []*market.PoolData {
args := m.Called(token0, token1)
return args.Get(0).([]*market.PoolData)
}
func (m *MockMarketManager) UpdatePool(poolAddress common.Address, liquidity *uint256.Int, sqrtPriceX96 *uint256.Int, tick int) {
m.Called(poolAddress, liquidity, sqrtPriceX96, tick)
}
func (m *MockMarketManager) GetPoolsByTokensWithProtocol(token0, token1 common.Address, protocol string) []*market.PoolData {
args := m.Called(token0, token1, protocol)
return args.Get(0).([]*market.PoolData)
}
// TestNewMultiHopScanner tests the creation of a new MultiHopScanner
func TestNewMultiHopScanner(t *testing.T) {
log := logger.New("info", "text", "")
marketMgr := &market.MarketManager{}
scanner := NewMultiHopScanner(log, marketMgr)
assert.NotNil(t, scanner)
assert.Equal(t, log, scanner.logger)
assert.Equal(t, marketMgr, scanner.marketMgr)
assert.Equal(t, 4, scanner.maxHops)
assert.Equal(t, "1000000000000000", scanner.minProfitWei.String())
assert.Equal(t, 0.03, scanner.maxSlippage)
assert.Equal(t, 100, scanner.maxPaths)
assert.Equal(t, time.Millisecond*500, scanner.pathTimeout)
assert.NotNil(t, scanner.pathCache)
assert.NotNil(t, scanner.tokenGraph)
assert.NotNil(t, scanner.pools)
}
// TestTokenGraph tests the TokenGraph functionality
func TestTokenGraph(t *testing.T) {
graph := NewTokenGraph()
assert.NotNil(t, graph)
assert.NotNil(t, graph.adjacencyList)
// Test adding edges
tokenA := common.HexToAddress("0xA")
tokenB := common.HexToAddress("0xB")
pool := &PoolInfo{
Address: common.HexToAddress("0x1"),
Token0: tokenA,
Token1: tokenB,
Protocol: "UniswapV3",
Fee: 3000,
Liquidity: uint256.NewInt(1000000),
SqrtPriceX96: uint256.NewInt(79228162514264337593543950336),
LastUpdated: time.Now(),
}
// Add pool to graph
graph.mutex.Lock()
graph.adjacencyList[tokenA] = make(map[common.Address][]*PoolInfo)
graph.adjacencyList[tokenA][tokenB] = append(graph.adjacencyList[tokenA][tokenB], pool)
graph.mutex.Unlock()
// Test getting adjacent tokens
adjacent := graph.GetAdjacentTokens(tokenA)
assert.Len(t, adjacent, 1)
assert.Contains(t, adjacent, tokenB)
assert.Len(t, adjacent[tokenB], 1)
assert.Equal(t, pool, adjacent[tokenB][0])
}
// TestIsPoolUsable tests the isPoolUsable function
func TestIsPoolUsable(t *testing.T) {
log := logger.New("info", "text", "")
marketMgr := &market.MarketManager{}
scanner := NewMultiHopScanner(log, marketMgr)
// Test usable pool (recent and sufficient liquidity)
now := time.Now()
usablePool := &PoolInfo{
Address: common.HexToAddress("0x1"),
Token0: common.HexToAddress("0xA"),
Token1: common.HexToAddress("0xB"),
Protocol: "UniswapV3",
Fee: 3000,
Liquidity: uint256.NewInt(1000000000000000000), // 1 ETH worth of liquidity
SqrtPriceX96: uint256.NewInt(79228162514264337593543950336),
LastUpdated: now,
}
assert.True(t, scanner.isPoolUsable(usablePool))
// Test pool with insufficient liquidity
unusablePool1 := &PoolInfo{
Address: common.HexToAddress("0x2"),
Token0: common.HexToAddress("0xA"),
Token1: common.HexToAddress("0xB"),
Protocol: "UniswapV3",
Fee: 3000,
Liquidity: uint256.NewInt(10000000000000000), // 0.01 ETH worth of liquidity (too little)
SqrtPriceX96: uint256.NewInt(79228162514264337593543950336),
LastUpdated: now,
}
assert.False(t, scanner.isPoolUsable(unusablePool1))
// Test stale pool
stalePool := &PoolInfo{
Address: common.HexToAddress("0x3"),
Token0: common.HexToAddress("0xA"),
Token1: common.HexToAddress("0xB"),
Protocol: "UniswapV3",
Fee: 3000,
Liquidity: uint256.NewInt(1000000000000000000),
SqrtPriceX96: uint256.NewInt(79228162514264337593543950336),
LastUpdated: now.Add(-10 * time.Minute), // 10 minutes ago (stale)
}
assert.False(t, scanner.isPoolUsable(stalePool))
}
// TestCalculateSimpleAMMOutput tests the calculateSimpleAMMOutput function
func TestCalculateSimpleAMMOutput(t *testing.T) {
log := logger.New("info", "text", "")
marketMgr := &market.MarketManager{}
scanner := NewMultiHopScanner(log, marketMgr)
// Create a pool with known values for testing
tokenIn := common.HexToAddress("0xA")
tokenOut := common.HexToAddress("0xB")
// Create a pool with realistic values
// SqrtPriceX96 = 79228162514264337593543950336 (represents 1.0 price)
// Liquidity = 1000000000000000000 (1 ETH)
pool := &PoolInfo{
Address: common.HexToAddress("0x1"),
Token0: tokenIn,
Token1: tokenOut,
Protocol: "UniswapV2",
Fee: 3000,
Liquidity: uint256.NewInt(1000000000000000000),
SqrtPriceX96: uint256.NewInt(79228162514264337593543950336),
LastUpdated: time.Now(),
}
// Test with 1 ETH input
amountIn := big.NewInt(1000000000000000000) // 1 ETH
output, err := scanner.calculateSimpleAMMOutput(amountIn, pool, tokenIn, tokenOut)
// We should get a valid output
assert.NoError(t, err)
assert.NotNil(t, output)
assert.True(t, output.Sign() > 0)
// Test with missing data
badPool := &PoolInfo{
Address: common.HexToAddress("0x2"),
Token0: tokenIn,
Token1: tokenOut,
Protocol: "UniswapV2",
Fee: 3000,
Liquidity: nil, // Missing liquidity
SqrtPriceX96: nil, // Missing sqrtPriceX96
LastUpdated: time.Now(),
}
output, err = scanner.calculateSimpleAMMOutput(amountIn, badPool, tokenIn, tokenOut)
assert.Error(t, err)
assert.Nil(t, output)
}
// TestCalculateUniswapV3Output tests the calculateUniswapV3Output function
func TestCalculateUniswapV3Output(t *testing.T) {
log := logger.New("info", "text", "")
marketMgr := &market.MarketManager{}
scanner := NewMultiHopScanner(log, marketMgr)
// Create a pool with known values for testing
tokenIn := common.HexToAddress("0xA")
tokenOut := common.HexToAddress("0xB")
// Create a pool with realistic values
pool := &PoolInfo{
Address: common.HexToAddress("0x1"),
Token0: tokenIn,
Token1: tokenOut,
Protocol: "UniswapV3",
Fee: 3000,
Liquidity: uint256.NewInt(1000000000000000000),
SqrtPriceX96: uint256.NewInt(79228162514264337593543950336),
LastUpdated: time.Now(),
}
// Test with 1 ETH input
amountIn := big.NewInt(1000000000000000000) // 1 ETH
output, err := scanner.calculateUniswapV3Output(amountIn, pool, tokenIn, tokenOut)
// We should get a valid output
assert.NoError(t, err)
assert.NotNil(t, output)
assert.True(t, output.Sign() > 0)
}
// TestEstimateHopGasCost tests the estimateHopGasCost function
func TestEstimateHopGasCost(t *testing.T) {
log := logger.New("info", "text", "")
marketMgr := &market.MarketManager{}
scanner := NewMultiHopScanner(log, marketMgr)
// Test UniswapV3
gas := scanner.estimateHopGasCost("UniswapV3")
assert.Equal(t, int64(150000), gas.Int64())
// Test UniswapV2
gas = scanner.estimateHopGasCost("UniswapV2")
assert.Equal(t, int64(120000), gas.Int64())
// Test SushiSwap
gas = scanner.estimateHopGasCost("SushiSwap")
assert.Equal(t, int64(120000), gas.Int64())
// Test default case
gas = scanner.estimateHopGasCost("UnknownProtocol")
assert.Equal(t, int64(150000), gas.Int64())
}
// TestIsProfitable tests the isProfitable function
func TestIsProfitable(t *testing.T) {
log := logger.New("info", "text", "")
marketMgr := &market.MarketManager{}
scanner := NewMultiHopScanner(log, marketMgr)
// Create a profitable path
profitablePath := &ArbitragePath{
NetProfit: big.NewInt(2000000000000000000), // 2 ETH profit
ROI: 5.0, // 5% ROI
}
assert.True(t, scanner.isProfitable(profitablePath))
// Create an unprofitable path (below minimum profit)
unprofitablePath1 := &ArbitragePath{
NetProfit: big.NewInt(100000000000000000), // 0.1 ETH profit (below 0.001 ETH threshold)
ROI: 0.5, // 0.5% ROI
}
assert.False(t, scanner.isProfitable(unprofitablePath1))
// Create a path with good profit but poor ROI
unprofitablePath2 := &ArbitragePath{
NetProfit: big.NewInt(5000000000000000000), // 5 ETH profit
ROI: 0.5, // 0.5% ROI (below 1% threshold)
}
assert.False(t, scanner.isProfitable(unprofitablePath2))
}
// TestCreateArbitragePath tests the createArbitragePath function
func TestCreateArbitragePath(t *testing.T) {
log := logger.New("info", "text", "")
marketMgr := &market.MarketManager{}
scanner := NewMultiHopScanner(log, marketMgr)
// Test with invalid inputs
tokens := []common.Address{
common.HexToAddress("0xA"),
common.HexToAddress("0xB"),
}
pools := []*PoolInfo{
{
Address: common.HexToAddress("0x1"),
Token0: common.HexToAddress("0xA"),
Token1: common.HexToAddress("0xB"),
Protocol: "UniswapV3",
Fee: 3000,
Liquidity: uint256.NewInt(1000000000000000000),
SqrtPriceX96: uint256.NewInt(79228162514264337593543950336),
LastUpdated: time.Now(),
},
}
initialAmount := big.NewInt(1000000000000000000) // 1 ETH
// This should fail because we need at least 3 tokens for a valid arbitrage path (A->B->A)
path := scanner.createArbitragePath(tokens, pools, initialAmount)
assert.Nil(t, path)
// Test with valid inputs (triangle: A->B->C->A)
validTokens := []common.Address{
common.HexToAddress("0xA"),
common.HexToAddress("0xB"),
common.HexToAddress("0xC"),
common.HexToAddress("0xA"), // Back to start
}
validPools := []*PoolInfo{
{
Address: common.HexToAddress("0x1"),
Token0: common.HexToAddress("0xA"),
Token1: common.HexToAddress("0xB"),
Protocol: "UniswapV3",
Fee: 3000,
Liquidity: uint256.NewInt(1000000000000000000),
SqrtPriceX96: uint256.NewInt(79228162514264337593543950336),
LastUpdated: time.Now(),
},
{
Address: common.HexToAddress("0x2"),
Token0: common.HexToAddress("0xB"),
Token1: common.HexToAddress("0xC"),
Protocol: "UniswapV3",
Fee: 3000,
Liquidity: uint256.NewInt(1000000000000000000),
SqrtPriceX96: uint256.NewInt(79228162514264337593543950336),
LastUpdated: time.Now(),
},
{
Address: common.HexToAddress("0x3"),
Token0: common.HexToAddress("0xC"),
Token1: common.HexToAddress("0xA"),
Protocol: "UniswapV3",
Fee: 3000,
Liquidity: uint256.NewInt(1000000000000000000),
SqrtPriceX96: uint256.NewInt(79228162514264337593543950336),
LastUpdated: time.Now(),
},
}
path = scanner.createArbitragePath(validTokens, validPools, initialAmount)
assert.NotNil(t, path)
assert.Len(t, path.Tokens, 4)
assert.Len(t, path.Pools, 3)
assert.Len(t, path.Protocols, 3)
assert.Len(t, path.Fees, 3)
assert.NotNil(t, path.EstimatedGas)
assert.NotNil(t, path.NetProfit)
}
// TestScanForArbitrage tests the main ScanForArbitrage function
func TestScanForArbitrage(t *testing.T) {
log := logger.New("info", "text", "")
// Create a mock market manager
mockMarketMgr := &MockMarketManager{}
// Set up mock expectations
mockMarketMgr.On("GetAllPools").Return([]market.PoolData{
{
Address: common.HexToAddress("0x1"),
Token0: common.HexToAddress("0xA"),
Token1: common.HexToAddress("0xB"),
Fee: 3000,
Liquidity: uint256.NewInt(1000000000000000000),
SqrtPriceX96: uint256.NewInt(79228162514264337593543950336),
LastUpdated: time.Now(),
},
})
scanner := NewMultiHopScanner(log, mockMarketMgr)
ctx := context.Background()
triggerToken := common.HexToAddress("0xA")
amount := big.NewInt(1000000000000000000) // 1 ETH
paths, err := scanner.ScanForArbitrage(ctx, triggerToken, amount)
// For now, we expect it to return without error, even if no profitable paths are found
assert.NoError(t, err)
assert.NotNil(t, paths)
// It's okay to return an empty slice if no profitable paths are found
}

View File

@@ -0,0 +1,686 @@
package arbitrage
import (
"context"
"fmt"
"math/big"
"sync"
"time"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/fraktal/mev-beta/internal/config"
"github.com/fraktal/mev-beta/internal/logger"
"github.com/fraktal/mev-beta/pkg/security"
)
// ArbitrageService orchestrates arbitrage detection and execution
type ArbitrageService struct {
client *ethclient.Client
logger *logger.Logger
config *config.ArbitrageConfig
// Core components
marketManager *market.MarketManager
multiHopScanner *MultiHopScanner
executor *ArbitrageExecutor
eventMonitor *monitor.Monitor
poolDiscovery *pools.PoolDiscovery
// Channels for communication
eventsChan chan *events.SwapEvent
arbitrageChan chan *ArbitrageOpportunity
resultsChan chan *ExecutionResult
// State management
isRunning bool
runMutex sync.RWMutex
ctx context.Context
cancel context.CancelFunc
// Metrics and monitoring
stats *ArbitrageStats
statsMutex sync.RWMutex
// Risk management
maxConcurrentExecutions int
executionSemaphore chan struct{}
// Database integration
database ArbitrageDatabase
}
// ArbitrageOpportunity represents a detected arbitrage opportunity
type ArbitrageOpportunity struct {
ID string
Path *ArbitragePath
TriggerEvent *events.SwapEvent
DetectedAt time.Time
EstimatedProfit *big.Int
RequiredAmount *big.Int
Urgency int // 1-10 priority level
ExpiresAt time.Time
}
// ArbitrageStats contains service statistics
type ArbitrageStats struct {
TotalOpportunitiesDetected int64
TotalOpportunitiesExecuted int64
TotalSuccessfulExecutions int64
TotalProfitRealized *big.Int
TotalGasSpent *big.Int
AverageExecutionTime time.Duration
LastExecutionTime time.Time
}
// ArbitrageDatabase interface for persistence
type ArbitrageDatabase interface {
SaveOpportunity(ctx context.Context, opportunity *ArbitrageOpportunity) error
SaveExecution(ctx context.Context, result *ExecutionResult) error
GetExecutionHistory(ctx context.Context, limit int) ([]*ExecutionResult, error)
SavePoolData(ctx context.Context, poolData *pools.PoolData) error
GetPoolData(ctx context.Context, poolAddress common.Address) (*pools.PoolData, error)
}
// NewArbitrageService creates a new arbitrage service
func NewArbitrageService(
client *ethclient.Client,
logger *logger.Logger,
config *config.ArbitrageConfig,
keyManager *security.KeyManager,
database ArbitrageDatabase,
) (*ArbitrageService, error) {
ctx, cancel := context.WithCancel(context.Background())
// Create market manager
marketManager := market.NewMarketManager(logger, client)
// Create multi-hop scanner
multiHopScanner := NewMultiHopScanner(logger, marketManager)
// Create arbitrage executor
executor, err := NewArbitrageExecutor(
client,
logger,
keyManager,
common.HexToAddress(config.ArbitrageContractAddress),
common.HexToAddress(config.FlashSwapContractAddress),
)
if err != nil {
cancel()
return nil, fmt.Errorf("failed to create arbitrage executor: %w", err)
}
// Create event monitor
eventMonitor := monitor.NewMonitor(logger, client)
// Create pool discovery service
poolDiscovery, err := pools.NewPoolDiscovery(client, logger, config.PoolDiscoveryConfig)
if err != nil {
cancel()
return nil, fmt.Errorf("failed to create pool discovery: %w", err)
}
// Initialize stats
stats := &ArbitrageStats{
TotalProfitRealized: big.NewInt(0),
TotalGasSpent: big.NewInt(0),
}
service := &ArbitrageService{
client: client,
logger: logger,
config: config,
marketManager: marketManager,
multiHopScanner: multiHopScanner,
executor: executor,
eventMonitor: eventMonitor,
poolDiscovery: poolDiscovery,
eventsChan: make(chan *events.SwapEvent, 1000),
arbitrageChan: make(chan *ArbitrageOpportunity, 100),
resultsChan: make(chan *ExecutionResult, 100),
ctx: ctx,
cancel: cancel,
stats: stats,
maxConcurrentExecutions: config.MaxConcurrentExecutions,
executionSemaphore: make(chan struct{}, config.MaxConcurrentExecutions),
database: database,
}
return service, nil
}
// Start begins the arbitrage service
func (as *ArbitrageService) Start() error {
as.runMutex.Lock()
defer as.runMutex.Unlock()
if as.isRunning {
return fmt.Errorf("arbitrage service is already running")
}
as.logger.Info("Starting arbitrage service...")
// Start pool discovery
if err := as.poolDiscovery.Start(as.ctx); err != nil {
return fmt.Errorf("failed to start pool discovery: %w", err)
}
// Start market manager
if err := as.marketManager.Start(as.ctx); err != nil {
return fmt.Errorf("failed to start market manager: %w", err)
}
// Start event monitor
if err := as.eventMonitor.Start(as.ctx); err != nil {
return fmt.Errorf("failed to start event monitor: %w", err)
}
// Start worker goroutines
go as.eventProcessor()
go as.arbitrageDetector()
go as.arbitrageExecutor()
go as.resultProcessor()
go as.poolDataProcessor()
go as.statsUpdater()
as.isRunning = true
as.logger.Info("Arbitrage service started successfully")
return nil
}
// Stop stops the arbitrage service
func (as *ArbitrageService) Stop() error {
as.runMutex.Lock()
defer as.runMutex.Unlock()
if !as.isRunning {
return nil
}
as.logger.Info("Stopping arbitrage service...")
// Cancel context to stop all workers
as.cancel()
// Stop components
as.eventMonitor.Stop()
as.marketManager.Stop()
as.poolDiscovery.Stop()
// Close channels
close(as.eventsChan)
close(as.arbitrageChan)
close(as.resultsChan)
as.isRunning = false
as.logger.Info("Arbitrage service stopped")
return nil
}
// eventProcessor processes incoming swap events
func (as *ArbitrageService) eventProcessor() {
defer as.logger.Info("Event processor stopped")
// Subscribe to swap events from the event monitor
swapEvents := as.eventMonitor.SubscribeToSwapEvents()
for {
select {
case <-as.ctx.Done():
return
case event := <-swapEvents:
if event != nil {
as.processSwapEvent(event)
}
}
}
}
// processSwapEvent processes a single swap event for arbitrage opportunities
func (as *ArbitrageService) processSwapEvent(event *events.SwapEvent) {
as.logger.Debug(fmt.Sprintf("Processing swap event: token0=%s, token1=%s, amount0=%s, amount1=%s",
event.Token0.Hex(), event.Token1.Hex(), event.Amount0.String(), event.Amount1.String()))
// Check if this swap is large enough to potentially move prices
if !as.isSignificantSwap(event) {
return
}
// Forward to arbitrage detection
select {
case as.eventsChan <- event:
case <-as.ctx.Done():
return
default:
as.logger.Warn("Event channel full, dropping swap event")
}
}
// isSignificantSwap checks if a swap is large enough to create arbitrage opportunities
func (as *ArbitrageService) isSignificantSwap(event *events.SwapEvent) bool {
// Convert amounts to absolute values for comparison
amount0Abs := new(big.Int).Abs(event.Amount0)
amount1Abs := new(big.Int).Abs(event.Amount1)
// Check if either amount is above our threshold
minSwapSize := big.NewInt(as.config.MinSignificantSwapSize)
return amount0Abs.Cmp(minSwapSize) > 0 || amount1Abs.Cmp(minSwapSize) > 0
}
// arbitrageDetector detects arbitrage opportunities from swap events
func (as *ArbitrageService) arbitrageDetector() {
defer as.logger.Info("Arbitrage detector stopped")
for {
select {
case <-as.ctx.Done():
return
case event := <-as.eventsChan:
if event != nil {
as.detectArbitrageOpportunities(event)
}
}
}
}
// detectArbitrageOpportunities scans for arbitrage opportunities triggered by an event
func (as *ArbitrageService) detectArbitrageOpportunities(event *events.SwapEvent) {
start := time.Now()
// Determine the tokens involved in potential arbitrage
tokens := []common.Address{event.Token0, event.Token1}
var allOpportunities []*ArbitrageOpportunity
// Scan for opportunities starting with each token
for _, token := range tokens {
// Determine appropriate amount to use for scanning
scanAmount := as.calculateScanAmount(event, token)
// Use multi-hop scanner to find arbitrage paths
paths, err := as.multiHopScanner.ScanForArbitrage(as.ctx, token, scanAmount)
if err != nil {
as.logger.Debug(fmt.Sprintf("Arbitrage scan failed for token %s: %v", token.Hex(), err))
continue
}
// Convert paths to opportunities
for _, path := range paths {
if as.isValidOpportunity(path) {
opportunity := &ArbitrageOpportunity{
ID: as.generateOpportunityID(path, event),
Path: path,
TriggerEvent: event,
DetectedAt: time.Now(),
EstimatedProfit: path.NetProfit,
RequiredAmount: scanAmount,
Urgency: as.calculateUrgency(path),
ExpiresAt: time.Now().Add(as.config.OpportunityTTL),
}
allOpportunities = append(allOpportunities, opportunity)
}
}
}
// Sort opportunities by urgency and profit
as.rankOpportunities(allOpportunities)
// Send top opportunities for execution
maxOpportunities := as.config.MaxOpportunitiesPerEvent
for i, opportunity := range allOpportunities {
if i >= maxOpportunities {
break
}
// Update stats
as.statsMutex.Lock()
as.stats.TotalOpportunitiesDetected++
as.statsMutex.Unlock()
// Save to database
if err := as.database.SaveOpportunity(as.ctx, opportunity); err != nil {
as.logger.Warn(fmt.Sprintf("Failed to save opportunity to database: %v", err))
}
// Send for execution
select {
case as.arbitrageChan <- opportunity:
case <-as.ctx.Done():
return
default:
as.logger.Warn("Arbitrage channel full, dropping opportunity")
}
}
elapsed := time.Since(start)
as.logger.Debug(fmt.Sprintf("Arbitrage detection completed in %v: found %d opportunities",
elapsed, len(allOpportunities)))
}
// isValidOpportunity validates an arbitrage opportunity
func (as *ArbitrageService) isValidOpportunity(path *ArbitragePath) bool {
// Check minimum profit threshold
minProfit := big.NewInt(as.config.MinProfitWei)
if path.NetProfit.Cmp(minProfit) < 0 {
return false
}
// Check minimum ROI
if path.ROI < as.config.MinROIPercent {
return false
}
// Check path freshness
if time.Since(path.LastUpdated) > as.config.MaxPathAge {
return false
}
// Check gas profitability
currentGasPrice, err := as.client.SuggestGasPrice(as.ctx)
if err != nil {
// Assume worst case if we can't get gas price
currentGasPrice = big.NewInt(as.config.MaxGasPriceWei)
}
return as.executor.IsProfitableAfterGas(path, currentGasPrice)
}
// calculateScanAmount determines the optimal amount to use for arbitrage scanning
func (as *ArbitrageService) calculateScanAmount(event *events.SwapEvent, token common.Address) *big.Int {
// Base amount on the swap size, but cap it for risk management
var swapAmount *big.Int
if token == event.Token0 {
swapAmount = new(big.Int).Abs(event.Amount0)
} else {
swapAmount = new(big.Int).Abs(event.Amount1)
}
// Use a fraction of the swap amount (default 10%)
scanAmount := new(big.Int).Div(swapAmount, big.NewInt(10))
// Ensure minimum scan amount
minAmount := big.NewInt(as.config.MinScanAmountWei)
if scanAmount.Cmp(minAmount) < 0 {
scanAmount = minAmount
}
// Cap maximum scan amount
maxAmount := big.NewInt(as.config.MaxScanAmountWei)
if scanAmount.Cmp(maxAmount) > 0 {
scanAmount = maxAmount
}
return scanAmount
}
// calculateUrgency calculates the urgency level of an opportunity
func (as *ArbitrageService) calculateUrgency(path *ArbitragePath) int {
// Base urgency on ROI
urgency := int(path.ROI / 2) // 2% ROI = urgency level 1
// Boost urgency for higher profits
profitETH := new(big.Float).SetInt(path.NetProfit)
profitETH.Quo(profitETH, big.NewFloat(1e18))
profitFloat, _ := profitETH.Float64()
if profitFloat > 1.0 { // > 1 ETH profit
urgency += 5
} else if profitFloat > 0.1 { // > 0.1 ETH profit
urgency += 2
}
// Cap urgency between 1 and 10
if urgency < 1 {
urgency = 1
}
if urgency > 10 {
urgency = 10
}
return urgency
}
// rankOpportunities sorts opportunities by priority
func (as *ArbitrageService) rankOpportunities(opportunities []*ArbitrageOpportunity) {
// Sort by urgency (descending) then by profit (descending)
for i := 0; i < len(opportunities); i++ {
for j := i + 1; j < len(opportunities); j++ {
iOpp := opportunities[i]
jOpp := opportunities[j]
if jOpp.Urgency > iOpp.Urgency {
opportunities[i], opportunities[j] = opportunities[j], opportunities[i]
} else if jOpp.Urgency == iOpp.Urgency {
if jOpp.EstimatedProfit.Cmp(iOpp.EstimatedProfit) > 0 {
opportunities[i], opportunities[j] = opportunities[j], opportunities[i]
}
}
}
}
}
// arbitrageExecutor executes arbitrage opportunities
func (as *ArbitrageService) arbitrageExecutor() {
defer as.logger.Info("Arbitrage executor stopped")
for {
select {
case <-as.ctx.Done():
return
case opportunity := <-as.arbitrageChan:
if opportunity != nil {
// Rate limit concurrent executions
select {
case as.executionSemaphore <- struct{}{}:
go as.executeOpportunity(opportunity)
case <-as.ctx.Done():
return
default:
as.logger.Warn("Too many concurrent executions, dropping opportunity")
}
}
}
}
}
// executeOpportunity executes a single arbitrage opportunity
func (as *ArbitrageService) executeOpportunity(opportunity *ArbitrageOpportunity) {
defer func() {
<-as.executionSemaphore // Release semaphore
}()
// Check if opportunity is still valid
if time.Now().After(opportunity.ExpiresAt) {
as.logger.Debug(fmt.Sprintf("Opportunity %s expired", opportunity.ID))
return
}
// Update stats
as.statsMutex.Lock()
as.stats.TotalOpportunitiesExecuted++
as.statsMutex.Unlock()
// Prepare execution parameters
params := &ArbitrageParams{
Path: opportunity.Path,
InputAmount: opportunity.RequiredAmount,
MinOutputAmount: as.calculateMinOutput(opportunity),
Deadline: big.NewInt(time.Now().Add(5 * time.Minute).Unix()),
FlashSwapData: []byte{}, // Additional data if needed
}
as.logger.Info(fmt.Sprintf("Executing arbitrage opportunity %s with estimated profit %s ETH",
opportunity.ID, formatEther(opportunity.EstimatedProfit)))
// Execute the arbitrage
result, err := as.executor.ExecuteArbitrage(as.ctx, params)
if err != nil {
as.logger.Error(fmt.Sprintf("Arbitrage execution failed for opportunity %s: %v",
opportunity.ID, err))
}
// Send result for processing
select {
case as.resultsChan <- result:
case <-as.ctx.Done():
return
default:
as.logger.Warn("Results channel full, dropping execution result")
}
}
// calculateMinOutput calculates minimum acceptable output for slippage protection
func (as *ArbitrageService) calculateMinOutput(opportunity *ArbitrageOpportunity) *big.Int {
// Calculate expected output
expectedOutput := new(big.Int).Add(opportunity.RequiredAmount, opportunity.EstimatedProfit)
// Apply slippage tolerance
slippageTolerance := as.config.SlippageTolerance
slippageMultiplier := big.NewFloat(1.0 - slippageTolerance)
expectedFloat := new(big.Float).SetInt(expectedOutput)
minOutputFloat := new(big.Float).Mul(expectedFloat, slippageMultiplier)
minOutput := new(big.Int)
minOutputFloat.Int(minOutput)
return minOutput
}
// resultProcessor processes execution results
func (as *ArbitrageService) resultProcessor() {
defer as.logger.Info("Result processor stopped")
for {
select {
case <-as.ctx.Done():
return
case result := <-as.resultsChan:
if result != nil {
as.processExecutionResult(result)
}
}
}
}
// processExecutionResult processes a single execution result
func (as *ArbitrageService) processExecutionResult(result *ExecutionResult) {
// Update statistics
as.statsMutex.Lock()
if result.Success {
as.stats.TotalSuccessfulExecutions++
as.stats.TotalProfitRealized.Add(as.stats.TotalProfitRealized, result.ProfitRealized)
}
gasCost := new(big.Int).Mul(result.GasPrice, big.NewInt(int64(result.GasUsed)))
as.stats.TotalGasSpent.Add(as.stats.TotalGasSpent, gasCost)
as.stats.LastExecutionTime = time.Now()
as.statsMutex.Unlock()
// Save to database
if err := as.database.SaveExecution(as.ctx, result); err != nil {
as.logger.Warn(fmt.Sprintf("Failed to save execution result to database: %v", err))
}
// Log results
if result.Success {
as.logger.Info(fmt.Sprintf("Arbitrage execution successful: TX %s, Profit: %s ETH, Gas: %d",
result.TransactionHash.Hex(), formatEther(result.ProfitRealized), result.GasUsed))
} else {
as.logger.Error(fmt.Sprintf("Arbitrage execution failed: TX %s, Error: %v",
result.TransactionHash.Hex(), result.Error))
}
}
// poolDataProcessor handles pool data updates and saves them to database
func (as *ArbitrageService) poolDataProcessor() {
defer as.logger.Info("Pool data processor stopped")
// Subscribe to pool discovery events
poolEvents := as.poolDiscovery.SubscribeToPoolEvents()
for {
select {
case <-as.ctx.Done():
return
case poolData := <-poolEvents:
if poolData != nil {
// Save pool data to database
if err := as.database.SavePoolData(as.ctx, poolData); err != nil {
as.logger.Warn(fmt.Sprintf("Failed to save pool data: %v", err))
}
// Update market manager with new pool
as.marketManager.AddPool(poolData)
as.logger.Debug(fmt.Sprintf("Added new pool: %s (%s/%s)",
poolData.Address.Hex(), poolData.Token0.Hex(), poolData.Token1.Hex()))
}
}
}
}
// statsUpdater periodically logs service statistics
func (as *ArbitrageService) statsUpdater() {
defer as.logger.Info("Stats updater stopped")
ticker := time.NewTicker(as.config.StatsUpdateInterval)
defer ticker.Stop()
for {
select {
case <-as.ctx.Done():
return
case <-ticker.C:
as.logStats()
}
}
}
// logStats logs current service statistics
func (as *ArbitrageService) logStats() {
as.statsMutex.RLock()
stats := *as.stats
as.statsMutex.RUnlock()
successRate := 0.0
if stats.TotalOpportunitiesExecuted > 0 {
successRate = float64(stats.TotalSuccessfulExecutions) / float64(stats.TotalOpportunitiesExecuted) * 100
}
as.logger.Info(fmt.Sprintf("Arbitrage Service Stats - Detected: %d, Executed: %d, Success Rate: %.2f%%, "+
"Total Profit: %s ETH, Total Gas: %s ETH",
stats.TotalOpportunitiesDetected,
stats.TotalOpportunitiesExecuted,
successRate,
formatEther(stats.TotalProfitRealized),
formatEther(stats.TotalGasSpent)))
}
// generateOpportunityID generates a unique ID for an arbitrage opportunity
func (as *ArbitrageService) generateOpportunityID(path *ArbitragePath, event *events.SwapEvent) string {
return fmt.Sprintf("%s_%s_%d", event.TxHash.Hex()[:10], path.Tokens[0].Hex()[:8], time.Now().UnixNano())
}
// GetStats returns current service statistics
func (as *ArbitrageService) GetStats() *ArbitrageStats {
as.statsMutex.RLock()
defer as.statsMutex.RUnlock()
// Return a copy to avoid race conditions
statsCopy := *as.stats
return &statsCopy
}
// IsRunning returns whether the service is currently running
func (as *ArbitrageService) IsRunning() bool {
as.runMutex.RLock()
defer as.runMutex.RUnlock()
return as.isRunning
}

View File

@@ -0,0 +1,831 @@
package arbitrage
import (
"context"
"fmt"
"math/big"
"sync"
"time"
"github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/fraktal/mev-beta/internal/config"
"github.com/fraktal/mev-beta/internal/logger"
"github.com/fraktal/mev-beta/pkg/security"
)
// TokenPair represents the two tokens in a pool
type TokenPair struct {
Token0 common.Address
Token1 common.Address
}
// ArbitrageOpportunity represents a detected arbitrage opportunity
type ArbitrageOpportunity struct {
ID string
Path *ArbitragePath
TriggerEvent *SimpleSwapEvent
DetectedAt time.Time
EstimatedProfit *big.Int
RequiredAmount *big.Int
Urgency int // 1-10 priority level
ExpiresAt time.Time
}
// ArbitrageStats contains service statistics
type ArbitrageStats struct {
TotalOpportunitiesDetected int64
TotalOpportunitiesExecuted int64
TotalSuccessfulExecutions int64
TotalProfitRealized *big.Int
TotalGasSpent *big.Int
AverageExecutionTime time.Duration
LastExecutionTime time.Time
}
// ArbitrageDatabase interface for persistence
type ArbitrageDatabase interface {
SaveOpportunity(ctx context.Context, opportunity *ArbitrageOpportunity) error
SaveExecution(ctx context.Context, result *ExecutionResult) error
GetExecutionHistory(ctx context.Context, limit int) ([]*ExecutionResult, error)
SavePoolData(ctx context.Context, poolData *SimplePoolData) error
GetPoolData(ctx context.Context, poolAddress common.Address) (*SimplePoolData, error)
}
// SimpleArbitrageService is a simplified arbitrage service without circular dependencies
type SimpleArbitrageService struct {
client *ethclient.Client
logger *logger.Logger
config *config.ArbitrageConfig
// Core components
multiHopScanner *MultiHopScanner
executor *ArbitrageExecutor
// Token cache for pool addresses
tokenCache map[common.Address]TokenPair
tokenCacheMutex sync.RWMutex
// State management
isRunning bool
runMutex sync.RWMutex
ctx context.Context
cancel context.CancelFunc
// Metrics and monitoring
stats *ArbitrageStats
statsMutex sync.RWMutex
// Database integration
database ArbitrageDatabase
}
// SimpleSwapEvent represents a swap event for arbitrage detection
type SimpleSwapEvent struct {
TxHash common.Hash
PoolAddress common.Address
Token0 common.Address
Token1 common.Address
Amount0 *big.Int
Amount1 *big.Int
SqrtPriceX96 *big.Int
Liquidity *big.Int
Tick int32
BlockNumber uint64
LogIndex uint
Timestamp time.Time
}
// SimplePoolData represents basic pool information
type SimplePoolData struct {
Address common.Address
Token0 common.Address
Token1 common.Address
Fee int64
Liquidity *big.Int
SqrtPriceX96 *big.Int
Tick int32
BlockNumber uint64
TxHash common.Hash
LogIndex uint
LastUpdated time.Time
}
// NewSimpleArbitrageService creates a new simplified arbitrage service
func NewSimpleArbitrageService(
client *ethclient.Client,
logger *logger.Logger,
config *config.ArbitrageConfig,
keyManager *security.KeyManager,
database ArbitrageDatabase,
) (*SimpleArbitrageService, error) {
ctx, cancel := context.WithCancel(context.Background())
// Create multi-hop scanner with simple market manager
multiHopScanner := NewMultiHopScanner(logger, nil)
// Create arbitrage executor
executor, err := NewArbitrageExecutor(
client,
logger,
keyManager,
common.HexToAddress(config.ArbitrageContractAddress),
common.HexToAddress(config.FlashSwapContractAddress),
)
if err != nil {
cancel()
return nil, fmt.Errorf("failed to create arbitrage executor: %w", err)
}
// Initialize stats
stats := &ArbitrageStats{
TotalProfitRealized: big.NewInt(0),
TotalGasSpent: big.NewInt(0),
}
service := &SimpleArbitrageService{
client: client,
logger: logger,
config: config,
multiHopScanner: multiHopScanner,
executor: executor,
ctx: ctx,
cancel: cancel,
stats: stats,
database: database,
tokenCache: make(map[common.Address]TokenPair),
}
return service, nil
}
// Start begins the simplified arbitrage service
func (sas *SimpleArbitrageService) Start() error {
sas.runMutex.Lock()
defer sas.runMutex.Unlock()
if sas.isRunning {
return fmt.Errorf("arbitrage service is already running")
}
sas.logger.Info("Starting simplified arbitrage service...")
// Start worker goroutines
go sas.statsUpdater()
go sas.blockchainMonitor()
sas.isRunning = true
sas.logger.Info("Simplified arbitrage service started successfully")
return nil
}
// Stop stops the arbitrage service
func (sas *SimpleArbitrageService) Stop() error {
sas.runMutex.Lock()
defer sas.runMutex.Unlock()
if !sas.isRunning {
return nil
}
sas.logger.Info("Stopping simplified arbitrage service...")
// Cancel context to stop all workers
sas.cancel()
sas.isRunning = false
sas.logger.Info("Simplified arbitrage service stopped")
return nil
}
// ProcessSwapEvent processes a swap event for arbitrage opportunities
func (sas *SimpleArbitrageService) ProcessSwapEvent(event *SimpleSwapEvent) error {
sas.logger.Debug(fmt.Sprintf("Processing swap event: token0=%s, token1=%s, amount0=%s, amount1=%s",
event.Token0.Hex(), event.Token1.Hex(), event.Amount0.String(), event.Amount1.String()))
// Check if this swap is large enough to potentially move prices
if !sas.isSignificantSwap(event) {
return nil
}
// Scan for arbitrage opportunities
return sas.detectArbitrageOpportunities(event)
}
// isSignificantSwap checks if a swap is large enough to create arbitrage opportunities
func (sas *SimpleArbitrageService) isSignificantSwap(event *SimpleSwapEvent) bool {
// Convert amounts to absolute values for comparison
amount0Abs := new(big.Int).Abs(event.Amount0)
amount1Abs := new(big.Int).Abs(event.Amount1)
// Check if either amount is above our threshold
minSwapSize := big.NewInt(sas.config.MinSignificantSwapSize)
return amount0Abs.Cmp(minSwapSize) > 0 || amount1Abs.Cmp(minSwapSize) > 0
}
// detectArbitrageOpportunities scans for arbitrage opportunities triggered by an event
func (sas *SimpleArbitrageService) detectArbitrageOpportunities(event *SimpleSwapEvent) error {
start := time.Now()
// Determine the tokens involved in potential arbitrage
tokens := []common.Address{event.Token0, event.Token1}
var allOpportunities []*ArbitrageOpportunity
// Scan for opportunities starting with each token
for _, token := range tokens {
// Determine appropriate amount to use for scanning
scanAmount := sas.calculateScanAmount(event, token)
// Use multi-hop scanner to find arbitrage paths
paths, err := sas.multiHopScanner.ScanForArbitrage(sas.ctx, token, scanAmount)
if err != nil {
sas.logger.Debug(fmt.Sprintf("Arbitrage scan failed for token %s: %v", token.Hex(), err))
continue
}
// Convert paths to opportunities
for _, path := range paths {
if sas.isValidOpportunity(path) {
opportunity := &ArbitrageOpportunity{
ID: sas.generateOpportunityID(path, event),
Path: path,
DetectedAt: time.Now(),
EstimatedProfit: path.NetProfit,
RequiredAmount: scanAmount,
Urgency: sas.calculateUrgency(path),
ExpiresAt: time.Now().Add(sas.config.OpportunityTTL),
}
allOpportunities = append(allOpportunities, opportunity)
}
}
}
// Sort opportunities by urgency and profit
sas.rankOpportunities(allOpportunities)
// Process top opportunities
maxOpportunities := sas.config.MaxOpportunitiesPerEvent
for i, opportunity := range allOpportunities {
if i >= maxOpportunities {
break
}
// Update stats
sas.statsMutex.Lock()
sas.stats.TotalOpportunitiesDetected++
sas.statsMutex.Unlock()
// Save to database
if err := sas.database.SaveOpportunity(sas.ctx, opportunity); err != nil {
sas.logger.Warn(fmt.Sprintf("Failed to save opportunity to database: %v", err))
}
// Execute if execution is enabled
if sas.config.MaxConcurrentExecutions > 0 {
go sas.executeOpportunity(opportunity)
}
}
elapsed := time.Since(start)
sas.logger.Debug(fmt.Sprintf("Arbitrage detection completed in %v: found %d opportunities",
elapsed, len(allOpportunities)))
return nil
}
// executeOpportunity executes a single arbitrage opportunity
func (sas *SimpleArbitrageService) executeOpportunity(opportunity *ArbitrageOpportunity) {
// Check if opportunity is still valid
if time.Now().After(opportunity.ExpiresAt) {
sas.logger.Debug(fmt.Sprintf("Opportunity %s expired", opportunity.ID))
return
}
// Update stats
sas.statsMutex.Lock()
sas.stats.TotalOpportunitiesExecuted++
sas.statsMutex.Unlock()
// Prepare execution parameters
params := &ArbitrageParams{
Path: opportunity.Path,
InputAmount: opportunity.RequiredAmount,
MinOutputAmount: sas.calculateMinOutput(opportunity),
Deadline: big.NewInt(time.Now().Add(5 * time.Minute).Unix()),
FlashSwapData: []byte{}, // Additional data if needed
}
sas.logger.Info(fmt.Sprintf("Executing arbitrage opportunity %s with estimated profit %s ETH",
opportunity.ID, formatEther(opportunity.EstimatedProfit)))
// Execute the arbitrage
result, err := sas.executor.ExecuteArbitrage(sas.ctx, params)
if err != nil {
sas.logger.Error(fmt.Sprintf("Arbitrage execution failed for opportunity %s: %v",
opportunity.ID, err))
return
}
// Process execution results
sas.processExecutionResult(result)
}
// Helper methods from the original service
func (sas *SimpleArbitrageService) isValidOpportunity(path *ArbitragePath) bool {
minProfit := big.NewInt(sas.config.MinProfitWei)
if path.NetProfit.Cmp(minProfit) < 0 {
return false
}
if path.ROI < sas.config.MinROIPercent {
return false
}
if time.Since(path.LastUpdated) > sas.config.MaxPathAge {
return false
}
currentGasPrice, err := sas.client.SuggestGasPrice(sas.ctx)
if err != nil {
currentGasPrice = big.NewInt(sas.config.MaxGasPriceWei)
}
return sas.executor.IsProfitableAfterGas(path, currentGasPrice)
}
func (sas *SimpleArbitrageService) calculateScanAmount(event *SimpleSwapEvent, token common.Address) *big.Int {
var swapAmount *big.Int
if token == event.Token0 {
swapAmount = new(big.Int).Abs(event.Amount0)
} else {
swapAmount = new(big.Int).Abs(event.Amount1)
}
scanAmount := new(big.Int).Div(swapAmount, big.NewInt(10))
minAmount := big.NewInt(sas.config.MinScanAmountWei)
if scanAmount.Cmp(minAmount) < 0 {
scanAmount = minAmount
}
maxAmount := big.NewInt(sas.config.MaxScanAmountWei)
if scanAmount.Cmp(maxAmount) > 0 {
scanAmount = maxAmount
}
return scanAmount
}
func (sas *SimpleArbitrageService) calculateUrgency(path *ArbitragePath) int {
urgency := int(path.ROI / 2)
profitETH := new(big.Float).SetInt(path.NetProfit)
profitETH.Quo(profitETH, big.NewFloat(1e18))
profitFloat, _ := profitETH.Float64()
if profitFloat > 1.0 {
urgency += 5
} else if profitFloat > 0.1 {
urgency += 2
}
if urgency < 1 {
urgency = 1
}
if urgency > 10 {
urgency = 10
}
return urgency
}
func (sas *SimpleArbitrageService) rankOpportunities(opportunities []*ArbitrageOpportunity) {
for i := 0; i < len(opportunities); i++ {
for j := i + 1; j < len(opportunities); j++ {
iOpp := opportunities[i]
jOpp := opportunities[j]
if jOpp.Urgency > iOpp.Urgency {
opportunities[i], opportunities[j] = opportunities[j], opportunities[i]
} else if jOpp.Urgency == iOpp.Urgency {
if jOpp.EstimatedProfit.Cmp(iOpp.EstimatedProfit) > 0 {
opportunities[i], opportunities[j] = opportunities[j], opportunities[i]
}
}
}
}
}
func (sas *SimpleArbitrageService) calculateMinOutput(opportunity *ArbitrageOpportunity) *big.Int {
expectedOutput := new(big.Int).Add(opportunity.RequiredAmount, opportunity.EstimatedProfit)
slippageTolerance := sas.config.SlippageTolerance
slippageMultiplier := big.NewFloat(1.0 - slippageTolerance)
expectedFloat := new(big.Float).SetInt(expectedOutput)
minOutputFloat := new(big.Float).Mul(expectedFloat, slippageMultiplier)
minOutput := new(big.Int)
minOutputFloat.Int(minOutput)
return minOutput
}
func (sas *SimpleArbitrageService) processExecutionResult(result *ExecutionResult) {
sas.statsMutex.Lock()
if result.Success {
sas.stats.TotalSuccessfulExecutions++
sas.stats.TotalProfitRealized.Add(sas.stats.TotalProfitRealized, result.ProfitRealized)
}
gasCost := new(big.Int).Mul(result.GasPrice, big.NewInt(int64(result.GasUsed)))
sas.stats.TotalGasSpent.Add(sas.stats.TotalGasSpent, gasCost)
sas.stats.LastExecutionTime = time.Now()
sas.statsMutex.Unlock()
if err := sas.database.SaveExecution(sas.ctx, result); err != nil {
sas.logger.Warn(fmt.Sprintf("Failed to save execution result to database: %v", err))
}
if result.Success {
sas.logger.Info(fmt.Sprintf("Arbitrage execution successful: TX %s, Profit: %s ETH, Gas: %d",
result.TransactionHash.Hex(), formatEther(result.ProfitRealized), result.GasUsed))
} else {
sas.logger.Error(fmt.Sprintf("Arbitrage execution failed: TX %s, Error: %v",
result.TransactionHash.Hex(), result.Error))
}
}
func (sas *SimpleArbitrageService) statsUpdater() {
defer sas.logger.Info("Stats updater stopped")
ticker := time.NewTicker(sas.config.StatsUpdateInterval)
defer ticker.Stop()
for {
select {
case <-sas.ctx.Done():
return
case <-ticker.C:
sas.logStats()
}
}
}
func (sas *SimpleArbitrageService) logStats() {
sas.statsMutex.RLock()
stats := *sas.stats
sas.statsMutex.RUnlock()
successRate := 0.0
if stats.TotalOpportunitiesExecuted > 0 {
successRate = float64(stats.TotalSuccessfulExecutions) / float64(stats.TotalOpportunitiesExecuted) * 100
}
sas.logger.Info(fmt.Sprintf("Arbitrage Service Stats - Detected: %d, Executed: %d, Success Rate: %.2f%%, "+
"Total Profit: %s ETH, Total Gas: %s ETH",
stats.TotalOpportunitiesDetected,
stats.TotalOpportunitiesExecuted,
successRate,
formatEther(stats.TotalProfitRealized),
formatEther(stats.TotalGasSpent)))
}
func (sas *SimpleArbitrageService) generateOpportunityID(path *ArbitragePath, event *SimpleSwapEvent) string {
return fmt.Sprintf("%s_%s_%d", event.TxHash.Hex()[:10], path.Tokens[0].Hex()[:8], time.Now().UnixNano())
}
func (sas *SimpleArbitrageService) GetStats() *ArbitrageStats {
sas.statsMutex.RLock()
defer sas.statsMutex.RUnlock()
statsCopy := *sas.stats
return &statsCopy
}
func (sas *SimpleArbitrageService) IsRunning() bool {
sas.runMutex.RLock()
defer sas.runMutex.RUnlock()
return sas.isRunning
}
// blockchainMonitor monitors the Arbitrum sequencer using the proper ArbitrumMonitor
func (sas *SimpleArbitrageService) blockchainMonitor() {
defer sas.logger.Info("Arbitrum sequencer monitor stopped")
sas.logger.Info("Starting Arbitrum sequencer monitor for MEV opportunities...")
sas.logger.Info("Initializing Arbitrum L2 parser for transaction analysis...")
// Create the proper Arbitrum monitor with sequencer reader
monitor, err := sas.createArbitrumMonitor()
if err != nil {
sas.logger.Error(fmt.Sprintf("Failed to create Arbitrum monitor: %v", err))
// Fallback to basic block monitoring
sas.fallbackBlockPolling()
return
}
sas.logger.Info("Arbitrum sequencer monitor created successfully")
sas.logger.Info("Starting to monitor Arbitrum sequencer feed for transactions...")
// Start the monitor
if err := monitor.Start(sas.ctx); err != nil {
sas.logger.Error(fmt.Sprintf("Failed to start Arbitrum monitor: %v", err))
sas.fallbackBlockPolling()
return
}
sas.logger.Info("Arbitrum sequencer monitoring started - processing live transactions")
// Keep the monitor running
<-sas.ctx.Done()
sas.logger.Info("Stopping Arbitrum sequencer monitor...")
}
// fallbackBlockPolling provides fallback block monitoring through polling
func (sas *SimpleArbitrageService) fallbackBlockPolling() {
sas.logger.Info("Using fallback block polling...")
ticker := time.NewTicker(3 * time.Second) // Poll every 3 seconds
defer ticker.Stop()
var lastBlock uint64
for {
select {
case <-sas.ctx.Done():
return
case <-ticker.C:
header, err := sas.client.HeaderByNumber(sas.ctx, nil)
if err != nil {
sas.logger.Debug(fmt.Sprintf("Failed to get latest block: %v", err))
continue
}
if header.Number.Uint64() > lastBlock {
lastBlock = header.Number.Uint64()
sas.processNewBlock(header)
}
}
}
}
// processNewBlock processes a new block looking for swap events
func (sas *SimpleArbitrageService) processNewBlock(header *types.Header) {
blockNumber := header.Number.Uint64()
// Skip processing if block has no transactions
if header.TxHash == (common.Hash{}) {
return
}
sas.logger.Info(fmt.Sprintf("Processing block %d for Uniswap V3 swap events", blockNumber))
// Instead of getting full block (which fails with unsupported tx types),
// we'll scan the block's logs directly for Uniswap V3 Swap events
swapEvents := sas.getSwapEventsFromBlock(blockNumber)
if len(swapEvents) > 0 {
sas.logger.Info(fmt.Sprintf("Found %d swap events in block %d", len(swapEvents), blockNumber))
// Process each swap event
for _, event := range swapEvents {
go func(e *SimpleSwapEvent) {
if err := sas.ProcessSwapEvent(e); err != nil {
sas.logger.Debug(fmt.Sprintf("Failed to process swap event: %v", err))
}
}(event)
}
}
}
// processTransaction analyzes a transaction for swap events
func (sas *SimpleArbitrageService) processTransaction(tx *types.Transaction, blockNumber uint64) bool {
// Get transaction receipt to access logs
receipt, err := sas.client.TransactionReceipt(sas.ctx, tx.Hash())
if err != nil {
return false // Skip if we can't get receipt
}
swapFound := false
// Look for Uniswap V3 Swap events
for _, log := range receipt.Logs {
event := sas.parseSwapLog(log, tx, blockNumber)
if event != nil {
swapFound = true
sas.logger.Info(fmt.Sprintf("Found swap event: %s/%s, amounts: %s/%s",
event.Token0.Hex()[:10], event.Token1.Hex()[:10],
event.Amount0.String(), event.Amount1.String()))
// Process the swap event asynchronously to avoid blocking
go func(e *SimpleSwapEvent) {
if err := sas.ProcessSwapEvent(e); err != nil {
sas.logger.Debug(fmt.Sprintf("Failed to process swap event: %v", err))
}
}(event)
}
}
return swapFound
}
// parseSwapLog attempts to parse a log as a Uniswap V3 Swap event
func (sas *SimpleArbitrageService) parseSwapLog(log *types.Log, tx *types.Transaction, blockNumber uint64) *SimpleSwapEvent {
// Uniswap V3 Pool Swap event signature
// Swap(indexed address sender, indexed address recipient, int256 amount0, int256 amount1, uint160 sqrtPriceX96, uint128 liquidity, int24 tick)
swapEventSig := common.HexToHash("0xc42079f94a6350d7e6235f29174924f928cc2ac818eb64fed8004e115fbcca67")
if len(log.Topics) == 0 || log.Topics[0] != swapEventSig {
return nil
}
// Parse the event data
if len(log.Topics) < 3 || len(log.Data) < 192 { // 6 * 32 bytes
return nil
}
// Extract indexed parameters (sender, recipient)
// sender := common.BytesToAddress(log.Topics[1].Bytes())
// recipient := common.BytesToAddress(log.Topics[2].Bytes())
// Extract non-indexed parameters from data
amount0 := new(big.Int).SetBytes(log.Data[0:32])
amount1 := new(big.Int).SetBytes(log.Data[32:64])
sqrtPriceX96 := new(big.Int).SetBytes(log.Data[64:96])
liquidity := new(big.Int).SetBytes(log.Data[96:128])
// Extract tick (int24, but stored as int256)
tickBytes := log.Data[128:160]
tick := new(big.Int).SetBytes(tickBytes)
if tick.Bit(255) == 1 { // Check if negative (two's complement)
tick.Sub(tick, new(big.Int).Lsh(big.NewInt(1), 256))
}
// Get pool tokens by querying the actual pool contract
token0, token1, err := sas.getPoolTokens(log.Address)
if err != nil {
return nil // Skip if we can't get pool tokens
}
return &SimpleSwapEvent{
TxHash: tx.Hash(),
PoolAddress: log.Address,
Token0: token0,
Token1: token1,
Amount0: amount0,
Amount1: amount1,
SqrtPriceX96: sqrtPriceX96,
Liquidity: liquidity,
Tick: int32(tick.Int64()),
BlockNumber: blockNumber,
LogIndex: log.Index,
Timestamp: time.Now(),
}
}
// getPoolTokens retrieves token addresses for a Uniswap V3 pool with caching
func (sas *SimpleArbitrageService) getPoolTokens(poolAddress common.Address) (token0, token1 common.Address, err error) {
// Check cache first
sas.tokenCacheMutex.RLock()
if cached, exists := sas.tokenCache[poolAddress]; exists {
sas.tokenCacheMutex.RUnlock()
return cached.Token0, cached.Token1, nil
}
sas.tokenCacheMutex.RUnlock()
// Create timeout context for contract calls
ctx, cancel := context.WithTimeout(sas.ctx, 5*time.Second)
defer cancel()
// Pre-computed function selectors for token0() and token1()
token0Selector := []byte{0x0d, 0xfe, 0x16, 0x81} // token0()
token1Selector := []byte{0xd2, 0x1c, 0xec, 0xd4} // token1()
// Call token0() function
token0Data, err := sas.client.CallContract(ctx, ethereum.CallMsg{
To: &poolAddress,
Data: token0Selector,
}, nil)
if err != nil {
return common.Address{}, common.Address{}, fmt.Errorf("failed to call token0(): %w", err)
}
// Call token1() function
token1Data, err := sas.client.CallContract(ctx, ethereum.CallMsg{
To: &poolAddress,
Data: token1Selector,
}, nil)
if err != nil {
return common.Address{}, common.Address{}, fmt.Errorf("failed to call token1(): %w", err)
}
// Parse the results
if len(token0Data) < 32 || len(token1Data) < 32 {
return common.Address{}, common.Address{}, fmt.Errorf("invalid token data length")
}
token0 = common.BytesToAddress(token0Data[12:32])
token1 = common.BytesToAddress(token1Data[12:32])
// Cache the result
sas.tokenCacheMutex.Lock()
sas.tokenCache[poolAddress] = TokenPair{Token0: token0, Token1: token1}
sas.tokenCacheMutex.Unlock()
return token0, token1, nil
}
// getSwapEventsFromBlock retrieves Uniswap V3 swap events from a specific block using log filtering
func (sas *SimpleArbitrageService) getSwapEventsFromBlock(blockNumber uint64) []*SimpleSwapEvent {
// Uniswap V3 Pool Swap event signature
swapEventSig := common.HexToHash("0xc42079f94a6350d7e6235f29174924f928cc2ac818eb64fed8004e115fbcca67")
// Create filter query for this specific block
query := ethereum.FilterQuery{
FromBlock: big.NewInt(int64(blockNumber)),
ToBlock: big.NewInt(int64(blockNumber)),
Topics: [][]common.Hash{{swapEventSig}},
}
// Get logs for this block
logs, err := sas.client.FilterLogs(sas.ctx, query)
if err != nil {
sas.logger.Debug(fmt.Sprintf("Failed to get logs for block %d: %v", blockNumber, err))
return nil
}
// Debug: Log how many logs we found for this block
if len(logs) > 0 {
sas.logger.Info(fmt.Sprintf("Found %d potential swap logs in block %d", len(logs), blockNumber))
}
var swapEvents []*SimpleSwapEvent
// Parse each log into a swap event
for _, log := range logs {
event := sas.parseSwapEvent(log, blockNumber)
if event != nil {
swapEvents = append(swapEvents, event)
sas.logger.Info(fmt.Sprintf("Successfully parsed swap event: pool=%s, amount0=%s, amount1=%s",
event.PoolAddress.Hex(), event.Amount0.String(), event.Amount1.String()))
} else {
sas.logger.Debug(fmt.Sprintf("Failed to parse swap log from pool %s", log.Address.Hex()))
}
}
return swapEvents
}
// parseSwapEvent parses a log entry into a SimpleSwapEvent
func (sas *SimpleArbitrageService) parseSwapEvent(log types.Log, blockNumber uint64) *SimpleSwapEvent {
// Validate log structure
if len(log.Topics) < 3 || len(log.Data) < 192 { // 6 * 32 bytes
sas.logger.Debug(fmt.Sprintf("Invalid log structure: topics=%d, data_len=%d", len(log.Topics), len(log.Data)))
return nil
}
// Extract non-indexed parameters from data
amount0 := new(big.Int).SetBytes(log.Data[0:32])
amount1 := new(big.Int).SetBytes(log.Data[32:64])
sqrtPriceX96 := new(big.Int).SetBytes(log.Data[64:96])
liquidity := new(big.Int).SetBytes(log.Data[96:128])
// Extract tick (int24, but stored as int256)
tickBytes := log.Data[128:160]
tick := new(big.Int).SetBytes(tickBytes)
if tick.Bit(255) == 1 { // Check if negative (two's complement)
tick.Sub(tick, new(big.Int).Lsh(big.NewInt(1), 256))
}
// Get pool tokens by querying the actual pool contract
token0, token1, err := sas.getPoolTokens(log.Address)
if err != nil {
sas.logger.Error(fmt.Sprintf("Failed to get tokens for pool %s: %v", log.Address.Hex(), err))
return nil // Skip if we can't get pool tokens
}
sas.logger.Debug(fmt.Sprintf("Successfully got pool tokens: %s/%s for pool %s",
token0.Hex(), token1.Hex(), log.Address.Hex()))
return &SimpleSwapEvent{
TxHash: log.TxHash,
PoolAddress: log.Address,
Token0: token0,
Token1: token1,
Amount0: amount0,
Amount1: amount1,
SqrtPriceX96: sqrtPriceX96,
Liquidity: liquidity,
Tick: int32(tick.Int64()),
BlockNumber: blockNumber,
LogIndex: log.Index,
Timestamp: time.Now(),
}
}