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>
This commit is contained in:
@@ -7,17 +7,23 @@ import (
|
||||
"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
|
||||
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
|
||||
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
|
||||
@@ -25,11 +31,13 @@ type RiskManager struct {
|
||||
lastReset time.Time
|
||||
|
||||
// Risk metrics
|
||||
totalTrades uint64
|
||||
successfulTrades uint64
|
||||
failedTrades uint64
|
||||
totalProfit *big.Int
|
||||
totalLoss *big.Int
|
||||
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
|
||||
@@ -40,17 +48,31 @@ type RiskManager struct {
|
||||
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
|
||||
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
|
||||
@@ -65,8 +87,11 @@ type CircuitBreaker struct {
|
||||
|
||||
// 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,
|
||||
@@ -87,6 +112,13 @@ func NewRiskManager(logger *logger.Logger) *RiskManager {
|
||||
},
|
||||
}
|
||||
|
||||
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()
|
||||
|
||||
@@ -99,15 +131,17 @@ func (rm *RiskManager) AssessOpportunity(opportunityID string, expectedProfit, g
|
||||
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: "",
|
||||
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
|
||||
@@ -116,6 +150,10 @@ func (rm *RiskManager) AssessOpportunity(opportunityID string, expectedProfit, g
|
||||
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)
|
||||
@@ -123,16 +161,18 @@ func (rm *RiskManager) AssessOpportunity(opportunityID string, expectedProfit, g
|
||||
}
|
||||
|
||||
// Check if we've exceeded daily loss limits
|
||||
if rm.dailyLoss.Cmp(rm.dailyLossLimit) > 0 {
|
||||
if cmp, err := rm.decimalConverter.Compare(dailyLossDec, rm.dailyLossLimitDecimal); err == nil && cmp > 0 {
|
||||
assessment.Reason = fmt.Sprintf("Daily loss limit exceeded: %s > %s",
|
||||
formatEther(rm.dailyLoss), formatEther(rm.dailyLossLimit))
|
||||
rm.decimalConverter.ToHumanReadable(dailyLossDec),
|
||||
rm.decimalConverter.ToHumanReadable(rm.dailyLossLimitDecimal))
|
||||
return assessment
|
||||
}
|
||||
|
||||
// Check minimum profit threshold
|
||||
if expectedProfit.Cmp(rm.minProfitThreshold) < 0 {
|
||||
if cmp, err := rm.decimalConverter.Compare(expectedProfitDec, rm.minProfitThresholdDecimal); err == nil && cmp < 0 {
|
||||
assessment.Reason = fmt.Sprintf("Profit below minimum threshold: %s < %s",
|
||||
formatEther(expectedProfit), formatEther(rm.minProfitThreshold))
|
||||
rm.decimalConverter.ToHumanReadable(expectedProfitDec),
|
||||
rm.decimalConverter.ToHumanReadable(rm.minProfitThresholdDecimal))
|
||||
return assessment
|
||||
}
|
||||
|
||||
@@ -144,9 +184,10 @@ func (rm *RiskManager) AssessOpportunity(opportunityID string, expectedProfit, g
|
||||
}
|
||||
|
||||
// Check gas price limits
|
||||
if gasPrice.Cmp(rm.maxGasPrice) > 0 {
|
||||
if cmp, err := rm.decimalConverter.Compare(gasPriceDec, rm.maxGasPriceDecimal); err == nil && cmp > 0 {
|
||||
assessment.Reason = fmt.Sprintf("Gas price exceeds limit: %s > %s",
|
||||
formatGwei(gasPrice), formatGwei(rm.maxGasPrice))
|
||||
rm.decimalConverter.ToHumanReadable(gasPriceDec),
|
||||
rm.decimalConverter.ToHumanReadable(rm.maxGasPriceDecimal))
|
||||
return assessment
|
||||
}
|
||||
|
||||
@@ -165,10 +206,12 @@ func (rm *RiskManager) AssessOpportunity(opportunityID string, expectedProfit, g
|
||||
// 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)
|
||||
@@ -200,12 +243,16 @@ func (rm *RiskManager) RecordTrade(success bool, profit, gasCost *big.Int) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -217,7 +264,10 @@ func (rm *RiskManager) RecordTrade(success bool, profit, gasCost *big.Int) {
|
||||
}
|
||||
|
||||
rm.logger.Debug(fmt.Sprintf("Trade recorded: Success=%t, Profit=%s, Gas=%s, DailyLoss=%s",
|
||||
success, formatEther(profit), formatEther(gasCost), formatEther(rm.dailyLoss)))
|
||||
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
|
||||
@@ -243,16 +293,16 @@ func (rm *RiskManager) GetStatistics() map[string]interface{} {
|
||||
"successful_trades": rm.successfulTrades,
|
||||
"failed_trades": rm.failedTrades,
|
||||
"success_rate": float64(rm.successfulTrades) / float64(max(1, rm.totalTrades)),
|
||||
"total_profit": formatEther(rm.totalProfit),
|
||||
"total_loss": formatEther(rm.totalLoss),
|
||||
"daily_loss": formatEther(rm.dailyLoss),
|
||||
"daily_loss_limit": formatEther(rm.dailyLossLimit),
|
||||
"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": formatEther(rm.minProfitThreshold),
|
||||
"min_profit_threshold": rm.decimalConverter.ToHumanReadable(rm.minProfitThresholdDecimal),
|
||||
"max_slippage": rm.maxSlippage * 100, // Convert to percentage
|
||||
"max_gas_price": formatGwei(rm.maxGasPrice),
|
||||
"max_gas_price": rm.decimalConverter.ToHumanReadable(rm.maxGasPriceDecimal),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
83
pkg/risk/manager_test.go
Normal file
83
pkg/risk/manager_test.go
Normal file
@@ -0,0 +1,83 @@
|
||||
package risk
|
||||
|
||||
import (
|
||||
"math/big"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/fraktal/mev-beta/internal/logger"
|
||||
"github.com/fraktal/mev-beta/pkg/math"
|
||||
)
|
||||
|
||||
func TestAssessOpportunityProvidesDecimalSnapshots(t *testing.T) {
|
||||
log := logger.New("debug", "text", "")
|
||||
rm := NewRiskManager(log)
|
||||
|
||||
expectedProfit := big.NewInt(20000000000000000) // 0.02 ETH
|
||||
gasCost := big.NewInt(500000000000000) // 0.0005 ETH
|
||||
gasPrice := big.NewInt(3000000000) // 3 gwei
|
||||
|
||||
assessment := rm.AssessOpportunity("op-1", expectedProfit, gasCost, 0.005, gasPrice)
|
||||
if assessment.MaxPositionSizeDecimal == nil {
|
||||
t.Fatalf("expected max position size decimal snapshot to be populated")
|
||||
}
|
||||
if assessment.RecommendedGasDecimal == nil {
|
||||
t.Fatalf("expected gas decimal snapshot to be populated")
|
||||
}
|
||||
|
||||
expectedMax := rm.fromWei(assessment.MaxPositionSize, "ETH")
|
||||
if cmp, err := rm.decimalConverter.Compare(assessment.MaxPositionSizeDecimal, expectedMax); err != nil || cmp != 0 {
|
||||
t.Fatalf("expected position decimal %s to match %s", rm.decimalConverter.ToHumanReadable(assessment.MaxPositionSizeDecimal), rm.decimalConverter.ToHumanReadable(expectedMax))
|
||||
}
|
||||
|
||||
expectedGas := rm.fromWei(assessment.RecommendedGas, "GWEI")
|
||||
if cmp, err := rm.decimalConverter.Compare(assessment.RecommendedGasDecimal, expectedGas); err != nil || cmp != 0 {
|
||||
t.Fatalf("expected gas decimal %s to match %s", rm.decimalConverter.ToHumanReadable(assessment.RecommendedGasDecimal), rm.decimalConverter.ToHumanReadable(expectedGas))
|
||||
}
|
||||
}
|
||||
|
||||
func TestAssessOpportunityRejectsBelowMinimumProfit(t *testing.T) {
|
||||
log := logger.New("debug", "text", "")
|
||||
rm := NewRiskManager(log)
|
||||
|
||||
belowThreshold := big.NewInt(5000000000000000) // 0.005 ETH < 0.01 ETH
|
||||
gasCost := big.NewInt(100000000000000) // 0.0001 ETH
|
||||
gasPrice := big.NewInt(1500000000) // 1.5 gwei
|
||||
|
||||
assessment := rm.AssessOpportunity("op-2", belowThreshold, gasCost, 0.001, gasPrice)
|
||||
if assessment.Acceptable {
|
||||
t.Fatalf("expected opportunity below profit threshold to be rejected")
|
||||
}
|
||||
if !strings.Contains(assessment.Reason, "Profit below minimum threshold") {
|
||||
t.Fatalf("unexpected rejection reason: %s", assessment.Reason)
|
||||
}
|
||||
|
||||
thresholdDecimal := rm.minProfitThresholdDecimal
|
||||
if cmp, err := rm.decimalConverter.Compare(assessment.MaxPositionSizeDecimal, rm.maxPositionSizeDecimal); err != nil || cmp != 0 {
|
||||
t.Fatalf("expected max position decimal to remain default when rejected")
|
||||
}
|
||||
if cmp, err := rm.decimalConverter.Compare(rm.minProfitThresholdDecimal, thresholdDecimal); err != nil || cmp != 0 {
|
||||
t.Fatalf("expected threshold decimal to remain unchanged")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRecordTradeTracksDecimalTotals(t *testing.T) {
|
||||
log := logger.New("debug", "text", "")
|
||||
rm := NewRiskManager(log)
|
||||
|
||||
profit := big.NewInt(20000000000000000) // 0.02 ETH
|
||||
gasCost := big.NewInt(5000000000000000) // 0.005 ETH
|
||||
|
||||
rm.RecordTrade(true, profit, gasCost)
|
||||
|
||||
expectedProfit, _ := math.NewUniversalDecimal(profit, 18, "ETH")
|
||||
if cmp, err := rm.decimalConverter.Compare(rm.totalProfitDecimal, expectedProfit); err != nil || cmp != 0 {
|
||||
t.Fatalf("expected total profit decimal %s to equal %s", rm.decimalConverter.ToHumanReadable(rm.totalProfitDecimal), rm.decimalConverter.ToHumanReadable(expectedProfit))
|
||||
}
|
||||
|
||||
rm.RecordTrade(false, nil, gasCost)
|
||||
expectedLoss, _ := math.NewUniversalDecimal(gasCost, 18, "ETH")
|
||||
if cmp, err := rm.decimalConverter.Compare(rm.totalLossDecimal, expectedLoss); err != nil || cmp != 0 {
|
||||
t.Fatalf("expected total loss decimal %s to equal %s", rm.decimalConverter.ToHumanReadable(rm.totalLossDecimal), rm.decimalConverter.ToHumanReadable(expectedLoss))
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
|
||||
"github.com/fraktal/mev-beta/internal/logger"
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user