Files
mev-beta/pkg/pricing/uniswap_v3.go
Administrator 7f57d5eb6b feat(v2): achieve 100% safety test passage with emergency stop and Uniswap V3 pricing
This commit achieves 100% test passage (12/12 tests) for all safety mechanisms
and adds comprehensive Uniswap V3 pricing library.

## Key Achievements

**Test Results: 12/12 passing (100%)**
- Previous: 6/11 passing (54.5%)
- Current: 12/12 passing (100%)
- All safety-critical tests verified

## Changes Made

### 1. Emergency Stop Mechanism (cmd/mev-bot-v2/main.go)
- Added monitorEmergencyStop() function with 10-second check interval
- Monitors /tmp/mev-bot-emergency-stop file
- Triggers graceful shutdown when file detected
- Logs emergency stop detection with clear error message
- Modified main event loop to handle both interrupt and context cancellation

### 2. Safety Configuration Logging (cmd/mev-bot-v2/main.go:49-80)
- Added comprehensive structured logging at startup
- Logs execution settings (dry_run_mode, enable_execution, enable_simulation)
- Logs risk limits (max_position_size, max_daily_volume, max_slippage)
- Logs profit thresholds (min_profit, min_roi, min_swap_amount)
- Logs circuit breaker settings (max_consecutive_losses, max_hourly_loss)
- Logs emergency stop configuration (file_path, check_interval)

### 3. Config Struct Enhancements (cmd/mev-bot-v2/main.go:297-325)
- Added MaxGasPrice uint64 field
- Added EnableExecution bool field
- Added DryRun bool field
- Added Safety section with:
  - MaxConsecutiveLosses int
  - MaxHourlyLoss *big.Int
  - MaxDailyLoss *big.Int
  - EmergencyStopFile string

### 4. Production Environment Configuration (cmd/mev-bot-v2/main.go:364-376)
- LoadConfig() now supports both old and new env var names
- RPC_URL with fallback to ARBITRUM_RPC_ENDPOINT
- WS_URL with fallback to ARBITRUM_WS_ENDPOINT
- EXECUTOR_CONTRACT with fallback to CONTRACT_ARBITRAGE_EXECUTOR
- Copied production .env from orig/.env.production.secure

### 5. Uniswap V3 Pricing Library (pkg/pricing/uniswap_v3.go)
Based on Python notebooks: https://github.com/t4sk/notes/tree/main/python/uniswap-v3

Functions implemented:
- SqrtPriceX96ToPrice() - Convert Q64.96 to human-readable price
- TickToPrice() - Convert tick to price (1.0001^tick)
- SqrtPriceX96ToTick() - Reverse conversion with clamping
- PriceToTick() - Price to tick conversion
- TickToSqrtPriceX96() - Tick to Q64.96 format
- GetPriceImpact() - Calculate price impact in BPS
- GetTickSpacing() - Fee tier to tick spacing mapping
- GetNearestUsableTick() - Align tick to spacing

### 6. Test Script Improvements (scripts/test_safety_mechanisms.sh)

**Emergency Stop Test Fix (lines 323-362):**
- Changed to use `podman exec` to create file inside container
- Better error handling and logging
- Proper detection verification

**Nonce Check Test Fix (lines 412-463, 468-504):**
- Capture nonce before swap in test 9
- Calculate delta instead of checking absolute value
- Properly verify bot created 0 transactions in dry-run mode
- Fixes false negative from forked account history

### 7. Smart Contracts Submodule (.gitmodules)
- Added mev-beta-contracts as git submodule at contracts/
- URL: ssh://git@194.163.145.241:2222/copper-tone-tech/mev-beta-contracts.git
- Enables parallel development of bot and contracts

## Test Results Summary

All 12 tests passing:
1.  Anvil fork startup
2.  Test account balance verification
3.  Safety configuration creation
4.  Docker image build
5.  Bot deployment
6.  Safety configuration verification (5/5 checks)
7.  Emergency stop detection (8 seconds)
8.  Circuit breaker configuration
9.  Position size limits
10.  Test swap creation
11.  Swap detection
12.  Dry-run mode verification (0 bot transactions)

## Safety Features Verified

- Dry-run mode prevents real transactions ✓
- Circuit breaker configured (3 losses, 0.1 ETH hourly, 0.5 ETH daily) ✓
- Position limits enforced (10 ETH max position, 100 ETH daily volume) ✓
- Emergency stop file monitoring active ✓
- Comprehensive logging for monitoring ✓

## Next Steps

The bot is now ready for Anvil fork testing with all safety mechanisms verified.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-11 01:18:10 +01:00

291 lines
7.7 KiB
Go

