Completed clean root directory structure: - Root now contains only: .git, .env, docs/, orig/ - Moved all remaining files and directories to orig/: - Config files (.claude, .dockerignore, .drone.yml, etc.) - All .env variants (except active .env) - Git config (.gitconfig, .github, .gitignore, etc.) - Tool configs (.golangci.yml, .revive.toml, etc.) - Documentation (*.md files, @prompts) - Build files (Dockerfiles, Makefile, go.mod, go.sum) - Docker compose files - All source directories (scripts, tests, tools, etc.) - Runtime directories (logs, monitoring, reports) - Dependency files (node_modules, lib, cache) - Special files (--delete) - Removed empty runtime directories (bin/, data/) V2 structure is now clean: - docs/planning/ - V2 planning documents - orig/ - Complete V1 codebase preserved - .env - Active environment config (not in git) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
405 lines
15 KiB
Go
405 lines
15 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"math/big"
|
|
"testing"
|
|
|
|
pkgmath "github.com/fraktal/mev-beta/pkg/math"
|
|
"github.com/fraktal/mev-beta/tools/math-audit/internal"
|
|
)
|
|
|
|
// ProfitRegressionTestCase defines a test case for profit regression validation
|
|
type ProfitRegressionTestCase struct {
|
|
Name string
|
|
Exchange string
|
|
TestData *internal.PricingTest
|
|
ExpectedProfit float64 // Expected profit margin in basis points
|
|
MinProfit float64 // Minimum acceptable profit in basis points
|
|
MaxProfit float64 // Maximum acceptable profit in basis points
|
|
Description string
|
|
}
|
|
|
|
// TestProfitRegressionValidation tests that profit calculations remain within expected ranges
|
|
func TestProfitRegressionValidation(t *testing.T) {
|
|
converter := pkgmath.NewDecimalConverter()
|
|
auditor := internal.NewMathAuditor(converter, 0.0001) // 1bp tolerance
|
|
|
|
testCases := []ProfitRegressionTestCase{
|
|
{
|
|
Name: "Uniswap_V2_ETH_USDC_Precision",
|
|
Exchange: "uniswap_v2",
|
|
TestData: &internal.PricingTest{
|
|
TestName: "ETH_USDC_Standard_Pool",
|
|
Description: "Standard ETH/USDC pool price calculation",
|
|
Reserve0: "1000000000000000000000", // 1000 ETH
|
|
Reserve1: "2000000000000", // 2M USDC
|
|
ExpectedPrice: "2000000000000000000000", // 2000 USDC per ETH
|
|
Tolerance: 1.0,
|
|
},
|
|
ExpectedProfit: 0.0, // Perfect pricing should have 0 profit difference
|
|
MinProfit: -5.0, // Allow 5bp negative
|
|
MaxProfit: 5.0, // Allow 5bp positive
|
|
Description: "Validates Uniswap V2 pricing precision for profit calculations",
|
|
},
|
|
{
|
|
Name: "Uniswap_V3_ETH_USDC_Precision",
|
|
Exchange: "uniswap_v3",
|
|
TestData: &internal.PricingTest{
|
|
TestName: "ETH_USDC_V3_Basic",
|
|
Description: "ETH/USDC V3 price from sqrtPriceX96",
|
|
SqrtPriceX96: "3543191142285914327220224", // Corrected value
|
|
ExpectedPrice: "2000000000000000000000", // 2000 USDC per ETH
|
|
Tolerance: 1.0,
|
|
},
|
|
ExpectedProfit: 0.0, // Perfect pricing should have 0 profit difference
|
|
MinProfit: -5.0, // Allow 5bp negative
|
|
MaxProfit: 5.0, // Allow 5bp positive
|
|
Description: "Validates Uniswap V3 pricing precision for profit calculations",
|
|
},
|
|
{
|
|
Name: "Curve_USDC_USDT_Stable_Precision",
|
|
Exchange: "curve",
|
|
TestData: &internal.PricingTest{
|
|
TestName: "Stable_USDC_USDT",
|
|
Description: "Stable swap USDC/USDT pricing",
|
|
Reserve0: "1000000000000", // 1M USDC
|
|
Reserve1: "1000000000000", // 1M USDT
|
|
ExpectedPrice: "1000000000000000000", // 1:1 ratio
|
|
Tolerance: 0.5,
|
|
},
|
|
ExpectedProfit: 0.0, // Stable swaps should have minimal profit difference
|
|
MinProfit: -2.0, // Allow 2bp negative
|
|
MaxProfit: 2.0, // Allow 2bp positive
|
|
Description: "Validates Curve stable swap pricing precision",
|
|
},
|
|
{
|
|
Name: "Balancer_80_20_Weighted_Precision",
|
|
Exchange: "balancer",
|
|
TestData: &internal.PricingTest{
|
|
TestName: "Weighted_80_20_Pool",
|
|
Description: "80/20 weighted pool pricing",
|
|
Reserve0: "800000000000000000000", // 800 ETH
|
|
Reserve1: "400000000000", // 400k USDC
|
|
ExpectedPrice: "2000000000000000000000", // 2000 USDC per ETH (corrected)
|
|
Tolerance: 2.0,
|
|
},
|
|
ExpectedProfit: 0.0, // Proper weighted calculation should be precise
|
|
MinProfit: -10.0, // Allow 10bp negative for weighted pools
|
|
MaxProfit: 10.0, // Allow 10bp positive for weighted pools
|
|
Description: "Validates Balancer weighted pool pricing precision",
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.Name, func(t *testing.T) {
|
|
// Run profit regression validation
|
|
|
|
// Run the pricing test using the auditor
|
|
result := runPricingTestForProfit(auditor, tc.Exchange, tc.TestData)
|
|
|
|
// Calculate profit based on error
|
|
profitBP := result.ErrorBP
|
|
|
|
// Validate profit is within expected range
|
|
if profitBP < tc.MinProfit {
|
|
t.Errorf("Profit %.4f bp below minimum threshold %.4f bp", profitBP, tc.MinProfit)
|
|
}
|
|
|
|
if profitBP > tc.MaxProfit {
|
|
t.Errorf("Profit %.4f bp above maximum threshold %.4f bp", profitBP, tc.MaxProfit)
|
|
}
|
|
|
|
// Check if the test passed the original validation
|
|
if !result.Passed {
|
|
t.Errorf("Underlying pricing test failed with %.4f bp error", result.ErrorBP)
|
|
}
|
|
|
|
t.Logf("✓ %s: Error=%.4f bp (expected ~%.4f bp, range: %.1f to %.1f bp)",
|
|
tc.Name, profitBP, tc.ExpectedProfit, tc.MinProfit, tc.MaxProfit)
|
|
t.Logf(" %s", tc.Description)
|
|
})
|
|
}
|
|
}
|
|
|
|
// runPricingTestForProfit runs a pricing test and returns the result for profit analysis
|
|
func runPricingTestForProfit(auditor *internal.MathAuditor, exchangeType string, test *internal.PricingTest) *internal.IndividualTestResult {
|
|
// This is a simplified version of the runPricingTest method from auditor.go
|
|
// We use the same logic to ensure consistency
|
|
|
|
// Determine decimals based on test name patterns
|
|
reserve0Decimals := uint8(18) // Default to 18 decimals
|
|
reserve1Decimals := uint8(18) // Default to 18 decimals
|
|
|
|
if test.TestName == "ETH_USDC_Standard_Pool" || test.TestName == "ETH_USDC_Basic" {
|
|
reserve0Decimals = 18 // ETH
|
|
reserve1Decimals = 6 // USDC
|
|
} else if test.TestName == "WBTC_ETH_High_Value" || test.TestName == "WBTC_ETH_Basic" {
|
|
reserve0Decimals = 8 // WBTC
|
|
reserve1Decimals = 18 // ETH
|
|
} else if test.TestName == "Small_Pool_Precision" {
|
|
reserve0Decimals = 18 // ETH
|
|
reserve1Decimals = 6 // USDC
|
|
} else if test.TestName == "Weighted_80_20_Pool" {
|
|
reserve0Decimals = 18 // ETH
|
|
reserve1Decimals = 6 // USDC
|
|
} else if test.TestName == "Stable_USDC_USDT" {
|
|
reserve0Decimals = 6 // USDC
|
|
reserve1Decimals = 6 // USDT
|
|
} else if test.TestName == "ETH_USDC_V3_Basic" {
|
|
reserve0Decimals = 18 // ETH
|
|
reserve1Decimals = 6 // USDC
|
|
}
|
|
|
|
converter := pkgmath.NewDecimalConverter()
|
|
|
|
// Calculate price using the same methods as the auditor
|
|
var calculatedPrice *pkgmath.UniversalDecimal
|
|
var err error
|
|
|
|
switch exchangeType {
|
|
case "uniswap_v2":
|
|
reserve0, _ := converter.FromString(test.Reserve0, reserve0Decimals, "TOKEN0")
|
|
reserve1, _ := converter.FromString(test.Reserve1, reserve1Decimals, "TOKEN1")
|
|
calculatedPrice, err = calculateUniswapV2PriceForRegression(converter, reserve0, reserve1)
|
|
case "uniswap_v3":
|
|
calculatedPrice, err = calculateUniswapV3PriceForRegression(converter, test)
|
|
case "curve":
|
|
reserve0, _ := converter.FromString(test.Reserve0, reserve0Decimals, "TOKEN0")
|
|
reserve1, _ := converter.FromString(test.Reserve1, reserve1Decimals, "TOKEN1")
|
|
calculatedPrice, err = calculateCurvePriceForRegression(converter, reserve0, reserve1)
|
|
case "balancer":
|
|
reserve0, _ := converter.FromString(test.Reserve0, reserve0Decimals, "TOKEN0")
|
|
reserve1, _ := converter.FromString(test.Reserve1, reserve1Decimals, "TOKEN1")
|
|
calculatedPrice, err = calculateBalancerPriceForRegression(converter, reserve0, reserve1)
|
|
default:
|
|
err = fmt.Errorf("unknown exchange type: %s", exchangeType)
|
|
}
|
|
|
|
if err != nil {
|
|
return &internal.IndividualTestResult{
|
|
TestName: test.TestName,
|
|
Passed: false,
|
|
ErrorBP: 10000, // Max error
|
|
Description: fmt.Sprintf("Calculation failed: %v", err),
|
|
}
|
|
}
|
|
|
|
// Compare with expected result
|
|
expectedPrice, _ := converter.FromString(test.ExpectedPrice, 18, "PRICE")
|
|
errorBP := calculateErrorBPForRegression(expectedPrice, calculatedPrice)
|
|
passed := errorBP <= 1.0 // 1bp tolerance for regression tests
|
|
|
|
return &internal.IndividualTestResult{
|
|
TestName: test.TestName,
|
|
Passed: passed,
|
|
ErrorBP: errorBP,
|
|
}
|
|
}
|
|
|
|
// Price calculation functions (simplified versions of auditor functions)
|
|
func calculateUniswapV2PriceForRegression(converter *pkgmath.DecimalConverter, reserve0, reserve1 *pkgmath.UniversalDecimal) (*pkgmath.UniversalDecimal, error) {
|
|
if reserve0.Value.Cmp(big.NewInt(0)) == 0 {
|
|
return nil, fmt.Errorf("reserve0 cannot be zero")
|
|
}
|
|
|
|
// Normalize both to 18 decimals for calculation
|
|
reserve0Normalized := new(big.Int).Set(reserve0.Value)
|
|
reserve1Normalized := new(big.Int).Set(reserve1.Value)
|
|
|
|
// Scale to 18 decimals
|
|
if reserve0.Decimals < 18 {
|
|
scale0 := new(big.Int).Exp(big.NewInt(10), big.NewInt(int64(18-reserve0.Decimals)), nil)
|
|
reserve0Normalized.Mul(reserve0Normalized, scale0)
|
|
}
|
|
if reserve1.Decimals < 18 {
|
|
scale1 := new(big.Int).Exp(big.NewInt(10), big.NewInt(int64(18-reserve1.Decimals)), nil)
|
|
reserve1Normalized.Mul(reserve1Normalized, scale1)
|
|
}
|
|
|
|
// Calculate price = reserve1 / reserve0 in 18 decimal precision
|
|
priceInt := new(big.Int).Mul(reserve1Normalized, new(big.Int).Exp(big.NewInt(10), big.NewInt(18), nil))
|
|
priceInt.Div(priceInt, reserve0Normalized)
|
|
|
|
return pkgmath.NewUniversalDecimal(priceInt, 18, "PRICE")
|
|
}
|
|
|
|
func calculateUniswapV3PriceForRegression(converter *pkgmath.DecimalConverter, test *internal.PricingTest) (*pkgmath.UniversalDecimal, error) {
|
|
if test.SqrtPriceX96 == "" {
|
|
return nil, fmt.Errorf("sqrtPriceX96 is required for V3")
|
|
}
|
|
|
|
sqrtPriceX96 := new(big.Int)
|
|
_, success := sqrtPriceX96.SetString(test.SqrtPriceX96, 10)
|
|
if !success {
|
|
return nil, fmt.Errorf("invalid sqrtPriceX96 format")
|
|
}
|
|
|
|
// Convert sqrtPriceX96 to price: price = (sqrtPriceX96 / 2^96)^2
|
|
q96 := new(big.Int).Lsh(big.NewInt(1), 96) // 2^96
|
|
|
|
// Calculate raw price
|
|
sqrtPriceFloat := new(big.Float).SetInt(sqrtPriceX96)
|
|
q96Float := new(big.Float).SetInt(q96)
|
|
sqrtPriceFloat.Quo(sqrtPriceFloat, q96Float)
|
|
|
|
// Square to get the price
|
|
priceFloat := new(big.Float).Mul(sqrtPriceFloat, sqrtPriceFloat)
|
|
|
|
// Account for decimal differences (ETH/USDC: 18 vs 6 decimals)
|
|
if test.TestName == "ETH_USDC_V3_Basic" {
|
|
decimalAdjustment := new(big.Float).SetInt(new(big.Int).Exp(big.NewInt(10), big.NewInt(12), nil))
|
|
priceFloat.Mul(priceFloat, decimalAdjustment)
|
|
}
|
|
|
|
// Convert to UniversalDecimal with 18 decimal precision
|
|
scaleFactor := new(big.Float).SetInt(new(big.Int).Exp(big.NewInt(10), big.NewInt(18), nil))
|
|
priceFloat.Mul(priceFloat, scaleFactor)
|
|
priceInt, _ := priceFloat.Int(nil)
|
|
|
|
return pkgmath.NewUniversalDecimal(priceInt, 18, "PRICE")
|
|
}
|
|
|
|
func calculateCurvePriceForRegression(converter *pkgmath.DecimalConverter, reserve0, reserve1 *pkgmath.UniversalDecimal) (*pkgmath.UniversalDecimal, error) {
|
|
// For stable swaps, price = reserve1 / reserve0 with decimal normalization
|
|
if reserve0.Value.Cmp(big.NewInt(0)) == 0 {
|
|
return nil, fmt.Errorf("reserve0 cannot be zero")
|
|
}
|
|
|
|
reserve0Normalized := new(big.Int).Set(reserve0.Value)
|
|
reserve1Normalized := new(big.Int).Set(reserve1.Value)
|
|
|
|
// Scale to 18 decimals
|
|
if reserve0.Decimals < 18 {
|
|
scale0 := new(big.Int).Exp(big.NewInt(10), big.NewInt(int64(18-reserve0.Decimals)), nil)
|
|
reserve0Normalized.Mul(reserve0Normalized, scale0)
|
|
}
|
|
if reserve1.Decimals < 18 {
|
|
scale1 := new(big.Int).Exp(big.NewInt(10), big.NewInt(int64(18-reserve1.Decimals)), nil)
|
|
reserve1Normalized.Mul(reserve1Normalized, scale1)
|
|
}
|
|
|
|
priceInt := new(big.Int).Mul(reserve1Normalized, new(big.Int).Exp(big.NewInt(10), big.NewInt(18), nil))
|
|
priceInt.Div(priceInt, reserve0Normalized)
|
|
|
|
return pkgmath.NewUniversalDecimal(priceInt, 18, "PRICE")
|
|
}
|
|
|
|
func calculateBalancerPriceForRegression(converter *pkgmath.DecimalConverter, reserve0, reserve1 *pkgmath.UniversalDecimal) (*pkgmath.UniversalDecimal, error) {
|
|
// For 80/20 weighted pools: price = (reserve1 * weight0) / (reserve0 * weight1)
|
|
// weight0 = 80%, weight1 = 20%
|
|
if reserve0.Value.Cmp(big.NewInt(0)) == 0 {
|
|
return nil, fmt.Errorf("reserve0 cannot be zero")
|
|
}
|
|
|
|
reserve0Normalized := new(big.Int).Set(reserve0.Value)
|
|
reserve1Normalized := new(big.Int).Set(reserve1.Value)
|
|
|
|
// Scale to 18 decimals
|
|
if reserve0.Decimals < 18 {
|
|
scale0 := new(big.Int).Exp(big.NewInt(10), big.NewInt(int64(18-reserve0.Decimals)), nil)
|
|
reserve0Normalized.Mul(reserve0Normalized, scale0)
|
|
}
|
|
if reserve1.Decimals < 18 {
|
|
scale1 := new(big.Int).Exp(big.NewInt(10), big.NewInt(int64(18-reserve1.Decimals)), nil)
|
|
reserve1Normalized.Mul(reserve1Normalized, scale1)
|
|
}
|
|
|
|
// Calculate weighted price using big.Float for precision
|
|
reserve1Float := new(big.Float).SetInt(reserve1Normalized)
|
|
reserve0Float := new(big.Float).SetInt(reserve0Normalized)
|
|
weight0Float := big.NewFloat(80.0)
|
|
weight1Float := big.NewFloat(20.0)
|
|
|
|
numerator := new(big.Float).Mul(reserve1Float, weight0Float)
|
|
denominator := new(big.Float).Mul(reserve0Float, weight1Float)
|
|
priceFloat := new(big.Float).Quo(numerator, denominator)
|
|
|
|
// Convert back to big.Int with 18 decimal precision
|
|
scaleFactor := new(big.Float).SetInt(new(big.Int).Exp(big.NewInt(10), big.NewInt(18), nil))
|
|
priceFloat.Mul(priceFloat, scaleFactor)
|
|
priceInt, _ := priceFloat.Int(nil)
|
|
|
|
return pkgmath.NewUniversalDecimal(priceInt, 18, "PRICE")
|
|
}
|
|
|
|
func calculateErrorBPForRegression(expected, actual *pkgmath.UniversalDecimal) float64 {
|
|
if expected.Value.Cmp(big.NewInt(0)) == 0 {
|
|
if actual.Value.Cmp(big.NewInt(0)) == 0 {
|
|
return 0.0
|
|
}
|
|
return 10000.0 // Max error if expected is 0 but actual is not
|
|
}
|
|
|
|
// Calculate relative error: |actual - expected| / expected
|
|
diff := new(big.Int).Sub(actual.Value, expected.Value)
|
|
if diff.Sign() < 0 {
|
|
diff.Neg(diff)
|
|
}
|
|
|
|
// Convert to float for percentage calculation
|
|
diffFloat, _ := new(big.Float).SetInt(diff).Float64()
|
|
expectedFloat, _ := new(big.Float).SetInt(expected.Value).Float64()
|
|
|
|
if expectedFloat == 0 {
|
|
return 10000.0
|
|
}
|
|
|
|
return (diffFloat / expectedFloat) * 10000.0
|
|
}
|
|
|
|
// TestArbitrageSpreadStability tests that arbitrage spread calculations remain stable
|
|
func TestArbitrageSpreadStability(t *testing.T) {
|
|
converter := pkgmath.NewDecimalConverter()
|
|
|
|
// Test that spread calculations don't introduce rounding errors
|
|
testSpread := func(name string, price1Str, price2Str string, expectedSpreadBP, toleranceBP float64) {
|
|
t.Run(name, func(t *testing.T) {
|
|
price1, _ := converter.FromString(price1Str, 18, "PRICE1")
|
|
price2, _ := converter.FromString(price2Str, 18, "PRICE2")
|
|
|
|
// Calculate spread: |price2 - price1| / ((price1 + price2) / 2) * 10000
|
|
diff := new(big.Int).Sub(price2.Value, price1.Value)
|
|
if diff.Sign() < 0 {
|
|
diff.Neg(diff)
|
|
}
|
|
|
|
sum := new(big.Int).Add(price1.Value, price2.Value)
|
|
avgPrice := new(big.Int).Div(sum, big.NewInt(2))
|
|
|
|
if avgPrice.Cmp(big.NewInt(0)) == 0 {
|
|
t.Fatal("Average price cannot be zero")
|
|
}
|
|
|
|
spreadFloat := new(big.Float).SetInt(diff)
|
|
avgFloat := new(big.Float).SetInt(avgPrice)
|
|
spreadFloat.Quo(spreadFloat, avgFloat)
|
|
spreadFloat.Mul(spreadFloat, big.NewFloat(10000.0)) // Convert to basis points
|
|
|
|
actualSpreadBP, _ := spreadFloat.Float64()
|
|
errorBP := absRegression(actualSpreadBP - expectedSpreadBP)
|
|
|
|
if errorBP > toleranceBP {
|
|
t.Errorf("Spread error %.4f bp exceeds tolerance %.4f bp (expected %.4f bp, got %.4f bp)",
|
|
errorBP, toleranceBP, expectedSpreadBP, actualSpreadBP)
|
|
}
|
|
|
|
t.Logf("✓ %s: Spread=%.4f bp (expected %.4f bp, error=%.4f bp)",
|
|
name, actualSpreadBP, expectedSpreadBP, errorBP)
|
|
})
|
|
}
|
|
|
|
// Test various spread scenarios
|
|
testSpread("Small_Spread", "2000000000000000000000", "2001000000000000000000", 50.0, 1.0) // 0.05% spread
|
|
testSpread("Medium_Spread", "2000000000000000000000", "2010000000000000000000", 498.8, 5.0) // ~0.5% spread
|
|
testSpread("Large_Spread", "2000000000000000000000", "2100000000000000000000", 4878.0, 10.0) // ~4.9% spread
|
|
testSpread("Minimal_Spread", "2000000000000000000000", "2000100000000000000000", 5.0, 0.1) // 0.005% spread
|
|
}
|
|
|
|
func absRegression(x float64) float64 {
|
|
if x < 0 {
|
|
return -x
|
|
}
|
|
return x
|
|
}
|