Restructured project for V2 refactor: **Structure Changes:** - Moved all V1 code to orig/ folder (preserved with git mv) - Created docs/planning/ directory - Added orig/README_V1.md explaining V1 preservation **Planning Documents:** - 00_V2_MASTER_PLAN.md: Complete architecture overview - Executive summary of critical V1 issues - High-level component architecture diagrams - 5-phase implementation roadmap - Success metrics and risk mitigation - 07_TASK_BREAKDOWN.md: Atomic task breakdown - 99+ hours of detailed tasks - Every task < 2 hours (atomic) - Clear dependencies and success criteria - Organized by implementation phase **V2 Key Improvements:** - Per-exchange parsers (factory pattern) - Multi-layer strict validation - Multi-index pool cache - Background validation pipeline - Comprehensive observability **Critical Issues Addressed:** - Zero address tokens (strict validation + cache enrichment) - Parsing accuracy (protocol-specific parsers) - No audit trail (background validation channel) - Inefficient lookups (multi-index cache) - Stats disconnection (event-driven metrics) Next Steps: 1. Review planning documents 2. Begin Phase 1: Foundation (P1-001 through P1-010) 3. Implement parsers in Phase 2 4. Build cache system in Phase 3 5. Add validation pipeline in Phase 4 6. Migrate and test in Phase 5 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
366 lines
16 KiB
Go
366 lines
16 KiB
Go
package exchanges
|
|
|
|
import (
|
|
"fmt"
|
|
"math/big"
|
|
|
|
"github.com/ethereum/go-ethereum/common"
|
|
"github.com/ethereum/go-ethereum/ethclient"
|
|
|
|
"github.com/fraktal/mev-beta/internal/logger"
|
|
"github.com/fraktal/mev-beta/pkg/math"
|
|
)
|
|
|
|
// TokenInfo represents token information
|
|
type TokenInfo struct {
|
|
Address string
|
|
Symbol string
|
|
Name string
|
|
Decimals uint8
|
|
}
|
|
|
|
// TokenPair represents a pair of tokens
|
|
type TokenPair struct {
|
|
Token0 TokenInfo
|
|
Token1 TokenInfo
|
|
}
|
|
|
|
// Use ExchangeType from math package to avoid duplication
|
|
|
|
// ExchangeConfig holds configuration for an exchange
|
|
type ExchangeConfig struct {
|
|
Type math.ExchangeType
|
|
Name string
|
|
FactoryAddress common.Address
|
|
RouterAddress common.Address
|
|
PoolInitCodeHash string
|
|
SwapSelector []byte
|
|
StableSwapSelector []byte
|
|
ChainID int64
|
|
SupportsFlashSwaps bool
|
|
RequiresApproval bool
|
|
MaxHops int
|
|
DefaultSlippagePercent float64
|
|
Url string
|
|
ApiUrl string
|
|
}
|
|
|
|
// PoolDetector interface for detecting pools
|
|
type PoolDetector interface {
|
|
GetAllPools(token0, token1 common.Address) ([]common.Address, error)
|
|
GetPoolForPair(token0, token1 common.Address) (common.Address, error)
|
|
GetSupportedFeeTiers() []int64
|
|
GetPoolType() string
|
|
}
|
|
|
|
// LiquidityFetcher interface for fetching liquidity data
|
|
type LiquidityFetcher interface {
|
|
GetPoolData(poolAddress common.Address) (*math.PoolData, error)
|
|
GetTokenReserves(poolAddress, token0, token1 common.Address) (*big.Int, *big.Int, error)
|
|
GetPoolPrice(poolAddress common.Address) (*big.Float, error)
|
|
GetLiquidityDepth(poolAddress, tokenIn common.Address, amount *big.Int) (*big.Int, error)
|
|
}
|
|
|
|
// SwapRouter interface for executing swaps
|
|
type SwapRouter interface {
|
|
CalculateSwap(tokenIn, tokenOut common.Address, amountIn *big.Int) (*big.Int, error)
|
|
GenerateSwapData(tokenIn, tokenOut common.Address, amountIn, minAmountOut *big.Int, deadline *big.Int) ([]byte, error)
|
|
GetSwapRoute(tokenIn, tokenOut common.Address) ([]common.Address, error)
|
|
ValidateSwap(tokenIn, tokenOut common.Address, amountIn *big.Int) error
|
|
}
|
|
|
|
// ExchangeRegistry manages multiple exchanges
|
|
type ExchangeRegistry struct {
|
|
client *ethclient.Client
|
|
logger *logger.Logger
|
|
exchanges map[math.ExchangeType]*ExchangeConfig
|
|
poolDetectors map[math.ExchangeType]PoolDetector
|
|
liquidityFetchers map[math.ExchangeType]LiquidityFetcher
|
|
swapRouters map[math.ExchangeType]SwapRouter
|
|
marketScanner *MarketScanner
|
|
}
|
|
|
|
// MarketScanner interface for scanning markets
|
|
type MarketScanner interface {
|
|
GetPoolsForTokenPair(token0, token1 common.Address) []common.Address
|
|
GetPoolInfo(poolAddress common.Address) (*math.PoolData, error)
|
|
}
|
|
|
|
// NewExchangeRegistry creates a new exchange registry
|
|
func NewExchangeRegistry(client *ethclient.Client, logger *logger.Logger) *ExchangeRegistry {
|
|
return &ExchangeRegistry{
|
|
client: client,
|
|
logger: logger,
|
|
exchanges: make(map[math.ExchangeType]*ExchangeConfig),
|
|
poolDetectors: make(map[math.ExchangeType]PoolDetector),
|
|
liquidityFetchers: make(map[math.ExchangeType]LiquidityFetcher),
|
|
swapRouters: make(map[math.ExchangeType]SwapRouter),
|
|
}
|
|
}
|
|
|
|
// LoadArbitrumExchanges loads all supported exchanges on Arbitrum
|
|
func (er *ExchangeRegistry) LoadArbitrumExchanges() error {
|
|
// Load Uniswap V3
|
|
uniswapV3Config := &ExchangeConfig{
|
|
Type: math.ExchangeUniswapV3,
|
|
Name: "Uniswap V3",
|
|
FactoryAddress: common.HexToAddress("0x1F98431c8aD98523631AE4a59f267346ea31F984"),
|
|
RouterAddress: common.HexToAddress("0xE592427A0AEce92De3Edee1F18E0157C05861564"),
|
|
PoolInitCodeHash: "0xe34f199b19b2b4f47d6b5b39c7bde33e2a0a4c36149e248e847a490f543b7f3f",
|
|
SwapSelector: []byte{0x38, 0xed, 0x17, 0x39}, // exactInputSingle
|
|
StableSwapSelector: []byte{},
|
|
ChainID: 42161,
|
|
SupportsFlashSwaps: true,
|
|
RequiresApproval: true,
|
|
MaxHops: 2,
|
|
DefaultSlippagePercent: 0.5,
|
|
Url: "https://uniswap.org",
|
|
ApiUrl: "https://api.uniswap.org",
|
|
}
|
|
|
|
er.exchanges[math.ExchangeUniswapV3] = uniswapV3Config
|
|
|
|
// Load SushiSwap
|
|
sushiSwapConfig := &ExchangeConfig{
|
|
Type: math.ExchangeSushiSwap,
|
|
Name: "SushiSwap",
|
|
FactoryAddress: common.HexToAddress("0xc35DADB65012eC5796536bD9864eD8773aBc74C4"),
|
|
RouterAddress: common.HexToAddress("0x1b02dA8Cb0d097eB8D57A175b88c7D8b47997506"),
|
|
PoolInitCodeHash: "0xe18a34eb0e04b04f7a0ac29a6e80748dca96319b42c54d679cb821dca90c6303",
|
|
SwapSelector: []byte{0x18, 0x2d, 0x2e, 0xdb}, // swapExactTokensForTokens
|
|
StableSwapSelector: []byte{},
|
|
ChainID: 42161,
|
|
SupportsFlashSwaps: true,
|
|
RequiresApproval: true,
|
|
MaxHops: 3,
|
|
DefaultSlippagePercent: 0.5,
|
|
Url: "https://sushi.com",
|
|
ApiUrl: "https://api.sushi.com",
|
|
}
|
|
|
|
er.exchanges[math.ExchangeSushiSwap] = sushiSwapConfig
|
|
|
|
// Load Curve
|
|
curveConfig := &ExchangeConfig{
|
|
Type: math.ExchangeCurve,
|
|
Name: "Curve",
|
|
FactoryAddress: common.HexToAddress("0xb1748c7970d18f7d0ebf0f89e1c3f3b98e65e8ad"),
|
|
RouterAddress: common.HexToAddress("0x4d7c19704623c38f54ec0d144c0ac63d0b76106e"),
|
|
PoolInitCodeHash: "",
|
|
SwapSelector: []byte{0x5b, 0x40, 0x2d, 0x3c}, // exchange
|
|
StableSwapSelector: []byte{0x79, 0x1a, 0xc9, 0x47}, // exchange_underlying
|
|
ChainID: 42161,
|
|
SupportsFlashSwaps: false,
|
|
RequiresApproval: true,
|
|
MaxHops: 2,
|
|
DefaultSlippagePercent: 0.1,
|
|
Url: "https://curve.fi",
|
|
ApiUrl: "https://api.curve.fi",
|
|
}
|
|
|
|
er.exchanges[math.ExchangeCurve] = curveConfig
|
|
|
|
// Load Balancer
|
|
balancerConfig := &ExchangeConfig{
|
|
Type: math.ExchangeBalancer,
|
|
Name: "Balancer",
|
|
FactoryAddress: common.HexToAddress("0xb33c178f5e23f3abf72a0dbfe955f1e69f2e47a6"),
|
|
RouterAddress: common.HexToAddress("0xba12222222228d8ba445958a75a0704d566bf2c8"),
|
|
PoolInitCodeHash: "",
|
|
SwapSelector: []byte{0x3e, 0x2, 0x76, 0x5e}, // swap
|
|
StableSwapSelector: []byte{},
|
|
ChainID: 42161,
|
|
SupportsFlashSwaps: true,
|
|
RequiresApproval: true,
|
|
MaxHops: 3,
|
|
DefaultSlippagePercent: 0.3,
|
|
Url: "https://balancer.fi",
|
|
ApiUrl: "https://api.balancer.fi",
|
|
}
|
|
|
|
er.exchanges[math.ExchangeBalancer] = balancerConfig
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetExchangeByType returns an exchange configuration by type
|
|
func (er *ExchangeRegistry) GetExchangeByType(exchangeType math.ExchangeType) *ExchangeConfig {
|
|
config, exists := er.exchanges[exchangeType]
|
|
if !exists {
|
|
return nil
|
|
}
|
|
return config
|
|
}
|
|
|
|
// GetExchangesForPair returns exchanges that support a token pair
|
|
func (er *ExchangeRegistry) GetExchangesForPair(token0, token1 common.Address) []*ExchangeConfig {
|
|
var supported []*ExchangeConfig
|
|
|
|
for _, config := range er.exchanges {
|
|
// In a real implementation, this would check if pools exist for this pair
|
|
// For now, we'll assume all exchanges support all pairs for testing
|
|
supported = append(supported, config)
|
|
}
|
|
|
|
return supported
|
|
}
|
|
|
|
// GetAllExchanges returns all registered exchanges
|
|
func (er *ExchangeRegistry) GetAllExchanges() []*ExchangeConfig {
|
|
var configs []*ExchangeConfig
|
|
for _, config := range er.exchanges {
|
|
configs = append(configs, config)
|
|
}
|
|
return configs
|
|
}
|
|
|
|
// GetAllExchangesMap returns all registered exchanges as a map
|
|
func (er *ExchangeRegistry) GetAllExchangesMap() map[math.ExchangeType]*ExchangeConfig {
|
|
return er.exchanges
|
|
}
|
|
|
|
// GetHighPriorityTokens returns high-priority tokens for scanning
|
|
func (er *ExchangeRegistry) GetHighPriorityTokens(limit int) []TokenInfo {
|
|
// Define high-priority tokens (ETH, USDC, USDT, WBTC, etc.)
|
|
// CRITICAL FIX: Use correct Arbitrum token addresses (not Ethereum/other chains)
|
|
highPriorityTokens := []TokenInfo{
|
|
{Address: "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1", Symbol: "WETH", Name: "Wrapped Ether", Decimals: 18},
|
|
{Address: "0xaf88d065e77c8cC2239327C5EDb3A432268e5831", Symbol: "USDC", Name: "USD Coin", Decimals: 6}, // FIXED: Correct Arbitrum USDC
|
|
{Address: "0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9", Symbol: "USDT", Name: "Tether USD", Decimals: 6},
|
|
{Address: "0x2f2a2543B76A4166549F7aaB2e75Bef0aefC5B0f", Symbol: "WBTC", Name: "Wrapped BTC", Decimals: 8}, // FIXED: Correct Arbitrum WBTC
|
|
{Address: "0xf97f4df75117a78c1A5a0DBb814Af92458539FB4", Symbol: "LINK", Name: "ChainLink Token", Decimals: 18}, // FIXED: Correct Arbitrum LINK
|
|
{Address: "0x11cDb42B0EB46D95f990BeDD4695A6e3fA034978", Symbol: "CRV", Name: "Curve DAO Token", Decimals: 18}, // FIXED: Correct Arbitrum CRV
|
|
{Address: "0x912CE59144191C1204E64559FE8253a0e49E6548", Symbol: "ARB", Name: "Arbitrum", Decimals: 18}, // ADDED: Arbitrum native token
|
|
{Address: "0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1", Symbol: "DAI", Name: "Dai Stablecoin", Decimals: 18}, // ADDED: DAI
|
|
{Address: "0xfc5A1A6EB076a2C7aD06eD22C90d7E710E35ad0a", Symbol: "GMX", Name: "GMX", Decimals: 18}, // ADDED: GMX
|
|
{Address: "0x9623063377AD1B27544C965cCd7342f7EA7e88C7", Symbol: "GRT", Name: "The Graph", Decimals: 18}, // ADDED: GRT
|
|
}
|
|
|
|
if limit > len(highPriorityTokens) {
|
|
limit = len(highPriorityTokens)
|
|
}
|
|
|
|
return highPriorityTokens[:limit]
|
|
}
|
|
|
|
// IsPairSupported checks if a token pair is supported by any exchange
|
|
func (er *ExchangeRegistry) IsPairSupported(token0, token1 common.Address) bool {
|
|
supportedExchanges := er.GetExchangesForPair(token0, token1)
|
|
return len(supportedExchanges) > 0
|
|
}
|
|
|
|
// ArbitragePath represents a simple arbitrage path for the exchanges package
|
|
type ArbitragePath struct {
|
|
TokenIn common.Address
|
|
TokenOut common.Address
|
|
Exchanges []math.ExchangeType
|
|
Pools []common.Address
|
|
}
|
|
|
|
// GetPoolDetector returns the pool detector for a specific exchange type
|
|
func (er *ExchangeRegistry) GetPoolDetector(exchangeType math.ExchangeType) PoolDetector {
|
|
return er.poolDetectors[exchangeType]
|
|
}
|
|
|
|
// GetLiquidityFetcher returns the liquidity fetcher for a specific exchange type
|
|
func (er *ExchangeRegistry) GetLiquidityFetcher(exchangeType math.ExchangeType) LiquidityFetcher {
|
|
return er.liquidityFetchers[exchangeType]
|
|
}
|
|
|
|
// GetSwapRouter returns the swap router for a specific exchange type
|
|
func (er *ExchangeRegistry) GetSwapRouter(exchangeType math.ExchangeType) SwapRouter {
|
|
return er.swapRouters[exchangeType]
|
|
}
|
|
|
|
// FindAllPaths finds all possible arbitrage paths between two tokens
|
|
func (er *ExchangeRegistry) FindAllPaths(tokenA, tokenB common.Address, maxHops int) ([]*ArbitragePath, error) {
|
|
// Simplified implementation for compilation - would be enhanced with actual path finding
|
|
paths := []*ArbitragePath{}
|
|
|
|
// For now, create a simple direct path
|
|
if er.IsPairSupported(tokenA, tokenB) {
|
|
path := &ArbitragePath{
|
|
TokenIn: tokenA,
|
|
TokenOut: tokenB,
|
|
Exchanges: []math.ExchangeType{math.ExchangeUniswapV3},
|
|
Pools: []common.Address{}, // Would be populated with actual pool addresses
|
|
}
|
|
paths = append(paths, path)
|
|
}
|
|
|
|
return paths, nil
|
|
}
|
|
|
|
// RegisterExchangeComponents registers pool detector, liquidity fetcher, and swap router for an exchange
|
|
func (er *ExchangeRegistry) RegisterExchangeComponents(
|
|
exchangeType math.ExchangeType,
|
|
poolDetector PoolDetector,
|
|
liquidityFetcher LiquidityFetcher,
|
|
swapRouter SwapRouter,
|
|
) {
|
|
if er.poolDetectors == nil {
|
|
er.poolDetectors = make(map[math.ExchangeType]PoolDetector)
|
|
}
|
|
if er.liquidityFetchers == nil {
|
|
er.liquidityFetchers = make(map[math.ExchangeType]LiquidityFetcher)
|
|
}
|
|
if er.swapRouters == nil {
|
|
er.swapRouters = make(map[math.ExchangeType]SwapRouter)
|
|
}
|
|
|
|
er.poolDetectors[exchangeType] = poolDetector
|
|
er.liquidityFetchers[exchangeType] = liquidityFetcher
|
|
er.swapRouters[exchangeType] = swapRouter
|
|
}
|
|
|
|
// InitializeExchangeComponents initializes all exchange components for an arbitrum chain
|
|
func (er *ExchangeRegistry) InitializeExchangeComponents(engine *math.ExchangePricingEngine) error {
|
|
if er.client == nil {
|
|
return fmt.Errorf("ethclient is required to initialize exchange components")
|
|
}
|
|
|
|
// Initialize Uniswap V2 components
|
|
uniswapV2PoolDetector := NewUniswapV2PoolDetector(er.client, er.logger, er.exchanges[math.ExchangeUniswapV2])
|
|
uniswapV2LiquidityFetcher := NewUniswapV2LiquidityFetcher(er.client, er.logger, er.exchanges[math.ExchangeUniswapV2], engine)
|
|
uniswapV2Router := NewUniswapV2SwapRouter(er.client, er.logger, er.exchanges[math.ExchangeUniswapV2], engine)
|
|
er.RegisterExchangeComponents(math.ExchangeUniswapV2, uniswapV2PoolDetector, uniswapV2LiquidityFetcher, uniswapV2Router)
|
|
|
|
// Initialize SushiSwap components (similar to Uniswap V2)
|
|
sushiPoolDetector := NewSushiSwapPoolDetector(er.client, er.logger, er.exchanges[math.ExchangeSushiSwap])
|
|
sushiLiquidityFetcher := NewSushiSwapLiquidityFetcher(er.client, er.logger, er.exchanges[math.ExchangeSushiSwap], engine)
|
|
sushiRouter := NewSushiSwapSwapRouter(er.client, er.logger, er.exchanges[math.ExchangeSushiSwap], engine)
|
|
er.RegisterExchangeComponents(math.ExchangeSushiSwap, sushiPoolDetector, sushiLiquidityFetcher, sushiRouter)
|
|
|
|
// Initialize Curve components
|
|
curvePoolDetector := NewCurvePoolDetector(er.client, er.logger, er.exchanges[math.ExchangeCurve])
|
|
curveLiquidityFetcher := NewCurveLiquidityFetcher(er.client, er.logger, er.exchanges[math.ExchangeCurve], engine)
|
|
curveRouter := NewCurveSwapRouter(er.client, er.logger, er.exchanges[math.ExchangeCurve], engine)
|
|
er.RegisterExchangeComponents(math.ExchangeCurve, curvePoolDetector, curveLiquidityFetcher, curveRouter)
|
|
|
|
// Initialize Balancer components
|
|
balancerPoolDetector := NewBalancerPoolDetector(er.client, er.logger, er.exchanges[math.ExchangeBalancer])
|
|
balancerLiquidityFetcher := NewBalancerLiquidityFetcher(er.client, er.logger, er.exchanges[math.ExchangeBalancer], engine)
|
|
balancerRouter := NewBalancerSwapRouter(er.client, er.logger, er.exchanges[math.ExchangeBalancer], engine)
|
|
er.RegisterExchangeComponents(math.ExchangeBalancer, balancerPoolDetector, balancerLiquidityFetcher, balancerRouter)
|
|
|
|
// Initialize PancakeSwap components
|
|
pancakePoolDetector := NewPancakeSwapPoolDetector(er.client, er.logger, er.exchanges[math.ExchangeSushiSwap])
|
|
pancakeLiquidityFetcher := NewPancakeSwapLiquidityFetcher(er.client, er.logger, er.exchanges[math.ExchangeSushiSwap], engine)
|
|
pancakeRouter := NewPancakeSwapSwapRouter(er.client, er.logger, er.exchanges[math.ExchangeSushiSwap], engine)
|
|
er.RegisterExchangeComponents(math.ExchangeSushiSwap, pancakePoolDetector, pancakeLiquidityFetcher, pancakeRouter)
|
|
|
|
// Initialize Kyber components
|
|
kyberPoolDetector := NewKyberPoolDetector(er.client, er.logger, er.exchanges[math.ExchangeKyber])
|
|
kyberLiquidityFetcher := NewKyberLiquidityFetcher(er.client, er.logger, er.exchanges[math.ExchangeKyber], engine)
|
|
kyberRouter := NewKyberSwapRouter(er.client, er.logger, er.exchanges[math.ExchangeKyber], engine)
|
|
er.RegisterExchangeComponents(math.ExchangeKyber, kyberPoolDetector, kyberLiquidityFetcher, kyberRouter)
|
|
|
|
// Initialize Uniswap V4 components
|
|
uniswapV4PoolDetector := NewUniswapV4PoolDetector(er.client, er.logger, er.exchanges[math.ExchangeUniswapV4])
|
|
uniswapV4LiquidityFetcher := NewUniswapV4LiquidityFetcher(er.client, er.logger, er.exchanges[math.ExchangeUniswapV4], engine)
|
|
uniswapV4Router := NewUniswapV4SwapRouter(er.client, er.logger, er.exchanges[math.ExchangeUniswapV4], engine)
|
|
er.RegisterExchangeComponents(math.ExchangeUniswapV4, uniswapV4PoolDetector, uniswapV4LiquidityFetcher, uniswapV4Router)
|
|
|
|
return nil
|
|
}
|