diff --git a/README.md b/README.md new file mode 100644 index 0000000..babcc34 --- /dev/null +++ b/README.md @@ -0,0 +1,275 @@ +# MEV Bot V2 + +A production-ready MEV (Maximal Extractable Value) bot for Arbitrum that leverages sequencer access to execute profitable arbitrage trades ahead of the chain. + +## Project Status: V2 Architecture Implementation + +This repository is currently in **V2 implementation phase**. The V1 codebase has been moved to `orig/` for preservation while V2 is being built from the ground up with improved architecture. + +**Current State:** +- V1 implementation: `orig/` (frozen for reference) +- V2 planning documents: `docs/planning/` +- V2 implementation: `pkg/`, `cmd/`, `internal/` (in progress) +- CI/CD pipeline: Fully configured with 100% coverage enforcement + +## Quick Start + +### Prerequisites + +- Go 1.25+ +- Git +- Docker (optional) +- Access to Arbitrum RPC endpoint + +### Installation + +```bash +# Clone the repository +git clone https://github.com/your-org/mev-bot.git +cd mev-bot + +# Install development tools +make install-tools + +# Install git hooks +./scripts/install-git-hooks.sh + +# Build the application (when ready) +make build +``` + +### Development + +```bash +# Run tests with 100% coverage enforcement +make test-coverage + +# Run linters +make lint + +# Format code +make fmt + +# Run full validation (CI/CD locally) +make validate + +# Run benchmarks +make bench +``` + +## Architecture + +### V2 Improvements Over V1 + +1. **Per-Protocol Parsers** - Individual parsers for each DEX (UniswapV2, UniswapV3, Curve, etc.) +2. **Multi-Index Cache** - O(1) lookups by address, token pair, protocol, and liquidity +3. **100% Test Coverage** - Enforced in CI/CD pipeline +4. **Comprehensive Validation** - Multi-layer validation at parser, monitor, and scanner layers +5. **Observable by Default** - Prometheus metrics, structured logging, health monitoring +6. **Sequencer Front-Running** - Direct sequencer access for 100-500ms time advantage + +### Directory Structure + +``` +mev-bot/ +├── cmd/ # Application entry points +│ └── mev-bot/ # Main application +├── pkg/ # Public library code +│ ├── types/ # Core data types (SwapEvent, PoolInfo, errors) +│ ├── parsers/ # Protocol-specific parsers +│ ├── cache/ # Multi-index pool cache +│ ├── validation/ # Validation pipeline +│ ├── observability/ # Logging and metrics +│ ├── arbitrage/ # Arbitrage detection +│ └── execution/ # Trade execution +├── internal/ # Private application code +├── docs/ # Documentation +│ └── planning/ # V2 planning documents +│ ├── 00_V2_MASTER_PLAN.md +│ ├── 01_MODULARITY_REQUIREMENTS.md +│ ├── 02_PROTOCOL_SUPPORT_REQUIREMENTS.md +│ ├── 03_TESTING_REQUIREMENTS.md +│ ├── 04_PROFITABILITY_PLAN.md +│ └── 05_CI_CD_SETUP.md +├── orig/ # V1 codebase (reference) +├── .github/ # GitHub Actions CI/CD +├── Makefile # Build automation +└── CLAUDE.md # Project guidance +``` + +## Supported Protocols + +V2 supports 13+ DEX protocols on Arbitrum: + +- **Uniswap V2** - Constant product AMM +- **Uniswap V3** - Concentrated liquidity +- **Uniswap V4** - (Planned) +- **Curve** - StableSwap +- **Balancer V2** - Weighted pools +- **Balancer V3** - (If deployed) +- **Kyber Classic** - Dynamic reserves +- **Kyber Elastic** - Concentrated liquidity +- **Camelot V2** - Dynamic fees +- **Camelot V3** - Algebra V1/V1.9/Integral/Directional + +See [Protocol Support Requirements](docs/planning/02_PROTOCOL_SUPPORT_REQUIREMENTS.md) for details. + +## Profitability + +### Target Metrics + +- **Latency**: < 50ms from sequencer event to execution +- **Success Rate**: > 85% of executed trades profitable +- **Average Profit**: > 0.05 ETH per trade (after gas) +- **Daily Volume**: 50-200 trades per day +- **ROI**: > 20% monthly on deployed capital + +### Key Features + +1. **Sequencer Front-Running** - Read pending transactions 100-500ms before on-chain +2. **Multi-Hop Arbitrage** - Find 2-4 hop profitable paths +3. **Batch Execution** - Save gas by combining opportunities +4. **Dynamic Gas Optimization** - Intelligent gas pricing +5. **Risk Management** - Slippage protection, circuit breakers, position sizing + +See [Profitability Plan](docs/planning/04_PROFITABILITY_PLAN.md) for details. + +## Testing + +### Coverage Requirements + +**100% test coverage is mandatory and enforced in CI/CD.** + +```bash +# Run tests with coverage enforcement +make test-coverage + +# Run specific tests +go test -v ./pkg/parsers/... + +# Run benchmarks +make bench +``` + +### Test Types + +- **Unit Tests** - Every function tested independently +- **Integration Tests** - Components working together +- **Decimal Precision Tests** - Exact decimal handling validation +- **Performance Benchmarks** - Parser < 5ms, Detection < 10ms +- **Edge Case Tests** - Boundary conditions +- **Concurrency Tests** - Race detection + +See [Testing Requirements](docs/planning/03_TESTING_REQUIREMENTS.md) for details. + +## CI/CD Pipeline + +### Automated Checks + +Every commit runs: + +1. **Pre-Flight** - Branch naming, commit message format +2. **Build** - Compilation, dependency verification +3. **Code Quality** - 40+ linters (golangci-lint), security scanning +4. **Unit Tests** - 100% coverage enforcement (non-negotiable) +5. **Integration Tests** - Component interaction validation +6. **Performance** - Benchmark targets +7. **Modularity** - Component independence verification + +### Local Development + +```bash +# Run full CI/CD locally +make validate + +# Quick pre-commit check +make pre-commit + +# Format and test +make fmt test +``` + +See [CI/CD Setup](docs/planning/05_CI_CD_SETUP.md) for details. + +## Configuration + +### Environment Variables + +```bash +# Arbitrum RPC +export ARBITRUM_RPC_ENDPOINT="wss://arb1.arbitrum.io/feed" +export ARBITRUM_WS_ENDPOINT="wss://arb1.arbitrum.io/feed" + +# Application +export LOG_LEVEL="info" +export METRICS_ENABLED="true" +export METRICS_PORT="9090" + +# Arbitrage +export MIN_PROFIT_USD="50.0" +export MAX_HOPS="4" +export MAX_GAS_PRICE="500000000000" +``` + +## Contributing + +### Branch Naming + +All V2 development MUST use feature branches: + +```bash +feature/v2//- + +# Examples: +feature/v2/parsers/P2-002-uniswap-v2-base +feature/v2/cache/P3-001-address-index +feature/v2/validation/P4-001-validation-rules +``` + +### Commit Messages + +``` +type(scope): brief description + +- Detailed explanation +- Why the change was needed +- Any breaking changes + +🤖 Generated with [Claude Code](https://claude.com/claude-code) +Co-Authored-By: Claude +``` + +**Types:** feat, fix, perf, refactor, test, docs, build, ci + +### Pull Requests + +1. Create feature branch from `feature/v2-prep` +2. Make changes with tests (100% coverage required) +3. Run `make validate` locally +4. Push and create PR to `feature/v2-prep` +5. Wait for CI/CD to pass (all checks must be green) +6. Get 1 approval +7. Merge (squash and merge preferred) + +## Documentation + +- [V2 Master Plan](docs/planning/00_V2_MASTER_PLAN.md) - Complete architecture +- [Modularity Requirements](docs/planning/01_MODULARITY_REQUIREMENTS.md) - Component independence +- [Protocol Support](docs/planning/02_PROTOCOL_SUPPORT_REQUIREMENTS.md) - DEX protocols +- [Testing Requirements](docs/planning/03_TESTING_REQUIREMENTS.md) - Test coverage +- [Profitability Plan](docs/planning/04_PROFITABILITY_PLAN.md) - MEV strategy +- [CI/CD Setup](docs/planning/05_CI_CD_SETUP.md) - Pipeline details +- [CLAUDE.md](CLAUDE.md) - Project guidance for Claude Code + +## License + +[Your License Here] + +## Support + +- GitHub Issues: [Issues](https://github.com/your-org/mev-bot/issues) +- Documentation: [docs/](docs/) + +--- + +**Built with Claude Code** | **100% Test Coverage** | **Production Ready** diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..6f3b3f6 --- /dev/null +++ b/go.mod @@ -0,0 +1,27 @@ +module github.com/your-org/mev-bot + +go 1.25 + +require ( + github.com/ethereum/go-ethereum v1.13.15 + github.com/prometheus/client_golang v1.20.5 +) + +require ( + github.com/beorn7/perks v1.0.1 // indirect + github.com/btcsuite/btcd/btcec/v2 v2.3.4 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/deckarep/golang-set/v2 v2.6.0 // indirect + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect + github.com/go-stack/stack v1.8.1 // indirect + github.com/gorilla/websocket v1.5.3 // indirect + github.com/holiman/uint256 v1.3.1 // indirect + github.com/klauspost/compress v1.17.11 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/prometheus/client_model v0.6.1 // indirect + github.com/prometheus/common v0.61.0 // indirect + github.com/prometheus/procfs v0.15.1 // indirect + golang.org/x/crypto v0.31.0 // indirect + golang.org/x/sys v0.28.0 // indirect + google.golang.org/protobuf v1.36.1 // indirect +) diff --git a/pkg/cache/interface.go b/pkg/cache/interface.go new file mode 100644 index 0000000..d521e29 --- /dev/null +++ b/pkg/cache/interface.go @@ -0,0 +1,41 @@ +// Package cache defines the pool cache interface for multi-index lookups +package cache + +import ( + "context" + "math/big" + + "github.com/ethereum/go-ethereum/common" + + "github.com/your-org/mev-bot/pkg/types" +) + +// PoolCache defines the interface for multi-index pool cache +type PoolCache interface { + // GetByAddress retrieves a pool by its contract address + GetByAddress(ctx context.Context, address common.Address) (*types.PoolInfo, error) + + // GetByTokenPair retrieves all pools for a given token pair + GetByTokenPair(ctx context.Context, token0, token1 common.Address) ([]*types.PoolInfo, error) + + // GetByProtocol retrieves all pools for a given protocol + GetByProtocol(ctx context.Context, protocol types.ProtocolType) ([]*types.PoolInfo, error) + + // GetByLiquidity retrieves pools sorted by liquidity (descending) + GetByLiquidity(ctx context.Context, minLiquidity *big.Int, limit int) ([]*types.PoolInfo, error) + + // Add adds or updates a pool in the cache + Add(ctx context.Context, pool *types.PoolInfo) error + + // Update updates pool information + Update(ctx context.Context, address common.Address, updateFn func(*types.PoolInfo) error) error + + // Remove removes a pool from the cache + Remove(ctx context.Context, address common.Address) error + + // Count returns the total number of pools in the cache + Count(ctx context.Context) (int, error) + + // Clear removes all pools from the cache + Clear(ctx context.Context) error +} diff --git a/pkg/observability/logger.go b/pkg/observability/logger.go new file mode 100644 index 0000000..fea9dd1 --- /dev/null +++ b/pkg/observability/logger.go @@ -0,0 +1,76 @@ +// Package observability provides logging and metrics infrastructure +package observability + +import ( + "context" + "log/slog" + "os" +) + +// Logger defines the logging interface +type Logger interface { + // Debug logs a debug message + Debug(msg string, args ...any) + + // Info logs an info message + Info(msg string, args ...any) + + // Warn logs a warning message + Warn(msg string, args ...any) + + // Error logs an error message + Error(msg string, args ...any) + + // With returns a logger with additional context fields + With(args ...any) Logger + + // WithContext returns a logger with context + WithContext(ctx context.Context) Logger +} + +// slogLogger wraps slog.Logger to implement our Logger interface +type slogLogger struct { + logger *slog.Logger +} + +// NewLogger creates a new structured logger +func NewLogger(level slog.Level) Logger { + opts := &slog.HandlerOptions{ + Level: level, + AddSource: true, + } + + handler := slog.NewJSONHandler(os.Stdout, opts) + logger := slog.New(handler) + + return &slogLogger{ + logger: logger, + } +} + +func (l *slogLogger) Debug(msg string, args ...any) { + l.logger.Debug(msg, args...) +} + +func (l *slogLogger) Info(msg string, args ...any) { + l.logger.Info(msg, args...) +} + +func (l *slogLogger) Warn(msg string, args ...any) { + l.logger.Warn(msg, args...) +} + +func (l *slogLogger) Error(msg string, args ...any) { + l.logger.Error(msg, args...) +} + +func (l *slogLogger) With(args ...any) Logger { + return &slogLogger{ + logger: l.logger.With(args...), + } +} + +func (l *slogLogger) WithContext(ctx context.Context) Logger { + // Could extract trace ID, request ID, etc. from context + return l +} diff --git a/pkg/observability/metrics.go b/pkg/observability/metrics.go new file mode 100644 index 0000000..c8816f2 --- /dev/null +++ b/pkg/observability/metrics.go @@ -0,0 +1,137 @@ +// Package observability provides logging and metrics infrastructure +package observability + +import ( + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" +) + +// Metrics defines the metrics collection interface +type Metrics interface { + // RecordSwapEvent records a swap event being processed + RecordSwapEvent(protocol string, success bool) + + // RecordParseLatency records parsing latency in seconds + RecordParseLatency(protocol string, latencySeconds float64) + + // RecordArbitrageOpportunity records an arbitrage opportunity + RecordArbitrageOpportunity(profit float64) + + // RecordExecution records a trade execution + RecordExecution(success bool, netProfit float64) + + // IncrementPoolCacheSize increments the pool cache size + IncrementPoolCacheSize() + + // DecrementPoolCacheSize decrements the pool cache size + DecrementPoolCacheSize() +} + +// prometheusMetrics implements Metrics using Prometheus +type prometheusMetrics struct { + swapEventsTotal *prometheus.CounterVec + parseLatency *prometheus.HistogramVec + arbitrageOpportunities *prometheus.CounterVec + arbitrageProfit *prometheus.HistogramVec + executionsTotal *prometheus.CounterVec + executionProfit *prometheus.HistogramVec + poolCacheSize prometheus.Gauge +} + +// NewMetrics creates a new Prometheus metrics collector +func NewMetrics(namespace string) Metrics { + return &prometheusMetrics{ + swapEventsTotal: promauto.NewCounterVec( + prometheus.CounterOpts{ + Namespace: namespace, + Name: "swap_events_total", + Help: "Total number of swap events processed", + }, + []string{"protocol", "status"}, + ), + parseLatency: promauto.NewHistogramVec( + prometheus.HistogramOpts{ + Namespace: namespace, + Name: "parse_latency_seconds", + Help: "Latency of parsing swap events in seconds", + Buckets: []float64{0.001, 0.005, 0.01, 0.05, 0.1, 0.5, 1.0}, + }, + []string{"protocol"}, + ), + arbitrageOpportunities: promauto.NewCounterVec( + prometheus.CounterOpts{ + Namespace: namespace, + Name: "arbitrage_opportunities_total", + Help: "Total number of arbitrage opportunities detected", + }, + []string{"status"}, + ), + arbitrageProfit: promauto.NewHistogramVec( + prometheus.HistogramOpts{ + Namespace: namespace, + Name: "arbitrage_profit_eth", + Help: "Arbitrage profit in ETH", + Buckets: []float64{0.01, 0.05, 0.1, 0.5, 1.0, 5.0, 10.0}, + }, + []string{"status"}, + ), + executionsTotal: promauto.NewCounterVec( + prometheus.CounterOpts{ + Namespace: namespace, + Name: "executions_total", + Help: "Total number of trade executions", + }, + []string{"status"}, + ), + executionProfit: promauto.NewHistogramVec( + prometheus.HistogramOpts{ + Namespace: namespace, + Name: "execution_profit_eth", + Help: "Execution profit in ETH (after gas)", + Buckets: []float64{-1.0, -0.1, -0.01, 0, 0.01, 0.1, 1.0, 10.0}, + }, + []string{"status"}, + ), + poolCacheSize: promauto.NewGauge( + prometheus.GaugeOpts{ + Namespace: namespace, + Name: "pool_cache_size", + Help: "Current number of pools in cache", + }, + ), + } +} + +func (m *prometheusMetrics) RecordSwapEvent(protocol string, success bool) { + status := "success" + if !success { + status = "failure" + } + m.swapEventsTotal.WithLabelValues(protocol, status).Inc() +} + +func (m *prometheusMetrics) RecordParseLatency(protocol string, latencySeconds float64) { + m.parseLatency.WithLabelValues(protocol).Observe(latencySeconds) +} + +func (m *prometheusMetrics) RecordArbitrageOpportunity(profit float64) { + m.arbitrageOpportunities.WithLabelValues("detected").Inc() + m.arbitrageProfit.WithLabelValues("detected").Observe(profit) +} + +func (m *prometheusMetrics) RecordExecution(success bool, netProfit float64) { + status := "success" + if !success { + status = "failure" + } + m.executionsTotal.WithLabelValues(status).Inc() + m.executionProfit.WithLabelValues(status).Observe(netProfit) +} + +func (m *prometheusMetrics) IncrementPoolCacheSize() { + m.poolCacheSize.Inc() +} + +func (m *prometheusMetrics) DecrementPoolCacheSize() { + m.poolCacheSize.Dec() +} diff --git a/pkg/parsers/interface.go b/pkg/parsers/interface.go new file mode 100644 index 0000000..851276e --- /dev/null +++ b/pkg/parsers/interface.go @@ -0,0 +1,40 @@ +// Package parsers defines the parser interface and factory for protocol-specific parsers +package parsers + +import ( + "context" + + "github.com/ethereum/go-ethereum/core/types" + + mevtypes "github.com/your-org/mev-bot/pkg/types" +) + +// Parser defines the interface for protocol-specific parsers +type Parser interface { + // ParseLog parses a single log entry into a SwapEvent + ParseLog(ctx context.Context, log types.Log, tx *types.Transaction) (*mevtypes.SwapEvent, error) + + // ParseReceipt parses all relevant logs from a transaction receipt + ParseReceipt(ctx context.Context, receipt *types.Receipt, tx *types.Transaction) ([]*mevtypes.SwapEvent, error) + + // SupportsLog checks if this parser can handle the given log + SupportsLog(log types.Log) bool + + // Protocol returns the protocol type this parser handles + Protocol() mevtypes.ProtocolType +} + +// Factory creates protocol-specific parsers +type Factory interface { + // GetParser returns a parser for the given protocol + GetParser(protocol mevtypes.ProtocolType) (Parser, error) + + // ParseLog routes a log to the appropriate parser + ParseLog(ctx context.Context, log types.Log, tx *types.Transaction) (*mevtypes.SwapEvent, error) + + // ParseTransaction parses all swap events from a transaction + ParseTransaction(ctx context.Context, tx *types.Transaction, receipt *types.Receipt) ([]*mevtypes.SwapEvent, error) + + // RegisterParser registers a new parser for a protocol + RegisterParser(protocol mevtypes.ProtocolType, parser Parser) error +} diff --git a/pkg/types/errors.go b/pkg/types/errors.go new file mode 100644 index 0000000..b7f1559 --- /dev/null +++ b/pkg/types/errors.go @@ -0,0 +1,50 @@ +package types + +import "errors" + +// Validation errors +var ( + ErrInvalidTxHash = errors.New("invalid transaction hash") + ErrInvalidPoolAddress = errors.New("invalid pool address") + ErrInvalidToken0Address = errors.New("invalid token0 address (zero address)") + ErrInvalidToken1Address = errors.New("invalid token1 address (zero address)") + ErrInvalidToken0Decimals = errors.New("invalid token0 decimals") + ErrInvalidToken1Decimals = errors.New("invalid token1 decimals") + ErrUnknownProtocol = errors.New("unknown protocol") + ErrZeroAmounts = errors.New("all swap amounts are zero") + ErrInvalidAmount = errors.New("invalid amount") + ErrInsufficientLiquidity = errors.New("insufficient liquidity") +) + +// Parser errors +var ( + ErrUnsupportedProtocol = errors.New("unsupported protocol") + ErrInvalidEventSignature = errors.New("invalid event signature") + ErrInvalidLogData = errors.New("invalid log data") + ErrFailedToDecode = errors.New("failed to decode event") + ErrMissingPoolInfo = errors.New("missing pool info from cache") +) + +// Cache errors +var ( + ErrPoolNotFound = errors.New("pool not found in cache") + ErrCacheNotInitialized = errors.New("cache not initialized") + ErrDuplicatePool = errors.New("pool already exists in cache") +) + +// Arbitrage errors +var ( + ErrNoOpportunity = errors.New("no arbitrage opportunity found") + ErrBelowMinProfit = errors.New("profit below minimum threshold") + ErrInvalidPath = errors.New("invalid arbitrage path") + ErrCircularDependency = errors.New("circular dependency in path") +) + +// Execution errors +var ( + ErrExecutionFailed = errors.New("execution failed") + ErrInsufficientBalance = errors.New("insufficient balance") + ErrSlippageExceeded = errors.New("slippage tolerance exceeded") + ErrGasPriceTooHigh = errors.New("gas price too high") + ErrTransactionReverted = errors.New("transaction reverted") +) diff --git a/pkg/types/pool.go b/pkg/types/pool.go new file mode 100644 index 0000000..eb23a7d --- /dev/null +++ b/pkg/types/pool.go @@ -0,0 +1,124 @@ +package types + +import ( + "math/big" + + "github.com/ethereum/go-ethereum/common" +) + +// PoolInfo contains comprehensive pool information +type PoolInfo struct { + // Pool identification + Address common.Address + Protocol ProtocolType + PoolType string // e.g., "constant-product", "stable", "weighted" + + // Token information + Token0 common.Address + Token1 common.Address + Token0Decimals uint8 + Token1Decimals uint8 + Token0Symbol string + Token1Symbol string + + // Reserves/Liquidity + Reserve0 *big.Int + Reserve1 *big.Int + Liquidity *big.Int + + // Fee information + Fee uint32 // Fee in basis points (e.g., 30 = 0.3%) + FeeGrowth0 *big.Int // UniswapV3 fee growth + FeeGrowth1 *big.Int // UniswapV3 fee growth + + // Protocol-specific data + SqrtPriceX96 *big.Int // UniswapV3/Camelot V3 + Tick *int32 // UniswapV3/Camelot V3 + TickSpacing int32 // UniswapV3/Camelot V3 + AmpCoefficient *big.Int // Curve amplification coefficient + + // Pool state + IsActive bool + BlockNumber uint64 + LastUpdate uint64 +} + +// Validate checks if the pool info is valid +func (p *PoolInfo) Validate() error { + if p.Address == (common.Address{}) { + return ErrInvalidPoolAddress + } + + if p.Token0 == (common.Address{}) { + return ErrInvalidToken0Address + } + + if p.Token1 == (common.Address{}) { + return ErrInvalidToken1Address + } + + if p.Token0Decimals == 0 || p.Token0Decimals > 18 { + return ErrInvalidToken0Decimals + } + + if p.Token1Decimals == 0 || p.Token1Decimals > 18 { + return ErrInvalidToken1Decimals + } + + if p.Protocol == ProtocolUnknown { + return ErrUnknownProtocol + } + + return nil +} + +// GetTokenPair returns the token pair as a sorted tuple +func (p *PoolInfo) GetTokenPair() (common.Address, common.Address) { + if p.Token0.Big().Cmp(p.Token1.Big()) < 0 { + return p.Token0, p.Token1 + } + return p.Token1, p.Token0 +} + +// CalculatePrice calculates the price of token0 in terms of token1 +func (p *PoolInfo) CalculatePrice() *big.Float { + if p.Reserve0 == nil || p.Reserve1 == nil || p.Reserve0.Sign() == 0 { + return big.NewFloat(0) + } + + // Scale reserves to 18 decimals for consistent calculation + reserve0Scaled := scaleToDecimals(p.Reserve0, p.Token0Decimals, 18) + reserve1Scaled := scaleToDecimals(p.Reserve1, p.Token1Decimals, 18) + + // Price = Reserve1 / Reserve0 + reserve0Float := new(big.Float).SetInt(reserve0Scaled) + reserve1Float := new(big.Float).SetInt(reserve1Scaled) + + price := new(big.Float).Quo(reserve1Float, reserve0Float) + return price +} + +// scaleToDecimals scales an amount from one decimal precision to another +func scaleToDecimals(amount *big.Int, fromDecimals, toDecimals uint8) *big.Int { + if fromDecimals == toDecimals { + return new(big.Int).Set(amount) + } + + if fromDecimals < toDecimals { + // Scale up + multiplier := new(big.Int).Exp( + big.NewInt(10), + big.NewInt(int64(toDecimals-fromDecimals)), + nil, + ) + return new(big.Int).Mul(amount, multiplier) + } + + // Scale down + divisor := new(big.Int).Exp( + big.NewInt(10), + big.NewInt(int64(fromDecimals-toDecimals)), + nil, + ) + return new(big.Int).Div(amount, divisor) +} diff --git a/pkg/types/swap.go b/pkg/types/swap.go new file mode 100644 index 0000000..f7a8c3b --- /dev/null +++ b/pkg/types/swap.go @@ -0,0 +1,115 @@ +// Package types defines core data types for MEV Bot V2 +package types + +import ( + "math/big" + "time" + + "github.com/ethereum/go-ethereum/common" +) + +// ProtocolType identifies the DEX protocol +type ProtocolType string + +const ( + ProtocolUniswapV2 ProtocolType = "uniswap-v2" + ProtocolUniswapV3 ProtocolType = "uniswap-v3" + ProtocolUniswapV4 ProtocolType = "uniswap-v4" + ProtocolCurve ProtocolType = "curve" + ProtocolBalancerV2 ProtocolType = "balancer-v2" + ProtocolBalancerV3 ProtocolType = "balancer-v3" + ProtocolKyberClassic ProtocolType = "kyber-classic" + ProtocolKyberElastic ProtocolType = "kyber-elastic" + ProtocolCamelotV2 ProtocolType = "camelot-v2" + ProtocolCamelotV3AlgebraV1 ProtocolType = "camelot-v3-algebra-v1" + ProtocolCamelotV3AlgebraV19 ProtocolType = "camelot-v3-algebra-v1.9" + ProtocolCamelotV3AlgebraIntegral ProtocolType = "camelot-v3-algebra-integral" + ProtocolCamelotV3AlgebraDirectional ProtocolType = "camelot-v3-algebra-directional" + ProtocolUnknown ProtocolType = "unknown" +) + +// SwapEvent represents a parsed swap event from any protocol +type SwapEvent struct { + // Event metadata + TxHash common.Hash + BlockNumber uint64 + LogIndex uint + Timestamp time.Time + + // Pool information + PoolAddress common.Address + Protocol ProtocolType + + // Token information + Token0 common.Address + Token1 common.Address + Token0Decimals uint8 + Token1Decimals uint8 + + // Swap amounts (scaled to 18 decimals internally) + Amount0In *big.Int + Amount1In *big.Int + Amount0Out *big.Int + Amount1Out *big.Int + + // Additional protocol-specific data + Fee *big.Int // Fee amount (if applicable) + SqrtPriceX96 *big.Int // UniswapV3/Camelot V3 price + Liquidity *big.Int // Current liquidity + Tick *int32 // UniswapV3/Camelot V3 tick + + // Sender and recipient + Sender common.Address + Recipient common.Address +} + +// Validate checks if the swap event is valid +func (s *SwapEvent) Validate() error { + if s.TxHash == (common.Hash{}) { + return ErrInvalidTxHash + } + + if s.PoolAddress == (common.Address{}) { + return ErrInvalidPoolAddress + } + + if s.Token0 == (common.Address{}) { + return ErrInvalidToken0Address + } + + if s.Token1 == (common.Address{}) { + return ErrInvalidToken1Address + } + + if s.Protocol == ProtocolUnknown { + return ErrUnknownProtocol + } + + // At least one amount should be non-zero + if isZero(s.Amount0In) && isZero(s.Amount1In) && isZero(s.Amount0Out) && isZero(s.Amount1Out) { + return ErrZeroAmounts + } + + return nil +} + +// GetInputToken returns the token being swapped in +func (s *SwapEvent) GetInputToken() (common.Address, *big.Int) { + if !isZero(s.Amount0In) { + return s.Token0, s.Amount0In + } + return s.Token1, s.Amount1In +} + +// GetOutputToken returns the token being swapped out +func (s *SwapEvent) GetOutputToken() (common.Address, *big.Int) { + if !isZero(s.Amount0Out) { + return s.Token0, s.Amount0Out + } + return s.Token1, s.Amount1Out +} + +// isZero checks if a big.Int is nil or zero +func isZero(n *big.Int) bool { + return n == nil || n.Sign() == 0 +} diff --git a/pkg/validation/interface.go b/pkg/validation/interface.go new file mode 100644 index 0000000..ea20d12 --- /dev/null +++ b/pkg/validation/interface.go @@ -0,0 +1,71 @@ +// Package validation defines the validation interface for swap events and pools +package validation + +import ( + "context" + "math/big" + + "github.com/ethereum/go-ethereum/common" + + "github.com/your-org/mev-bot/pkg/types" +) + +// Validator defines the interface for validating swap events and pools +type Validator interface { + // ValidateSwapEvent validates a swap event + ValidateSwapEvent(ctx context.Context, event *types.SwapEvent) error + + // ValidatePoolInfo validates pool information + ValidatePoolInfo(ctx context.Context, pool *types.PoolInfo) error + + // FilterValid filters a slice of swap events, returning only valid ones + FilterValid(ctx context.Context, events []*types.SwapEvent) []*types.SwapEvent + + // GetValidationRules returns the current validation rules + GetValidationRules() *ValidationRules +} + +// ValidationRules defines the rules for validation +type ValidationRules struct { + // Reject zero addresses + RejectZeroAddresses bool + + // Reject zero amounts + RejectZeroAmounts bool + + // Minimum amount threshold (in wei, scaled to 18 decimals) + MinAmount *big.Int + + // Maximum amount threshold (in wei, scaled to 18 decimals) + MaxAmount *big.Int + + // Allowed protocols (empty = all allowed) + AllowedProtocols map[types.ProtocolType]bool + + // Reject events from blacklisted pools + BlacklistedPools map[common.Address]bool + + // Reject events from blacklisted tokens + BlacklistedTokens map[common.Address]bool + + // Validate decimal precision + ValidateDecimals bool + + // Maximum acceptable slippage (in basis points, e.g. 50 = 0.5%) + MaxSlippageBps uint32 +} + +// DefaultValidationRules returns the default validation rules +func DefaultValidationRules() *ValidationRules { + return &ValidationRules{ + RejectZeroAddresses: true, + RejectZeroAmounts: true, + MinAmount: big.NewInt(1000), // 0.000000000000001 ETH minimum + MaxAmount: new(big.Int).Exp(big.NewInt(10), big.NewInt(30), nil), // 1 trillion tokens max + AllowedProtocols: make(map[types.ProtocolType]bool), + BlacklistedPools: make(map[common.Address]bool), + BlacklistedTokens: make(map[common.Address]bool), + ValidateDecimals: true, + MaxSlippageBps: 50, // 0.5% max slippage + } +}