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