package execution import ( "context" "log/slog" "math/big" "os" "testing" "time" "github.com/ethereum/go-ethereum/common" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/your-org/mev-bot/pkg/arbitrage" ) func TestDefaultRiskManagerConfig(t *testing.T) { config := DefaultRiskManagerConfig() assert.NotNil(t, config) assert.True(t, config.Enabled) assert.NotNil(t, config.MaxPositionSize) assert.NotNil(t, config.MaxDailyVolume) assert.NotNil(t, config.MinProfitThreshold) assert.Equal(t, float64(0.01), config.MinROI) assert.Equal(t, uint16(200), config.MaxSlippageBPS) assert.Equal(t, uint64(5), config.MaxConcurrentTxs) } func TestNewRiskManager(t *testing.T) { logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) manager := NewRiskManager(nil, nil, logger) assert.NotNil(t, manager) assert.NotNil(t, manager.config) assert.NotNil(t, manager.activeTxs) assert.NotNil(t, manager.dailyVolume) assert.NotNil(t, manager.recentFailures) assert.False(t, manager.circuitBreakerOpen) } func TestRiskManager_AssessRisk_Success(t *testing.T) { logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) config := DefaultRiskManagerConfig() config.SimulationEnabled = false // Disable simulation for unit test manager := NewRiskManager(config, nil, logger) opp := &arbitrage.Opportunity{ InputAmount: big.NewInt(1e18), OutputAmount: big.NewInt(1.1e18), NetProfit: big.NewInt(0.1e18), ROI: 0.1, // 10% EstimatedGas: 150000, } tx := &SwapTransaction{ MaxFeePerGas: big.NewInt(50e9), // 50 gwei MaxPriorityFeePerGas: big.NewInt(2e9), // 2 gwei GasLimit: 180000, Slippage: 50, // 0.5% } assessment, err := manager.AssessRisk(context.Background(), opp, tx) require.NoError(t, err) assert.NotNil(t, assessment) assert.True(t, assessment.Approved) assert.Empty(t, assessment.Warnings) } func TestRiskManager_AssessRisk_CircuitBreakerOpen(t *testing.T) { logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) config := DefaultRiskManagerConfig() config.SimulationEnabled = false manager := NewRiskManager(config, nil, logger) // Open circuit breaker manager.openCircuitBreaker() opp := &arbitrage.Opportunity{ InputAmount: big.NewInt(1e18), OutputAmount: big.NewInt(1.1e18), NetProfit: big.NewInt(0.1e18), ROI: 0.1, EstimatedGas: 150000, } tx := &SwapTransaction{ MaxFeePerGas: big.NewInt(50e9), MaxPriorityFeePerGas: big.NewInt(2e9), GasLimit: 180000, Slippage: 50, } assessment, err := manager.AssessRisk(context.Background(), opp, tx) require.NoError(t, err) assert.False(t, assessment.Approved) assert.Contains(t, assessment.Reason, "circuit breaker") } func TestRiskManager_AssessRisk_PositionSizeExceeded(t *testing.T) { logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) config := DefaultRiskManagerConfig() config.SimulationEnabled = false manager := NewRiskManager(config, nil, logger) // Create opportunity with amount exceeding max position size largeAmount := new(big.Int).Add(config.MaxPositionSize, big.NewInt(1)) opp := &arbitrage.Opportunity{ InputAmount: largeAmount, OutputAmount: new(big.Int).Mul(largeAmount, big.NewInt(11)), NetProfit: big.NewInt(1e18), ROI: 0.1, EstimatedGas: 150000, } tx := &SwapTransaction{ MaxFeePerGas: big.NewInt(50e9), MaxPriorityFeePerGas: big.NewInt(2e9), GasLimit: 180000, Slippage: 50, } assessment, err := manager.AssessRisk(context.Background(), opp, tx) require.NoError(t, err) assert.False(t, assessment.Approved) assert.Contains(t, assessment.Reason, "position size") } func TestRiskManager_AssessRisk_DailyVolumeExceeded(t *testing.T) { logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) config := DefaultRiskManagerConfig() config.SimulationEnabled = false manager := NewRiskManager(config, nil, logger) // Set daily volume to max manager.dailyVolume = config.MaxDailyVolume opp := &arbitrage.Opportunity{ InputAmount: big.NewInt(1e18), OutputAmount: big.NewInt(1.1e18), NetProfit: big.NewInt(0.1e18), ROI: 0.1, EstimatedGas: 150000, } tx := &SwapTransaction{ MaxFeePerGas: big.NewInt(50e9), MaxPriorityFeePerGas: big.NewInt(2e9), GasLimit: 180000, Slippage: 50, } assessment, err := manager.AssessRisk(context.Background(), opp, tx) require.NoError(t, err) assert.False(t, assessment.Approved) assert.Contains(t, assessment.Reason, "daily volume") } func TestRiskManager_AssessRisk_GasPriceTooHigh(t *testing.T) { logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) config := DefaultRiskManagerConfig() config.SimulationEnabled = false manager := NewRiskManager(config, nil, logger) opp := &arbitrage.Opportunity{ InputAmount: big.NewInt(1e18), OutputAmount: big.NewInt(1.1e18), NetProfit: big.NewInt(0.1e18), ROI: 0.1, EstimatedGas: 150000, } // Set gas price above max tx := &SwapTransaction{ MaxFeePerGas: new(big.Int).Add(config.MaxGasPrice, big.NewInt(1)), MaxPriorityFeePerGas: big.NewInt(2e9), GasLimit: 180000, Slippage: 50, } assessment, err := manager.AssessRisk(context.Background(), opp, tx) require.NoError(t, err) assert.False(t, assessment.Approved) assert.Contains(t, assessment.Reason, "gas price") } func TestRiskManager_AssessRisk_ProfitTooLow(t *testing.T) { logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) config := DefaultRiskManagerConfig() config.SimulationEnabled = false manager := NewRiskManager(config, nil, logger) // Profit below threshold lowProfit := new(big.Int).Sub(config.MinProfitThreshold, big.NewInt(1)) opp := &arbitrage.Opportunity{ InputAmount: big.NewInt(1e18), OutputAmount: new(big.Int).Add(big.NewInt(1e18), lowProfit), NetProfit: lowProfit, ROI: 0.00001, EstimatedGas: 150000, } tx := &SwapTransaction{ MaxFeePerGas: big.NewInt(50e9), MaxPriorityFeePerGas: big.NewInt(2e9), GasLimit: 180000, Slippage: 50, } assessment, err := manager.AssessRisk(context.Background(), opp, tx) require.NoError(t, err) assert.False(t, assessment.Approved) assert.Contains(t, assessment.Reason, "profit") } func TestRiskManager_AssessRisk_ROITooLow(t *testing.T) { logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) config := DefaultRiskManagerConfig() config.SimulationEnabled = false manager := NewRiskManager(config, nil, logger) opp := &arbitrage.Opportunity{ InputAmount: big.NewInt(1e18), OutputAmount: big.NewInt(1.005e18), NetProfit: big.NewInt(0.005e18), // 0.5% ROI, below 1% threshold ROI: 0.005, EstimatedGas: 150000, } tx := &SwapTransaction{ MaxFeePerGas: big.NewInt(50e9), MaxPriorityFeePerGas: big.NewInt(2e9), GasLimit: 180000, Slippage: 50, } assessment, err := manager.AssessRisk(context.Background(), opp, tx) require.NoError(t, err) assert.False(t, assessment.Approved) assert.Contains(t, assessment.Reason, "ROI") } func TestRiskManager_AssessRisk_SlippageTooHigh(t *testing.T) { logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) config := DefaultRiskManagerConfig() config.SimulationEnabled = false manager := NewRiskManager(config, nil, logger) opp := &arbitrage.Opportunity{ InputAmount: big.NewInt(1e18), OutputAmount: big.NewInt(1.1e18), NetProfit: big.NewInt(0.1e18), ROI: 0.1, EstimatedGas: 150000, } tx := &SwapTransaction{ MaxFeePerGas: big.NewInt(50e9), MaxPriorityFeePerGas: big.NewInt(2e9), GasLimit: 180000, Slippage: 300, // 3%, above 2% max } assessment, err := manager.AssessRisk(context.Background(), opp, tx) require.NoError(t, err) assert.False(t, assessment.Approved) assert.Contains(t, assessment.Reason, "slippage") } func TestRiskManager_AssessRisk_ConcurrentLimitExceeded(t *testing.T) { logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) config := DefaultRiskManagerConfig() config.SimulationEnabled = false config.MaxConcurrentTxs = 2 manager := NewRiskManager(config, nil, logger) // Add max concurrent transactions manager.activeTxs[common.HexToHash("0x01")] = &ActiveTransaction{} manager.activeTxs[common.HexToHash("0x02")] = &ActiveTransaction{} opp := &arbitrage.Opportunity{ InputAmount: big.NewInt(1e18), OutputAmount: big.NewInt(1.1e18), NetProfit: big.NewInt(0.1e18), ROI: 0.1, EstimatedGas: 150000, } tx := &SwapTransaction{ MaxFeePerGas: big.NewInt(50e9), MaxPriorityFeePerGas: big.NewInt(2e9), GasLimit: 180000, Slippage: 50, } assessment, err := manager.AssessRisk(context.Background(), opp, tx) require.NoError(t, err) assert.False(t, assessment.Approved) assert.Contains(t, assessment.Reason, "concurrent") } func TestRiskManager_TrackTransaction(t *testing.T) { logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) manager := NewRiskManager(nil, nil, logger) hash := common.HexToHash("0x123") opp := &arbitrage.Opportunity{ InputAmount: big.NewInt(1e18), NetProfit: big.NewInt(0.1e18), } gasPrice := big.NewInt(50e9) manager.TrackTransaction(hash, opp, gasPrice) // Check transaction is tracked manager.mu.RLock() tx, exists := manager.activeTxs[hash] manager.mu.RUnlock() assert.True(t, exists) assert.NotNil(t, tx) assert.Equal(t, hash, tx.Hash) assert.Equal(t, opp.InputAmount, tx.Amount) assert.Equal(t, gasPrice, tx.GasPrice) } func TestRiskManager_UntrackTransaction(t *testing.T) { logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) manager := NewRiskManager(nil, nil, logger) hash := common.HexToHash("0x123") manager.activeTxs[hash] = &ActiveTransaction{Hash: hash} manager.UntrackTransaction(hash) // Check transaction is no longer tracked manager.mu.RLock() _, exists := manager.activeTxs[hash] manager.mu.RUnlock() assert.False(t, exists) } func TestRiskManager_RecordFailure(t *testing.T) { logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) config := DefaultRiskManagerConfig() config.CircuitBreakerFailures = 3 config.CircuitBreakerWindow = 1 * time.Minute manager := NewRiskManager(config, nil, logger) hash := common.HexToHash("0x123") // Record failures below threshold manager.RecordFailure(hash, "test failure 1") assert.False(t, manager.circuitBreakerOpen) manager.RecordFailure(hash, "test failure 2") assert.False(t, manager.circuitBreakerOpen) // Third failure should open circuit breaker manager.RecordFailure(hash, "test failure 3") assert.True(t, manager.circuitBreakerOpen) } func TestRiskManager_RecordSuccess(t *testing.T) { logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) manager := NewRiskManager(nil, nil, logger) hash := common.HexToHash("0x123") actualProfit := big.NewInt(0.1e18) manager.RecordSuccess(hash, actualProfit) // Check that recent failures were cleared assert.Empty(t, manager.recentFailures) } func TestRiskManager_CircuitBreaker_Cooldown(t *testing.T) { logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) config := DefaultRiskManagerConfig() config.CircuitBreakerCooldown = 1 * time.Millisecond manager := NewRiskManager(config, nil, logger) // Open circuit breaker manager.openCircuitBreaker() assert.True(t, manager.circuitBreakerOpen) // Wait for cooldown time.Sleep(2 * time.Millisecond) // Circuit breaker should be closed after cooldown assert.False(t, manager.checkCircuitBreaker()) } func TestRiskManager_checkConcurrentLimit(t *testing.T) { logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) config := DefaultRiskManagerConfig() config.MaxConcurrentTxs = 3 manager := NewRiskManager(config, nil, logger) // Add transactions below limit manager.activeTxs[common.HexToHash("0x01")] = &ActiveTransaction{} manager.activeTxs[common.HexToHash("0x02")] = &ActiveTransaction{} assert.True(t, manager.checkConcurrentLimit()) // Add transaction at limit manager.activeTxs[common.HexToHash("0x03")] = &ActiveTransaction{} assert.False(t, manager.checkConcurrentLimit()) } func TestRiskManager_checkPositionSize(t *testing.T) { logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) config := DefaultRiskManagerConfig() config.MaxPositionSize = big.NewInt(10e18) manager := NewRiskManager(config, nil, logger) assert.True(t, manager.checkPositionSize(big.NewInt(5e18))) assert.True(t, manager.checkPositionSize(big.NewInt(10e18))) assert.False(t, manager.checkPositionSize(big.NewInt(11e18))) } func TestRiskManager_checkDailyVolume(t *testing.T) { logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) config := DefaultRiskManagerConfig() config.MaxDailyVolume = big.NewInt(100e18) manager := NewRiskManager(config, nil, logger) manager.dailyVolume = big.NewInt(90e18) assert.True(t, manager.checkDailyVolume(big.NewInt(5e18))) assert.True(t, manager.checkDailyVolume(big.NewInt(10e18))) assert.False(t, manager.checkDailyVolume(big.NewInt(15e18))) } func TestRiskManager_updateDailyVolume(t *testing.T) { logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) manager := NewRiskManager(nil, nil, logger) initialVolume := big.NewInt(10e18) manager.dailyVolume = initialVolume addAmount := big.NewInt(5e18) manager.updateDailyVolume(addAmount) expectedVolume := new(big.Int).Add(initialVolume, addAmount) assert.Equal(t, expectedVolume, manager.dailyVolume) } func TestRiskManager_checkGasPrice(t *testing.T) { logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) config := DefaultRiskManagerConfig() config.MaxGasPrice = big.NewInt(100e9) // 100 gwei manager := NewRiskManager(config, nil, logger) assert.True(t, manager.checkGasPrice(big.NewInt(50e9))) assert.True(t, manager.checkGasPrice(big.NewInt(100e9))) assert.False(t, manager.checkGasPrice(big.NewInt(101e9))) } func TestRiskManager_checkGasCost(t *testing.T) { logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) config := DefaultRiskManagerConfig() config.MaxGasCost = big.NewInt(0.1e18) // 0.1 ETH manager := NewRiskManager(config, nil, logger) assert.True(t, manager.checkGasCost(big.NewInt(0.05e18))) assert.True(t, manager.checkGasCost(big.NewInt(0.1e18))) assert.False(t, manager.checkGasCost(big.NewInt(0.11e18))) } func TestRiskManager_checkMinProfit(t *testing.T) { logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) config := DefaultRiskManagerConfig() config.MinProfitThreshold = big.NewInt(0.01e18) // 0.01 ETH manager := NewRiskManager(config, nil, logger) assert.False(t, manager.checkMinProfit(big.NewInt(0.005e18))) assert.True(t, manager.checkMinProfit(big.NewInt(0.01e18))) assert.True(t, manager.checkMinProfit(big.NewInt(0.02e18))) } func TestRiskManager_checkMinROI(t *testing.T) { logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) config := DefaultRiskManagerConfig() config.MinROI = 0.01 // 1% manager := NewRiskManager(config, nil, logger) assert.False(t, manager.checkMinROI(0.005)) // 0.5% assert.True(t, manager.checkMinROI(0.01)) // 1% assert.True(t, manager.checkMinROI(0.02)) // 2% } func TestRiskManager_checkSlippage(t *testing.T) { logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) config := DefaultRiskManagerConfig() config.MaxSlippageBPS = 200 // 2% manager := NewRiskManager(config, nil, logger) assert.True(t, manager.checkSlippage(100)) // 1% assert.True(t, manager.checkSlippage(200)) // 2% assert.False(t, manager.checkSlippage(300)) // 3% } func TestRiskManager_GetActiveTransactions(t *testing.T) { logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) manager := NewRiskManager(nil, nil, logger) // Add some active transactions manager.activeTxs[common.HexToHash("0x01")] = &ActiveTransaction{Hash: common.HexToHash("0x01")} manager.activeTxs[common.HexToHash("0x02")] = &ActiveTransaction{Hash: common.HexToHash("0x02")} activeTxs := manager.GetActiveTransactions() assert.Len(t, activeTxs, 2) } func TestRiskManager_GetStats(t *testing.T) { logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) manager := NewRiskManager(nil, nil, logger) // Add some state manager.activeTxs[common.HexToHash("0x01")] = &ActiveTransaction{} manager.dailyVolume = big.NewInt(50e18) manager.circuitBreakerOpen = true stats := manager.GetStats() assert.NotNil(t, stats) assert.Equal(t, 1, stats["active_transactions"]) assert.Equal(t, "50000000000000000000", stats["daily_volume"]) assert.Equal(t, true, stats["circuit_breaker_open"]) } func TestRiskManager_AssessRisk_WithWarnings(t *testing.T) { logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) config := DefaultRiskManagerConfig() config.SimulationEnabled = false manager := NewRiskManager(config, nil, logger) // Create opportunity with high gas cost (should generate warning) opp := &arbitrage.Opportunity{ InputAmount: big.NewInt(1e18), OutputAmount: big.NewInt(1.1e18), NetProfit: big.NewInt(0.1e18), ROI: 0.1, EstimatedGas: 2000000, // Very high gas } tx := &SwapTransaction{ MaxFeePerGas: big.NewInt(50e9), MaxPriorityFeePerGas: big.NewInt(2e9), GasLimit: 2400000, Slippage: 50, } assessment, err := manager.AssessRisk(context.Background(), opp, tx) require.NoError(t, err) assert.True(t, assessment.Approved) // Should still be approved assert.NotEmpty(t, assessment.Warnings) // But with warnings } // Benchmark tests func BenchmarkRiskManager_AssessRisk(b *testing.B) { logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) config := DefaultRiskManagerConfig() config.SimulationEnabled = false manager := NewRiskManager(config, nil, logger) opp := &arbitrage.Opportunity{ InputAmount: big.NewInt(1e18), OutputAmount: big.NewInt(1.1e18), NetProfit: big.NewInt(0.1e18), ROI: 0.1, EstimatedGas: 150000, } tx := &SwapTransaction{ MaxFeePerGas: big.NewInt(50e9), MaxPriorityFeePerGas: big.NewInt(2e9), GasLimit: 180000, Slippage: 50, } b.ResetTimer() for i := 0; i < b.N; i++ { _, _ = manager.AssessRisk(context.Background(), opp, tx) } } func BenchmarkRiskManager_checkCircuitBreaker(b *testing.B) { logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) manager := NewRiskManager(nil, nil, logger) b.ResetTimer() for i := 0; i < b.N; i++ { _ = manager.checkCircuitBreaker() } }