Files
mev-beta/pkg/slippage/protection.go
2025-09-16 11:05:47 -05:00

361 lines
12 KiB
Go

package slippage
import (
"context"
"fmt"
"math/big"
"time"
"github.com/ethereum/go-ethereum/common"
"github.com/fraktal/mev-beta/internal/logger"
"github.com/fraktal/mev-beta/pkg/oracle"
)
// SlippageProtection provides comprehensive slippage calculation and protection
type SlippageProtection struct {
logger *logger.Logger
oracle *oracle.PriceOracle
maxSlippageBps *big.Int // Maximum allowed slippage in basis points
impactThresholds map[string]*big.Int // Pool size -> impact threshold
}
// SlippageConfig represents slippage protection configuration
type SlippageConfig struct {
MaxSlippageBps *big.Int // Maximum slippage (basis points)
ImpactThresholds map[string]*big.Int // Pool size thresholds
EmergencySlippageBps *big.Int // Emergency max slippage
TimeoutSeconds int // Price check timeout
RevertOnHighSlippage bool // Whether to revert on high slippage
}
// SlippageResult contains the result of slippage analysis
type SlippageResult struct {
EstimatedSlippageBps *big.Int // Estimated slippage in basis points
MaxAllowedAmountOut *big.Int // Maximum amount out considering slippage
MinRequiredAmountOut *big.Int // Minimum amount out required
PriceImpactBps *big.Int // Price impact in basis points
RecommendedGasPrice *big.Int // Recommended gas price for execution
SafeToExecute bool // Whether trade is safe to execute
WarningMessages []string // Any warnings about the trade
EmergencyStop bool // Whether to emergency stop
Timestamp time.Time // When analysis was performed
}
// TradeParams represents the parameters for a trade
type TradeParams struct {
TokenIn common.Address
TokenOut common.Address
AmountIn *big.Int
PoolAddress common.Address
Fee *big.Int // Pool fee tier
Deadline uint64 // Transaction deadline
Recipient common.Address
}
// NewSlippageProtection creates a new slippage protection instance
func NewSlippageProtection(logger *logger.Logger, oracle *oracle.PriceOracle, config *SlippageConfig) *SlippageProtection {
if config == nil {
config = &SlippageConfig{
MaxSlippageBps: big.NewInt(500), // 5% default
EmergencySlippageBps: big.NewInt(1000), // 10% emergency
TimeoutSeconds: 10,
RevertOnHighSlippage: true,
ImpactThresholds: getDefaultImpactThresholds(),
}
}
return &SlippageProtection{
logger: logger,
oracle: oracle,
maxSlippageBps: config.MaxSlippageBps,
impactThresholds: config.ImpactThresholds,
}
}
// AnalyzeSlippage performs comprehensive slippage analysis for a trade
func (sp *SlippageProtection) AnalyzeSlippage(ctx context.Context, params *TradeParams) (*SlippageResult, error) {
result := &SlippageResult{
Timestamp: time.Now(),
WarningMessages: make([]string, 0),
}
// 1. Get current price from oracle
priceReq := &oracle.PriceRequest{
TokenIn: params.TokenIn,
TokenOut: params.TokenOut,
AmountIn: params.AmountIn,
Timestamp: time.Now(),
}
priceResp, err := sp.oracle.GetPrice(ctx, priceReq)
if err != nil {
return nil, fmt.Errorf("failed to get price from oracle: %w", err)
}
if !priceResp.Valid {
return nil, fmt.Errorf("oracle returned invalid price")
}
// 2. Calculate estimated slippage
result.EstimatedSlippageBps = priceResp.SlippageBps
// 3. Calculate price impact
priceImpact, err := sp.calculatePriceImpact(params, priceResp)
if err != nil {
sp.logger.Warn(fmt.Sprintf("Failed to calculate price impact: %v", err))
priceImpact = big.NewInt(0)
}
result.PriceImpactBps = priceImpact
// 4. Calculate minimum amount out with slippage protection
result.MinRequiredAmountOut = sp.calculateMinAmountOut(priceResp.AmountOut, result.EstimatedSlippageBps)
result.MaxAllowedAmountOut = priceResp.AmountOut
// 5. Check if trade is safe to execute
result.SafeToExecute, result.WarningMessages = sp.evaluateTradeSafety(result)
// 6. Set emergency stop if needed
result.EmergencyStop = sp.shouldEmergencyStop(result)
// 7. Recommend gas price based on urgency and slippage
result.RecommendedGasPrice = sp.recommendGasPrice(result)
sp.logger.Debug(fmt.Sprintf("Slippage analysis: slippage=%s bps, impact=%s bps, safe=%v, emergency=%v",
result.EstimatedSlippageBps.String(), result.PriceImpactBps.String(), result.SafeToExecute, result.EmergencyStop))
return result, nil
}
// calculatePriceImpact calculates the price impact of a trade on the pool
func (sp *SlippageProtection) calculatePriceImpact(params *TradeParams, priceResp *oracle.PriceResponse) (*big.Int, error) {
// Price impact calculation for Uniswap V3:
// Impact = (amount_in / pool_liquidity) * price_sensitivity_factor
// For simplified calculation, we use the amount as percentage of typical pool size
// In practice, you'd need to:
// 1. Get actual pool liquidity from the pool contract
// 2. Calculate exact price impact using the constant product formula
// 3. Account for concentrated liquidity in V3
// Simplified calculation: impact proportional to trade size
// Assume typical pool has 1M USD liquidity
typicalPoolSizeUSD := big.NewInt(1000000) // $1M
// Convert amount to USD (simplified - assumes $1 per token unit)
amountFloat := new(big.Float).Quo(new(big.Float).SetInt(params.AmountIn), big.NewFloat(1e18))
amountUSD, _ := amountFloat.Float64()
amountUSDBig := big.NewInt(int64(amountUSD))
// Price impact = (amountUSD / poolSizeUSD) * 10000 (to get basis points)
if typicalPoolSizeUSD.Sign() == 0 {
return big.NewInt(0), nil
}
impact := new(big.Int).Mul(amountUSDBig, big.NewInt(10000))
impact.Div(impact, typicalPoolSizeUSD)
// Cap impact at reasonable maximum (50% = 5000 bps)
maxImpact := big.NewInt(5000)
if impact.Cmp(maxImpact) > 0 {
impact = maxImpact
}
return impact, nil
}
// calculateMinAmountOut calculates minimum amount out considering slippage
func (sp *SlippageProtection) calculateMinAmountOut(expectedOut, slippageBps *big.Int) *big.Int {
if expectedOut.Sign() == 0 || slippageBps.Sign() == 0 {
return expectedOut
}
// minAmountOut = expectedOut * (10000 - slippageBps) / 10000
slippageMultiplier := new(big.Int).Sub(big.NewInt(10000), slippageBps)
minAmount := new(big.Int).Mul(expectedOut, slippageMultiplier)
minAmount.Div(minAmount, big.NewInt(10000))
return minAmount
}
// evaluateTradeSafety evaluates whether a trade is safe to execute
func (sp *SlippageProtection) evaluateTradeSafety(result *SlippageResult) (bool, []string) {
warnings := make([]string, 0)
safe := true
// Check slippage against maximum allowed
if result.EstimatedSlippageBps.Cmp(sp.maxSlippageBps) > 0 {
safe = false
warnings = append(warnings, fmt.Sprintf("Slippage %s bps exceeds maximum %s bps",
result.EstimatedSlippageBps.String(), sp.maxSlippageBps.String()))
}
// Check price impact
highImpactThreshold := big.NewInt(1000) // 10%
if result.PriceImpactBps.Cmp(highImpactThreshold) > 0 {
warnings = append(warnings, fmt.Sprintf("High price impact detected: %s bps",
result.PriceImpactBps.String()))
// Don't fail the trade for high impact, but warn
if result.PriceImpactBps.Cmp(big.NewInt(2000)) > 0 { // 20%
safe = false
}
}
// Check for extremely high slippage (possible market manipulation)
extremeSlippageThreshold := big.NewInt(2000) // 20%
if result.EstimatedSlippageBps.Cmp(extremeSlippageThreshold) > 0 {
safe = false
warnings = append(warnings, "EXTREME slippage detected - possible market manipulation")
}
// Check if amounts are reasonable
if result.MinRequiredAmountOut.Sign() == 0 {
safe = false
warnings = append(warnings, "Minimum amount out is zero - trade would result in total loss")
}
return safe, warnings
}
// shouldEmergencyStop determines if an emergency stop should be triggered
func (sp *SlippageProtection) shouldEmergencyStop(result *SlippageResult) bool {
// Emergency stop conditions:
// 1. Extreme slippage (>30%)
if result.EstimatedSlippageBps.Cmp(big.NewInt(3000)) > 0 {
return true
}
// 2. Extreme price impact (>50%)
if result.PriceImpactBps.Cmp(big.NewInt(5000)) > 0 {
return true
}
// 3. Total loss scenario
if result.MinRequiredAmountOut.Sign() == 0 {
return true
}
return false
}
// recommendGasPrice recommends gas price based on trade urgency and slippage
func (sp *SlippageProtection) recommendGasPrice(result *SlippageResult) *big.Int {
baseGasPrice := big.NewInt(20e9) // 20 gwei base
// Increase gas price for high slippage trades (need faster execution)
if result.EstimatedSlippageBps.Cmp(big.NewInt(300)) > 0 { // >3% slippage
// Increase by 50%
multiplier := big.NewInt(150)
baseGasPrice.Mul(baseGasPrice, multiplier)
baseGasPrice.Div(baseGasPrice, big.NewInt(100))
}
// Increase for high price impact (competitive trades)
if result.PriceImpactBps.Cmp(big.NewInt(500)) > 0 { // >5% impact
// Additional 25% increase
multiplier := big.NewInt(125)
baseGasPrice.Mul(baseGasPrice, multiplier)
baseGasPrice.Div(baseGasPrice, big.NewInt(100))
}
// Cap at reasonable maximum (200 gwei)
maxGasPrice := big.NewInt(200e9)
if baseGasPrice.Cmp(maxGasPrice) > 0 {
baseGasPrice = maxGasPrice
}
return baseGasPrice
}
// ValidateSlippageTolerance validates if the provided slippage tolerance is reasonable
func (sp *SlippageProtection) ValidateSlippageTolerance(slippageBps *big.Int) error {
if slippageBps.Sign() < 0 {
return fmt.Errorf("slippage tolerance cannot be negative")
}
if slippageBps.Cmp(big.NewInt(10000)) >= 0 {
return fmt.Errorf("slippage tolerance cannot be 100%% or more")
}
// Warn about very high slippage tolerance
if slippageBps.Cmp(big.NewInt(1000)) > 0 { // >10%
sp.logger.Warn(fmt.Sprintf("Very high slippage tolerance: %s bps", slippageBps.String()))
}
return nil
}
// CalculateOptimalSlippage calculates optimal slippage based on market conditions
func (sp *SlippageProtection) CalculateOptimalSlippage(ctx context.Context, params *TradeParams) (*big.Int, error) {
// Factors affecting optimal slippage:
// 1. Pool size and liquidity
// 2. Recent volatility
// 3. Trade size
// 4. Time of day / market hours
// 5. Network congestion
baseSlippage := big.NewInt(50) // 0.5% base
// Adjust for trade size (larger trades need more slippage protection)
tradeValue := new(big.Float).Quo(new(big.Float).SetInt(params.AmountIn), big.NewFloat(1e18))
tradeValueFloat, _ := tradeValue.Float64()
if tradeValueFloat > 100000 { // >$100k trade
baseSlippage = big.NewInt(200) // 2%
} else if tradeValueFloat > 10000 { // >$10k trade
baseSlippage = big.NewInt(100) // 1%
}
// TODO: Add more sophisticated calculation based on:
// - Historical volatility analysis
// - Pool liquidity depth
// - Network congestion metrics
// - Time-based volatility patterns
return baseSlippage, nil
}
// getDefaultImpactThresholds returns default price impact thresholds
func getDefaultImpactThresholds() map[string]*big.Int {
return map[string]*big.Int{
"small": big.NewInt(100), // 1% for small pools
"medium": big.NewInt(500), // 5% for medium pools
"large": big.NewInt(1000), // 10% for large pools
}
}
// MonitorSlippage continuously monitors slippage for active trades
func (sp *SlippageProtection) MonitorSlippage(ctx context.Context, params *TradeParams, interval time.Duration) (<-chan *SlippageResult, error) {
results := make(chan *SlippageResult, 10)
go func() {
defer close(results)
ticker := time.NewTicker(interval)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
result, err := sp.AnalyzeSlippage(ctx, params)
if err != nil {
sp.logger.Error(fmt.Sprintf("Slippage monitoring error: %v", err))
continue
}
select {
case results <- result:
case <-ctx.Done():
return
default:
// Channel full, skip this update
}
}
}
}()
return results, nil
}