feat: create v2-prep branch with comprehensive planning

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>
This commit is contained in:
Administrator
2025-11-10 10:14:26 +01:00
parent 1773daffe7
commit 803de231ba
411 changed files with 20390 additions and 8680 deletions

View File

@@ -0,0 +1,349 @@
package contracts
import (
"context"
"fmt"
"math/big"
"strings"
"time"
"github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/fraktal/mev-beta/internal/logger"
)
// ContractType represents the detected type of a contract
type ContractType int
const (
ContractTypeUnknown ContractType = iota
ContractTypeERC20Token
ContractTypeUniswapV2Pool
ContractTypeUniswapV3Pool
ContractTypeUniswapV2Router
ContractTypeUniswapV3Router
ContractTypeUniversalRouter
ContractTypeFactory
ContractTypeEOA // Externally Owned Account
)
// String returns the string representation of the contract type
func (ct ContractType) String() string {
switch ct {
case ContractTypeERC20Token:
return "ERC-20"
case ContractTypeUniswapV2Pool:
return "UniswapV2Pool"
case ContractTypeUniswapV3Pool:
return "UniswapV3Pool"
case ContractTypeUniswapV2Router:
return "UniswapV2Router"
case ContractTypeUniswapV3Router:
return "UniswapV3Router"
case ContractTypeUniversalRouter:
return "UniversalRouter"
case ContractTypeFactory:
return "Factory"
case ContractTypeEOA:
return "EOA"
default:
return "Unknown"
}
}
// DetectionResult contains the result of contract type detection
type DetectionResult struct {
ContractType ContractType
IsContract bool
HasCode bool
SupportedFunctions []string
Confidence float64 // 0.0 to 1.0
Error error
Warnings []string
}
// ContractDetector provides runtime contract type detection
type ContractDetector struct {
client *ethclient.Client
logger *logger.Logger
cache map[common.Address]*DetectionResult
timeout time.Duration
}
// NewContractDetector creates a new contract detector
func NewContractDetector(client *ethclient.Client, logger *logger.Logger) *ContractDetector {
return &ContractDetector{
client: client,
logger: logger,
cache: make(map[common.Address]*DetectionResult),
timeout: 5 * time.Second,
}
}
// DetectContractType determines the type of contract at the given address
func (cd *ContractDetector) DetectContractType(ctx context.Context, address common.Address) *DetectionResult {
// Check cache first
if result, exists := cd.cache[address]; exists {
return result
}
result := &DetectionResult{
ContractType: ContractTypeUnknown,
IsContract: false,
HasCode: false,
SupportedFunctions: []string{},
Confidence: 0.0,
Warnings: []string{},
}
// Create context with timeout
ctxWithTimeout, cancel := context.WithTimeout(ctx, cd.timeout)
defer cancel()
// Check if address has code (is a contract)
code, err := cd.client.CodeAt(ctxWithTimeout, address, nil)
if err != nil {
result.Error = fmt.Errorf("failed to get code at address: %w", err)
cd.cache[address] = result
return result
}
// If no code, it's an EOA
if len(code) == 0 {
result.ContractType = ContractTypeEOA
result.IsContract = false
result.Confidence = 1.0
cd.cache[address] = result
return result
}
result.IsContract = true
result.HasCode = true
// Detect contract type by testing function signatures
contractType, confidence, supportedFunctions := cd.detectByFunctionSignatures(ctxWithTimeout, address)
result.ContractType = contractType
result.Confidence = confidence
result.SupportedFunctions = supportedFunctions
// Additional validation for high-confidence detection
if confidence > 0.8 {
if err := cd.validateContractType(ctxWithTimeout, address, contractType); err != nil {
result.Warnings = append(result.Warnings, fmt.Sprintf("validation warning: %v", err))
result.Confidence *= 0.8 // Reduce confidence
}
}
cd.cache[address] = result
return result
}
// detectByFunctionSignatures detects contract type by testing known function signatures
func (cd *ContractDetector) detectByFunctionSignatures(ctx context.Context, address common.Address) (ContractType, float64, []string) {
supportedFunctions := []string{}
scores := make(map[ContractType]float64)
// Test ERC-20 functions
erc20Functions := map[string][]byte{
"name()": {0x06, 0xfd, 0xde, 0x03},
"symbol()": {0x95, 0xd8, 0x9b, 0x41},
"decimals()": {0x31, 0x3c, 0xe5, 0x67},
"totalSupply()": {0x18, 0x16, 0x0d, 0xdd},
"balanceOf()": {0x70, 0xa0, 0x82, 0x31},
}
erc20Score := cd.testFunctionSignatures(ctx, address, erc20Functions, &supportedFunctions)
if erc20Score > 0.6 {
scores[ContractTypeERC20Token] = erc20Score
}
// Test Uniswap V2 Pool functions
v2PoolFunctions := map[string][]byte{
"token0()": {0x0d, 0xfe, 0x16, 0x81},
"token1()": {0xd2, 0x12, 0x20, 0xa7},
"getReserves()": {0x09, 0x02, 0xf1, 0xac},
"price0CumulativeLast()": {0x54, 0x1c, 0x5c, 0xfa},
"kLast()": {0x7d, 0xc0, 0xd1, 0xd0},
}
v2PoolScore := cd.testFunctionSignatures(ctx, address, v2PoolFunctions, &supportedFunctions)
if v2PoolScore > 0.6 {
scores[ContractTypeUniswapV2Pool] = v2PoolScore
}
// Test Uniswap V3 Pool functions
v3PoolFunctions := map[string][]byte{
"token0()": {0x0d, 0xfe, 0x16, 0x81},
"token1()": {0xd2, 0x12, 0x20, 0xa7},
"fee()": {0xdd, 0xca, 0x3f, 0x43},
"slot0()": {0x38, 0x50, 0xc7, 0xbd},
"liquidity()": {0x1a, 0x68, 0x65, 0x0f},
"tickSpacing()": {0xd0, 0xc9, 0x32, 0x07},
}
v3PoolScore := cd.testFunctionSignatures(ctx, address, v3PoolFunctions, &supportedFunctions)
if v3PoolScore > 0.6 {
scores[ContractTypeUniswapV3Pool] = v3PoolScore
}
// Test Router functions
routerFunctions := map[string][]byte{
"WETH()": {0xad, 0x5c, 0x46, 0x48},
"swapExactTokensForTokens()": {0x38, 0xed, 0x17, 0x39},
"factory()": {0xc4, 0x5a, 0x01, 0x55},
}
routerScore := cd.testFunctionSignatures(ctx, address, routerFunctions, &supportedFunctions)
if routerScore > 0.5 {
scores[ContractTypeUniswapV2Router] = routerScore
}
// Find highest scoring type
var bestType ContractType = ContractTypeUnknown
var bestScore float64 = 0.0
for contractType, score := range scores {
if score > bestScore {
bestScore = score
bestType = contractType
}
}
return bestType, bestScore, supportedFunctions
}
// testFunctionSignatures tests if a contract supports given function signatures
func (cd *ContractDetector) testFunctionSignatures(ctx context.Context, address common.Address, functions map[string][]byte, supportedFunctions *[]string) float64 {
supported := 0
total := len(functions)
for funcName, signature := range functions {
// Test the function call
_, err := cd.client.CallContract(ctx, ethereum.CallMsg{
To: &address,
Data: signature,
}, nil)
if err == nil {
supported++
*supportedFunctions = append(*supportedFunctions, funcName)
} else if !strings.Contains(err.Error(), "execution reverted") {
// If it's not a revert, it might be a network error, so we don't count it against
total--
}
}
if total == 0 {
return 0.0
}
return float64(supported) / float64(total)
}
// validateContractType performs additional validation for detected contract types
func (cd *ContractDetector) validateContractType(ctx context.Context, address common.Address, contractType ContractType) error {
switch contractType {
case ContractTypeERC20Token:
return cd.validateERC20(ctx, address)
case ContractTypeUniswapV2Pool:
return cd.validateUniswapV2Pool(ctx, address)
case ContractTypeUniswapV3Pool:
return cd.validateUniswapV3Pool(ctx, address)
default:
return nil // No additional validation for other types
}
}
// validateERC20 validates that a contract is actually an ERC-20 token
func (cd *ContractDetector) validateERC20(ctx context.Context, address common.Address) error {
// Test decimals() - should return a reasonable value (0-18)
decimalsData := []byte{0x31, 0x3c, 0xe5, 0x67} // decimals()
result, err := cd.client.CallContract(ctx, ethereum.CallMsg{
To: &address,
Data: decimalsData,
}, nil)
if err != nil {
return fmt.Errorf("decimals() call failed: %w", err)
}
if len(result) == 32 {
decimals := new(big.Int).SetBytes(result).Uint64()
if decimals > 18 {
return fmt.Errorf("unrealistic decimals value: %d", decimals)
}
}
return nil
}
// validateUniswapV2Pool validates that a contract is actually a Uniswap V2 pool
func (cd *ContractDetector) validateUniswapV2Pool(ctx context.Context, address common.Address) error {
// Test getReserves() - should return 3 values
getReservesData := []byte{0x09, 0x02, 0xf1, 0xac} // getReserves()
result, err := cd.client.CallContract(ctx, ethereum.CallMsg{
To: &address,
Data: getReservesData,
}, nil)
if err != nil {
return fmt.Errorf("getReserves() call failed: %w", err)
}
// Should return 3 uint112 values (reserves + timestamp)
if len(result) != 96 { // 3 * 32 bytes
return fmt.Errorf("unexpected getReserves() return length: %d", len(result))
}
return nil
}
// validateUniswapV3Pool validates that a contract is actually a Uniswap V3 pool
func (cd *ContractDetector) validateUniswapV3Pool(ctx context.Context, address common.Address) error {
// Test slot0() - should return current state
slot0Data := []byte{0x38, 0x50, 0xc7, 0xbd} // slot0()
result, err := cd.client.CallContract(ctx, ethereum.CallMsg{
To: &address,
Data: slot0Data,
}, nil)
if err != nil {
return fmt.Errorf("slot0() call failed: %w", err)
}
// Should return multiple values including sqrtPriceX96
if len(result) < 32 {
return fmt.Errorf("unexpected slot0() return length: %d", len(result))
}
return nil
}
// IsERC20Token checks if an address is an ERC-20 token
func (cd *ContractDetector) IsERC20Token(ctx context.Context, address common.Address) bool {
result := cd.DetectContractType(ctx, address)
return result.ContractType == ContractTypeERC20Token && result.Confidence > 0.7
}
// IsUniswapPool checks if an address is a Uniswap pool (V2 or V3)
func (cd *ContractDetector) IsUniswapPool(ctx context.Context, address common.Address) bool {
result := cd.DetectContractType(ctx, address)
return (result.ContractType == ContractTypeUniswapV2Pool || result.ContractType == ContractTypeUniswapV3Pool) && result.Confidence > 0.7
}
// IsRouter checks if an address is a router contract
func (cd *ContractDetector) IsRouter(ctx context.Context, address common.Address) bool {
result := cd.DetectContractType(ctx, address)
return (result.ContractType == ContractTypeUniswapV2Router ||
result.ContractType == ContractTypeUniswapV3Router ||
result.ContractType == ContractTypeUniversalRouter) && result.Confidence > 0.7
}
// ClearCache clears the detection cache
func (cd *ContractDetector) ClearCache() {
cd.cache = make(map[common.Address]*DetectionResult)
}
// GetCacheSize returns the number of cached results
func (cd *ContractDetector) GetCacheSize() int {
return len(cd.cache)
}