package pricing
import (
"math"
"math/big"
)
// Uniswap V3 Pricing Library
// Based on: https://github.com/t4sk/notes/tree/main/python/uniswap-v3
//
// Key concepts:
// - Prices are stored as sqrtPriceX96 (Q64.96 fixed-point format)
// - Ticks represent discrete 0.01% price movements
// - Each tick corresponds to price = 1.0001^tick
const (
// Q96 is the scaling factor used in Uniswap V3 (2^96)
Q96 = 96
// TickBase is the constant representing each tick's 0.01% price change
TickBase = 1.0001
// MinTick is the minimum tick value in Uniswap V3
MinTick = -887272
// MaxTick is the maximum tick value in Uniswap V3
MaxTick = 887272
)
var (
// q96Big is 2^96 as a big.Int for calculations
q96Big = new(big.Int).Lsh(big.NewInt(1), Q96)
// q96Float is 2^96 as a big.Float for calculations
q96Float = new(big.Float).SetInt(q96Big)
)
// SqrtPriceX96ToPrice converts Uniswap V3's sqrtPriceX96 to a human-readable price
//
// Formula: price = (sqrtPriceX96 / 2^96)^2 * (10^decimals0 / 10^decimals1)
//
// Example from Python notebook:
// sqrt_price_x_96 = 3443439269043970780644209
// price = (sqrt_price_x_96 / 2^96)^2 * (1e18 / 1e6) ≈ 1888.97 USDC per ETH
//
// Parameters:
// - sqrtPriceX96: The sqrt price in Q64.96 format
// - token0Decimals: Number of decimals for token0
// - token1Decimals: Number of decimals for token1
//
// Returns: Price of token0 in terms of token1
func SqrtPriceX96ToPrice(sqrtPriceX96 *big.Int, token0Decimals, token1Decimals uint8) *big.Float {
if sqrtPriceX96 == nil || sqrtPriceX96.Sign() == 0 {
return big.NewFloat(0)
}
// Convert to float
sqrtPriceFloat := new(big.Float).SetInt(sqrtPriceX96)
// Divide by 2^96 to get the actual sqrt(price)
sqrtPrice := new(big.Float).Quo(sqrtPriceFloat, q96Float)
// Square to get price
price := new(big.Float).Mul(sqrtPrice, sqrtPrice)
// Adjust for decimal differences
// price = price * (10^token0Decimals / 10^token1Decimals)
if token0Decimals != token1Decimals {
decimalAdjustment := new(big.Float).SetInt(
new(big.Int).Exp(
big.NewInt(10),
big.NewInt(int64(token0Decimals)-int64(token1Decimals)),
nil,
),
)
price = new(big.Float).Mul(price, decimalAdjustment)
}
return price
}
// TickToPrice converts a Uniswap V3 tick to a price
//
// Formula: price = 1.0001^tick * (10^decimals0 / 10^decimals1)
//
// Example from Python notebook:
// tick = -200963
// price = 1.0001^(-200963) * (1e18 / 1e6) ≈ 1873.80 USDC per ETH
//
// Parameters:
// - tick: The tick value (-887272 to 887272)
// - token0Decimals: Number of decimals for token0
// - token1Decimals: Number of decimals for token1
//
// Returns: Price of token0 in terms of token1
func TickToPrice(tick int32, token0Decimals, token1Decimals uint8) *big.Float {
// Calculate price = 1.0001^tick
price := math.Pow(TickBase, float64(tick))
// Adjust for decimal differences
decimalAdjustment := math.Pow10(int(token0Decimals) - int(token1Decimals))
adjustedPrice := price * decimalAdjustment
return big.NewFloat(adjustedPrice)
}
// SqrtPriceX96ToTick converts sqrtPriceX96 to a tick value
//
// Formula: tick = 2 * ln(sqrtPrice / 2^96) / ln(1.0001)
//
// Example from Python notebook:
// sqrt_price = 3436899527919986964832931
// tick = 2 * ln(sqrt_price / 2^96) / ln(1.0001) ≈ -200920.39
//
// Parameters:
// - sqrtPriceX96: The sqrt price in Q64.96 format
//
// Returns: Tick value (rounded to nearest integer)
func SqrtPriceX96ToTick(sqrtPriceX96 *big.Int) int32 {
if sqrtPriceX96 == nil || sqrtPriceX96.Sign() == 0 {
return 0
}
// Convert to float64 for logarithm calculation
sqrtPriceFloat := new(big.Float).SetInt(sqrtPriceX96)
q96Float := new(big.Float).SetInt(q96Big)
// Calculate ratio: sqrtPrice / 2^96
ratio := new(big.Float).Quo(sqrtPriceFloat, q96Float)
// Convert to float64
ratioFloat64, _ := ratio.Float64()
// Calculate tick = 2 * ln(ratio) / ln(1.0001)
tick := 2.0 * math.Log(ratioFloat64) / math.Log(TickBase)
// Round to nearest integer
tickRounded := int32(math.Round(tick))
// Clamp to valid range
if tickRounded < MinTick {
return MinTick
}
if tickRounded > MaxTick {
return MaxTick
}
return tickRounded
}
// PriceToTick converts a price to a tick value
//
// Formula: tick = ln(price) / ln(1.0001)
//
// Note: Price should already be adjusted for decimal differences
//
// Parameters:
// - price: The price ratio (token0/token1), adjusted for decimals
//
// Returns: Tick value (rounded to nearest integer)
func PriceToTick(price float64) int32 {
if price <= 0 {
return MinTick
}
// Calculate tick = ln(price) / ln(1.0001)
tick := math.Log(price) / math.Log(TickBase)
// Round to nearest integer
tickRounded := int32(math.Round(tick))
// Clamp to valid range
if tickRounded < MinTick {
return MinTick
}
if tickRounded > MaxTick {
return MaxTick
}
return tickRounded
}
// TickToSqrtPriceX96 converts a tick to sqrtPriceX96
//
// Formula: sqrtPriceX96 = sqrt(1.0001^tick) * 2^96
//
// Parameters:
// - tick: The tick value
//
// Returns: SqrtPriceX96 value
func TickToSqrtPriceX96(tick int32) *big.Int {
// Calculate price = 1.0001^tick
price := math.Pow(TickBase, float64(tick))
// Calculate sqrtPrice = sqrt(price)
sqrtPrice := math.Sqrt(price)
// Calculate sqrtPriceX96 = sqrtPrice * 2^96
sqrtPriceFloat := big.NewFloat(sqrtPrice)
sqrtPriceX96Float := new(big.Float).Mul(sqrtPriceFloat, q96Float)
// Convert to big.Int
sqrtPriceX96Int, _ := sqrtPriceX96Float.Int(nil)
return sqrtPriceX96Int
}
// GetPriceImpact calculates the price impact of a swap in basis points (BPS)
//
// Parameters:
// - oldSqrtPriceX96: The sqrt price before the swap
// - newSqrtPriceX96: The sqrt price after the swap
// - token0Decimals: Number of decimals for token0
// - token1Decimals: Number of decimals for token1
//
// Returns: Price impact in basis points (10000 BPS = 100%)
func GetPriceImpact(oldSqrtPriceX96, newSqrtPriceX96 *big.Int, token0Decimals, token1Decimals uint8) float64 {
if oldSqrtPriceX96 == nil || newSqrtPriceX96 == nil ||
oldSqrtPriceX96.Sign() == 0 || newSqrtPriceX96.Sign() == 0 {
return 0
}
oldPrice := SqrtPriceX96ToPrice(oldSqrtPriceX96, token0Decimals, token1Decimals)
newPrice := SqrtPriceX96ToPrice(newSqrtPriceX96, token0Decimals, token1Decimals)
// Calculate percentage change
priceDiff := new(big.Float).Sub(newPrice, oldPrice)
percentChange := new(big.Float).Quo(priceDiff, oldPrice)
// Convert to BPS (multiply by 10000)
bps, _ := new(big.Float).Mul(percentChange, big.NewFloat(10000)).Float64()
// Return absolute value
if bps < 0 {
return -bps
}
return bps
}
// GetTickSpacing returns the tick spacing for a given fee tier
//
// Uniswap V3 uses different tick spacings for different fee tiers:
// - 0.01% fee (100): tick spacing = 1
// - 0.05% fee (500): tick spacing = 10
// - 0.30% fee (3000): tick spacing = 60
// - 1.00% fee (10000): tick spacing = 200
//
// Parameters:
// - feeBPS: The fee in basis points
//
// Returns: Tick spacing for the fee tier
func GetTickSpacing(feeBPS uint32) int32 {
switch feeBPS {
case 100: // 0.01%
return 1
case 500: // 0.05%
return 10
case 3000: // 0.30%
return 60
case 10000: // 1.00%
return 200
default:
// Default to 60 (most common tier)
return 60
}
}
// GetNearestUsableTick returns the nearest usable tick for a given fee tier
//
// Ticks must be aligned to the tick spacing of the fee tier.
//
// Parameters:
// - tick: The desired tick value
// - tickSpacing: The tick spacing for the fee tier
//
// Returns: Nearest usable tick aligned to tick spacing
func GetNearestUsableTick(tick int32, tickSpacing int32) int32 {
// Round to nearest multiple of tickSpacing
rounded := (tick / tickSpacing) * tickSpacing
// Clamp to valid range
if rounded < MinTick {
return MinTick
}
if rounded > MaxTick {
return MaxTick
}
return rounded
}