361 lines
12 KiB
Go
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
|
|
}
|