diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..611941c --- /dev/null +++ b/.env.example @@ -0,0 +1,147 @@ +# MEV Bot V2 Configuration +# Copy this file to .env and fill in your values + +# ============================================================================ +# NETWORK CONFIGURATION +# ============================================================================ + +# Arbitrum RPC URL (for forking with Anvil) +ARBITRUM_RPC_URL=https://arb1.arbitrum.io/rpc + +# Block number to fork from (optional, defaults to latest) +# FORK_BLOCK_NUMBER=latest + +# Local Anvil URLs (used by bot when running in docker-compose) +# RPC_URL=http://anvil:8545 +# WS_URL=ws://anvil:8546 +# SEQUENCER_WS_URL=ws://anvil:8546 + +# Production Arbitrum URLs (when not using Anvil) +# RPC_URL=https://arb1.arbitrum.io/rpc +# WS_URL=wss://arb1.arbitrum.io/ws +# SEQUENCER_WS_URL=wss://arb1.arbitrum.io/ws + +# Private RPC endpoint (optional, for faster execution) +# PRIVATE_RPC_URL= +# USE_PRIVATE_RPC=false + +# ============================================================================ +# WALLET CONFIGURATION +# ============================================================================ + +# Private key for the wallet that will execute trades +# IMPORTANT: Never commit this file with real keys! +# Use a dedicated wallet for testing with test funds only +PRIVATE_KEY=0000000000000000000000000000000000000000000000000000000000000000 + +# ============================================================================ +# SMART CONTRACT ADDRESSES +# ============================================================================ + +# Flashloan executor contract (deployed on Arbitrum) +# Leave as zero address if not deployed yet +EXECUTOR_CONTRACT=0x0000000000000000000000000000000000000000 + +# ============================================================================ +# TRADING PARAMETERS +# ============================================================================ + +# Minimum profit threshold (in wei, 0.01 ETH = 10000000000000000) +MIN_PROFIT=10000000000000000 + +# Minimum ROI percentage (1% = 0.01) +MIN_ROI=0.01 + +# Maximum slippage in basis points (200 = 2%) +MAX_SLIPPAGE_BPS=200 + +# Minimum swap amount (in wei, 0.001 ETH = 1000000000000000) +MIN_SWAP_AMOUNT=1000000000000000 + +# Minimum pool liquidity (in wei, 1 ETH = 1000000000000000000) +MIN_POOL_LIQUIDITY=1000000000000000000 + +# ============================================================================ +# RISK MANAGEMENT +# ============================================================================ + +# Maximum position size per trade (in wei, 10 ETH = 10000000000000000000) +MAX_POSITION_SIZE=10000000000000000000 + +# Maximum daily volume (in wei, 100 ETH = 100000000000000000000) +MAX_DAILY_VOLUME=100000000000000000000 + +# Maximum gas limit per transaction +MAX_GAS_LIMIT=3000000 + +# Gas price strategy (fast, normal, slow) +GAS_PRICE_STRATEGY=fast + +# ============================================================================ +# ARBITRAGE DETECTION +# ============================================================================ + +# Maximum hops in arbitrage path +MAX_HOPS=3 + +# Maximum paths to explore per token +MAX_PATHS=100 + +# Maximum concurrent detection operations +MAX_CONCURRENT_DETECTION=10 + +# ============================================================================ +# EXECUTION SETTINGS +# ============================================================================ + +# Enable transaction simulation before execution +ENABLE_SIMULATION=true + +# Enable front-running of detected opportunities +ENABLE_FRONT_RUNNING=true + +# Number of confirmations to wait +CONFIRMATION_BLOCKS=1 + +# Transaction timeout (in seconds) +TX_TIMEOUT=300 + +# Maximum retries for failed transactions +MAX_RETRIES=3 + +# ============================================================================ +# POOL DISCOVERY +# ============================================================================ + +# Maximum number of pools to discover +MAX_POOLS_TO_DISCOVER=1000 + +# ============================================================================ +# PERFORMANCE TUNING +# ============================================================================ + +# Number of worker threads for transaction processing +WORKER_COUNT=10 + +# Transaction buffer size +BUFFER_SIZE=1000 + +# ============================================================================ +# MONITORING +# ============================================================================ + +# Metrics server port +METRICS_PORT=9090 + +# Log level (debug, info, warn, error) +LOG_LEVEL=info + +# ============================================================================ +# TESTING CONFIGURATION +# ============================================================================ + +# Enable dry-run mode (log opportunities but don't execute) +# DRY_RUN=false + +# Enable test mode with reduced thresholds +# TEST_MODE=false diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7b5c84c --- /dev/null +++ b/.gitignore @@ -0,0 +1,74 @@ +# Environment files with secrets +.env +.env.local +.env.*.local + +# Private keys and wallets +*.key +*.keystore +keystore/ +wallets/ + +# Build artifacts +bin/ +build/ +dist/ +*.exe +*.dll +*.so +*.dylib + +# Test coverage +*.out +coverage.html +coverage.txt + +# IDE files +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS files +.DS_Store +Thumbs.db + +# Logs +logs/ +*.log + +# Data directories +data/ +*.db +*.sqlite + +# Docker volumes +volumes/ + +# Temporary files +tmp/ +temp/ +*.tmp + +# Dependency directories +vendor/ +node_modules/ + +# Compiled binaries +mev-bot-v2 +mev-bot-v1 + +# Monitoring data +prometheus-data/ +grafana-data/ + +# Backup files +*.bak +*.backup +*~ + +# Archive files +*.tar +*.tar.gz +*.zip diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..23956e2 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,55 @@ +# Multi-stage build for MEV Bot V2 + +# Stage 1: Build +FROM golang:1.21-alpine AS builder + +# Install build dependencies +RUN apk add --no-cache git make gcc musl-dev linux-headers + +# Set working directory +WORKDIR /build + +# Copy go mod files +COPY go.mod go.sum ./ + +# Download dependencies +RUN go mod download + +# Copy source code +COPY . . + +# Build the application +RUN go build -o mev-bot-v2 -ldflags="-w -s" ./cmd/mev-bot-v2 + +# Stage 2: Runtime +FROM alpine:latest + +# Install runtime dependencies +RUN apk add --no-cache ca-certificates tzdata + +# Create non-root user +RUN addgroup -g 1000 mevbot && \ + adduser -D -u 1000 -G mevbot mevbot + +# Set working directory +WORKDIR /app + +# Copy binary from builder +COPY --from=builder /build/mev-bot-v2 . + +# Create directories +RUN mkdir -p /app/logs /app/data && \ + chown -R mevbot:mevbot /app + +# Switch to non-root user +USER mevbot + +# Expose metrics port +EXPOSE 9090 + +# Health check +HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ + CMD wget --no-verbose --tries=1 --spider http://localhost:9090/metrics || exit 1 + +# Run the application +ENTRYPOINT ["/app/mev-bot-v2"] diff --git a/TESTING.md b/TESTING.md new file mode 100644 index 0000000..692dd16 --- /dev/null +++ b/TESTING.md @@ -0,0 +1,446 @@ +# MEV Bot V2 - Local Fork Testing Guide + +This guide explains how to test the MEV Bot V2 using a local Arbitrum fork with Anvil. + +## Overview + +Instead of deploying to a public testnet, we use Foundry's Anvil to create a local fork of Arbitrum mainnet. This allows us to: + +- ✅ Test with real mainnet state (pools, liquidity, etc.) +- ✅ Execute transactions instantly (no waiting for block confirmations) +- ✅ Use unlimited test funds +- ✅ Reset and replay scenarios easily +- ✅ Debug without spending real gas +- ✅ Test front-running scenarios safely + +## Prerequisites + +1. **Docker & Docker Compose** installed +2. **Foundry** (for cast commands) - `curl -L https://foundry.paradigm.xyz | bash && foundryup` +3. **Test wallet with private key** (NEVER use real funds) + +## Quick Start + +### 1. Initial Setup + +```bash +# Copy environment file +cp .env.example .env + +# Edit .env and set your test private key +nano .env # Set PRIVATE_KEY to a test wallet key + +# Set up the local fork +./scripts/setup-local-fork.sh +``` + +This script will: +- Start Anvil fork of Arbitrum +- Fund your test wallet with 100 ETH +- Verify connection and balances + +### 2. Start the Full Stack + +```bash +# Start all services (Anvil, MEV Bot, Prometheus, Grafana) +docker-compose up -d + +# View MEV bot logs +docker-compose logs -f mev-bot + +# View Anvil logs +docker-compose logs -f anvil +``` + +### 3. Create Test Swaps + +```bash +# Create a test swap for the bot to detect +./scripts/create-test-swap.sh +``` + +This will: +- Wrap ETH to WETH +- Approve SushiSwap router +- Execute a 0.1 ETH → USDC swap +- The MEV bot should detect this as a potential front-run opportunity + +### 4. Monitor Activity + +**View Metrics:** +```bash +# Prometheus metrics +open http://localhost:9090 + +# Query example: mev_bot_opportunities_found +``` + +**View Grafana Dashboard:** +```bash +# Grafana UI +open http://localhost:3000 +# Login: admin / admin +``` + +**View Bot Logs:** +```bash +docker-compose logs -f mev-bot | grep -i "opportunity\|profit\|execution" +``` + +## Architecture + +``` +┌─────────────────┐ +│ Anvil Fork │ ← Forked Arbitrum mainnet state +│ (Port 8545) │ +└────────┬────────┘ + │ + ├─────────► Pool Discovery (queries real pools) + │ + ├─────────► Sequencer Reader (monitors pending txs) + │ + └─────────► Executor (sends front-run txs) + │ + ▼ + ┌──────────────────┐ + │ MEV Bot V2 │ + └──────────────────┘ + │ + ▼ + ┌──────────────────┐ + │ Prometheus │ ← Metrics + │ (Port 9090) │ + └──────────────────┘ + │ + ▼ + ┌──────────────────┐ + │ Grafana │ ← Visualization + │ (Port 3000) │ + └──────────────────┘ +``` + +## Configuration + +### Environment Variables + +Edit `.env` to configure the bot: + +**Critical Settings:** +```bash +# Your test wallet private key +PRIVATE_KEY=your_test_key_here + +# Minimum profit to execute (0.01 ETH) +MIN_PROFIT=10000000000000000 + +# Enable front-running +ENABLE_FRONT_RUNNING=true + +# Enable simulation before execution +ENABLE_SIMULATION=true +``` + +**Risk Parameters:** +```bash +# Maximum position size (10 ETH) +MAX_POSITION_SIZE=10000000000000000000 + +# Maximum daily volume (100 ETH) +MAX_DAILY_VOLUME=100000000000000000000 +``` + +**Performance Tuning:** +```bash +# Worker threads +WORKER_COUNT=10 + +# Buffer size +BUFFER_SIZE=1000 + +# Log level +LOG_LEVEL=debug +``` + +### Anvil Configuration + +You can customize the fork in `docker-compose.yml`: + +```yaml +anvil: + command: > + anvil + --fork-url ${ARBITRUM_RPC_URL} + --fork-block-number ${FORK_BLOCK_NUMBER:-latest} # Specific block or latest + --chain-id 42161 + --accounts 10 # Test accounts + --balance 10000 # ETH per account + --block-time 1 # 1 second blocks +``` + +## Testing Scenarios + +### Scenario 1: Simple Arbitrage Detection + +```bash +# 1. Start the bot +docker-compose up -d mev-bot + +# 2. Create imbalance by swapping on one DEX +./scripts/create-test-swap.sh + +# 3. Check if bot detected opportunity +docker-compose logs mev-bot | grep "opportunity" +``` + +### Scenario 2: Front-Running Test + +```bash +# 1. Enable front-running in .env +ENABLE_FRONT_RUNNING=true + +# 2. Restart bot +docker-compose restart mev-bot + +# 3. Create a large swap +./scripts/create-test-swap.sh + +# 4. Check if bot front-ran the transaction +docker-compose logs mev-bot | grep "front-running" +``` + +### Scenario 3: Multi-Hop Arbitrage + +```bash +# Create price imbalance across multiple pools +# Swap WETH → USDC on SushiSwap +# Swap USDC → WBTC on Uniswap V3 +# Swap WBTC → WETH on Camelot + +# Bot should detect triangular arbitrage opportunity +docker-compose logs mev-bot | grep "triangular" +``` + +### Scenario 4: Stress Test + +```bash +# Generate many swaps quickly +for i in {1..100}; do + ./scripts/create-test-swap.sh & +done + +# Monitor processing latency +docker-compose logs mev-bot | grep "latency" +``` + +## Monitoring & Debugging + +### Check Pool Discovery + +```bash +# View discovered pools +docker-compose logs mev-bot | grep "discovered pool" + +# Check total pools cached +docker-compose logs mev-bot | grep "pools_cached" +``` + +### Check Sequencer Connection + +```bash +# Verify WebSocket connection +docker-compose logs mev-bot | grep "connected to sequencer" + +# Check transaction processing +docker-compose logs mev-bot | grep "tx_processed" +``` + +### Check Opportunity Detection + +```bash +# View detected opportunities +docker-compose logs mev-bot | grep "opportunities_found" + +# View execution attempts +docker-compose logs mev-bot | grep "executions_attempted" +``` + +### Check Performance Metrics + +```bash +# Prometheus queries +curl http://localhost:9090/api/v1/query?query=mev_bot_parse_latency_seconds +curl http://localhost:9090/api/v1/query?query=mev_bot_detect_latency_seconds +curl http://localhost:9090/api/v1/query?query=mev_bot_execute_latency_seconds +``` + +### Debug Mode + +```bash +# Enable debug logging +echo "LOG_LEVEL=debug" >> .env + +# Restart with debug logs +docker-compose restart mev-bot + +# View detailed logs +docker-compose logs -f mev-bot +``` + +## Common Issues + +### Issue: Anvil not starting + +```bash +# Check if port 8545 is already in use +lsof -i :8545 + +# Kill existing process or change port in docker-compose.yml +``` + +### Issue: Bot not discovering pools + +```bash +# Check RPC connectivity +docker-compose exec anvil cast block-number --rpc-url http://localhost:8545 + +# Check pool discovery logs +docker-compose logs mev-bot | grep "pool discovery" + +# Verify token addresses are correct for Arbitrum +``` + +### Issue: No opportunities detected + +```bash +# 1. Verify pools were discovered +docker-compose logs mev-bot | grep "pools_cached" + +# 2. Check minimum thresholds +# Lower MIN_PROFIT in .env for testing + +# 3. Create larger price imbalances +# Increase swap amounts in create-test-swap.sh +``` + +### Issue: Transactions reverting + +```bash +# Enable simulation to catch errors before execution +ENABLE_SIMULATION=true + +# Check revert reasons +docker-compose logs mev-bot | grep "revert" + +# Verify wallet has sufficient balance +docker-compose exec anvil cast balance --rpc-url http://localhost:8545 +``` + +## Advanced Testing + +### Forking from Specific Block + +```bash +# Set specific block in .env +FORK_BLOCK_NUMBER=180000000 + +# Restart Anvil +docker-compose restart anvil +``` + +### Impersonate Accounts + +```bash +# Use Anvil's account impersonation to test edge cases +docker-compose exec anvil cast rpc anvil_impersonateAccount
+``` + +### Manipulate State + +```bash +# Set arbitrary balances +docker-compose exec anvil cast rpc anvil_setBalance
+ +# Mine blocks +docker-compose exec anvil cast rpc anvil_mine 100 + +# Set next block timestamp +docker-compose exec anvil cast rpc evm_setNextBlockTimestamp +``` + +### Reset Fork + +```bash +# Reset to original fork state +docker-compose exec anvil cast rpc anvil_reset + +# Re-run pool discovery +docker-compose restart mev-bot +``` + +## Performance Benchmarks + +Target metrics for production readiness: + +- ✅ **Parse Latency:** < 5ms per transaction +- ✅ **Detect Latency:** < 10ms per opportunity scan +- ✅ **Execute Latency:** < 30ms decision time +- ✅ **Total Latency:** < 50ms end-to-end +- ✅ **Throughput:** > 100 tx/second processing +- ✅ **Memory:** < 500MB steady state +- ✅ **CPU:** < 50% on 4 cores + +### Benchmarking Commands + +```bash +# Measure parse latency +docker-compose logs mev-bot | grep "avg_parse_latency" + +# Measure detection latency +docker-compose logs mev-bot | grep "avg_detect_latency" + +# Measure execution latency +docker-compose logs mev-bot | grep "avg_execute_latency" + +# Check memory usage +docker stats mev-bot-v2 + +# Load test with many concurrent swaps +./scripts/load-test.sh +``` + +## Cleanup + +```bash +# Stop all services +docker-compose down + +# Remove volumes (clears metrics data) +docker-compose down -v + +# Clean up logs +rm -rf logs/* +``` + +## Next Steps + +Once local testing is successful: + +1. **Optimize Parameters:** Tune MIN_PROFIT, MAX_SLIPPAGE, etc. +2. **Deploy Flashloan Contract:** Deploy executor contract to Arbitrum testnet +3. **Testnet Testing:** Test on Arbitrum Goerli with real test ETH +4. **Mainnet Deployment:** Deploy with conservative parameters and monitoring + +## Resources + +- **Anvil Docs:** https://book.getfoundry.sh/anvil/ +- **Arbitrum Docs:** https://docs.arbitrum.io/ +- **Prometheus Docs:** https://prometheus.io/docs/ +- **Grafana Docs:** https://grafana.com/docs/ + +## Support + +For issues or questions: +1. Check logs: `docker-compose logs mev-bot` +2. Review configuration: `.env` +3. Check Anvil status: `docker-compose logs anvil` +4. Verify metrics: `http://localhost:9090` diff --git a/config/grafana/datasources/prometheus.yml b/config/grafana/datasources/prometheus.yml new file mode 100644 index 0000000..228561d --- /dev/null +++ b/config/grafana/datasources/prometheus.yml @@ -0,0 +1,11 @@ +apiVersion: 1 + +datasources: + - name: Prometheus + type: prometheus + access: proxy + url: http://prometheus:9090 + isDefault: true + editable: true + jsonData: + timeInterval: 15s diff --git a/config/prometheus.yml b/config/prometheus.yml new file mode 100644 index 0000000..0c5d422 --- /dev/null +++ b/config/prometheus.yml @@ -0,0 +1,16 @@ +global: + scrape_interval: 15s + scrape_timeout: 10s + evaluation_interval: 15s + +scrape_configs: + - job_name: 'mev-bot' + static_configs: + - targets: ['mev-bot:9090'] + labels: + instance: 'mev-bot-v2' + environment: 'testing' + + - job_name: 'prometheus' + static_configs: + - targets: ['localhost:9090'] diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..9bf16e7 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,104 @@ +version: '3.8' + +services: + # Anvil fork of Arbitrum for local testing + anvil: + image: ghcr.io/foundry-rs/foundry:latest + container_name: mev-bot-anvil + ports: + - "8545:8545" + - "8546:8546" + command: > + anvil + --fork-url ${ARBITRUM_RPC_URL:-https://arb1.arbitrum.io/rpc} + --fork-block-number ${FORK_BLOCK_NUMBER:-latest} + --host 0.0.0.0 + --port 8545 + --chain-id 42161 + --accounts 10 + --balance 10000 + --gas-limit 30000000 + --gas-price 0 + --base-fee 0 + --no-mining + --block-time 1 + networks: + - mev-network + healthcheck: + test: ["CMD", "cast", "block-number", "--rpc-url", "http://localhost:8545"] + interval: 5s + timeout: 3s + retries: 5 + + # MEV Bot V2 + mev-bot: + build: + context: . + dockerfile: Dockerfile + container_name: mev-bot-v2 + depends_on: + anvil: + condition: service_healthy + env_file: + - .env + environment: + # Override RPC URLs to point to local Anvil fork + - RPC_URL=http://anvil:8545 + - WS_URL=ws://anvil:8546 + - SEQUENCER_WS_URL=ws://anvil:8546 + # Testing configuration + - ENABLE_SIMULATION=true + - ENABLE_FRONT_RUNNING=true + - LOG_LEVEL=debug + networks: + - mev-network + restart: unless-stopped + volumes: + - ./logs:/app/logs + - ./data:/app/data + + # Prometheus for metrics collection + prometheus: + image: prom/prometheus:latest + container_name: mev-bot-prometheus + ports: + - "9090:9090" + volumes: + - ./config/prometheus.yml:/etc/prometheus/prometheus.yml + - prometheus-data:/prometheus + command: + - '--config.file=/etc/prometheus/prometheus.yml' + - '--storage.tsdb.path=/prometheus' + - '--web.console.libraries=/usr/share/prometheus/console_libraries' + - '--web.console.templates=/usr/share/prometheus/consoles' + networks: + - mev-network + restart: unless-stopped + + # Grafana for metrics visualization + grafana: + image: grafana/grafana:latest + container_name: mev-bot-grafana + ports: + - "3000:3000" + environment: + - GF_SECURITY_ADMIN_USER=admin + - GF_SECURITY_ADMIN_PASSWORD=admin + - GF_INSTALL_PLUGINS= + volumes: + - grafana-data:/var/lib/grafana + - ./config/grafana/dashboards:/etc/grafana/provisioning/dashboards + - ./config/grafana/datasources:/etc/grafana/provisioning/datasources + networks: + - mev-network + depends_on: + - prometheus + restart: unless-stopped + +networks: + mev-network: + driver: bridge + +volumes: + prometheus-data: + grafana-data: diff --git a/pkg/pools/discovery.go b/pkg/pools/discovery.go new file mode 100644 index 0000000..5d1167b --- /dev/null +++ b/pkg/pools/discovery.go @@ -0,0 +1,397 @@ +package pools + +import ( + "context" + "fmt" + "log/slog" + "math/big" + "sync" + + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/ethclient" + + "github.com/your-org/mev-bot/pkg/cache" + mevtypes "github.com/your-org/mev-bot/pkg/types" +) + +// Known factory addresses on Arbitrum +var ( + UniswapV2FactoryAddress = common.HexToAddress("0xf1D7CC64Fb4452F05c498126312eBE29f30Fbcf9") // SushiSwap + UniswapV3FactoryAddress = common.HexToAddress("0x1F98431c8aD98523631AE4a59f267346ea31F984") // Uniswap V3 + CamelotFactoryAddress = common.HexToAddress("0x6EcCab422D763aC031210895C81787E87B43A652") // Camelot + CurveRegistryAddress = common.HexToAddress("0x445FE580eF8d70FF569aB36e80c647af338db351") // Curve (mainnet, example) +) + +// Top traded tokens on Arbitrum +var TopTokens = []common.Address{ + common.HexToAddress("0x82aF49447D8a07e3bd95BD0d56f35241523fBab1"), // WETH + common.HexToAddress("0xFF970a61A04b1cA14834A43f5dE4533eBDDB5CC8"), // USDC + common.HexToAddress("0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9"), // USDT + common.HexToAddress("0x2f2a2543B76A4166549F7aaB2e75Bef0aefC5B0f"), // WBTC + common.HexToAddress("0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1"), // DAI + common.HexToAddress("0xf97f4df75117a78c1A5a0DBb814Af92458539FB4"), // LINK + common.HexToAddress("0xFA7F8980b0f1E64A2062791cc3b0871572f1F7f0"), // UNI +} + +// DiscoveryConfig contains configuration for pool discovery +type DiscoveryConfig struct { + // Connection + RPCURL string + + // Discovery parameters + StartBlock uint64 + MaxPools int + MinLiquidity *big.Int + BatchSize int + ConcurrentFetches int + + // Token pairs to discover + TokenPairs []TokenPair +} + +// TokenPair represents a pair of tokens +type TokenPair struct { + Token0 common.Address + Token1 common.Address +} + +// DefaultDiscoveryConfig returns default configuration +func DefaultDiscoveryConfig() *DiscoveryConfig { + // Generate pairs from top tokens + pairs := make([]TokenPair, 0) + for i := 0; i < len(TopTokens); i++ { + for j := i + 1; j < len(TopTokens); j++ { + pairs = append(pairs, TokenPair{ + Token0: TopTokens[i], + Token1: TopTokens[j], + }) + } + } + + return &DiscoveryConfig{ + RPCURL: "https://arb1.arbitrum.io/rpc", + StartBlock: 0, + MaxPools: 1000, + MinLiquidity: big.NewInt(1e18), // 1 ETH minimum + BatchSize: 100, + ConcurrentFetches: 10, + TokenPairs: pairs, + } +} + +// Discovery discovers pools on Arbitrum +type Discovery struct { + config *DiscoveryConfig + client *ethclient.Client + cache *cache.PoolCache + logger *slog.Logger + + mu sync.Mutex + poolsDiscovered int +} + +// NewDiscovery creates a new pool discovery service +func NewDiscovery(config *DiscoveryConfig, cache *cache.PoolCache, logger *slog.Logger) (*Discovery, error) { + if config == nil { + config = DefaultDiscoveryConfig() + } + + client, err := ethclient.Dial(config.RPCURL) + if err != nil { + return nil, fmt.Errorf("failed to connect to RPC: %w", err) + } + + return &Discovery{ + config: config, + client: client, + cache: cache, + logger: logger.With("component", "pool_discovery"), + }, nil +} + +// DiscoverAll discovers all pools from known DEXes +func (d *Discovery) DiscoverAll(ctx context.Context) error { + d.logger.Info("starting pool discovery") + + // Discover UniswapV2-style pools (SushiSwap, Camelot, etc.) + if err := d.discoverUniswapV2Pools(ctx); err != nil { + d.logger.Error("uniswap v2 discovery failed", "error", err) + } + + // Discover UniswapV3 pools + if err := d.discoverUniswapV3Pools(ctx); err != nil { + d.logger.Error("uniswap v3 discovery failed", "error", err) + } + + d.logger.Info("pool discovery complete", "pools_discovered", d.poolsDiscovered, "total_cached", d.cache.Count()) + return nil +} + +// discoverUniswapV2Pools discovers UniswapV2-style pools +func (d *Discovery) discoverUniswapV2Pools(ctx context.Context) error { + d.logger.Info("discovering UniswapV2-style pools") + + factories := []struct { + address common.Address + protocol string + }{ + {UniswapV2FactoryAddress, mevtypes.ProtocolUniswapV2}, + {CamelotFactoryAddress, mevtypes.ProtocolCamelot}, + } + + for _, factory := range factories { + d.logger.Info("querying factory", "protocol", factory.protocol, "address", factory.address.Hex()) + + // Query each token pair + for _, pair := range d.config.TokenPairs { + select { + case <-ctx.Done(): + return ctx.Err() + default: + } + + poolAddr, err := d.getUniswapV2Pool(ctx, factory.address, pair.Token0, pair.Token1) + if err != nil { + continue + } + + if poolAddr == (common.Address{}) { + continue // Pool doesn't exist + } + + // Fetch pool info + poolInfo, err := d.fetchUniswapV2PoolInfo(ctx, poolAddr, pair.Token0, pair.Token1, factory.protocol) + if err != nil { + d.logger.Debug("failed to fetch pool info", "pool", poolAddr.Hex(), "error", err) + continue + } + + // Check minimum liquidity + if poolInfo.LiquidityUSD.Cmp(d.config.MinLiquidity) < 0 { + continue + } + + // Add to cache + if err := d.cache.Add(poolInfo); err != nil { + d.logger.Warn("failed to add pool to cache", "pool", poolAddr.Hex(), "error", err) + continue + } + + d.mu.Lock() + d.poolsDiscovered++ + d.mu.Unlock() + + d.logger.Debug("discovered pool", + "protocol", factory.protocol, + "pool", poolAddr.Hex(), + "token0", pair.Token0.Hex(), + "token1", pair.Token1.Hex(), + "liquidity", poolInfo.LiquidityUSD.String(), + ) + } + } + + return nil +} + +// getUniswapV2Pool gets a UniswapV2 pool address for a token pair +func (d *Discovery) getUniswapV2Pool(ctx context.Context, factory common.Address, token0, token1 common.Address) (common.Address, error) { + // getPair(address,address) returns (address) + // This is a simplified version - in production, use generated bindings + calldata := append([]byte{0xe6, 0xa4, 0x39, 0x05}, // getPair selector + append(padLeft(token0.Bytes(), 32), padLeft(token1.Bytes(), 32)...)...) + + result, err := d.client.CallContract(ctx, map[string]interface{}{ + "to": factory, + "data": common.Bytes2Hex(calldata), + }, nil) + if err != nil { + return common.Address{}, err + } + + if len(result) == 0 { + return common.Address{}, nil + } + + return common.BytesToAddress(result[12:]), nil +} + +// fetchUniswapV2PoolInfo fetches pool information +func (d *Discovery) fetchUniswapV2PoolInfo(ctx context.Context, poolAddr, token0, token1 common.Address, protocol string) (*mevtypes.PoolInfo, error) { + // getReserves() returns (uint112,uint112,uint32) + // Simplified - in production use generated bindings + calldata := []byte{0x09, 0x02, 0xf1, 0xac} // getReserves selector + + result, err := d.client.CallContract(ctx, map[string]interface{}{ + "to": poolAddr, + "data": common.Bytes2Hex(calldata), + }, nil) + if err != nil { + return nil, err + } + + if len(result) < 64 { + return nil, fmt.Errorf("invalid reserves response") + } + + reserve0 := new(big.Int).SetBytes(result[0:32]) + reserve1 := new(big.Int).SetBytes(result[32:64]) + + // Estimate liquidity in USD (simplified - in production, use price oracle) + liquidityUSD := new(big.Int).Add(reserve0, reserve1) + + return &mevtypes.PoolInfo{ + Address: poolAddr, + Protocol: protocol, + Token0: token0, + Token1: token1, + Reserve0: reserve0, + Reserve1: reserve1, + Fee: 300, // 0.3% for UniswapV2 + LiquidityUSD: liquidityUSD, + }, nil +} + +// discoverUniswapV3Pools discovers UniswapV3 pools +func (d *Discovery) discoverUniswapV3Pools(ctx context.Context) error { + d.logger.Info("discovering UniswapV3 pools") + + // UniswapV3 has multiple fee tiers + feeTiers := []uint32{100, 500, 3000, 10000} + + for _, pair := range d.config.TokenPairs { + for _, fee := range feeTiers { + select { + case <-ctx.Done(): + return ctx.Err() + default: + } + + poolAddr, err := d.getUniswapV3Pool(ctx, pair.Token0, pair.Token1, fee) + if err != nil { + continue + } + + if poolAddr == (common.Address{}) { + continue // Pool doesn't exist + } + + // Fetch pool info + poolInfo, err := d.fetchUniswapV3PoolInfo(ctx, poolAddr, pair.Token0, pair.Token1, fee) + if err != nil { + d.logger.Debug("failed to fetch pool info", "pool", poolAddr.Hex(), "error", err) + continue + } + + // Check minimum liquidity + if poolInfo.LiquidityUSD.Cmp(d.config.MinLiquidity) < 0 { + continue + } + + // Add to cache + if err := d.cache.Add(poolInfo); err != nil { + d.logger.Warn("failed to add pool to cache", "pool", poolAddr.Hex(), "error", err) + continue + } + + d.mu.Lock() + d.poolsDiscovered++ + d.mu.Unlock() + + d.logger.Debug("discovered pool", + "protocol", mevtypes.ProtocolUniswapV3, + "pool", poolAddr.Hex(), + "token0", pair.Token0.Hex(), + "token1", pair.Token1.Hex(), + "fee", fee, + "liquidity", poolInfo.LiquidityUSD.String(), + ) + + // Check if we've reached max pools + if d.poolsDiscovered >= d.config.MaxPools { + return nil + } + } + } + + return nil +} + +// getUniswapV3Pool gets a UniswapV3 pool address +func (d *Discovery) getUniswapV3Pool(ctx context.Context, token0, token1 common.Address, fee uint32) (common.Address, error) { + // getPool(address,address,uint24) returns (address) + // Simplified - in production use generated bindings + feeBytes := make([]byte, 32) + copy(feeBytes[29:], big.NewInt(int64(fee)).Bytes()) + + calldata := append([]byte{0x17, 0x79, 0x05, 0x7a}, // getPool selector + append(append(padLeft(token0.Bytes(), 32), padLeft(token1.Bytes(), 32)...), feeBytes...)...) + + result, err := d.client.CallContract(ctx, map[string]interface{}{ + "to": UniswapV3FactoryAddress, + "data": common.Bytes2Hex(calldata), + }, nil) + if err != nil { + return common.Address{}, err + } + + if len(result) == 0 { + return common.Address{}, nil + } + + return common.BytesToAddress(result[12:]), nil +} + +// fetchUniswapV3PoolInfo fetches UniswapV3 pool information +func (d *Discovery) fetchUniswapV3PoolInfo(ctx context.Context, poolAddr, token0, token1 common.Address, fee uint32) (*mevtypes.PoolInfo, error) { + // liquidity() returns (uint128) + // Simplified - in production use generated bindings + calldata := []byte{0x1a, 0x68, 0x65, 0x02} // liquidity selector + + result, err := d.client.CallContract(ctx, map[string]interface{}{ + "to": poolAddr, + "data": common.Bytes2Hex(calldata), + }, nil) + if err != nil { + return nil, err + } + + if len(result) < 16 { + return nil, fmt.Errorf("invalid liquidity response") + } + + liquidity := new(big.Int).SetBytes(result[16:32]) + + return &mevtypes.PoolInfo{ + Address: poolAddr, + Protocol: mevtypes.ProtocolUniswapV3, + Token0: token0, + Token1: token1, + Reserve0: liquidity, // Simplified + Reserve1: liquidity, + Fee: fee, + LiquidityUSD: liquidity, + }, nil +} + +// padLeft pads bytes to the left with zeros +func padLeft(data []byte, length int) []byte { + if len(data) >= length { + return data + } + padded := make([]byte, length) + copy(padded[length-len(data):], data) + return padded +} + +// GetStats returns discovery statistics +func (d *Discovery) GetStats() map[string]interface{} { + d.mu.Lock() + defer d.mu.Unlock() + + return map[string]interface{}{ + "pools_discovered": d.poolsDiscovered, + "pools_cached": d.cache.Count(), + } +} diff --git a/pkg/sequencer/reader.go b/pkg/sequencer/reader.go new file mode 100644 index 0000000..5844c68 --- /dev/null +++ b/pkg/sequencer/reader.go @@ -0,0 +1,461 @@ +package sequencer + +import ( + "context" + "encoding/json" + "fmt" + "log/slog" + "math/big" + "sync" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/ethclient" + "github.com/gorilla/websocket" + + "github.com/your-org/mev-bot/pkg/arbitrage" + "github.com/your-org/mev-bot/pkg/cache" + "github.com/your-org/mev-bot/pkg/execution" + "github.com/your-org/mev-bot/pkg/parsers" + "github.com/your-org/mev-bot/pkg/validation" +) + +// ReaderConfig contains configuration for the sequencer reader +type ReaderConfig struct { + // WebSocket connection + WSURL string + ReconnectDelay time.Duration + MaxReconnectDelay time.Duration + PingInterval time.Duration + + // RPC for fetching full transactions + RPCURL string + + // Processing + WorkerCount int + BufferSize int + + // Filtering + MinProfit *big.Int + EnableFrontRunning bool + + // Performance + MaxProcessingTime time.Duration +} + +// DefaultReaderConfig returns default configuration +func DefaultReaderConfig() *ReaderConfig { + return &ReaderConfig{ + WSURL: "wss://arb1.arbitrum.io/ws", + ReconnectDelay: 1 * time.Second, + MaxReconnectDelay: 60 * time.Second, + PingInterval: 30 * time.Second, + RPCURL: "https://arb1.arbitrum.io/rpc", + WorkerCount: 10, + BufferSize: 1000, + MinProfit: big.NewInt(0.01e18), // 0.01 ETH + EnableFrontRunning: true, + MaxProcessingTime: 50 * time.Millisecond, + } +} + +// Reader reads pending transactions from the Arbitrum sequencer +type Reader struct { + config *ReaderConfig + logger *slog.Logger + + // Components + parsers *parsers.Factory + validator *validation.Validator + poolCache *cache.PoolCache + detector *arbitrage.Detector + executor *execution.Executor + + // Connections + wsConn *websocket.Conn + rpcClient *ethclient.Client + + // Channels + txHashes chan string + stopCh chan struct{} + wg sync.WaitGroup + + // State + mu sync.RWMutex + connected bool + lastProcessed time.Time + processedCount uint64 + opportunityCount uint64 + executionCount uint64 + + // Metrics (placeholders for actual metrics) + txReceived uint64 + txProcessed uint64 + parseErrors uint64 + validationErrors uint64 + opportunitiesFound uint64 + executionsAttempted uint64 + avgParseLatency time.Duration + avgDetectLatency time.Duration + avgExecuteLatency time.Duration +} + +// NewReader creates a new sequencer reader +func NewReader( + config *ReaderConfig, + parsers *parsers.Factory, + validator *validation.Validator, + poolCache *cache.PoolCache, + detector *arbitrage.Detector, + executor *execution.Executor, + logger *slog.Logger, +) (*Reader, error) { + if config == nil { + config = DefaultReaderConfig() + } + + // Connect to RPC for fetching full transactions + rpcClient, err := ethclient.Dial(config.RPCURL) + if err != nil { + return nil, fmt.Errorf("failed to connect to RPC: %w", err) + } + + return &Reader{ + config: config, + logger: logger.With("component", "sequencer_reader"), + parsers: parsers, + validator: validator, + poolCache: poolCache, + detector: detector, + executor: executor, + rpcClient: rpcClient, + txHashes: make(chan string, config.BufferSize), + stopCh: make(chan struct{}), + }, nil +} + +// Start starts the sequencer reader +func (r *Reader) Start(ctx context.Context) error { + r.logger.Info("starting sequencer reader") + + // Start workers + for i := 0; i < r.config.WorkerCount; i++ { + r.wg.Add(1) + go r.worker(ctx, i) + } + + // Start connection manager + r.wg.Add(1) + go r.maintainConnection(ctx) + + // Wait for context cancellation + <-ctx.Done() + + r.logger.Info("stopping sequencer reader") + close(r.stopCh) + r.wg.Wait() + + return ctx.Err() +} + +// maintainConnection maintains the WebSocket connection with automatic reconnection +func (r *Reader) maintainConnection(ctx context.Context) { + defer r.wg.Done() + + reconnectDelay := r.config.ReconnectDelay + + for { + select { + case <-ctx.Done(): + return + default: + } + + // Connect to sequencer + conn, err := r.connect(ctx) + if err != nil { + r.logger.Error("connection failed", "error", err, "retry_in", reconnectDelay) + time.Sleep(reconnectDelay) + + // Exponential backoff + reconnectDelay *= 2 + if reconnectDelay > r.config.MaxReconnectDelay { + reconnectDelay = r.config.MaxReconnectDelay + } + continue + } + + // Reset backoff on successful connection + reconnectDelay = r.config.ReconnectDelay + r.wsConn = conn + + r.mu.Lock() + r.connected = true + r.mu.Unlock() + + r.logger.Info("connected to sequencer") + + // Subscribe to pending transactions + if err := r.subscribe(ctx, conn); err != nil { + r.logger.Error("subscription failed", "error", err) + conn.Close() + continue + } + + // Read messages until connection fails + if err := r.readMessages(ctx, conn); err != nil { + r.logger.Error("connection lost", "error", err) + } + + r.mu.Lock() + r.connected = false + r.mu.Unlock() + + conn.Close() + } +} + +// connect establishes a WebSocket connection +func (r *Reader) connect(ctx context.Context) (*websocket.Conn, error) { + dialer := websocket.DefaultDialer + dialer.HandshakeTimeout = 10 * time.Second + + conn, _, err := dialer.DialContext(ctx, r.config.WSURL, nil) + if err != nil { + return nil, fmt.Errorf("dial failed: %w", err) + } + + // Set read/write deadlines + conn.SetReadDeadline(time.Now().Add(r.config.PingInterval * 2)) + conn.SetWriteDeadline(time.Now().Add(10 * time.Second)) + + return conn, nil +} + +// subscribe subscribes to pending transactions +func (r *Reader) subscribe(ctx context.Context, conn *websocket.Conn) error { + // Subscribe to newPendingTransactions + sub := map[string]interface{}{ + "jsonrpc": "2.0", + "id": 1, + "method": "eth_subscribe", + "params": []interface{}{"newPendingTransactions"}, + } + + if err := conn.WriteJSON(sub); err != nil { + return fmt.Errorf("subscription write failed: %w", err) + } + + // Read subscription response + var resp map[string]interface{} + if err := conn.ReadJSON(&resp); err != nil { + return fmt.Errorf("subscription response failed: %w", err) + } + + r.logger.Info("subscribed to pending transactions", "response", resp) + return nil +} + +// readMessages reads messages from the WebSocket connection +func (r *Reader) readMessages(ctx context.Context, conn *websocket.Conn) error { + for { + select { + case <-ctx.Done(): + return ctx.Err() + case <-r.stopCh: + return nil + default: + } + + // Set read deadline + conn.SetReadDeadline(time.Now().Add(r.config.PingInterval * 2)) + + var msg map[string]interface{} + if err := conn.ReadJSON(&msg); err != nil { + return fmt.Errorf("read failed: %w", err) + } + + // Extract transaction hash from notification + if params, ok := msg["params"].(map[string]interface{}); ok { + if result, ok := params["result"].(string); ok { + // Send to worker pool + select { + case r.txHashes <- result: + r.txReceived++ + case <-ctx.Done(): + return ctx.Err() + default: + r.logger.Warn("tx buffer full, dropping tx") + } + } + } + } +} + +// worker processes transaction hashes +func (r *Reader) worker(ctx context.Context, id int) { + defer r.wg.Done() + + logger := r.logger.With("worker", id) + + for { + select { + case <-ctx.Done(): + return + case <-r.stopCh: + return + case txHash := <-r.txHashes: + if err := r.processTxHash(ctx, txHash); err != nil { + logger.Debug("processing error", "tx", txHash, "error", err) + } + } + } +} + +// processTxHash processes a transaction hash +func (r *Reader) processTxHash(ctx context.Context, txHash string) error { + startTime := time.Now() + + // Enforce max processing time + procCtx, cancel := context.WithTimeout(ctx, r.config.MaxProcessingTime) + defer cancel() + + // Fetch full transaction + tx, isPending, err := r.rpcClient.TransactionByHash(procCtx, common.HexToHash(txHash)) + if err != nil { + return fmt.Errorf("fetch tx failed: %w", err) + } + + if !isPending { + return nil // Skip already mined transactions + } + + parseStart := time.Now() + + // Parse transaction events + events, err := r.parsers.ParseTransaction(tx) + if err != nil { + r.parseErrors++ + return fmt.Errorf("parse failed: %w", err) + } + + if len(events) == 0 { + return nil // No swap events + } + + r.avgParseLatency = time.Since(parseStart) + + // Validate events + validEvents := r.validator.FilterValid(events) + if len(validEvents) == 0 { + r.validationErrors++ + return nil + } + + detectStart := time.Now() + + // Detect arbitrage opportunities for each swap + for _, event := range validEvents { + // Get input token from the swap + inputToken := event.GetInputToken() + + // Detect opportunities starting with this token + opportunities, err := r.detector.DetectOpportunities(procCtx, inputToken) + if err != nil { + continue + } + + r.avgDetectLatency = time.Since(detectStart) + + // Execute profitable opportunities + for _, opp := range opportunities { + if opp.NetProfit.Cmp(r.config.MinProfit) > 0 { + r.opportunitiesFound++ + r.opportunityCount++ + + if r.config.EnableFrontRunning { + execStart := time.Now() + go r.executeFrontRun(ctx, opp, tx) + r.avgExecuteLatency = time.Since(execStart) + } + } + } + } + + r.txProcessed++ + r.processedCount++ + r.lastProcessed = time.Now() + + totalLatency := time.Since(startTime) + if totalLatency > r.config.MaxProcessingTime { + r.logger.Warn("processing too slow", "latency", totalLatency, "target", r.config.MaxProcessingTime) + } + + return nil +} + +// executeFrontRun executes a front-running transaction +func (r *Reader) executeFrontRun(ctx context.Context, opp *arbitrage.Opportunity, targetTx *types.Transaction) { + r.executionsAttempted++ + r.executionCount++ + + r.logger.Info("front-running opportunity", + "opportunity_id", opp.ID, + "type", opp.Type, + "profit", opp.NetProfit.String(), + "roi", fmt.Sprintf("%.2f%%", opp.ROI*100), + "target_tx", targetTx.Hash().Hex(), + ) + + // Execute the arbitrage + result, err := r.executor.Execute(ctx, opp) + if err != nil { + r.logger.Error("execution failed", + "opportunity_id", opp.ID, + "error", err, + ) + return + } + + if result.Success { + r.logger.Info("execution succeeded", + "opportunity_id", opp.ID, + "tx_hash", result.TxHash.Hex(), + "actual_profit", result.ActualProfit.String(), + "gas_cost", result.GasCost.String(), + "duration", result.Duration, + ) + } else { + r.logger.Warn("execution failed", + "opportunity_id", opp.ID, + "tx_hash", result.TxHash.Hex(), + "error", result.Error, + ) + } +} + +// GetStats returns current statistics +func (r *Reader) GetStats() map[string]interface{} { + r.mu.RLock() + defer r.mu.RUnlock() + + return map[string]interface{}{ + "connected": r.connected, + "tx_received": r.txReceived, + "tx_processed": r.txProcessed, + "parse_errors": r.parseErrors, + "validation_errors": r.validationErrors, + "opportunities_found": r.opportunitiesFound, + "executions_attempted": r.executionsAttempted, + "avg_parse_latency": r.avgParseLatency.String(), + "avg_detect_latency": r.avgDetectLatency.String(), + "avg_execute_latency": r.avgExecuteLatency.String(), + "last_processed": r.lastProcessed.Format(time.RFC3339), + } +} + +// Stop stops the sequencer reader +func (r *Reader) Stop() { + close(r.stopCh) +} diff --git a/scripts/create-test-swap.sh b/scripts/create-test-swap.sh new file mode 100755 index 0000000..ca092d7 --- /dev/null +++ b/scripts/create-test-swap.sh @@ -0,0 +1,105 @@ +#!/bin/bash +set -e + +# MEV Bot V2 - Create Test Swap Script +# This script creates a test swap on the Anvil fork for the bot to detect + +# Colors for output +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +RED='\033[0;31m' +NC='\033[0m' # No Color + +echo "🔄 Creating test swap on Anvil fork..." + +# Load environment variables +if [ ! -f .env ]; then + echo -e "${RED}❌ Error: .env file not found${NC}" + exit 1 +fi +source .env + +# Token addresses on Arbitrum +WETH="0x82aF49447D8a07e3bd95BD0d56f35241523fBab1" +USDC="0xFF970a61A04b1cA14834A43f5dE4533eBDDB5CC8" + +# SushiSwap Router on Arbitrum +SUSHISWAP_ROUTER="0x1b02dA8Cb0d097eB8D57A175b88c7D8b47997506" + +# Amount to swap (0.1 ETH) +AMOUNT="100000000000000000" + +echo -e "${YELLOW}Token In: WETH ($WETH)${NC}" +echo -e "${YELLOW}Token Out: USDC ($USDC)${NC}" +echo -e "${YELLOW}Amount: 0.1 ETH${NC}" + +# Step 1: Wrap ETH to WETH +echo "" +echo "📦 Step 1: Wrapping ETH to WETH..." +docker-compose exec -T anvil cast send $WETH \ + "deposit()" \ + --value 0.1ether \ + --private-key $PRIVATE_KEY \ + --rpc-url http://localhost:8545 + +WETH_BALANCE=$(docker-compose exec -T anvil cast call $WETH \ + "balanceOf(address)(uint256)" \ + $(docker-compose exec -T anvil cast wallet address $PRIVATE_KEY) \ + --rpc-url http://localhost:8545) +echo -e "${GREEN}✅ WETH Balance: $WETH_BALANCE${NC}" + +# Step 2: Approve router to spend WETH +echo "" +echo "✅ Step 2: Approving SushiSwap router..." +docker-compose exec -T anvil cast send $WETH \ + "approve(address,uint256)" \ + $SUSHISWAP_ROUTER \ + "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" \ + --private-key $PRIVATE_KEY \ + --rpc-url http://localhost:8545 + +echo -e "${GREEN}✅ Approval confirmed${NC}" + +# Step 3: Execute swap +echo "" +echo "🔄 Step 3: Executing swap on SushiSwap..." +echo -e "${YELLOW}This swap should be detected by the MEV bot!${NC}" + +# Build swap calldata +# swapExactTokensForTokens(uint256 amountIn, uint256 amountOutMin, address[] path, address to, uint256 deadline) +DEADLINE=$(($(date +%s) + 3600)) +TO=$(docker-compose exec -T anvil cast wallet address $PRIVATE_KEY) + +docker-compose exec -T anvil cast send $SUSHISWAP_ROUTER \ + "swapExactTokensForTokens(uint256,uint256,address[],address,uint256)" \ + $AMOUNT \ + "0" \ + "[$WETH,$USDC]" \ + $TO \ + $DEADLINE \ + --private-key $PRIVATE_KEY \ + --rpc-url http://localhost:8545 + +echo "" +echo -e "${GREEN}✅ Test swap executed!${NC}" + +# Check balances +echo "" +echo "📊 Final balances:" +WETH_BALANCE=$(docker-compose exec -T anvil cast call $WETH \ + "balanceOf(address)(uint256)" \ + $TO \ + --rpc-url http://localhost:8545) +echo -e " WETH: $WETH_BALANCE" + +USDC_BALANCE=$(docker-compose exec -T anvil cast call $USDC \ + "balanceOf(address)(uint256)" \ + $TO \ + --rpc-url http://localhost:8545) +echo -e " USDC: $USDC_BALANCE" + +echo "" +echo -e "${GREEN}✨ Test swap complete!${NC}" +echo "" +echo "Check the MEV bot logs to see if it detected the opportunity:" +echo " docker-compose logs -f mev-bot" diff --git a/scripts/setup-local-fork.sh b/scripts/setup-local-fork.sh new file mode 100755 index 0000000..610fcd0 --- /dev/null +++ b/scripts/setup-local-fork.sh @@ -0,0 +1,86 @@ +#!/bin/bash +set -e + +# MEV Bot V2 - Local Fork Setup Script +# This script sets up the Anvil fork with test liquidity and scenarios + +echo "🚀 Setting up MEV Bot V2 local testing environment..." + +# Colors for output +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +RED='\033[0;31m' +NC='\033[0m' # No Color + +# Check if .env exists +if [ ! -f .env ]; then + echo -e "${YELLOW}⚠️ No .env file found. Copying from .env.example...${NC}" + cp .env.example .env + echo -e "${RED}⚠️ IMPORTANT: Edit .env and set your PRIVATE_KEY before continuing!${NC}" + exit 1 +fi + +# Load environment variables +source .env + +# Check if private key is set +if [ "$PRIVATE_KEY" == "0000000000000000000000000000000000000000000000000000000000000000" ]; then + echo -e "${RED}❌ Error: PRIVATE_KEY not set in .env file${NC}" + echo -e "${YELLOW}Please set a test private key in .env${NC}" + exit 1 +fi + +echo -e "${GREEN}✅ Configuration loaded${NC}" + +# Start Anvil fork +echo "🔧 Starting Anvil fork of Arbitrum..." +docker-compose up -d anvil + +# Wait for Anvil to be ready +echo "⏳ Waiting for Anvil to be ready..." +max_attempts=30 +attempt=0 +while [ $attempt -lt $max_attempts ]; do + if docker-compose exec -T anvil cast block-number --rpc-url http://localhost:8545 &>/dev/null; then + echo -e "${GREEN}✅ Anvil is ready${NC}" + break + fi + attempt=$((attempt + 1)) + sleep 2 +done + +if [ $attempt -eq $max_attempts ]; then + echo -e "${RED}❌ Anvil failed to start${NC}" + exit 1 +fi + +# Get current block number +BLOCK_NUMBER=$(docker-compose exec -T anvil cast block-number --rpc-url http://localhost:8545) +echo -e "${GREEN}📦 Forked at block: $BLOCK_NUMBER${NC}" + +# Get wallet address from private key +WALLET_ADDRESS=$(docker-compose exec -T anvil cast wallet address $PRIVATE_KEY) +echo -e "${GREEN}💼 Wallet address: $WALLET_ADDRESS${NC}" + +# Fund the wallet with test ETH +echo "💰 Funding wallet with 100 test ETH..." +docker-compose exec -T anvil cast send --value 100ether --private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 $WALLET_ADDRESS --rpc-url http://localhost:8545 + +# Get wallet balance +BALANCE=$(docker-compose exec -T anvil cast balance $WALLET_ADDRESS --rpc-url http://localhost:8545) +echo -e "${GREEN}💵 Wallet balance: $(echo "scale=4; $BALANCE / 1000000000000000000" | bc) ETH${NC}" + +echo "" +echo -e "${GREEN}✨ Local fork setup complete!${NC}" +echo "" +echo "Next steps:" +echo " 1. Start the full stack: docker-compose up -d" +echo " 2. View logs: docker-compose logs -f mev-bot" +echo " 3. View metrics: http://localhost:9090" +echo " 4. View Grafana: http://localhost:3000 (admin/admin)" +echo "" +echo "Testing commands:" +echo " - Check Anvil: docker-compose exec anvil cast block-number --rpc-url http://localhost:8545" +echo " - Get balance: docker-compose exec anvil cast balance $WALLET_ADDRESS --rpc-url http://localhost:8545" +echo " - Create test swap: ./scripts/create-test-swap.sh" +echo ""