417 lines
15 KiB
Markdown
417 lines
15 KiB
Markdown
# MEV Bot Profitability Audit - Critical Blockers Identified
|
|
|
|
**Date:** November 4, 2025
|
|
**Status:** ZERO EXECUTIONS - 100% Blocker Rate
|
|
**Executive Summary:** The MEV bot detects opportunities but fails to execute ANY arbitrage trades. Analysis reveals 10 critical blockers preventing profitability.
|
|
|
|
---
|
|
|
|
## CRITICAL FINDINGS
|
|
|
|
### Primary Issue: Zero Execution Rate
|
|
- **Detected Opportunities:** Yes (multiple per minute)
|
|
- **Executed Opportunities:** 0 (ZERO - 100% failure rate)
|
|
- **Success Rate:** 0.00%
|
|
- **Net Profit:** 0.000000 ETH (no successful trades)
|
|
|
|
From logs:
|
|
```
|
|
2025/11/02 15:22:38 [INFO] Arbitrage Service Stats - Detected: 0, Executed: 0, Successful: 0, Success Rate: 0.00%, Total Profit: 0.000000 ETH
|
|
```
|
|
|
|
All detected opportunities show: `isExecutable:false rejectReason:negative profit after gas and slippage costs`
|
|
|
|
---
|
|
|
|
## RANKED BLOCKERS BY IMPACT (Most Critical First)
|
|
|
|
### BLOCKER #1: Zero Amount Detection - CATASTROPHIC
|
|
**Impact:** Blocks 95%+ of all opportunity attempts
|
|
**Location:** `pkg/profitcalc/profit_calc.go` lines 104-134
|
|
**Problem:**
|
|
```go
|
|
// Lines 108-118: Rejects all zero/nil amounts
|
|
if amountIn == nil || amountOut == nil || amountIn.Sign() <= 0 || amountOut.Sign() <= 0 {
|
|
opportunity.IsExecutable = false
|
|
opportunity.RejectReason = "invalid swap amounts (nil or zero)"
|
|
opportunity.Confidence = 0.0
|
|
return opportunity
|
|
}
|
|
|
|
// Lines 121-134: Rejects dust amounts below 0.0001 ETH
|
|
if amountIn.Cmp(minAmount) < 0 || amountOut.Cmp(minAmount) < 0 {
|
|
opportunity.IsExecutable = false
|
|
opportunity.RejectReason = "dust amounts below threshold..."
|
|
return opportunity
|
|
}
|
|
```
|
|
|
|
**Log Evidence:**
|
|
```
|
|
Amount In: 0.000000 tokens
|
|
Amount Out: 0.000000 tokens
|
|
rejectReason:negative profit after gas and slippage costs
|
|
```
|
|
|
|
**Root Cause:** Token amount extraction from swap events is broken. The `AnalyzeSwapOpportunity` method receives ZERO amounts even when swaps are clearly happening (e.g., "Amount Out: 1611.982004 tokens" logged but marked as invalid).
|
|
|
|
**Data Flow Issue:**
|
|
1. Swap events are parsed → amounts extracted wrong
|
|
2. Amounts are zero or dust when passed to `AnalyzeSwapOpportunity`
|
|
3. Opportunity immediately rejected
|
|
4. Never reaches execution logic
|
|
|
|
**Why Not Fixed:** The token decimals or amount parsing is broken in the event parser (`pkg/arbitrum/parser/core.go` or `pkg/events/parser.go`). Amounts are being lost or miscalculated during extraction.
|
|
|
|
---
|
|
|
|
### BLOCKER #2: Token Graph Empty - CRITICAL
|
|
**Impact:** No arbitrage paths can be found
|
|
**Location:** `pkg/arbitrage/multihop.go` lines 167-173
|
|
**Problem:**
|
|
```go
|
|
adjacent := mhs.tokenGraph.GetAdjacentTokens(startToken)
|
|
if len(adjacent) == 0 {
|
|
mhs.logger.Warn(fmt.Sprintf("⚠️ Start token %s has no adjacent tokens in graph!", startToken.Hex()))
|
|
}
|
|
```
|
|
|
|
**Analysis:** The token graph is manually populated with hardcoded pool addresses (lines 522-594) that:
|
|
1. Use PLACEHOLDER liquidity values (all set to `uint256.NewInt(1000000000000000000)`)
|
|
2. Placeholder `SqrtPriceX96` values (never updated from actual pools)
|
|
3. Never connect to the actual Arbitrum mainnet state
|
|
4. Have LastUpdated = time.Zero (fails `isPoolUsable()` check at line 703)
|
|
|
|
**Why Not Fixed:** Pool discovery is DISABLED intentionally:
|
|
```go
|
|
// From cmd/mev-bot/main.go line 15:
|
|
// "github.com/fraktal/mev-beta/internal/tokens" // Not used - pool discovery disabled
|
|
```
|
|
|
|
The system:
|
|
- Loads 314 pools from cache (`logs: Loaded 314 pools from cache`)
|
|
- But NEVER adds them to the token graph used for arbitrage scanning
|
|
- Token graph is manually populated with 8 hardcoded pools (all with stale data)
|
|
- No connection between pool cache and token graph
|
|
|
|
---
|
|
|
|
### BLOCKER #3: Batch Fetcher Contract Call Reverts - HIGH
|
|
**Impact:** Cannot fetch pool state data for ANY pool
|
|
**Location:** `pkg/scanner/market/scanner.go` line 139-142
|
|
**Problem:**
|
|
```
|
|
2025/11/02 15:22:49 [WARN] Failed to fetch batch 0-1: batch fetch V3 data failed: execution reverted
|
|
(repeated 25+ times)
|
|
```
|
|
|
|
**Root Cause:** The DataFetcher contract call reverts on Arbitrum:
|
|
- Contract address: `0xC6BD82306943c0F3104296a46113ca0863723cBD`
|
|
- Method: `batchFetchV3Data()` (fixed from broken `batchFetchAllData`)
|
|
- Error: execution reverted (likely contract is not deployed or wrong address)
|
|
|
|
**Impact Chain:**
|
|
1. Cannot fetch real pool liquidity
|
|
2. Falls back to placeholder values
|
|
3. Profit calculations use placeholder data
|
|
4. All calculations are mathematically invalid
|
|
|
|
From logs:
|
|
```go
|
|
// pkg/scanner/market/scanner.go lines 145-150:
|
|
dataFetcherAddrStr := os.Getenv("CONTRACT_DATA_FETCHER")
|
|
if dataFetcherAddrStr == "" {
|
|
dataFetcherAddrStr = "0x42105682F819891698E76cfE6897F10b75f8aabc" // Fallback
|
|
}
|
|
```
|
|
|
|
The fallback address (0x421056...) doesn't match the one in config (0xC6BD82...). Contract may not be deployed at this address.
|
|
|
|
---
|
|
|
|
### BLOCKER #4: Profit Margin Calculation Invalid - HIGH
|
|
**Impact:** All profitable opportunities marked as unprofitable
|
|
**Location:** `pkg/profitcalc/profit_calc.go` lines 261-294
|
|
**Problem:**
|
|
```
|
|
profitMargin:-4.466551104702248e-09 (EXTREME NEGATIVE VALUE)
|
|
profitMargin:-38365.84743877249 (EXTREME NEGATIVE VALUE)
|
|
```
|
|
|
|
**Log Evidence from actual opportunities:**
|
|
```
|
|
rejectReason:negative profit after gas and slippage costs
|
|
estimatedProfitETH:0.000000
|
|
gasCostETH:0.000007
|
|
netProfitETH:-0.000007
|
|
profitMargin:-330178.9776420681
|
|
```
|
|
|
|
**Root Cause:** When amounts are zero or near-zero:
|
|
1. All calculations use zero amounts
|
|
2. Net profit = 0 - 0 - 0.000007 = NEGATIVE
|
|
3. Profit margin = negative / 0 = UNDEFINED EXTREME VALUE
|
|
4. Circuit breaker rejects it
|
|
|
|
**Mathematical Issue:**
|
|
- Line 264: `profitMargin := new(big.Float).Quo(netProfit, amountOut)`
|
|
- When amountOut is near-zero, Quo operation produces extreme values
|
|
- Lines 270-283: Bounds checking is BACKWARDS:
|
|
```go
|
|
if profitMarginFloat > 1.0 { // reject if > 100%
|
|
opportunity.IsExecutable = false
|
|
} else if profitMarginFloat < -1.0 { // reject if < -100%
|
|
opportunity.IsExecutable = false
|
|
}
|
|
```
|
|
The check rejects BOTH extreme values (correct) BUT the logic doesn't prevent -330,000 from being calculated
|
|
|
|
---
|
|
|
|
### BLOCKER #5: Unknown Token Filtering - MEDIUM-HIGH
|
|
**Impact:** 95% of tokens cannot be priced
|
|
**Location:** `pkg/profitcalc/profit_calc.go` lines 152-161
|
|
**Problem:**
|
|
```go
|
|
if arbitrageRoute == nil {
|
|
opportunity.IsExecutable = false
|
|
opportunity.RejectReason = fmt.Sprintf("unknown token - cannot price %s or %s", tokenA.Hex()[:10], tokenB.Hex()[:10])
|
|
opportunity.Confidence = 0.05 // Lower confidence for unknown token
|
|
return opportunity
|
|
}
|
|
```
|
|
|
|
**Analysis:** The PriceFeed doesn't have pricing for most Arbitrum tokens:
|
|
- Only 6 tokens cached: `logs: Loaded 6 tokens from cache`
|
|
- Arbitrum has 100+ tradeable tokens
|
|
- Any token pair missing from PriceFeed = immediate rejection
|
|
- Falls back to simplified spread calculation (~30 bps, unrealistic)
|
|
|
|
**Impact:**
|
|
- Cannot determine actual profitability
|
|
- Uses fallback calculation: `profit = amountOut * 0.003 - amountIn * 0.003`
|
|
- This is mathematically incorrect for arbitrage
|
|
|
|
---
|
|
|
|
### BLOCKER #6: Execution Code Path Unreachable - CRITICAL
|
|
**Impact:** Even if opportunities were valid, they wouldn't execute
|
|
**Location:** `pkg/arbitrage/service.go` (ExecuteArbitrage method)
|
|
**Problem:**
|
|
The arbitrage execution chain is:
|
|
1. Market scanner detects opportunity → `SetOpportunityForwarder` routes to MultiHopScanner
|
|
2. MultiHopScanner.ScanForArbitrage finds paths → returns paths
|
|
3. **DEAD END:** Nobody calls the executor with the paths
|
|
|
|
From logs and code review:
|
|
- No evidence of `arb.Executor.Execute()` or `arb.FlashExecutor.Execute()` being called
|
|
- The opportunities are detected but never forwarded to actual execution
|
|
- `ExecuteArbitrage` method in service.go exists but is never invoked
|
|
|
|
**Missing Link:** The opportunity forwarder interface is set but:
|
|
```go
|
|
// pkg/scanner/market/scanner.go line 86-88:
|
|
func (s *MarketScanner) SetOpportunityForwarder(forwarder OpportunityForwarder) {
|
|
s.opportunityForwarder = forwarder
|
|
s.logger.Info("✅ Opportunity forwarder set - will route to multi-hop scanner")
|
|
}
|
|
```
|
|
|
|
But nothing actually calls `s.opportunityForwarder.ExecuteArbitrage()` with valid opportunities.
|
|
|
|
---
|
|
|
|
### BLOCKER #7: Minimum Profit Threshold Too High - MEDIUM
|
|
**Impact:** Rejects legitimate opportunities
|
|
**Location:** `pkg/arbitrage/detection_engine.go` line 188-190
|
|
**Problem:**
|
|
```go
|
|
engine.config.MinProfitThreshold, _ = engine.decimalConverter.FromString("0.001", 18, "ETH")
|
|
// = 0.001 ETH minimum (~$2 at current prices)
|
|
```
|
|
|
|
**Analysis:** The minimum is actually reasonable (0.001 ETH = ~$2), BUT:
|
|
1. Gas cost on Arbitrum = 100-300k gas @ 0.1-0.5 gwei = $0.0002-0.001
|
|
2. Minimum 0.5% spread opportunity (across DEXs) = rare
|
|
3. With ZERO amounts being detected, threshold becomes irrelevant
|
|
|
|
**Sub-blocker:** Configuration also has:
|
|
```yaml
|
|
# config/arbitrum_production.yaml line 263:
|
|
min_profit_wei: 1000000000000000 # $2.00 minimum profit
|
|
min_roi_percent: 0.05 # Minimum 0.05% ROI
|
|
min_confidence_score: 0.6 # Minimum 60% confidence
|
|
```
|
|
|
|
But all detected opportunities have `confidence: 0.1` (10%), so they fail this check.
|
|
|
|
---
|
|
|
|
### BLOCKER #8: RPC BatchFetch Contract Wrong/Not Deployed - MEDIUM
|
|
**Impact:** Cannot fetch pool state in batches (heavy RPC usage)
|
|
**Location:** `pkg/datafetcher/batch_fetcher.go`
|
|
**Problem:**
|
|
- Config points to `0xC6BD82306943c0F3104296a46113ca0863723cBD`
|
|
- Code fallback: `0x42105682F819891698E76cfE6897F10b75f8aabc`
|
|
- Neither address works (contract reverts or doesn't exist)
|
|
- System falls back to individual RPC calls for each pool
|
|
|
|
**Impact:**
|
|
- RPC rate limits hit immediately
|
|
- Cannot fetch data for 314 cached pools
|
|
- System stalls on rate limiting
|
|
|
|
---
|
|
|
|
### BLOCKER #9: No Live Execution Framework Integration - CRITICAL
|
|
**Impact:** Flash loan executor never triggers
|
|
**Location:** `pkg/arbitrage/flash_executor.go` and `pkg/arbitrage/executor.go`
|
|
**Problem:**
|
|
Flash executors are initialized but NEVER called:
|
|
```go
|
|
// pkg/arbitrage/service.go line 42-43:
|
|
flashExecutor *FlashSwapExecutor
|
|
liveFramework *LiveExecutionFramework
|
|
```
|
|
|
|
Both are initialized in `NewArbitrageService()` but:
|
|
- No goroutines call their Execute methods
|
|
- No event listeners forward opportunities to them
|
|
- They sit dormant while opportunities pass by
|
|
|
|
**Missing:** The actual execution loop that should:
|
|
1. Listen for valid opportunities
|
|
2. Call `flashExecutor.ExecuteFlashSwap()`
|
|
3. Monitor transaction result
|
|
4. Track profit/loss
|
|
|
|
Instead, the code only logs opportunities and never executes.
|
|
|
|
---
|
|
|
|
### BLOCKER #10: Event Parser Extracts Zero Amounts - HIGH
|
|
**Impact:** All swap events show 0 token amounts
|
|
**Location:** `pkg/arbitrum/parser/core.go` or `pkg/events/parser.go`
|
|
**Problem:**
|
|
Swap events are being parsed with ZERO amounts when they contain data:
|
|
```
|
|
Log entry shows: "Amount Out: 1611.982004 tokens"
|
|
But parsed as: amount0Out:0, amount1Out:0
|
|
```
|
|
|
|
**Evidence from logs:**
|
|
```
|
|
Amount In: 0.000000 tokens
|
|
Amount Out: 0.000000 tokens
|
|
```
|
|
|
|
Yet the same transaction logs show real amounts being swapped. The parser is either:
|
|
1. Extracting wrong event fields
|
|
2. Not handling token decimals correctly
|
|
3. Using wrong event signature for Swap events
|
|
4. Losing data during amount sign conversion
|
|
|
|
This is the FIRST failure point in the entire pipeline.
|
|
|
|
---
|
|
|
|
## SECONDARY ISSUES
|
|
|
|
### Issue A: Pool Blacklist Prevents Scanning
|
|
- Valid pools are being blacklisted due to transient RPC errors
|
|
- Once blacklisted, never attempted again
|
|
- System self-limits to fewer and fewer pools
|
|
|
|
### Issue B: Configuration Mismatch
|
|
- Multiple min profit thresholds at different levels
|
|
- Different enabled/disabled features in different configs
|
|
- Some features marked as DISABLED in code but ENABLED in config
|
|
|
|
### Issue C: No Actual Wallet Funding Check
|
|
- Bot generates keys but never verifies wallet has funds
|
|
- Tries to execute trades with 0 balance wallet
|
|
- Transactions fail but error handling swallows the message
|
|
|
|
### Issue D: Rate Limiting Too Aggressive
|
|
- Max 5 concurrent RPC calls (`config/arbitrum_production.yaml` line 336)
|
|
- 20 requests/second on growth plan (very conservative)
|
|
- System rate limits itself before hitting Chainstack limits
|
|
|
|
---
|
|
|
|
## ROOT CAUSE ANALYSIS
|
|
|
|
The system has ARCHITECTURAL SEPARATION:
|
|
1. **Detection Pipeline** (WORKING): Finds opportunities ✅
|
|
2. **Data Fetch Pipeline** (BROKEN): Cannot get pool state ❌
|
|
3. **Profit Calculation** (BROKEN): Uses invalid amounts ❌
|
|
4. **Execution Pipeline** (DISCONNECTED): Never triggered ❌
|
|
|
|
**Why No Executions:**
|
|
```
|
|
Event Parsed → Amounts = ZERO → Profit Rejected → Execution Never Attempted
|
|
↑
|
|
ROOT CAUSE
|
|
```
|
|
|
|
The token amount extraction is broken at the event parser level. Everything downstream (profit calc, execution decisions) becomes irrelevant because the input data is invalid.
|
|
|
|
---
|
|
|
|
## RECOMMENDATIONS (Priority Order)
|
|
|
|
### P0: Fix Event Parser Amount Extraction
|
|
- Review `pkg/events/parser.go` - validate Swap event parsing
|
|
- Check token decimal handling in amount conversion
|
|
- Verify event signature matching (Uniswap V3 Swap vs V2)
|
|
- Test with real swap transactions to verify amount extraction
|
|
|
|
### P1: Connect Token Graph to Pool Cache
|
|
- Link the 314 cached pools to the token graph
|
|
- Implement pool discovery loop that updates token graph
|
|
- Verify graph has minimum 20+ tokens with 50+ pools
|
|
|
|
### P2: Fix Batch Fetcher Contract
|
|
- Deploy actual DataFetcher contract or find correct address
|
|
- Test `batchFetchV3Data()` call on Arbitrum
|
|
- Implement fallback to individual RPC calls with proper caching
|
|
|
|
### P3: Connect Execution Pipeline
|
|
- Add event listener that forwards VALID opportunities to executor
|
|
- Implement execution goroutine
|
|
- Add transaction monitoring and result tracking
|
|
|
|
### P4: Verify Wallet Funding
|
|
- Check wallet balance before any execution attempt
|
|
- Require minimum balance (0.05 ETH) for flash loan execution
|
|
- Add pre-flight validation
|
|
|
|
---
|
|
|
|
## TESTING CHECKLIST
|
|
|
|
- [ ] Swap event parser extracts amounts correctly
|
|
- [ ] Profit calculator receives non-zero amounts
|
|
- [ ] 50+ opportunities marked as `isExecutable:true`
|
|
- [ ] TokenGraph has 20+ tokens, 50+ pool connections
|
|
- [ ] DataFetcher contract calls succeed
|
|
- [ ] First profitable arbitrage executes successfully
|
|
- [ ] Transaction confirmed on-chain
|
|
- [ ] Profit > gas cost
|
|
|
|
---
|
|
|
|
## ESTIMATED FIX EFFORT
|
|
|
|
| Blocker | Effort | Time |
|
|
|---------|--------|------|
|
|
| #1 - Event Parser Fix | 2-4 hours | Debug + unit test + integration test |
|
|
| #2 - Token Graph Link | 1-2 hours | Wire up pool cache to graph |
|
|
| #3 - Batch Fetcher | 2-3 hours | Deploy/fix contract or remove |
|
|
| #6 - Execution Connection | 2-3 hours | Wire event → executor pipeline |
|
|
| #10 - Validation | 1-2 hours | Add amount validation in parser |
|
|
| **Total** | **8-14 hours** | **Full system operational** |
|
|
|
|
**Expected Outcome:** System should achieve first profitable execution within 24 hours of fixing blockers #1-2.
|
|
|