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:
@@ -3,11 +3,14 @@ package math
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math"
|
||||
"math/big"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
|
||||
"github.com/fraktal/mev-beta/pkg/security"
|
||||
"github.com/fraktal/mev-beta/pkg/types"
|
||||
)
|
||||
|
||||
@@ -73,10 +76,10 @@ func NewArbitrageCalculator(gasEstimator GasEstimator) *ArbitrageCalculator {
|
||||
dc := NewDecimalConverter()
|
||||
|
||||
// Default configuration
|
||||
minProfit, _ := dc.FromString("0.01", 18, "ETH") // 0.01 ETH minimum
|
||||
maxImpact, _ := dc.FromString("2", 4, "PERCENT") // 2% max price impact
|
||||
maxSlip, _ := dc.FromString("1", 4, "PERCENT") // 1% max slippage
|
||||
maxGas, _ := dc.FromString("50", 9, "GWEI") // 50 gwei max gas
|
||||
minProfit, _ := dc.FromString("0.01", 18, "ETH") // 0.01 ETH minimum
|
||||
maxImpact, _ := dc.FromString("0.02", 4, "PERCENT") // 2% max price impact
|
||||
maxSlip, _ := dc.FromString("0.01", 4, "PERCENT") // 1% max slippage
|
||||
maxGas, _ := dc.FromString("50", 9, "GWEI") // 50 gwei max gas
|
||||
|
||||
return &ArbitrageCalculator{
|
||||
pricingEngine: NewExchangePricingEngine(),
|
||||
@@ -89,6 +92,17 @@ func NewArbitrageCalculator(gasEstimator GasEstimator) *ArbitrageCalculator {
|
||||
}
|
||||
}
|
||||
|
||||
func toDecimalAmount(ud *UniversalDecimal) types.DecimalAmount {
|
||||
if ud == nil {
|
||||
return types.DecimalAmount{}
|
||||
}
|
||||
return types.DecimalAmount{
|
||||
Value: ud.Value.String(),
|
||||
Decimals: ud.Decimals,
|
||||
Symbol: ud.Symbol,
|
||||
}
|
||||
}
|
||||
|
||||
// CalculateArbitrageOpportunity performs comprehensive arbitrage analysis
|
||||
func (calc *ArbitrageCalculator) CalculateArbitrageOpportunity(
|
||||
path []*PoolData,
|
||||
@@ -117,7 +131,7 @@ func (calc *ArbitrageCalculator) CalculateArbitrageOpportunity(
|
||||
}
|
||||
|
||||
// Step 4: Calculate profits (convert to common denomination - ETH)
|
||||
_, netProfit, profitPercentage, err := calc.calculateProfits(
|
||||
grossProfit, netProfit, profitPercentage, err := calc.calculateProfits(
|
||||
inputAmount, finalOutput, totalGasCost, inputToken, outputToken)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error calculating profits: %w", err)
|
||||
@@ -155,22 +169,43 @@ func (calc *ArbitrageCalculator) CalculateArbitrageOpportunity(
|
||||
}
|
||||
|
||||
opportunity := &types.ArbitrageOpportunity{
|
||||
Path: pathStrings,
|
||||
Pools: poolStrings,
|
||||
AmountIn: inputAmount.Value,
|
||||
Profit: netProfit.Value,
|
||||
NetProfit: netProfit.Value,
|
||||
GasEstimate: totalGasCost.Value,
|
||||
ROI: func() float64 { f, _ := profitPercentage.Value.Float64(); return f }(),
|
||||
Path: pathStrings,
|
||||
Pools: poolStrings,
|
||||
AmountIn: inputAmount.Value,
|
||||
RequiredAmount: inputAmount.Value,
|
||||
Profit: grossProfit.Value,
|
||||
NetProfit: netProfit.Value,
|
||||
EstimatedProfit: grossProfit.Value,
|
||||
GasEstimate: totalGasCost.Value,
|
||||
ROI: func() float64 {
|
||||
// Convert percentage from 4-decimal format to actual percentage
|
||||
f, _ := profitPercentage.Value.Float64()
|
||||
return f / 10000.0 // Convert from 4-decimal format to actual percentage
|
||||
}(),
|
||||
Protocol: "multi", // Default protocol for multi-step arbitrage
|
||||
ExecutionTime: executionTime,
|
||||
Confidence: confidence,
|
||||
PriceImpact: func() float64 { f, _ := totalPriceImpact.Value.Float64(); return f }(),
|
||||
MaxSlippage: 0.01, // Default 1% max slippage
|
||||
TokenIn: common.HexToAddress(inputToken.Address),
|
||||
TokenOut: common.HexToAddress(outputToken.Address),
|
||||
Timestamp: time.Now().Unix(),
|
||||
Risk: riskAssessment.OverallRisk,
|
||||
PriceImpact: func() float64 {
|
||||
// Convert percentage from 4-decimal format to actual percentage
|
||||
f, _ := totalPriceImpact.Value.Float64()
|
||||
return f / 10000.0 // Convert from 4-decimal format to actual percentage
|
||||
}(),
|
||||
MaxSlippage: 0.01, // Default 1% max slippage
|
||||
TokenIn: common.HexToAddress(inputToken.Address),
|
||||
TokenOut: common.HexToAddress(outputToken.Address),
|
||||
Timestamp: time.Now().Unix(),
|
||||
DetectedAt: time.Now(),
|
||||
ExpiresAt: time.Now().Add(5 * time.Minute),
|
||||
Risk: riskAssessment.OverallRisk,
|
||||
}
|
||||
opportunity.Quantities = &types.OpportunityQuantities{
|
||||
AmountIn: toDecimalAmount(inputAmount),
|
||||
AmountOut: toDecimalAmount(finalOutput),
|
||||
GrossProfit: toDecimalAmount(grossProfit),
|
||||
NetProfit: toDecimalAmount(netProfit),
|
||||
GasCost: toDecimalAmount(totalGasCost),
|
||||
ProfitPercent: toDecimalAmount(profitPercentage),
|
||||
PriceImpact: toDecimalAmount(totalPriceImpact),
|
||||
}
|
||||
|
||||
return opportunity, nil
|
||||
@@ -260,7 +295,13 @@ func (calc *ArbitrageCalculator) calculateTotalGasCost(route []ExchangeStep) (*U
|
||||
}
|
||||
|
||||
// Convert to gas cost in ETH
|
||||
totalGasBig := big.NewInt(int64(totalGas))
|
||||
totalGasInt64, err := security.SafeUint64ToInt64(totalGas)
|
||||
if err != nil {
|
||||
// This is very unlikely for gas calculations, but handle safely
|
||||
// Use maximum safe value as fallback
|
||||
totalGasInt64 = math.MaxInt64
|
||||
}
|
||||
totalGasBig := big.NewInt(totalGasInt64)
|
||||
totalGasDecimal, err := NewUniversalDecimal(totalGasBig, 0, "GAS")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -551,38 +592,107 @@ func (calc *ArbitrageCalculator) convertToETH(amount *UniversalDecimal, token To
|
||||
}
|
||||
|
||||
// IsOpportunityProfitable checks if opportunity meets minimum criteria
|
||||
// IsOpportunityProfitable checks if an opportunity meets profitability criteria
|
||||
func (calc *ArbitrageCalculator) IsOpportunityProfitable(opportunity *types.ArbitrageOpportunity) bool {
|
||||
// Check minimum profit threshold (simplified comparison)
|
||||
if opportunity.NetProfit.Cmp(big.NewInt(1000000000000000)) < 0 { // 0.001 ETH minimum
|
||||
if opportunity == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check maximum price impact threshold (5% max)
|
||||
if opportunity.PriceImpact > 0.05 {
|
||||
// Check minimum profit threshold
|
||||
if !calc.checkProfitThreshold(opportunity) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check maximum price impact
|
||||
if !calc.checkPriceImpactThreshold(opportunity) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check risk level
|
||||
if opportunity.Risk >= 0.8 { // High risk threshold
|
||||
if !calc.checkRiskLevel(opportunity) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check confidence threshold
|
||||
if opportunity.Confidence < 0.3 {
|
||||
if !calc.checkConfidenceThreshold(opportunity) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// checkProfitThreshold checks if the opportunity meets minimum profit requirements
|
||||
func (calc *ArbitrageCalculator) checkProfitThreshold(opportunity *types.ArbitrageOpportunity) bool {
|
||||
if opportunity.Quantities != nil {
|
||||
if netProfitUD, err := calc.decimalAmountToUniversal(opportunity.Quantities.NetProfit); err == nil {
|
||||
if cmp, err := calc.decimalConverter.Compare(netProfitUD, calc.minProfitThreshold); err == nil && cmp < 0 {
|
||||
return false
|
||||
}
|
||||
}
|
||||
} else if opportunity.NetProfit != nil {
|
||||
if opportunity.NetProfit.Cmp(calc.minProfitThreshold.Value) < 0 {
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// checkPriceImpactThreshold checks if the opportunity is below maximum price impact
|
||||
func (calc *ArbitrageCalculator) checkPriceImpactThreshold(opportunity *types.ArbitrageOpportunity) bool {
|
||||
if opportunity.Quantities != nil {
|
||||
if impactUD, err := calc.decimalAmountToUniversal(opportunity.Quantities.PriceImpact); err == nil {
|
||||
if cmp, err := calc.decimalConverter.Compare(impactUD, calc.maxPriceImpact); err == nil && cmp > 0 {
|
||||
return false
|
||||
}
|
||||
}
|
||||
} else {
|
||||
maxImpactFloat := float64(calc.maxPriceImpact.Value.Int64()) / math.Pow10(int(calc.maxPriceImpact.Decimals))
|
||||
if opportunity.PriceImpact > maxImpactFloat {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// checkRiskLevel checks if the opportunity's risk is acceptable
|
||||
func (calc *ArbitrageCalculator) checkRiskLevel(opportunity *types.ArbitrageOpportunity) bool {
|
||||
return opportunity.Risk < 0.8 // High risk threshold
|
||||
}
|
||||
|
||||
// checkConfidenceThreshold checks if the opportunity has sufficient confidence
|
||||
func (calc *ArbitrageCalculator) checkConfidenceThreshold(opportunity *types.ArbitrageOpportunity) bool {
|
||||
return opportunity.Confidence >= 0.3
|
||||
}
|
||||
|
||||
// SortOpportunitiesByProfitability sorts opportunities by net profit descending
|
||||
func (calc *ArbitrageCalculator) SortOpportunitiesByProfitability(opportunities []*types.ArbitrageOpportunity) {
|
||||
sort.Slice(opportunities, func(i, j int) bool {
|
||||
// Simple comparison using big.Int.Cmp for sorting
|
||||
return opportunities[i].NetProfit.Cmp(opportunities[j].NetProfit) > 0 // Descending order (highest profit first)
|
||||
left, errL := calc.decimalAmountToUniversal(opportunities[i].Quantities.NetProfit)
|
||||
right, errR := calc.decimalAmountToUniversal(opportunities[j].Quantities.NetProfit)
|
||||
if errL == nil && errR == nil {
|
||||
cmp, err := calc.decimalConverter.Compare(left, right)
|
||||
if err == nil {
|
||||
return cmp > 0
|
||||
}
|
||||
}
|
||||
// Fallback to canonical big.Int comparison
|
||||
return opportunities[i].NetProfit.Cmp(opportunities[j].NetProfit) > 0 // Descending order
|
||||
})
|
||||
}
|
||||
|
||||
func (calc *ArbitrageCalculator) decimalAmountToUniversal(dec types.DecimalAmount) (*UniversalDecimal, error) {
|
||||
if dec.Value == "" {
|
||||
return nil, fmt.Errorf("decimal amount empty")
|
||||
}
|
||||
val, ok := new(big.Int).SetString(dec.Value, 10)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("invalid decimal amount %s", dec.Value)
|
||||
}
|
||||
return NewUniversalDecimal(val, dec.Decimals, dec.Symbol)
|
||||
}
|
||||
|
||||
// CalculateArbitrage calculates arbitrage opportunity for a given path and input amount
|
||||
func (calc *ArbitrageCalculator) CalculateArbitrage(ctx context.Context, inputAmount *UniversalDecimal, path []*PoolData) (*types.ArbitrageOpportunity, error) {
|
||||
if len(path) == 0 {
|
||||
|
||||
175
pkg/math/arbitrage_calculator_test.go
Normal file
175
pkg/math/arbitrage_calculator_test.go
Normal file
@@ -0,0 +1,175 @@
|
||||
package math
|
||||
|
||||
import (
|
||||
"math/big"
|
||||
"testing"
|
||||
|
||||
"github.com/fraktal/mev-beta/pkg/types"
|
||||
)
|
||||
|
||||
type stubGasEstimator struct {
|
||||
price *UniversalDecimal
|
||||
}
|
||||
|
||||
func (s stubGasEstimator) EstimateSwapGas(exchange ExchangeType, poolData *PoolData) (uint64, error) {
|
||||
return 100_000, nil
|
||||
}
|
||||
|
||||
func (s stubGasEstimator) EstimateFlashSwapGas(route []*PoolData) (uint64, error) {
|
||||
return 50_000, nil
|
||||
}
|
||||
|
||||
func (s stubGasEstimator) GetCurrentGasPrice() (*UniversalDecimal, error) {
|
||||
return s.price, nil
|
||||
}
|
||||
|
||||
func TestIsOpportunityProfitableRespectsThreshold(t *testing.T) {
|
||||
estimator := stubGasEstimator{price: func() *UniversalDecimal {
|
||||
ud, _ := NewUniversalDecimal(big.NewInt(1_000_000_000), 9, "GWEI")
|
||||
return ud
|
||||
}()}
|
||||
calc := NewArbitrageCalculator(estimator)
|
||||
|
||||
belowThreshold, _ := NewUniversalDecimal(big.NewInt(9_000_000_000_000_000), 18, "ETH")
|
||||
priceImpact, _ := NewUniversalDecimal(big.NewInt(100), 4, "PERCENT")
|
||||
|
||||
opportunity := &types.ArbitrageOpportunity{
|
||||
NetProfit: belowThreshold.Value,
|
||||
PriceImpact: 0.01,
|
||||
Confidence: 0.5,
|
||||
Quantities: &types.OpportunityQuantities{
|
||||
NetProfit: toDecimalAmount(belowThreshold),
|
||||
PriceImpact: toDecimalAmount(priceImpact),
|
||||
AmountIn: toDecimalAmount(belowThreshold),
|
||||
AmountOut: toDecimalAmount(belowThreshold),
|
||||
GrossProfit: toDecimalAmount(belowThreshold),
|
||||
GasCost: toDecimalAmount(belowThreshold),
|
||||
ProfitPercent: toDecimalAmount(priceImpact),
|
||||
},
|
||||
}
|
||||
|
||||
if calc.IsOpportunityProfitable(opportunity) {
|
||||
t.Fatalf("expected below-threshold opportunity to be rejected")
|
||||
}
|
||||
|
||||
aboveThreshold, _ := NewUniversalDecimal(big.NewInt(2_000_000_000_000_0000), 18, "ETH")
|
||||
opportunity.NetProfit = aboveThreshold.Value
|
||||
opportunity.Quantities.NetProfit = toDecimalAmount(aboveThreshold)
|
||||
if !calc.IsOpportunityProfitable(opportunity) {
|
||||
t.Fatalf("expected opportunity above threshold to be accepted")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSortOpportunitiesByProfitabilityUsesDecimals(t *testing.T) {
|
||||
estimator := stubGasEstimator{price: func() *UniversalDecimal {
|
||||
ud, _ := NewUniversalDecimal(big.NewInt(1_000_000_000), 9, "GWEI")
|
||||
return ud
|
||||
}()}
|
||||
calc := NewArbitrageCalculator(estimator)
|
||||
|
||||
a, _ := NewUniversalDecimal(big.NewInt(1_500_000_000_000_0000), 18, "ETH")
|
||||
b, _ := NewUniversalDecimal(big.NewInt(5_000_000_000_000_000), 18, "ETH")
|
||||
|
||||
oppA := &types.ArbitrageOpportunity{
|
||||
NetProfit: a.Value,
|
||||
Quantities: &types.OpportunityQuantities{
|
||||
NetProfit: toDecimalAmount(a),
|
||||
},
|
||||
}
|
||||
oppB := &types.ArbitrageOpportunity{
|
||||
NetProfit: b.Value,
|
||||
Quantities: &types.OpportunityQuantities{
|
||||
NetProfit: toDecimalAmount(b),
|
||||
},
|
||||
}
|
||||
|
||||
opps := []*types.ArbitrageOpportunity{oppB, oppA}
|
||||
calc.SortOpportunitiesByProfitability(opps)
|
||||
|
||||
if opps[0] != oppA {
|
||||
t.Fatalf("expected higher decimal profit opportunity first")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCalculateArbitrageOpportunitySetsQuantities(t *testing.T) {
|
||||
estimator := stubGasEstimator{price: func() *UniversalDecimal {
|
||||
ud, _ := NewUniversalDecimal(big.NewInt(1_000_000_000), 9, "GWEI")
|
||||
return ud
|
||||
}()}
|
||||
calc := NewArbitrageCalculator(estimator)
|
||||
|
||||
pool := &PoolData{
|
||||
Address: "0xpool",
|
||||
ExchangeType: ExchangeUniswapV2,
|
||||
Token0: TokenInfo{Address: "0x0", Symbol: "TOKEN0", Decimals: 18},
|
||||
Token1: TokenInfo{Address: "0x1", Symbol: "TOKEN1", Decimals: 18},
|
||||
}
|
||||
|
||||
amountIn, _ := NewUniversalDecimal(big.NewInt(1_000_000_000_000_000), 18, "TOKEN0")
|
||||
|
||||
opportunity, err := calc.CalculateArbitrageOpportunity([]*PoolData{pool}, amountIn, pool.Token0, pool.Token1)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if opportunity.Quantities == nil {
|
||||
t.Fatalf("expected quantities to be populated")
|
||||
}
|
||||
if opportunity.Quantities.NetProfit.Value == "" {
|
||||
t.Fatalf("expected net profit decimal to have value")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCalculateMinimumOutputAppliesSlippage(t *testing.T) {
|
||||
estimator := stubGasEstimator{price: func() *UniversalDecimal {
|
||||
ud, _ := NewUniversalDecimal(big.NewInt(1_000_000_000), 9, "GWEI")
|
||||
return ud
|
||||
}()}
|
||||
calc := NewArbitrageCalculator(estimator)
|
||||
|
||||
expected, _ := NewUniversalDecimal(big.NewInt(1_000_000_000_000_000_000), 18, "ETH")
|
||||
minOutput, err := calc.calculateMinimumOutput(expected)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
// Default max slippage is 1% -> expect 0.99 ETH
|
||||
expectedMin, _ := NewUniversalDecimal(big.NewInt(990000000000000000), 18, "ETH")
|
||||
cmp, err := calc.decimalConverter.Compare(minOutput, expectedMin)
|
||||
if err != nil || cmp != 0 {
|
||||
t.Fatalf("expected min output 0.99 ETH, got %s", calc.decimalConverter.ToHumanReadable(minOutput))
|
||||
}
|
||||
}
|
||||
|
||||
func TestCalculateProfitsCapturesSpread(t *testing.T) {
|
||||
estimator := stubGasEstimator{price: func() *UniversalDecimal {
|
||||
ud, _ := NewUniversalDecimal(big.NewInt(1_000_000_000), 9, "GWEI")
|
||||
return ud
|
||||
}()}
|
||||
calc := NewArbitrageCalculator(estimator)
|
||||
|
||||
amountIn, _ := NewUniversalDecimal(big.NewInt(10_000_000_000_000_000), 18, "ETH") // 0.01
|
||||
amountOut, _ := NewUniversalDecimal(big.NewInt(12_000_000_000_000_000), 18, "ETH")
|
||||
gasCost, _ := NewUniversalDecimal(big.NewInt(500_000_000_000_000), 18, "ETH")
|
||||
|
||||
gross, net, pct, err := calc.calculateProfits(amountIn, amountOut, gasCost, TokenInfo{Symbol: "ETH", Decimals: 18}, TokenInfo{Symbol: "ETH", Decimals: 18})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
expectedGross, _ := NewUniversalDecimal(big.NewInt(2_000_000_000_000_000), 18, "ETH")
|
||||
cmp, err := calc.decimalConverter.Compare(gross, expectedGross)
|
||||
if err != nil || cmp != 0 {
|
||||
t.Fatalf("expected gross profit 0.002 ETH, got %s", calc.decimalConverter.ToHumanReadable(gross))
|
||||
}
|
||||
|
||||
expectedNet, _ := NewUniversalDecimal(big.NewInt(1_500_000_000_000_000), 18, "ETH")
|
||||
cmp, err = calc.decimalConverter.Compare(net, expectedNet)
|
||||
if err != nil || cmp != 0 {
|
||||
t.Fatalf("expected net profit 0.0015 ETH, got %s", calc.decimalConverter.ToHumanReadable(net))
|
||||
}
|
||||
|
||||
if pct == nil || pct.Value.Sign() <= 0 {
|
||||
t.Fatalf("expected positive profit percentage")
|
||||
}
|
||||
}
|
||||
@@ -4,8 +4,9 @@ import (
|
||||
"math/big"
|
||||
"sync"
|
||||
|
||||
"github.com/fraktal/mev-beta/pkg/uniswap"
|
||||
"github.com/holiman/uint256"
|
||||
|
||||
"github.com/fraktal/mev-beta/pkg/uniswap"
|
||||
)
|
||||
|
||||
// Cached mathematical constants to avoid recomputation
|
||||
|
||||
@@ -33,12 +33,31 @@ func NewDecimalConverter() *DecimalConverter {
|
||||
return dc
|
||||
}
|
||||
|
||||
// NewUniversalDecimal creates a new universal decimal with validation
|
||||
// NewUniversalDecimal creates a new universal decimal with comprehensive validation
|
||||
func NewUniversalDecimal(value *big.Int, decimals uint8, symbol string) (*UniversalDecimal, error) {
|
||||
// Validate decimal places
|
||||
if decimals > 18 {
|
||||
return nil, fmt.Errorf("decimal places cannot exceed 18, got %d for token %s", decimals, symbol)
|
||||
}
|
||||
|
||||
// Validate symbol
|
||||
if symbol == "" {
|
||||
return nil, fmt.Errorf("symbol cannot be empty")
|
||||
}
|
||||
|
||||
// Validate value bounds - prevent extremely large values that could cause overflow
|
||||
if value != nil {
|
||||
// Check for reasonable bounds - max value should not exceed what can be represented
|
||||
// in financial calculations (roughly 2^256 / 10^18 for safety)
|
||||
maxValue := new(big.Int)
|
||||
maxValue.Exp(big.NewInt(10), big.NewInt(60), nil) // 10^60 max value for safety
|
||||
|
||||
absValue := new(big.Int).Abs(value)
|
||||
if absValue.Cmp(maxValue) > 0 {
|
||||
return nil, fmt.Errorf("value %s exceeds maximum safe value for token %s", value.String(), symbol)
|
||||
}
|
||||
}
|
||||
|
||||
if value == nil {
|
||||
value = big.NewInt(0)
|
||||
}
|
||||
@@ -83,18 +102,37 @@ func (dc *DecimalConverter) FromString(valueStr string, decimals uint8, symbol s
|
||||
return nil, fmt.Errorf("invalid number format: %s for token %s", valueStr, symbol)
|
||||
}
|
||||
|
||||
// Heuristic: if the string length is >= decimals, treat as raw value
|
||||
// This handles cases like "1000000000000000000" (18 chars, 18 decimals) as raw
|
||||
// But treats "1" (1 char, 18 decimals) as human-readable
|
||||
// Improved heuristic for distinguishing raw vs human-readable values:
|
||||
// 1. If value is very large relative to what a human would typically enter, treat as raw
|
||||
// 2. If value is small (< 1000), treat as human-readable
|
||||
// 3. Use length as secondary indicator
|
||||
|
||||
valueInt := value.Int64() // Safe since we parsed it successfully
|
||||
|
||||
// If the value is very small (less than 1000), it's likely human-readable
|
||||
if valueInt < 1000 {
|
||||
// Treat as human-readable value - convert to smallest unit
|
||||
scalingFactor := dc.getScalingFactor(decimals)
|
||||
scaledValue := new(big.Int).Mul(value, scalingFactor)
|
||||
return NewUniversalDecimal(scaledValue, decimals, symbol)
|
||||
}
|
||||
|
||||
// If the value looks like it could be raw wei (very large), treat as raw
|
||||
if len(valueStr) >= int(decimals) && decimals > 0 {
|
||||
// Treat as raw value in smallest unit
|
||||
return NewUniversalDecimal(value, decimals, symbol)
|
||||
}
|
||||
|
||||
// Treat as human-readable value - convert to smallest unit
|
||||
// For intermediate values, use a more sophisticated check
|
||||
// If the number would represent more than 1000 tokens when treated as human-readable,
|
||||
// it's probably meant to be raw
|
||||
if valueInt > 1000 {
|
||||
return NewUniversalDecimal(value, decimals, symbol)
|
||||
}
|
||||
|
||||
// Default: treat as human-readable
|
||||
scalingFactor := dc.getScalingFactor(decimals)
|
||||
scaledValue := new(big.Int).Mul(value, scalingFactor)
|
||||
|
||||
return NewUniversalDecimal(scaledValue, decimals, symbol)
|
||||
}
|
||||
|
||||
@@ -226,8 +264,16 @@ func (dc *DecimalConverter) ConvertTo(from *UniversalDecimal, toDecimals uint8,
|
||||
return NewUniversalDecimal(convertedValue, toDecimals, toSymbol)
|
||||
}
|
||||
|
||||
// Multiply performs precise multiplication between different decimal tokens
|
||||
// Multiply performs precise multiplication between different decimal tokens with overflow protection
|
||||
func (dc *DecimalConverter) Multiply(a, b *UniversalDecimal, resultDecimals uint8, resultSymbol string) (*UniversalDecimal, error) {
|
||||
// Check for overflow potential before multiplication
|
||||
maxSafeValue := new(big.Int)
|
||||
maxSafeValue.Exp(big.NewInt(10), big.NewInt(30), nil) // Conservative limit for multiplication
|
||||
|
||||
if a.Value.Cmp(maxSafeValue) > 0 || b.Value.Cmp(maxSafeValue) > 0 {
|
||||
return nil, fmt.Errorf("values too large for safe multiplication: %s * %s", a.Symbol, b.Symbol)
|
||||
}
|
||||
|
||||
// Multiply raw values
|
||||
product := new(big.Int).Mul(a.Value, b.Value)
|
||||
|
||||
@@ -268,13 +314,21 @@ func (dc *DecimalConverter) Divide(numerator, denominator *UniversalDecimal, res
|
||||
return NewUniversalDecimal(quotient, resultDecimals, resultSymbol)
|
||||
}
|
||||
|
||||
// Add adds two UniversalDecimals with same precision
|
||||
// Add adds two UniversalDecimals with same precision and overflow protection
|
||||
func (dc *DecimalConverter) Add(a, b *UniversalDecimal) (*UniversalDecimal, error) {
|
||||
if a.Decimals != b.Decimals {
|
||||
return nil, fmt.Errorf("cannot add tokens with different decimals: %s(%d) + %s(%d)",
|
||||
a.Symbol, a.Decimals, b.Symbol, b.Decimals)
|
||||
}
|
||||
|
||||
// Check for potential overflow before performing addition
|
||||
maxSafeValue := new(big.Int)
|
||||
maxSafeValue.Exp(big.NewInt(10), big.NewInt(59), nil) // 10^59 for safety margin
|
||||
|
||||
if a.Value.Cmp(maxSafeValue) > 0 || b.Value.Cmp(maxSafeValue) > 0 {
|
||||
return nil, fmt.Errorf("values too large for safe addition: %s + %s", a.Symbol, b.Symbol)
|
||||
}
|
||||
|
||||
sum := new(big.Int).Add(a.Value, b.Value)
|
||||
resultSymbol := a.Symbol
|
||||
if a.Symbol != b.Symbol {
|
||||
|
||||
@@ -17,6 +17,8 @@ const (
|
||||
ExchangeTraderJoe ExchangeType = "traderjoe"
|
||||
ExchangeRamses ExchangeType = "ramses"
|
||||
ExchangeCurve ExchangeType = "curve"
|
||||
ExchangeKyber ExchangeType = "kyber"
|
||||
ExchangeUniswapV4 ExchangeType = "uniswap_v4"
|
||||
)
|
||||
|
||||
// ExchangePricer interface for exchange-specific price calculations
|
||||
|
||||
@@ -158,11 +158,15 @@ func TestPercentageCalculations(t *testing.T) {
|
||||
|
||||
percentageFloat, _ := percentage.Value.Float64()
|
||||
|
||||
t.Logf("Calculated percentage: %.6f%%", percentageFloat)
|
||||
// Convert from raw value to actual percentage (divide by 10^decimals)
|
||||
// Since percentage has 4 decimals, divide by 10000 to get actual percentage value
|
||||
actualPercentage := percentageFloat / 10000.0
|
||||
|
||||
if percentageFloat < tc.expectedRange[0] || percentageFloat > tc.expectedRange[1] {
|
||||
t.Logf("Calculated percentage: %.6f%%", actualPercentage)
|
||||
|
||||
if actualPercentage < tc.expectedRange[0] || actualPercentage > tc.expectedRange[1] {
|
||||
t.Errorf("Percentage %.6f%% outside expected range [%.3f%%, %.3f%%]",
|
||||
percentageFloat, tc.expectedRange[0], tc.expectedRange[1])
|
||||
actualPercentage, tc.expectedRange[0], tc.expectedRange[1])
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user