Restructured project for V2 refactor: **Structure Changes:** - Moved all V1 code to orig/ folder (preserved with git mv) - Created docs/planning/ directory - Added orig/README_V1.md explaining V1 preservation **Planning Documents:** - 00_V2_MASTER_PLAN.md: Complete architecture overview - Executive summary of critical V1 issues - High-level component architecture diagrams - 5-phase implementation roadmap - Success metrics and risk mitigation - 07_TASK_BREAKDOWN.md: Atomic task breakdown - 99+ hours of detailed tasks - Every task < 2 hours (atomic) - Clear dependencies and success criteria - Organized by implementation phase **V2 Key Improvements:** - Per-exchange parsers (factory pattern) - Multi-layer strict validation - Multi-index pool cache - Background validation pipeline - Comprehensive observability **Critical Issues Addressed:** - Zero address tokens (strict validation + cache enrichment) - Parsing accuracy (protocol-specific parsers) - No audit trail (background validation channel) - Inefficient lookups (multi-index cache) - Stats disconnection (event-driven metrics) Next Steps: 1. Review planning documents 2. Begin Phase 1: Foundation (P1-001 through P1-010) 3. Implement parsers in Phase 2 4. Build cache system in Phase 3 5. Add validation pipeline in Phase 4 6. Migrate and test in Phase 5 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1011 lines
32 KiB
Go
1011 lines
32 KiB
Go
package math
|
|
|
|
import (
|
|
"fmt"
|
|
"math"
|
|
"math/big"
|
|
)
|
|
|
|
// ExchangeMath provides exchange-specific mathematical calculations
|
|
type ExchangeMath interface {
|
|
CalculateAmountOut(amountIn, reserveIn, reserveOut *big.Int, fee uint32) (*big.Int, error)
|
|
CalculateAmountIn(amountOut, reserveIn, reserveOut *big.Int, fee uint32) (*big.Int, error)
|
|
CalculatePriceImpact(amountIn, reserveIn, reserveOut *big.Int) (float64, error)
|
|
GetSpotPrice(reserveIn, reserveOut *big.Int) (*big.Float, error)
|
|
CalculateSlippage(expectedOut, actualOut *big.Int) (float64, error)
|
|
}
|
|
|
|
// UniswapV2Math implements Uniswap V2 constant product formula
|
|
type UniswapV2Math struct{}
|
|
|
|
// UniswapV3Math implements Uniswap V3 concentrated liquidity math
|
|
type UniswapV3Math struct{}
|
|
|
|
// CurveMath implements Curve Finance StableSwap math
|
|
type CurveMath struct{}
|
|
|
|
// BalancerMath implements Balancer weighted pool math
|
|
type BalancerMath struct{}
|
|
|
|
// ConstantSumMath implements basic constant sum AMM math
|
|
type ConstantSumMath struct{}
|
|
|
|
// ========== Uniswap V2 Math ==========
|
|
|
|
// NewUniswapV2Math creates a new Uniswap V2 math calculator
|
|
func NewUniswapV2Math() *UniswapV2Math {
|
|
return &UniswapV2Math{}
|
|
}
|
|
|
|
// CalculateAmountOut calculates output amount for Uniswap V2 (x * y = k)
|
|
func (u *UniswapV2Math) CalculateAmountOut(amountIn, reserveIn, reserveOut *big.Int, fee uint32) (*big.Int, error) {
|
|
if amountIn.Sign() <= 0 || reserveIn.Sign() <= 0 || reserveOut.Sign() <= 0 {
|
|
return nil, fmt.Errorf("invalid amounts: amountIn=%s, reserveIn=%s, reserveOut=%s",
|
|
amountIn.String(), reserveIn.String(), reserveOut.String())
|
|
}
|
|
|
|
// Calculate fee (default 3000 = 0.3%)
|
|
if fee == 0 {
|
|
fee = 3000
|
|
}
|
|
|
|
// amountInWithFee = amountIn * (10000 - fee)
|
|
feeFactor := big.NewInt(int64(10000 - fee))
|
|
amountInWithFee := new(big.Int).Mul(amountIn, feeFactor)
|
|
|
|
// numerator = amountInWithFee * reserveOut
|
|
numerator := new(big.Int).Mul(amountInWithFee, reserveOut)
|
|
|
|
// denominator = reserveIn * 10000 + amountInWithFee
|
|
denominator := new(big.Int).Mul(reserveIn, big.NewInt(10000))
|
|
denominator.Add(denominator, amountInWithFee)
|
|
|
|
// Check for division by zero
|
|
if denominator.Sign() == 0 {
|
|
return nil, fmt.Errorf("division by zero in amountOut calculation")
|
|
}
|
|
|
|
// amountOut = numerator / denominator
|
|
amountOut := new(big.Int).Div(numerator, denominator)
|
|
|
|
return amountOut, nil
|
|
}
|
|
|
|
// CalculateAmountIn calculates input amount for Uniswap V2
|
|
func (u *UniswapV2Math) CalculateAmountIn(amountOut, reserveIn, reserveOut *big.Int, fee uint32) (*big.Int, error) {
|
|
if amountOut.Sign() <= 0 || reserveIn.Sign() <= 0 || reserveOut.Sign() <= 0 {
|
|
return nil, fmt.Errorf("invalid amounts")
|
|
}
|
|
|
|
if amountOut.Cmp(reserveOut) >= 0 {
|
|
return nil, fmt.Errorf("insufficient liquidity")
|
|
}
|
|
|
|
if fee == 0 {
|
|
fee = 3000
|
|
}
|
|
|
|
// numerator = reserveIn * amountOut * 10000
|
|
numerator := new(big.Int).Mul(reserveIn, amountOut)
|
|
numerator.Mul(numerator, big.NewInt(10000))
|
|
|
|
// denominator = (reserveOut - amountOut) * (10000 - fee)
|
|
denominator := new(big.Int).Sub(reserveOut, amountOut)
|
|
|
|
// Check if the calculation is valid (amountOut must be less than reserveOut)
|
|
if denominator.Sign() <= 0 {
|
|
return nil, fmt.Errorf("invalid swap: amountOut (%s) >= reserveOut (%s)", amountOut.String(), reserveOut.String())
|
|
}
|
|
|
|
denominator.Mul(denominator, big.NewInt(int64(10000-fee)))
|
|
|
|
// Check for division by zero
|
|
if denominator.Sign() == 0 {
|
|
return nil, fmt.Errorf("division by zero in amountIn calculation")
|
|
}
|
|
|
|
// amountIn = numerator / denominator + 1 (round up)
|
|
amountIn := new(big.Int).Div(numerator, denominator)
|
|
amountIn.Add(amountIn, big.NewInt(1))
|
|
|
|
return amountIn, nil
|
|
}
|
|
|
|
// CalculatePriceImpact calculates price impact for Uniswap V2
|
|
func (u *UniswapV2Math) CalculatePriceImpact(amountIn, reserveIn, reserveOut *big.Int) (float64, error) {
|
|
// Check for nil pointers first
|
|
if amountIn == nil || reserveIn == nil || reserveOut == nil {
|
|
return 0, fmt.Errorf("nil pointer encountered")
|
|
}
|
|
|
|
if amountIn.Sign() <= 0 || reserveIn.Sign() <= 0 || reserveOut.Sign() <= 0 {
|
|
return 0, fmt.Errorf("invalid amounts")
|
|
}
|
|
|
|
// Price before = reserveOut / reserveIn
|
|
priceBefore := new(big.Float).Quo(new(big.Float).SetInt(reserveOut), new(big.Float).SetInt(reserveIn))
|
|
|
|
// Calculate amount out
|
|
amountOut, err := u.CalculateAmountOut(amountIn, reserveIn, reserveOut, 3000)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
// New reserves after swap
|
|
newReserveIn := new(big.Int).Add(reserveIn, amountIn)
|
|
newReserveOut := new(big.Int).Sub(reserveOut, amountOut)
|
|
|
|
// Price after = newReserveOut / newReserveIn
|
|
priceAfter := new(big.Float).Quo(new(big.Float).SetInt(newReserveOut), new(big.Float).SetInt(newReserveIn))
|
|
|
|
// Price impact = (priceBefore - priceAfter) / priceBefore
|
|
impact := new(big.Float).Sub(priceBefore, priceAfter)
|
|
impact.Quo(impact, priceBefore)
|
|
|
|
impactFloat, _ := impact.Float64()
|
|
return math.Abs(impactFloat), nil
|
|
}
|
|
|
|
// GetSpotPrice returns current spot price for Uniswap V2
|
|
func (u *UniswapV2Math) GetSpotPrice(reserveIn, reserveOut *big.Int) (*big.Float, error) {
|
|
if reserveIn.Sign() <= 0 || reserveOut.Sign() <= 0 {
|
|
return nil, fmt.Errorf("invalid reserves")
|
|
}
|
|
|
|
return new(big.Float).Quo(new(big.Float).SetInt(reserveOut), new(big.Float).SetInt(reserveIn)), nil
|
|
}
|
|
|
|
// CalculateSlippage calculates slippage between expected and actual output
|
|
func (u *UniswapV2Math) CalculateSlippage(expectedOut, actualOut *big.Int) (float64, error) {
|
|
if expectedOut.Sign() <= 0 || actualOut.Sign() <= 0 {
|
|
return 0, fmt.Errorf("invalid amounts")
|
|
}
|
|
|
|
// Slippage = (expectedOut - actualOut) / expectedOut
|
|
diff := new(big.Float).Sub(new(big.Float).SetInt(expectedOut), new(big.Float).SetInt(actualOut))
|
|
slippage := new(big.Float).Quo(diff, new(big.Float).SetInt(expectedOut))
|
|
|
|
slippageFloat, _ := slippage.Float64()
|
|
return math.Abs(slippageFloat), nil
|
|
}
|
|
|
|
// ========== Uniswap V3 Math ==========
|
|
|
|
// NewUniswapV3Math creates a new Uniswap V3 math calculator
|
|
func NewUniswapV3Math() *UniswapV3Math {
|
|
return &UniswapV3Math{}
|
|
}
|
|
|
|
// CalculateAmountOut calculates output for Uniswap V3 concentrated liquidity
|
|
func (u *UniswapV3Math) CalculateAmountOut(amountIn, sqrtPriceX96, liquidity *big.Int, fee uint32) (*big.Int, error) {
|
|
if amountIn.Sign() <= 0 || sqrtPriceX96.Sign() <= 0 || liquidity.Sign() <= 0 {
|
|
return nil, fmt.Errorf("invalid parameters")
|
|
}
|
|
|
|
if fee == 0 {
|
|
fee = 3000 // Default 0.3%
|
|
}
|
|
|
|
// Apply fee: amountInWithFee = amountIn * (1000000 - fee) / 1000000
|
|
feeFactor := big.NewInt(int64(1000000 - fee))
|
|
amountInWithFee := new(big.Int).Mul(amountIn, feeFactor)
|
|
amountInWithFee.Div(amountInWithFee, big.NewInt(1000000))
|
|
|
|
// Simplified V3 calculation (for exact implementation, need tick math)
|
|
// This approximates the swap for small amounts
|
|
|
|
// Calculate price change
|
|
Q96 := new(big.Int).Lsh(big.NewInt(1), 96)
|
|
priceChange := new(big.Int).Mul(amountInWithFee, Q96)
|
|
priceChange.Div(priceChange, liquidity)
|
|
|
|
// New sqrt price
|
|
newSqrtPriceX96 := new(big.Int).Add(sqrtPriceX96, priceChange)
|
|
|
|
// Calculate amount out using price difference
|
|
priceDiff := new(big.Int).Sub(newSqrtPriceX96, sqrtPriceX96)
|
|
amountOut := new(big.Int).Mul(liquidity, priceDiff)
|
|
amountOut.Div(amountOut, sqrtPriceX96)
|
|
|
|
return amountOut, nil
|
|
}
|
|
|
|
// CalculateAmountIn calculates input for Uniswap V3
|
|
func (u *UniswapV3Math) CalculateAmountIn(amountOut, sqrtPriceX96, liquidity *big.Int, fee uint32) (*big.Int, error) {
|
|
if amountOut.Sign() <= 0 || sqrtPriceX96.Sign() <= 0 || liquidity.Sign() <= 0 {
|
|
return nil, fmt.Errorf("invalid parameters")
|
|
}
|
|
|
|
// Simplified reverse calculation
|
|
Q96 := new(big.Int).Lsh(big.NewInt(1), 96)
|
|
|
|
// Calculate required price change
|
|
priceChange := new(big.Int).Mul(amountOut, sqrtPriceX96)
|
|
priceChange.Div(priceChange, liquidity)
|
|
|
|
// Calculate amount in before fees
|
|
amountInBeforeFee := new(big.Int).Mul(priceChange, liquidity)
|
|
amountInBeforeFee.Div(amountInBeforeFee, Q96)
|
|
|
|
// Apply fee
|
|
if fee == 0 {
|
|
fee = 3000
|
|
}
|
|
|
|
feeFactor := big.NewInt(int64(1000000 - fee))
|
|
amountIn := new(big.Int).Mul(amountInBeforeFee, big.NewInt(1000000))
|
|
amountIn.Div(amountIn, feeFactor)
|
|
|
|
return amountIn, nil
|
|
}
|
|
|
|
// CalculatePriceImpact calculates price impact for Uniswap V3
|
|
func (u *UniswapV3Math) CalculatePriceImpact(amountIn, sqrtPriceX96, liquidity *big.Int) (float64, error) {
|
|
// Check for nil pointers first
|
|
if amountIn == nil || sqrtPriceX96 == nil || liquidity == nil {
|
|
return 0, fmt.Errorf("nil pointer encountered")
|
|
}
|
|
|
|
if amountIn.Sign() <= 0 || sqrtPriceX96.Sign() <= 0 || liquidity.Sign() <= 0 {
|
|
return 0, fmt.Errorf("invalid parameters")
|
|
}
|
|
|
|
// Calculate new sqrt price after swap
|
|
Q96 := new(big.Int).Lsh(big.NewInt(1), 96)
|
|
priceChange := new(big.Int).Mul(amountIn, Q96)
|
|
priceChange.Div(priceChange, liquidity)
|
|
|
|
newSqrtPriceX96 := new(big.Int).Add(sqrtPriceX96, priceChange)
|
|
|
|
// Convert to regular prices using big.Float for precision
|
|
Q96Float := new(big.Float).SetInt(Q96)
|
|
|
|
// priceBefore = (sqrtPriceX96 / 2^96)^2
|
|
priceBefore := new(big.Float).SetInt(sqrtPriceX96)
|
|
priceBefore.Quo(priceBefore, Q96Float)
|
|
priceBefore.Mul(priceBefore, priceBefore)
|
|
|
|
// priceAfter = (newSqrtPriceX96 / 2^96)^2
|
|
priceAfter := new(big.Float).SetInt(newSqrtPriceX96)
|
|
priceAfter.Quo(priceAfter, Q96Float)
|
|
priceAfter.Mul(priceAfter, priceAfter)
|
|
|
|
// Check if priceBefore is zero or very small
|
|
if priceBefore.Sign() == 0 {
|
|
return 0, fmt.Errorf("price before is zero - invalid calculation")
|
|
}
|
|
|
|
// Check if priceBefore is too small (less than 1e-18)
|
|
minPrice := big.NewFloat(1e-18)
|
|
if priceBefore.Cmp(minPrice) < 0 {
|
|
return 0, fmt.Errorf("price too small for reliable calculation")
|
|
}
|
|
|
|
// Calculate impact = (priceAfter - priceBefore) / priceBefore
|
|
impact := new(big.Float).Sub(priceAfter, priceBefore)
|
|
impact.Quo(impact, priceBefore)
|
|
|
|
impactFloat, _ := impact.Float64()
|
|
return math.Abs(impactFloat), nil
|
|
}
|
|
|
|
// GetSpotPrice returns current spot price for Uniswap V3
|
|
func (u *UniswapV3Math) GetSpotPrice(sqrtPriceX96, _ *big.Int) (*big.Float, error) {
|
|
if sqrtPriceX96.Sign() <= 0 {
|
|
return nil, fmt.Errorf("invalid sqrt price")
|
|
}
|
|
|
|
// Price = (sqrtPriceX96 / 2^96)^2
|
|
Q96 := new(big.Int).Lsh(big.NewInt(1), 96)
|
|
price := new(big.Int).Mul(sqrtPriceX96, sqrtPriceX96)
|
|
price.Div(price, new(big.Int).Mul(Q96, Q96))
|
|
|
|
return new(big.Float).SetInt(price), nil
|
|
}
|
|
|
|
// CalculateSlippage calculates slippage for Uniswap V3
|
|
func (u *UniswapV3Math) CalculateSlippage(expectedOut, actualOut *big.Int) (float64, error) {
|
|
if expectedOut.Sign() <= 0 || actualOut.Sign() <= 0 {
|
|
return 0, fmt.Errorf("invalid amounts")
|
|
}
|
|
|
|
diff := new(big.Float).Sub(new(big.Float).SetInt(expectedOut), new(big.Float).SetInt(actualOut))
|
|
slippage := new(big.Float).Quo(diff, new(big.Float).SetInt(expectedOut))
|
|
|
|
slippageFloat, _ := slippage.Float64()
|
|
return math.Abs(slippageFloat), nil
|
|
}
|
|
|
|
// ========== Curve Finance Math ==========
|
|
|
|
// NewCurveMath creates a new Curve math calculator
|
|
func NewCurveMath() *CurveMath {
|
|
return &CurveMath{}
|
|
}
|
|
|
|
// CalculateAmountOut calculates output for Curve StableSwap
|
|
func (c *CurveMath) CalculateAmountOut(amountIn, balance0, balance1 *big.Int, fee uint32) (*big.Int, error) {
|
|
if amountIn.Sign() <= 0 || balance0.Sign() <= 0 || balance1.Sign() <= 0 {
|
|
return nil, fmt.Errorf("invalid amounts")
|
|
}
|
|
|
|
// Simplified Curve calculation (A = 100 for stable pools)
|
|
A := big.NewInt(100)
|
|
|
|
// Calculate D (total deposit)
|
|
D := c.calculateD(balance0, balance1, A)
|
|
|
|
// New balance after adding amountIn
|
|
newBalance0 := new(big.Int).Add(balance0, amountIn)
|
|
|
|
// Calculate new balance1 using Curve formula
|
|
newBalance1 := c.getY(newBalance0, D, A)
|
|
|
|
// Amount out = balance1 - newBalance1
|
|
amountOut := new(big.Int).Sub(balance1, newBalance1)
|
|
|
|
// Apply fee
|
|
if fee == 0 {
|
|
fee = 400 // Default 0.04%
|
|
}
|
|
|
|
feeAmount := new(big.Int).Mul(amountOut, big.NewInt(int64(fee)))
|
|
feeAmount.Div(feeAmount, big.NewInt(1000000))
|
|
|
|
amountOut.Sub(amountOut, feeAmount)
|
|
|
|
return amountOut, nil
|
|
}
|
|
|
|
// calculateD calculates the D invariant for Curve
|
|
func (c *CurveMath) calculateD(balance0, balance1, A *big.Int) *big.Int {
|
|
// Simplified D calculation for 2-coin pool
|
|
// D = 2 * sqrt(x * y) for stable coins (approximation)
|
|
|
|
sum := new(big.Int).Add(balance0, balance1)
|
|
product := new(big.Int).Mul(balance0, balance1)
|
|
|
|
// Newton's method approximation for D
|
|
D := new(big.Int).Set(sum)
|
|
|
|
for i := 0; i < 10; i++ { // 10 iterations for convergence
|
|
// Calculate new D
|
|
numerator := new(big.Int).Mul(product, big.NewInt(4))
|
|
denominator := new(big.Int).Mul(D, D)
|
|
|
|
if denominator.Sign() == 0 {
|
|
break
|
|
}
|
|
|
|
ratio := new(big.Int).Div(numerator, denominator)
|
|
newD := new(big.Int).Add(D, ratio)
|
|
newD.Div(newD, big.NewInt(2))
|
|
|
|
// Check convergence
|
|
diff := new(big.Int).Sub(newD, D)
|
|
if diff.CmpAbs(big.NewInt(1)) <= 0 {
|
|
break
|
|
}
|
|
|
|
D = newD
|
|
}
|
|
|
|
return D
|
|
}
|
|
|
|
// getY calculates the new balance using Curve formula
|
|
func (c *CurveMath) getY(newX, D, A *big.Int) *big.Int {
|
|
// Simplified calculation for 2-coin pool
|
|
// Solve for y in the Curve invariant equation
|
|
|
|
// For stable coins, approximately: y = D - x
|
|
y := new(big.Int).Sub(D, newX)
|
|
|
|
// Ensure positive result
|
|
if y.Sign() <= 0 {
|
|
y = big.NewInt(1)
|
|
}
|
|
|
|
return y
|
|
}
|
|
|
|
// CalculateAmountIn calculates input for Curve
|
|
func (c *CurveMath) CalculateAmountIn(amountOut, balance0, balance1 *big.Int, fee uint32) (*big.Int, error) {
|
|
// Reverse calculation - simplified
|
|
A := big.NewInt(100)
|
|
D := c.calculateD(balance0, balance1, A)
|
|
|
|
// Calculate new balance1 after removing amountOut
|
|
newBalance1 := new(big.Int).Sub(balance1, amountOut)
|
|
|
|
// Calculate required balance0
|
|
newBalance0 := c.getY(newBalance1, D, A)
|
|
|
|
// Amount in = newBalance0 - balance0
|
|
amountIn := new(big.Int).Sub(newBalance0, balance0)
|
|
|
|
// Apply fee
|
|
if fee == 0 {
|
|
fee = 400
|
|
}
|
|
|
|
feeMultiplier := big.NewInt(int64(1000000 + fee))
|
|
amountIn.Mul(amountIn, feeMultiplier)
|
|
amountIn.Div(amountIn, big.NewInt(1000000))
|
|
|
|
return amountIn, nil
|
|
}
|
|
|
|
// CalculatePriceImpact calculates price impact for Curve
|
|
func (c *CurveMath) CalculatePriceImpact(amountIn, balance0, balance1 *big.Int) (float64, error) {
|
|
// Check for nil pointers first
|
|
if amountIn == nil || balance0 == nil || balance1 == nil {
|
|
return 0, fmt.Errorf("nil pointer encountered")
|
|
}
|
|
|
|
if amountIn.Sign() <= 0 || balance0.Sign() <= 0 || balance1.Sign() <= 0 {
|
|
return 0, fmt.Errorf("invalid amounts")
|
|
}
|
|
|
|
// Price before = balance1 / balance0
|
|
priceBefore := new(big.Float).Quo(new(big.Float).SetInt(balance1), new(big.Float).SetInt(balance0))
|
|
|
|
// Calculate amount out
|
|
amountOut, err := c.CalculateAmountOut(amountIn, balance0, balance1, 400)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
// New balances
|
|
newBalance0 := new(big.Int).Add(balance0, amountIn)
|
|
newBalance1 := new(big.Int).Sub(balance1, amountOut)
|
|
|
|
// Price after
|
|
priceAfter := new(big.Float).Quo(new(big.Float).SetInt(newBalance1), new(big.Float).SetInt(newBalance0))
|
|
|
|
// Calculate impact
|
|
impact := new(big.Float).Sub(priceBefore, priceAfter)
|
|
impact.Quo(impact, priceBefore)
|
|
|
|
impactFloat, _ := impact.Float64()
|
|
return math.Abs(impactFloat), nil
|
|
}
|
|
|
|
// GetSpotPrice returns current spot price for Curve
|
|
func (c *CurveMath) GetSpotPrice(balance0, balance1 *big.Int) (*big.Float, error) {
|
|
if balance0.Sign() <= 0 || balance1.Sign() <= 0 {
|
|
return nil, fmt.Errorf("invalid balances")
|
|
}
|
|
|
|
return new(big.Float).Quo(new(big.Float).SetInt(balance1), new(big.Float).SetInt(balance0)), nil
|
|
}
|
|
|
|
// CalculateSlippage calculates slippage for Curve
|
|
func (c *CurveMath) CalculateSlippage(expectedOut, actualOut *big.Int) (float64, error) {
|
|
if expectedOut.Sign() <= 0 || actualOut.Sign() <= 0 {
|
|
return 0, fmt.Errorf("invalid amounts")
|
|
}
|
|
|
|
diff := new(big.Float).Sub(new(big.Float).SetInt(expectedOut), new(big.Float).SetInt(actualOut))
|
|
slippage := new(big.Float).Quo(diff, new(big.Float).SetInt(expectedOut))
|
|
|
|
slippageFloat, _ := slippage.Float64()
|
|
return math.Abs(slippageFloat), nil
|
|
}
|
|
|
|
// ========== Kyber Math ==========
|
|
|
|
// CalculateAmountOut calculates output for Kyber Elastic
|
|
func (k *KyberMath) CalculateAmountOut(amountIn, sqrtPriceX96, liquidity *big.Int, fee uint32) (*big.Int, error) {
|
|
if amountIn.Sign() <= 0 || sqrtPriceX96.Sign() <= 0 || liquidity.Sign() <= 0 {
|
|
return nil, fmt.Errorf("invalid parameters")
|
|
}
|
|
|
|
// Kyber Elastic uses concentrated liquidity similar to Uniswap V3
|
|
// but with different fee structures and mechanisms
|
|
|
|
if fee == 0 {
|
|
fee = 1000 // Default 0.1% for Kyber
|
|
}
|
|
|
|
// Apply fee: amountInWithFee = amountIn * (1000000 - fee) / 1000000
|
|
feeFactor := big.NewInt(int64(1000000 - fee))
|
|
amountInWithFee := new(big.Int).Mul(amountIn, feeFactor)
|
|
amountInWithFee.Div(amountInWithFee, big.NewInt(1000000))
|
|
|
|
// Calculate price change using liquidity and amountIn
|
|
Q96 := new(big.Int).Lsh(big.NewInt(1), 96)
|
|
priceChange := new(big.Int).Mul(amountInWithFee, Q96)
|
|
priceChange.Div(priceChange, liquidity)
|
|
|
|
// Calculate new sqrt price after swap
|
|
newSqrtPriceX96 := new(big.Int).Add(sqrtPriceX96, priceChange)
|
|
|
|
// Calculate amount out based on price difference and liquidity
|
|
priceDiff := new(big.Int).Sub(newSqrtPriceX96, sqrtPriceX96)
|
|
amountOut := new(big.Int).Mul(liquidity, priceDiff)
|
|
amountOut.Div(amountOut, sqrtPriceX96)
|
|
|
|
return amountOut, nil
|
|
}
|
|
|
|
// CalculateAmountIn calculates input for Kyber Elastic
|
|
func (k *KyberMath) CalculateAmountIn(amountOut, sqrtPriceX96, liquidity *big.Int, fee uint32) (*big.Int, error) {
|
|
if amountOut.Sign() <= 0 || sqrtPriceX96.Sign() <= 0 || liquidity.Sign() <= 0 {
|
|
return nil, fmt.Errorf("invalid parameters")
|
|
}
|
|
|
|
if fee == 0 {
|
|
fee = 1000
|
|
}
|
|
|
|
// Calculate required price change
|
|
Q96 := new(big.Int).Lsh(big.NewInt(1), 96)
|
|
priceChange := new(big.Int).Mul(amountOut, sqrtPriceX96)
|
|
priceChange.Div(priceChange, liquidity)
|
|
|
|
// Calculate amount in before fees
|
|
amountInBeforeFee := new(big.Int).Mul(priceChange, liquidity)
|
|
amountInBeforeFee.Div(amountInBeforeFee, Q96)
|
|
|
|
// Apply fee
|
|
feeFactor := big.NewInt(int64(1000000 - fee))
|
|
amountIn := new(big.Int).Mul(amountInBeforeFee, big.NewInt(1000000))
|
|
amountIn.Div(amountIn, feeFactor)
|
|
|
|
return amountIn, nil
|
|
}
|
|
|
|
// CalculatePriceImpact calculates price impact for Kyber
|
|
func (k *KyberMath) CalculatePriceImpact(amountIn, sqrtPriceX96, liquidity *big.Int) (float64, error) {
|
|
if amountIn.Sign() <= 0 || sqrtPriceX96.Sign() <= 0 || liquidity.Sign() <= 0 {
|
|
return 0, fmt.Errorf("invalid parameters")
|
|
}
|
|
|
|
// Calculate new sqrt price after swap
|
|
Q96 := new(big.Int).Lsh(big.NewInt(1), 96)
|
|
priceChange := new(big.Int).Mul(amountIn, Q96)
|
|
priceChange.Div(priceChange, liquidity)
|
|
|
|
newSqrtPriceX96 := new(big.Int).Add(sqrtPriceX96, priceChange)
|
|
|
|
// Convert to regular prices using big.Float for precision
|
|
Q96Float := new(big.Float).SetInt(Q96)
|
|
|
|
// priceBefore = (sqrtPriceX96 / 2^96)^2
|
|
priceBefore := new(big.Float).SetInt(sqrtPriceX96)
|
|
priceBefore.Quo(priceBefore, Q96Float)
|
|
priceBefore.Mul(priceBefore, priceBefore)
|
|
|
|
// priceAfter = (newSqrtPriceX96 / 2^96)^2
|
|
priceAfter := new(big.Float).SetInt(newSqrtPriceX96)
|
|
priceAfter.Quo(priceAfter, Q96Float)
|
|
priceAfter.Mul(priceAfter, priceAfter)
|
|
|
|
// Check if priceBefore is zero or very small
|
|
if priceBefore.Sign() == 0 {
|
|
return 0, fmt.Errorf("price before is zero - invalid calculation")
|
|
}
|
|
|
|
// Check if priceBefore is too small (less than 1e-18)
|
|
minPrice := big.NewFloat(1e-18)
|
|
if priceBefore.Cmp(minPrice) < 0 {
|
|
return 0, fmt.Errorf("price too small for reliable calculation")
|
|
}
|
|
|
|
// Calculate impact = (priceAfter - priceBefore) / priceBefore
|
|
impact := new(big.Float).Sub(priceAfter, priceBefore)
|
|
impact.Quo(impact, priceBefore)
|
|
|
|
impactFloat, _ := impact.Float64()
|
|
return math.Abs(impactFloat), nil
|
|
}
|
|
|
|
// GetSpotPrice returns current spot price for Kyber
|
|
func (k *KyberMath) GetSpotPrice(sqrtPriceX96, _ *big.Int) (*big.Float, error) {
|
|
if sqrtPriceX96.Sign() <= 0 {
|
|
return nil, fmt.Errorf("invalid sqrt price")
|
|
}
|
|
|
|
// Price = (sqrtPriceX96 / 2^96)^2
|
|
Q96 := new(big.Int).Lsh(big.NewInt(1), 96)
|
|
price := new(big.Int).Mul(sqrtPriceX96, sqrtPriceX96)
|
|
price.Div(price, new(big.Int).Mul(Q96, Q96))
|
|
|
|
return new(big.Float).SetInt(price), nil
|
|
}
|
|
|
|
// CalculateSlippage calculates slippage for Kyber
|
|
func (k *KyberMath) CalculateSlippage(expectedOut, actualOut *big.Int) (float64, error) {
|
|
if expectedOut.Sign() <= 0 || actualOut.Sign() <= 0 {
|
|
return 0, fmt.Errorf("invalid amounts")
|
|
}
|
|
|
|
diff := new(big.Float).Sub(new(big.Float).SetInt(expectedOut), new(big.Float).SetInt(actualOut))
|
|
slippage := new(big.Float).Quo(diff, new(big.Float).SetInt(expectedOut))
|
|
|
|
slippageFloat, _ := slippage.Float64()
|
|
return math.Abs(slippageFloat), nil
|
|
}
|
|
|
|
// ========== Balancer Math ==========
|
|
|
|
// CalculateAmountOut calculates output for Balancer weighted pools
|
|
func (b *BalancerMath) CalculateAmountOut(amountIn, reserveIn, reserveOut *big.Int, fee uint32) (*big.Int, error) {
|
|
if amountIn.Sign() <= 0 || reserveIn.Sign() <= 0 || reserveOut.Sign() <= 0 {
|
|
return nil, fmt.Errorf("invalid amounts")
|
|
}
|
|
|
|
// For Balancer, we use weighted pool formula
|
|
// amountOut = reserveOut * (1 - (reserveIn / (reserveIn + amountIn * feeFactor))^weightRatio)
|
|
|
|
if fee == 0 {
|
|
fee = 1000 // Default 0.1% fee
|
|
}
|
|
|
|
// Calculate fee factor
|
|
feeFactor := float64(10000-fee) / 10000.0
|
|
|
|
// Calculate effective input amount after fee
|
|
amountInWithFee := new(big.Float).SetInt(amountIn)
|
|
amountInWithFee.Mul(amountInWithFee, big.NewFloat(feeFactor))
|
|
|
|
// Convert to big.Float for precise calculations
|
|
reserveInFloat := new(big.Float).SetInt(reserveIn)
|
|
reserveOutFloat := new(big.Float).SetInt(reserveOut)
|
|
|
|
// Calculate numerator: reserveIn + (amountIn * feeFactor)
|
|
numerator := new(big.Float).Add(reserveInFloat, amountInWithFee)
|
|
|
|
// Calculate ratio: reserveIn / numerator
|
|
ratio := new(big.Float).Quo(reserveInFloat, numerator)
|
|
|
|
// For equal weights (simplified approach)
|
|
// Calculate amountOut = reserveOut * (1 - ratio)
|
|
denominatorFloat := new(big.Float).SetInt(big.NewInt(1))
|
|
result := new(big.Float).Sub(denominatorFloat, ratio)
|
|
result.Mul(reserveOutFloat, result)
|
|
|
|
// Convert back to big.Int
|
|
amountOut := new(big.Int)
|
|
result.Int(amountOut)
|
|
|
|
return amountOut, nil
|
|
}
|
|
|
|
// CalculateAmountIn calculates input for Balancer weighted pools
|
|
func (b *BalancerMath) CalculateAmountIn(amountOut, reserveIn, reserveOut *big.Int, fee uint32) (*big.Int, error) {
|
|
if amountOut.Sign() <= 0 || reserveIn.Sign() <= 0 || reserveOut.Sign() <= 0 {
|
|
return nil, fmt.Errorf("invalid amounts")
|
|
}
|
|
|
|
if amountOut.Cmp(reserveOut) >= 0 {
|
|
return nil, fmt.Errorf("insufficient liquidity")
|
|
}
|
|
|
|
if fee == 0 {
|
|
fee = 1000
|
|
}
|
|
|
|
// Calculate fee factor
|
|
feeFactor := float64(10000) / float64(10000-fee)
|
|
|
|
// Calculate reserveOut after swap
|
|
newReserveOut := new(big.Int).Sub(reserveOut, amountOut)
|
|
|
|
if newReserveOut.Sign() <= 0 {
|
|
return nil, fmt.Errorf("insufficient liquidity")
|
|
}
|
|
|
|
// Calculate amountIn using weighted pool formula
|
|
// Using simplified approach for equal weights
|
|
reserveOutFloat := new(big.Float).SetInt(reserveOut)
|
|
newReserveOutFloat := new(big.Float).SetInt(newReserveOut)
|
|
|
|
// Calculate ratio: reserveOut / newReserveOut
|
|
ratio := new(big.Float).Quo(reserveOutFloat, newReserveOutFloat)
|
|
|
|
// For equal weights, we just take the ratio as is and subtract 1
|
|
denominatorFloat := new(big.Float).SetInt(big.NewInt(1))
|
|
result := new(big.Float).Sub(ratio, denominatorFloat)
|
|
|
|
// Multiply by reserveIn
|
|
reserveInFloat := new(big.Float).SetInt(reserveIn)
|
|
result.Mul(reserveInFloat, result)
|
|
|
|
// Adjust by fee factor
|
|
result.Mul(result, big.NewFloat(feeFactor))
|
|
|
|
// Convert back to big.Int
|
|
amountIn := new(big.Int)
|
|
result.Int(amountIn)
|
|
|
|
// Add 1 to ensure we have enough for the swap (rounding up)
|
|
amountIn.Add(amountIn, big.NewInt(1))
|
|
|
|
return amountIn, nil
|
|
}
|
|
|
|
// CalculatePriceImpact calculates price impact for Balancer
|
|
func (b *BalancerMath) CalculatePriceImpact(amountIn, reserveIn, reserveOut *big.Int) (float64, error) {
|
|
// Check for nil pointers first
|
|
if amountIn == nil || reserveIn == nil || reserveOut == nil {
|
|
return 0, fmt.Errorf("nil pointer encountered")
|
|
}
|
|
|
|
if amountIn.Sign() <= 0 || reserveIn.Sign() <= 0 || reserveOut.Sign() <= 0 {
|
|
return 0, fmt.Errorf("invalid amounts")
|
|
}
|
|
|
|
// Price before = reserveOut / reserveIn
|
|
priceBefore := new(big.Float).Quo(new(big.Float).SetInt(reserveOut), new(big.Float).SetInt(reserveIn))
|
|
|
|
// Calculate amount out
|
|
amountOut, err := b.CalculateAmountOut(amountIn, reserveIn, reserveOut, 1000)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
// New reserves after swap
|
|
newReserveIn := new(big.Int).Add(reserveIn, amountIn)
|
|
newReserveOut := new(big.Int).Sub(reserveOut, amountOut)
|
|
|
|
// Price after = newReserveOut / newReserveIn
|
|
priceAfter := new(big.Float).Quo(new(big.Float).SetInt(newReserveOut), new(big.Float).SetInt(newReserveIn))
|
|
|
|
// Price impact = (priceBefore - priceAfter) / priceBefore
|
|
impact := new(big.Float).Sub(priceBefore, priceAfter)
|
|
impact.Quo(impact, priceBefore)
|
|
|
|
impactFloat, _ := impact.Float64()
|
|
return math.Abs(impactFloat), nil
|
|
}
|
|
|
|
// GetSpotPrice returns current spot price for Balancer
|
|
func (b *BalancerMath) GetSpotPrice(reserveIn, reserveOut *big.Int) (*big.Float, error) {
|
|
if reserveIn.Sign() <= 0 || reserveOut.Sign() <= 0 {
|
|
return nil, fmt.Errorf("invalid reserves")
|
|
}
|
|
|
|
return new(big.Float).Quo(new(big.Float).SetInt(reserveOut), new(big.Float).SetInt(reserveIn)), nil
|
|
}
|
|
|
|
// CalculateSlippage calculates slippage for Balancer
|
|
func (b *BalancerMath) CalculateSlippage(expectedOut, actualOut *big.Int) (float64, error) {
|
|
if expectedOut.Sign() <= 0 || actualOut.Sign() <= 0 {
|
|
return 0, fmt.Errorf("invalid amounts")
|
|
}
|
|
|
|
// Slippage = (expectedOut - actualOut) / expectedOut
|
|
diff := new(big.Float).Sub(new(big.Float).SetInt(expectedOut), new(big.Float).SetInt(actualOut))
|
|
slippage := new(big.Float).Quo(diff, new(big.Float).SetInt(expectedOut))
|
|
|
|
slippageFloat, _ := slippage.Float64()
|
|
return math.Abs(slippageFloat), nil
|
|
}
|
|
|
|
// ========== Constant Sum Math ==========
|
|
|
|
// CalculateAmountOut calculates output for Constant Sum AMM (like Uniswap V1)
|
|
func (c *ConstantSumMath) CalculateAmountOut(amountIn, reserveIn, reserveOut *big.Int, fee uint32) (*big.Int, error) {
|
|
if amountIn.Sign() <= 0 || reserveIn.Sign() <= 0 || reserveOut.Sign() <= 0 {
|
|
return nil, fmt.Errorf("invalid amounts")
|
|
}
|
|
|
|
// In constant sum, price is always 1:1 (ignoring fees)
|
|
// amountOut = amountIn * (1 - fee)
|
|
if fee == 0 {
|
|
fee = 3000 // Default 0.3% fee
|
|
}
|
|
|
|
feeFactor := big.NewInt(int64(10000 - fee))
|
|
amountOut := new(big.Int).Mul(amountIn, feeFactor)
|
|
amountOut.Div(amountOut, big.NewInt(10000))
|
|
|
|
return amountOut, nil
|
|
}
|
|
|
|
// CalculateAmountIn calculates input for Constant Sum AMM
|
|
func (c *ConstantSumMath) CalculateAmountIn(amountOut, reserveIn, reserveOut *big.Int, fee uint32) (*big.Int, error) {
|
|
if amountOut.Sign() <= 0 || reserveIn.Sign() <= 0 || reserveOut.Sign() <= 0 {
|
|
return nil, fmt.Errorf("invalid amounts")
|
|
}
|
|
|
|
if fee == 0 {
|
|
fee = 3000
|
|
}
|
|
|
|
// amountIn = amountOut / (1 - fee)
|
|
feeFactor := big.NewInt(int64(10000 - fee))
|
|
amountIn := new(big.Int).Mul(amountOut, big.NewInt(10000))
|
|
amountIn.Div(amountIn, feeFactor)
|
|
|
|
return amountIn, nil
|
|
}
|
|
|
|
// CalculatePriceImpact calculates price impact for Constant Sum (should be 0)
|
|
func (c *ConstantSumMath) CalculatePriceImpact(amountIn, reserveIn, reserveOut *big.Int) (float64, error) {
|
|
if amountIn.Sign() <= 0 || reserveIn.Sign() <= 0 || reserveOut.Sign() <= 0 {
|
|
return 0, fmt.Errorf("invalid amounts")
|
|
}
|
|
|
|
// In constant sum, there is no price impact (ignoring fees)
|
|
return 0, nil
|
|
}
|
|
|
|
// GetSpotPrice returns current spot price for Constant Sum (should be 1, ignoring fees)
|
|
func (c *ConstantSumMath) GetSpotPrice(reserveIn, reserveOut *big.Int) (*big.Float, error) {
|
|
if reserveIn.Sign() <= 0 || reserveOut.Sign() <= 0 {
|
|
return nil, fmt.Errorf("invalid reserves")
|
|
}
|
|
|
|
// In constant sum, price is always 1:1 (ignoring fees)
|
|
return big.NewFloat(1.0), nil
|
|
}
|
|
|
|
// CalculateSlippage calculates slippage for Constant Sum (same as fees)
|
|
func (c *ConstantSumMath) CalculateSlippage(expectedOut, actualOut *big.Int) (float64, error) {
|
|
if expectedOut.Sign() <= 0 || actualOut.Sign() <= 0 {
|
|
return 0, fmt.Errorf("invalid amounts")
|
|
}
|
|
|
|
// Slippage = (expectedOut - actualOut) / expectedOut
|
|
diff := new(big.Float).Sub(new(big.Float).SetInt(expectedOut), new(big.Float).SetInt(actualOut))
|
|
slippage := new(big.Float).Quo(diff, new(big.Float).SetInt(expectedOut))
|
|
|
|
slippageFloat, _ := slippage.Float64()
|
|
return math.Abs(slippageFloat), nil
|
|
}
|
|
|
|
// ========== Math Factory ==========
|
|
|
|
// MathCalculator provides a unified interface for all exchange math
|
|
type MathCalculator struct {
|
|
uniswapV2 *UniswapV2Math
|
|
uniswapV3 *UniswapV3Math
|
|
curve *CurveMath
|
|
kyber *KyberMath
|
|
balancer *BalancerMath
|
|
constantSum *ConstantSumMath
|
|
}
|
|
|
|
// NewMathCalculator creates a new unified math calculator
|
|
func NewMathCalculator() *MathCalculator {
|
|
return &MathCalculator{
|
|
uniswapV2: NewUniswapV2Math(),
|
|
uniswapV3: NewUniswapV3Math(),
|
|
curve: NewCurveMath(),
|
|
kyber: &KyberMath{},
|
|
balancer: &BalancerMath{},
|
|
constantSum: &ConstantSumMath{},
|
|
}
|
|
}
|
|
|
|
// GetMathForExchange returns the appropriate math calculator for an exchange
|
|
func (mc *MathCalculator) GetMathForExchange(exchangeType string) ExchangeMath {
|
|
switch exchangeType {
|
|
case "uniswap_v2", "sushiswap":
|
|
return mc.uniswapV2
|
|
case "uniswap_v3", "camelot_v3":
|
|
return mc.uniswapV3
|
|
case "curve":
|
|
return mc.curve
|
|
case "kyber_elastic", "kyber_classic":
|
|
return mc.kyber
|
|
case "balancer":
|
|
return mc.balancer
|
|
case "constant_sum":
|
|
return mc.constantSum
|
|
default:
|
|
return mc.uniswapV2 // Default fallback
|
|
}
|
|
}
|
|
|
|
// CalculateOptimalArbitrage calculates optimal arbitrage between exchanges
|
|
func (mc *MathCalculator) CalculateOptimalArbitrage(
|
|
exchangeA, exchangeB string,
|
|
reservesA, reservesB [2]*big.Int,
|
|
feesA, feesB uint32,
|
|
) (*ArbitrageResult, error) {
|
|
|
|
mathA := mc.GetMathForExchange(exchangeA)
|
|
mathB := mc.GetMathForExchange(exchangeB)
|
|
|
|
// Get spot prices
|
|
priceA, err := mathA.GetSpotPrice(reservesA[0], reservesA[1])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
priceB, err := mathB.GetSpotPrice(reservesB[0], reservesB[1])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Calculate price difference
|
|
priceDiff := new(big.Float).Sub(priceA, priceB)
|
|
priceDiff.Quo(priceDiff, priceA)
|
|
|
|
priceDiffFloat, _ := priceDiff.Float64()
|
|
|
|
// Only proceed if price difference > 0.5%
|
|
if math.Abs(priceDiffFloat) < 0.005 {
|
|
return nil, fmt.Errorf("insufficient price difference: %f", priceDiffFloat)
|
|
}
|
|
|
|
// Find optimal amount using binary search
|
|
optimalAmount := mc.findOptimalAmount(mathA, mathB, reservesA, reservesB, feesA, feesB)
|
|
|
|
// Calculate expected profit
|
|
amountOut1, _ := mathA.CalculateAmountOut(optimalAmount, reservesA[0], reservesA[1], feesA)
|
|
amountOut2, _ := mathB.CalculateAmountIn(amountOut1, reservesB[1], reservesB[0], feesB)
|
|
|
|
profit := new(big.Int).Sub(amountOut2, optimalAmount)
|
|
|
|
return &ArbitrageResult{
|
|
AmountIn: optimalAmount,
|
|
ExpectedProfit: profit,
|
|
PriceDiff: priceDiffFloat,
|
|
ExchangeA: exchangeA,
|
|
ExchangeB: exchangeB,
|
|
}, nil
|
|
}
|
|
|
|
// ArbitrageResult represents the result of arbitrage calculation
|
|
type ArbitrageResult struct {
|
|
AmountIn *big.Int
|
|
ExpectedProfit *big.Int
|
|
PriceDiff float64
|
|
ExchangeA string
|
|
ExchangeB string
|
|
}
|
|
|
|
// findOptimalAmount uses binary search to find optimal arbitrage amount
|
|
func (mc *MathCalculator) findOptimalAmount(
|
|
mathA, mathB ExchangeMath,
|
|
reservesA, reservesB [2]*big.Int,
|
|
feesA, feesB uint32,
|
|
) *big.Int {
|
|
|
|
// Binary search for optimal amount
|
|
min := big.NewInt(1000000000000000) // 0.001 ETH
|
|
max := new(big.Int).Div(reservesA[0], big.NewInt(10)) // 10% of reserve
|
|
|
|
bestAmount := new(big.Int).Set(min)
|
|
bestProfit := big.NewInt(0)
|
|
|
|
for i := 0; i < 20; i++ { // 20 iterations
|
|
mid := new(big.Int).Add(min, max)
|
|
mid.Div(mid, big.NewInt(2))
|
|
|
|
// Calculate profit at this amount
|
|
amountOut1, err1 := mathA.CalculateAmountOut(mid, reservesA[0], reservesA[1], feesA)
|
|
if err1 != nil {
|
|
max = mid
|
|
continue
|
|
}
|
|
|
|
amountOut2, err2 := mathB.CalculateAmountIn(amountOut1, reservesB[1], reservesB[0], feesB)
|
|
if err2 != nil {
|
|
max = mid
|
|
continue
|
|
}
|
|
|
|
profit := new(big.Int).Sub(amountOut2, mid)
|
|
|
|
if profit.Cmp(bestProfit) > 0 {
|
|
bestProfit = profit
|
|
bestAmount = new(big.Int).Set(mid)
|
|
}
|
|
|
|
// Adjust search range
|
|
if profit.Sign() > 0 {
|
|
min = mid
|
|
} else {
|
|
max = mid
|
|
}
|
|
}
|
|
|
|
return bestAmount
|
|
}
|