Files
mev-beta/pkg/risk/manager.go
Krypto Kajun 850223a953 fix(multicall): resolve critical multicall parsing corruption issues
- Added comprehensive bounds checking to prevent buffer overruns in multicall parsing
- Implemented graduated validation system (Strict/Moderate/Permissive) to reduce false positives
- Added LRU caching system for address validation with 10-minute TTL
- Enhanced ABI decoder with missing Universal Router and Arbitrum-specific DEX signatures
- Fixed duplicate function declarations and import conflicts across multiple files
- Added error recovery mechanisms with multiple fallback strategies
- Updated tests to handle new validation behavior for suspicious addresses
- Fixed parser test expectations for improved validation system
- Applied gofmt formatting fixes to ensure code style compliance
- Fixed mutex copying issues in monitoring package by introducing MetricsSnapshot
- Resolved critical security vulnerabilities in heuristic address extraction
- Progress: Updated TODO audit from 10% to 35% complete

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-17 00:12:55 -05:00

540 lines
17 KiB
Go

package risk
import (
"fmt"
"math/big"
"sync"
"time"
"github.com/fraktal/mev-beta/internal/logger"
"github.com/fraktal/mev-beta/pkg/math"
)
// RiskManager manages risk for MEV operations
type RiskManager struct {
logger *logger.Logger
mu sync.RWMutex
decimalConverter *math.DecimalConverter
// Position limits
maxPositionSize *big.Int // Maximum position size in wei
dailyLossLimit *big.Int // Maximum daily loss in wei
maxConcurrent int // Maximum concurrent positions
maxPositionSizeDecimal *math.UniversalDecimal
dailyLossLimitDecimal *math.UniversalDecimal
minProfitThresholdDecimal *math.UniversalDecimal
maxGasPriceDecimal *math.UniversalDecimal
// Current state
currentPositions int
dailyLoss *big.Int
lastReset time.Time
// Risk metrics
totalTrades uint64
successfulTrades uint64
failedTrades uint64
totalProfit *big.Int
totalLoss *big.Int
totalProfitDecimal *math.UniversalDecimal
totalLossDecimal *math.UniversalDecimal
// Configuration
minProfitThreshold *big.Int // Minimum profit threshold in wei
maxSlippage float64 // Maximum acceptable slippage
maxGasPrice *big.Int // Maximum gas price willing to pay
// Circuit breaker
circuitBreaker *CircuitBreaker
}
func (rm *RiskManager) fromWei(value *big.Int, symbol string) *math.UniversalDecimal {
if value == nil {
zero, _ := math.NewUniversalDecimal(big.NewInt(0), 18, symbol)
return zero
}
decimals := uint8(18)
if symbol == "GWEI" {
decimals = 9
}
return rm.decimalConverter.FromWei(value, decimals, symbol)
}
// RiskAssessment represents a risk assessment for an MEV opportunity
type RiskAssessment struct {
OpportunityID string
RiskScore float64 // 0-1, higher = riskier
Confidence float64 // 0-1, higher = more confident
MaxPositionSize *big.Int // Maximum position size for this opportunity
RecommendedGas *big.Int // Recommended gas price
SlippageLimit float64 // Maximum slippage for this opportunity
Profitability float64 // Expected ROI percentage
Acceptable bool // Whether the opportunity passes risk checks
Reason string // Reason for acceptance/rejection
MaxPositionSizeDecimal *math.UniversalDecimal
RecommendedGasDecimal *math.UniversalDecimal
}
// CircuitBreaker manages circuit breaking for risk control
type CircuitBreaker struct {
failures int
lastFailure time.Time
resetTimeout time.Duration
failureThreshold int
open bool
mu sync.RWMutex
}
// NewRiskManager creates a new risk manager
func NewRiskManager(logger *logger.Logger) *RiskManager {
dc := math.NewDecimalConverter()
rm := &RiskManager{
logger: logger,
decimalConverter: dc,
maxPositionSize: big.NewInt(1000000000000000000), // 1 ETH
dailyLossLimit: big.NewInt(100000000000000000), // 0.1 ETH
maxConcurrent: 5,
currentPositions: 0,
dailyLoss: big.NewInt(0),
lastReset: time.Now(),
totalTrades: 0,
successfulTrades: 0,
failedTrades: 0,
totalProfit: big.NewInt(0),
totalLoss: big.NewInt(0),
minProfitThreshold: big.NewInt(10000000000000000), // 0.01 ETH
maxSlippage: 0.01, // 1%
maxGasPrice: big.NewInt(20000000000), // 20 gwei
circuitBreaker: &CircuitBreaker{
resetTimeout: 5 * time.Minute,
failureThreshold: 3,
},
}
rm.maxPositionSizeDecimal, _ = math.NewUniversalDecimal(new(big.Int).Set(rm.maxPositionSize), 18, "ETH")
rm.dailyLossLimitDecimal, _ = math.NewUniversalDecimal(new(big.Int).Set(rm.dailyLossLimit), 18, "ETH")
rm.minProfitThresholdDecimal, _ = math.NewUniversalDecimal(new(big.Int).Set(rm.minProfitThreshold), 18, "ETH")
rm.maxGasPriceDecimal, _ = math.NewUniversalDecimal(new(big.Int).Set(rm.maxGasPrice), 9, "GWEI")
rm.totalProfitDecimal, _ = math.NewUniversalDecimal(big.NewInt(0), 18, "ETH")
rm.totalLossDecimal, _ = math.NewUniversalDecimal(big.NewInt(0), 18, "ETH")
// Start daily reset timer
go rm.dailyReset()
return rm
}
// AssessOpportunity assesses the risk of an MEV opportunity
func (rm *RiskManager) AssessOpportunity(opportunityID string, expectedProfit, gasCost *big.Int, slippage float64, gasPrice *big.Int) *RiskAssessment {
rm.mu.Lock()
defer rm.mu.Unlock()
assessment := &RiskAssessment{
OpportunityID: opportunityID,
RiskScore: 0.0,
Confidence: 0.0,
MaxPositionSize: big.NewInt(0),
RecommendedGas: big.NewInt(0),
SlippageLimit: 0.0,
Profitability: 0.0,
Acceptable: false,
Reason: "",
MaxPositionSizeDecimal: rm.maxPositionSizeDecimal,
RecommendedGasDecimal: rm.maxGasPriceDecimal,
}
// Check circuit breaker
if rm.circuitBreaker.IsOpen() {
assessment.Reason = "Circuit breaker is open"
return assessment
}
expectedProfitDec := rm.fromWei(expectedProfit, "ETH")
gasPriceDec := rm.fromWei(gasPrice, "GWEI")
dailyLossDec := rm.fromWei(rm.dailyLoss, "ETH")
// Check if we've exceeded concurrent position limits
if rm.currentPositions >= rm.maxConcurrent {
assessment.Reason = fmt.Sprintf("Maximum concurrent positions reached: %d", rm.maxConcurrent)
return assessment
}
// Check if we've exceeded daily loss limits
if cmp, err := rm.decimalConverter.Compare(dailyLossDec, rm.dailyLossLimitDecimal); err == nil && cmp > 0 {
assessment.Reason = fmt.Sprintf("Daily loss limit exceeded: %s > %s",
rm.decimalConverter.ToHumanReadable(dailyLossDec),
rm.decimalConverter.ToHumanReadable(rm.dailyLossLimitDecimal))
return assessment
}
// Check minimum profit threshold
if cmp, err := rm.decimalConverter.Compare(expectedProfitDec, rm.minProfitThresholdDecimal); err == nil && cmp < 0 {
assessment.Reason = fmt.Sprintf("Profit below minimum threshold: %s < %s",
rm.decimalConverter.ToHumanReadable(expectedProfitDec),
rm.decimalConverter.ToHumanReadable(rm.minProfitThresholdDecimal))
return assessment
}
// Check slippage tolerance
if slippage > rm.maxSlippage {
assessment.Reason = fmt.Sprintf("Slippage exceeds limit: %.2f%% > %.2f%%",
slippage*100, rm.maxSlippage*100)
return assessment
}
// Check gas price limits
if cmp, err := rm.decimalConverter.Compare(gasPriceDec, rm.maxGasPriceDecimal); err == nil && cmp > 0 {
assessment.Reason = fmt.Sprintf("Gas price exceeds limit: %s > %s",
rm.decimalConverter.ToHumanReadable(gasPriceDec),
rm.decimalConverter.ToHumanReadable(rm.maxGasPriceDecimal))
return assessment
}
// Calculate risk score based on multiple factors
riskScore := rm.calculateRiskScore(expectedProfit, gasCost, slippage, gasPrice)
assessment.RiskScore = riskScore
// Calculate confidence based on historical performance
confidence := rm.calculateConfidence()
assessment.Confidence = confidence
// Calculate profitability (ROI percentage)
profitability := rm.calculateProfitability(expectedProfit, gasCost)
assessment.Profitability = profitability
// Determine maximum position size based on risk
maxPosition := rm.calculateMaxPositionSize(riskScore, expectedProfit, gasCost)
assessment.MaxPositionSize = maxPosition
assessment.MaxPositionSizeDecimal = rm.fromWei(maxPosition, "ETH")
// Recommend gas price based on network conditions and risk
recommendedGas := rm.calculateRecommendedGas(gasPrice, riskScore)
assessment.RecommendedGas = recommendedGas
assessment.RecommendedGasDecimal = rm.fromWei(recommendedGas, "GWEI")
// Set slippage limit based on risk tolerance
slippageLimit := rm.calculateSlippageLimit(riskScore)
assessment.SlippageLimit = slippageLimit
// Determine if opportunity is acceptable
acceptable := rm.isAcceptable(riskScore, profitability, confidence)
assessment.Acceptable = acceptable
if acceptable {
assessment.Reason = "Opportunity passes all risk checks"
} else {
assessment.Reason = fmt.Sprintf("Risk score too high: %.2f", riskScore)
}
rm.logger.Debug(fmt.Sprintf("Risk assessment for %s: Risk=%.2f, Confidence=%.2f, Profitability=%.2f%%, Acceptable=%t",
opportunityID, riskScore, confidence, profitability, acceptable))
return assessment
}
// RecordTrade records the result of a trade for risk management
func (rm *RiskManager) RecordTrade(success bool, profit, gasCost *big.Int) {
rm.mu.Lock()
defer rm.mu.Unlock()
rm.totalTrades++
if success {
rm.successfulTrades++
if profit != nil {
rm.totalProfit.Add(rm.totalProfit, profit)
profitDec := rm.fromWei(profit, "ETH")
rm.totalProfitDecimal, _ = rm.decimalConverter.Add(rm.totalProfitDecimal, profitDec)
}
} else {
rm.failedTrades++
if gasCost != nil {
rm.totalLoss.Add(rm.totalLoss, gasCost)
rm.dailyLoss.Add(rm.dailyLoss, gasCost)
lossDec := rm.fromWei(gasCost, "ETH")
rm.totalLossDecimal, _ = rm.decimalConverter.Add(rm.totalLossDecimal, lossDec)
}
}
// Update circuit breaker
if !success {
rm.circuitBreaker.RecordFailure()
} else {
rm.circuitBreaker.RecordSuccess()
}
rm.logger.Debug(fmt.Sprintf("Trade recorded: Success=%t, Profit=%s, Gas=%s, DailyLoss=%s",
success,
rm.decimalConverter.ToHumanReadable(rm.fromWei(profit, "ETH")),
rm.decimalConverter.ToHumanReadable(rm.fromWei(gasCost, "ETH")),
rm.decimalConverter.ToHumanReadable(rm.fromWei(rm.dailyLoss, "ETH"))))
}
// UpdatePositionCount updates the current position count
func (rm *RiskManager) UpdatePositionCount(delta int) {
rm.mu.Lock()
defer rm.mu.Unlock()
rm.currentPositions += delta
if rm.currentPositions < 0 {
rm.currentPositions = 0
}
rm.logger.Debug(fmt.Sprintf("Position count updated: %d", rm.currentPositions))
}
// GetStatistics returns risk management statistics
func (rm *RiskManager) GetStatistics() map[string]interface{} {
rm.mu.RLock()
defer rm.mu.RUnlock()
return map[string]interface{}{
"total_trades": rm.totalTrades,
"successful_trades": rm.successfulTrades,
"failed_trades": rm.failedTrades,
"success_rate": float64(rm.successfulTrades) / float64(max(1, rm.totalTrades)),
"total_profit": rm.decimalConverter.ToHumanReadable(rm.totalProfitDecimal),
"total_loss": rm.decimalConverter.ToHumanReadable(rm.totalLossDecimal),
"daily_loss": rm.decimalConverter.ToHumanReadable(rm.fromWei(rm.dailyLoss, "ETH")),
"daily_loss_limit": rm.decimalConverter.ToHumanReadable(rm.dailyLossLimitDecimal),
"current_positions": rm.currentPositions,
"max_concurrent": rm.maxConcurrent,
"circuit_breaker_open": rm.circuitBreaker.IsOpen(),
"min_profit_threshold": rm.decimalConverter.ToHumanReadable(rm.minProfitThresholdDecimal),
"max_slippage": rm.maxSlippage * 100, // Convert to percentage
"max_gas_price": rm.decimalConverter.ToHumanReadable(rm.maxGasPriceDecimal),
}
}
// dailyReset resets daily counters
func (rm *RiskManager) dailyReset() {
ticker := time.NewTicker(24 * time.Hour)
defer ticker.Stop()
for {
<-ticker.C
rm.mu.Lock()
rm.dailyLoss = big.NewInt(0)
rm.lastReset = time.Now()
rm.mu.Unlock()
rm.logger.Info("Daily risk counters reset")
}
}
// calculateRiskScore calculates a risk score based on multiple factors
func (rm *RiskManager) calculateRiskScore(expectedProfit, gasCost *big.Int, slippage float64, gasPrice *big.Int) float64 {
// Base risk (0-0.3)
baseRisk := 0.1
// Gas risk (0-0.3) - higher gas = higher risk
gasRisk := 0.0
if gasPrice != nil && rm.maxGasPrice != nil && rm.maxGasPrice.Sign() > 0 {
gasRatio := new(big.Float).Quo(new(big.Float).SetInt(gasPrice), new(big.Float).SetInt(rm.maxGasPrice))
gasRatioFloat, _ := gasRatio.Float64()
gasRisk = gasRatioFloat * 0.3
if gasRisk > 0.3 {
gasRisk = 0.3
}
}
// Slippage risk (0-0.2) - higher slippage = higher risk
slippageRisk := slippage * 0.2
if slippageRisk > 0.2 {
slippageRisk = 0.2
}
// Profit risk (0-0.2) - lower profit = higher relative risk
profitRisk := 0.0
if expectedProfit != nil && expectedProfit.Sign() > 0 {
// Lower profits are riskier relative to gas costs
if gasCost != nil && gasCost.Sign() > 0 {
profitRatio := new(big.Float).Quo(new(big.Float).SetInt(gasCost), new(big.Float).SetInt(expectedProfit))
profitRatioFloat, _ := profitRatio.Float64()
profitRisk = profitRatioFloat * 0.2
if profitRisk > 0.2 {
profitRisk = 0.2
}
}
}
totalRisk := baseRisk + gasRisk + slippageRisk + profitRisk
if totalRisk > 1.0 {
totalRisk = 1.0
}
return totalRisk
}
// calculateConfidence calculates confidence based on historical performance
func (rm *RiskManager) calculateConfidence() float64 {
if rm.totalTrades == 0 {
return 0.5 // Default confidence for new system
}
successRate := float64(rm.successfulTrades) / float64(rm.totalTrades)
confidence := successRate * 0.8 // Weight success rate at 80%
// Add bonus for high volume of trades
volumeBonus := float64(min(rm.totalTrades, 1000)) / 1000.0 * 0.2 // Max 20% bonus
confidence += volumeBonus
if confidence > 1.0 {
confidence = 1.0
}
return confidence
}
// calculateProfitability calculates profitability as ROI percentage
func (rm *RiskManager) calculateProfitability(expectedProfit, gasCost *big.Int) float64 {
if expectedProfit == nil || expectedProfit.Sign() <= 0 {
return 0.0
}
netProfit := new(big.Int).Sub(expectedProfit, gasCost)
if netProfit.Sign() <= 0 {
return 0.0
}
// For profitability calculation, we need an investment amount
// Use a reasonable default investment (e.g., 1 ETH)
investment := big.NewInt(1000000000000000000) // 1 ETH
roi := new(big.Float).Quo(new(big.Float).SetInt(netProfit), new(big.Float).SetInt(investment))
roi.Mul(roi, big.NewFloat(100))
roiFloat, _ := roi.Float64()
return roiFloat
}
// calculateMaxPositionSize calculates maximum position size based on risk
func (rm *RiskManager) calculateMaxPositionSize(riskScore float64, expectedProfit, gasCost *big.Int) *big.Int {
// Start with maximum position size
maxPosition := new(big.Int).Set(rm.maxPositionSize)
// Reduce position size based on risk score
riskMultiplier := 1.0 - riskScore
maxPositionFloat := new(big.Float).SetInt(maxPosition)
maxPositionFloat.Mul(maxPositionFloat, big.NewFloat(riskMultiplier))
result := new(big.Int)
maxPositionFloat.Int(result)
return result
}
// calculateRecommendedGas calculates recommended gas price based on risk
func (rm *RiskManager) calculateRecommendedGas(gasPrice *big.Int, riskScore float64) *big.Int {
if gasPrice == nil {
return big.NewInt(0)
}
// For high-risk opportunities, recommend higher gas price for faster execution
// For low-risk opportunities, can use lower gas price
gasMultiplier := 1.0 + (riskScore * 0.5) // Up to 50% increase for high risk
recommendedGas := new(big.Float).SetInt(gasPrice)
recommendedGas.Mul(recommendedGas, big.NewFloat(gasMultiplier))
result := new(big.Int)
recommendedGas.Int(result)
return result
}
// calculateSlippageLimit calculates slippage limit based on risk
func (rm *RiskManager) calculateSlippageLimit(riskScore float64) float64 {
// For high-risk opportunities, allow less slippage
// For low-risk opportunities, can tolerate more slippage
slippageLimit := rm.maxSlippage * (1.0 - riskScore*0.5) // Reduce by up to 50% for high risk
return slippageLimit
}
// isAcceptable determines if an opportunity is acceptable based on risk criteria
func (rm *RiskManager) isAcceptable(riskScore, profitability, confidence float64) bool {
// Must pass all individual criteria AND overall risk threshold
if riskScore > 0.7 {
return false // Too risky regardless of other factors
}
if profitability < 1.0 {
return false // Less than 1% ROI
}
if confidence < 0.3 {
return false // Low confidence
}
// Overall weighted score
weightedScore := (riskScore * 0.4) + ((100 - profitability) * 0.3) + ((1 - confidence) * 0.3)
return weightedScore < 0.5
}
// max returns the larger of two integers
func max(a, b uint64) uint64 {
if a > b {
return a
}
return b
}
// min returns the smaller of two integers
func min(a, b uint64) uint64 {
if a < b {
return a
}
return b
}
// formatEther formats a big.Int wei amount as ETH string
func formatEther(wei *big.Int) string {
if wei == nil {
return "0"
}
ether := new(big.Float).Quo(new(big.Float).SetInt(wei), big.NewFloat(1e18))
result, _ := ether.Float64()
return fmt.Sprintf("%.6f", result)
}
// formatGwei formats a big.Int wei amount as Gwei string
func formatGwei(wei *big.Int) string {
if wei == nil {
return "0"
}
gwei := new(big.Float).Quo(new(big.Float).SetInt(wei), big.NewFloat(1e9))
result, _ := gwei.Float64()
return fmt.Sprintf("%.2f", result)
}
// IsOpen checks if the circuit breaker is open
func (cb *CircuitBreaker) IsOpen() bool {
cb.mu.RLock()
defer cb.mu.RUnlock()
return cb.open
}
// RecordFailure records a failure in the circuit breaker
func (cb *CircuitBreaker) RecordFailure() {
cb.mu.Lock()
defer cb.mu.Unlock()
cb.failures++
cb.lastFailure = time.Now()
if cb.failures >= cb.failureThreshold {
cb.open = true
}
}
// RecordSuccess records a success in the circuit breaker and potentially closes it
func (cb *CircuitBreaker) RecordSuccess() {
cb.mu.Lock()
defer cb.mu.Unlock()
// Reset failures if enough time has passed
if time.Since(cb.lastFailure) > cb.resetTimeout {
cb.failures = 0
cb.open = false
}
}