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>
This commit is contained in:
290
pkg/pricing/uniswap_v3.go
Normal file
290
pkg/pricing/uniswap_v3.go
Normal file
@@ -0,0 +1,290 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user