View File

@@ -0,0 +1,239 @@
package contracts
import (
"context"
"fmt"
"github.com/ethereum/go-ethereum/common"
"github.com/fraktal/mev-beta/internal/logger"
)
// FunctionSignature represents a known function signature
type FunctionSignature struct {
Name string
Selector []byte
AllowedTypes []ContractType
}
// SignatureValidator validates function calls against contract types
type SignatureValidator struct {
detector *ContractDetector
logger *logger.Logger
signatures map[string]*FunctionSignature
}
// NewSignatureValidator creates a new function signature validator
func NewSignatureValidator(detector *ContractDetector, logger *logger.Logger) *SignatureValidator {
sv := &SignatureValidator{
detector: detector,
logger: logger,
signatures: make(map[string]*FunctionSignature),
}
// Initialize known function signatures
sv.initializeSignatures()
return sv
}
// initializeSignatures initializes the known function signatures and their allowed contract types
func (sv *SignatureValidator) initializeSignatures() {
// ERC-20 token functions
sv.signatures["name()"] = &FunctionSignature{
Name: "name()",
Selector: []byte{0x06, 0xfd, 0xde, 0x03},
AllowedTypes: []ContractType{ContractTypeERC20Token},
}
sv.signatures["symbol()"] = &FunctionSignature{
Name: "symbol()",
Selector: []byte{0x95, 0xd8, 0x9b, 0x41},
AllowedTypes: []ContractType{ContractTypeERC20Token},
}
sv.signatures["decimals()"] = &FunctionSignature{
Name: "decimals()",
Selector: []byte{0x31, 0x3c, 0xe5, 0x67},
AllowedTypes: []ContractType{ContractTypeERC20Token},
}
sv.signatures["totalSupply()"] = &FunctionSignature{
Name: "totalSupply()",
Selector: []byte{0x18, 0x16, 0x0d, 0xdd},
AllowedTypes: []ContractType{ContractTypeERC20Token},
}
sv.signatures["balanceOf()"] = &FunctionSignature{
Name: "balanceOf()",
Selector: []byte{0x70, 0xa0, 0x82, 0x31},
AllowedTypes: []ContractType{ContractTypeERC20Token},
}
// Uniswap V2 Pool functions
sv.signatures["token0()"] = &FunctionSignature{
Name: "token0()",
Selector: []byte{0x0d, 0xfe, 0x16, 0x81},
AllowedTypes: []ContractType{
ContractTypeUniswapV2Pool,
ContractTypeUniswapV3Pool,
},
}
sv.signatures["token1()"] = &FunctionSignature{
Name: "token1()",
Selector: []byte{0xd2, 0x12, 0x20, 0xa7},
AllowedTypes: []ContractType{
ContractTypeUniswapV2Pool,
ContractTypeUniswapV3Pool,
},
}
sv.signatures["getReserves()"] = &FunctionSignature{
Name: "getReserves()",
Selector: []byte{0x09, 0x02, 0xf1, 0xac},
AllowedTypes: []ContractType{ContractTypeUniswapV2Pool},
}
// Uniswap V3 Pool specific functions
sv.signatures["slot0()"] = &FunctionSignature{
Name: "slot0()",
Selector: []byte{0x38, 0x50, 0xc7, 0xbd},
AllowedTypes: []ContractType{ContractTypeUniswapV3Pool},
}
sv.signatures["fee()"] = &FunctionSignature{
Name: "fee()",
Selector: []byte{0xdd, 0xca, 0x3f, 0x43},
AllowedTypes: []ContractType{ContractTypeUniswapV3Pool},
}
sv.signatures["liquidity()"] = &FunctionSignature{
Name: "liquidity()",
Selector: []byte{0x1a, 0x68, 0x65, 0x0f},
AllowedTypes: []ContractType{ContractTypeUniswapV3Pool},
}
sv.signatures["tickSpacing()"] = &FunctionSignature{
Name: "tickSpacing()",
Selector: []byte{0xd0, 0xc9, 0x32, 0x07},
AllowedTypes: []ContractType{ContractTypeUniswapV3Pool},
}
// Router functions
sv.signatures["WETH()"] = &FunctionSignature{
Name: "WETH()",
Selector: []byte{0xad, 0x5c, 0x46, 0x48},
AllowedTypes: []ContractType{
ContractTypeUniswapV2Router,
ContractTypeUniswapV3Router,
},
}
sv.signatures["factory()"] = &FunctionSignature{
Name: "factory()",
Selector: []byte{0xc4, 0x5a, 0x01, 0x55},
AllowedTypes: []ContractType{
ContractTypeUniswapV2Router,
ContractTypeUniswapV3Router,
},
}
}
// ValidationResult contains the result of function signature validation
type ValidationResult struct {
IsValid bool
FunctionName string
ContractType ContractType
Error error
Warnings []string
}
// ValidateFunctionCall validates if a function can be called on a contract
func (sv *SignatureValidator) ValidateFunctionCall(ctx context.Context, contractAddress common.Address, functionSelector []byte) *ValidationResult {
result := &ValidationResult{
IsValid: false,
Warnings: []string{},
}
// Detect contract type
detection := sv.detector.DetectContractType(ctx, contractAddress)
result.ContractType = detection.ContractType
if detection.Error != nil {
result.Error = fmt.Errorf("contract type detection failed: %w", detection.Error)
return result
}
// Find matching function signature
var matchedSignature *FunctionSignature
for _, sig := range sv.signatures {
if len(sig.Selector) >= 4 && len(functionSelector) >= 4 {
if sig.Selector[0] == functionSelector[0] &&
sig.Selector[1] == functionSelector[1] &&
sig.Selector[2] == functionSelector[2] &&
sig.Selector[3] == functionSelector[3] {
matchedSignature = sig
result.FunctionName = sig.Name
break
}
}
}
// If no signature match found, warn but allow (could be unknown function)
if matchedSignature == nil {
result.IsValid = true
result.Warnings = append(result.Warnings, fmt.Sprintf("unknown function selector: %x", functionSelector))
return result
}
// Check if the detected contract type is allowed for this function
allowed := false
for _, allowedType := range matchedSignature.AllowedTypes {
if detection.ContractType == allowedType {
allowed = true
break
}
}
if !allowed {
result.Error = fmt.Errorf("function %s cannot be called on contract type %s (allowed types: %v)",
matchedSignature.Name, detection.ContractType.String(), matchedSignature.AllowedTypes)
return result
}
// Check confidence level
if detection.Confidence < 0.7 {
result.Warnings = append(result.Warnings, fmt.Sprintf("low confidence in contract type detection: %.2f", detection.Confidence))
}
result.IsValid = true
return result
}
// ValidateToken0Call specifically validates token0() function calls
func (sv *SignatureValidator) ValidateToken0Call(ctx context.Context, contractAddress common.Address) *ValidationResult {
token0Selector := []byte{0x0d, 0xfe, 0x16, 0x81}
return sv.ValidateFunctionCall(ctx, contractAddress, token0Selector)
}
// ValidateToken1Call specifically validates token1() function calls
func (sv *SignatureValidator) ValidateToken1Call(ctx context.Context, contractAddress common.Address) *ValidationResult {
token1Selector := []byte{0xd2, 0x12, 0x20, 0xa7}
return sv.ValidateFunctionCall(ctx, contractAddress, token1Selector)
}
// ValidateGetReservesCall specifically validates getReserves() function calls
func (sv *SignatureValidator) ValidateGetReservesCall(ctx context.Context, contractAddress common.Address) *ValidationResult {
getReservesSelector := []byte{0x09, 0x02, 0xf1, 0xac}
return sv.ValidateFunctionCall(ctx, contractAddress, getReservesSelector)
}
// ValidateSlot0Call specifically validates slot0() function calls for Uniswap V3
func (sv *SignatureValidator) ValidateSlot0Call(ctx context.Context, contractAddress common.Address) *ValidationResult {
slot0Selector := []byte{0x38, 0x50, 0xc7, 0xbd}
return sv.ValidateFunctionCall(ctx, contractAddress, slot0Selector)
}
// GetSupportedFunctions returns the functions supported by a contract type
func (sv *SignatureValidator) GetSupportedFunctions(contractType ContractType) []string {
var functions []string
for _, sig := range sv.signatures {
for _, allowedType := range sig.AllowedTypes {
if allowedType == contractType {
functions = append(functions, sig.Name)
break
}
}
}
return functions
}