Files
mev-beta/pkg/execution/risk_manager_test.go
Administrator 29f88bafd9
Some checks failed
V2 CI/CD Pipeline / Pre-Flight Checks (push) Has been cancelled
V2 CI/CD Pipeline / Build & Dependencies (push) Has been cancelled
V2 CI/CD Pipeline / Code Quality & Linting (push) Has been cancelled
V2 CI/CD Pipeline / Unit Tests (100% Coverage Required) (push) Has been cancelled
V2 CI/CD Pipeline / Integration Tests (push) Has been cancelled
V2 CI/CD Pipeline / Performance Benchmarks (push) Has been cancelled
V2 CI/CD Pipeline / Decimal Precision Validation (push) Has been cancelled
V2 CI/CD Pipeline / Modularity Validation (push) Has been cancelled
V2 CI/CD Pipeline / Final Validation Summary (push) Has been cancelled
test(execution): add comprehensive test suite for execution engine
Add comprehensive unit tests for all execution engine components:

Component Test Coverage:
- UniswapV2 encoder: 15 test cases + benchmarks
- UniswapV3 encoder: 20 test cases + benchmarks
- Curve encoder: 16 test cases + benchmarks
- Flashloan manager: 18 test cases + benchmarks
- Transaction builder: 15 test cases + benchmarks
- Risk manager: 25 test cases + benchmarks
- Executor: 20 test cases + benchmarks

Test Categories:
- Happy path scenarios
- Error handling and edge cases
- Zero/invalid inputs
- Boundary conditions (max amounts, limits)
- Concurrent operations (nonce management)
- Configuration validation
- State management

Key Test Features:
- Protocol-specific encoding validation
- ABI encoding correctness
- Gas calculation accuracy
- Slippage calculation
- Nonce management thread safety
- Circuit breaker behavior
- Risk assessment rules
- Transaction lifecycle

Total: 129 test cases + performance benchmarks
Target: 100% test coverage for execution engine

Related to Phase 4 (Execution Engine) implementation.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-10 18:24:58 +01:00

634 lines
18 KiB
Go

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()
}
}