package arbitrum import ( "context" "fmt" "math/big" "strings" "sync" "time" "github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum/accounts/abi" "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/security" ) // TokenMetadata contains comprehensive token information type TokenMetadata struct { Address common.Address `json:"address"` Symbol string `json:"symbol"` Name string `json:"name"` Decimals uint8 `json:"decimals"` TotalSupply *big.Int `json:"totalSupply"` IsStablecoin bool `json:"isStablecoin"` IsWrapped bool `json:"isWrapped"` Category string `json:"category"` // "blue-chip", "defi", "meme", "unknown" // Price information PriceUSD float64 `json:"priceUSD"` PriceETH float64 `json:"priceETH"` LastUpdated time.Time `json:"lastUpdated"` // Liquidity information TotalLiquidityUSD float64 `json:"totalLiquidityUSD"` MainPool common.Address `json:"mainPool"` // Risk assessment RiskScore float64 `json:"riskScore"` // 0.0 (safe) to 1.0 (high risk) IsVerified bool `json:"isVerified"` // Technical details ContractVerified bool `json:"contractVerified"` Implementation common.Address `json:"implementation"` // For proxy contracts } // TokenMetadataService manages token metadata extraction and caching type TokenMetadataService struct { client *ethclient.Client logger *logger.Logger // Caching cache map[common.Address]*TokenMetadata cacheMu sync.RWMutex cacheTTL time.Duration // Known tokens registry knownTokens map[common.Address]*TokenMetadata // Contract ABIs erc20ABI string proxyABI string } // NewTokenMetadataService creates a new token metadata service func NewTokenMetadataService(client *ethclient.Client, logger *logger.Logger) *TokenMetadataService { service := &TokenMetadataService{ client: client, logger: logger, cache: make(map[common.Address]*TokenMetadata), cacheTTL: 1 * time.Hour, knownTokens: getKnownArbitrumTokens(), erc20ABI: getERC20ABI(), proxyABI: getProxyABI(), } return service } // GetTokenMetadata retrieves comprehensive metadata for a token func (s *TokenMetadataService) GetTokenMetadata(ctx context.Context, tokenAddr common.Address) (*TokenMetadata, error) { // Check cache first if cached := s.getCachedMetadata(tokenAddr); cached != nil { return cached, nil } // Check known tokens registry if known, exists := s.knownTokens[tokenAddr]; exists { s.cacheMetadata(tokenAddr, known) return known, nil } // Extract metadata from contract metadata, err := s.extractMetadataFromContract(ctx, tokenAddr) if err != nil { return nil, fmt.Errorf("failed to extract token metadata: %w", err) } // Enhance with additional data if err := s.enhanceMetadata(ctx, metadata); err != nil { s.logger.Debug(fmt.Sprintf("Failed to enhance metadata for %s: %v", tokenAddr.Hex(), err)) } // Cache the result s.cacheMetadata(tokenAddr, metadata) return metadata, nil } // extractMetadataFromContract extracts basic ERC20 metadata from the contract func (s *TokenMetadataService) extractMetadataFromContract(ctx context.Context, tokenAddr common.Address) (*TokenMetadata, error) { contractABI, err := abi.JSON(strings.NewReader(s.erc20ABI)) if err != nil { return nil, fmt.Errorf("failed to parse ERC20 ABI: %w", err) } metadata := &TokenMetadata{ Address: tokenAddr, LastUpdated: time.Now(), } // Get symbol if symbol, err := s.callStringMethod(ctx, tokenAddr, contractABI, "symbol"); err == nil { metadata.Symbol = symbol } else { s.logger.Debug(fmt.Sprintf("Failed to get symbol for %s: %v", tokenAddr.Hex(), err)) metadata.Symbol = "UNKNOWN" } // Get name if name, err := s.callStringMethod(ctx, tokenAddr, contractABI, "name"); err == nil { metadata.Name = name } else { s.logger.Debug(fmt.Sprintf("Failed to get name for %s: %v", tokenAddr.Hex(), err)) metadata.Name = "Unknown Token" } // Get decimals if decimals, err := s.callUint8Method(ctx, tokenAddr, contractABI, "decimals"); err == nil { metadata.Decimals = decimals } else { s.logger.Debug(fmt.Sprintf("Failed to get decimals for %s: %v", tokenAddr.Hex(), err)) metadata.Decimals = 18 // Default to 18 decimals } // Get total supply if totalSupply, err := s.callBigIntMethod(ctx, tokenAddr, contractABI, "totalSupply"); err == nil { metadata.TotalSupply = totalSupply } else { s.logger.Debug(fmt.Sprintf("Failed to get total supply for %s: %v", tokenAddr.Hex(), err)) metadata.TotalSupply = big.NewInt(0) } // Check if contract is verified metadata.ContractVerified = s.isContractVerified(ctx, tokenAddr) // Categorize token metadata.Category = s.categorizeToken(metadata) // Assess risk metadata.RiskScore = s.assessRisk(metadata) return metadata, nil } // enhanceMetadata adds additional information to token metadata func (s *TokenMetadataService) enhanceMetadata(ctx context.Context, metadata *TokenMetadata) error { // Check if it's a stablecoin metadata.IsStablecoin = s.isStablecoin(metadata.Symbol, metadata.Name) // Check if it's a wrapped token metadata.IsWrapped = s.isWrappedToken(metadata.Symbol, metadata.Name) // Mark as verified if it's a known token metadata.IsVerified = s.isVerifiedToken(metadata.Address) // Check for proxy contract if impl, err := s.getProxyImplementation(ctx, metadata.Address); err == nil && impl != (common.Address{}) { metadata.Implementation = impl } return nil } // callStringMethod calls a contract method that returns a string func (s *TokenMetadataService) callStringMethod(ctx context.Context, contractAddr common.Address, contractABI abi.ABI, method string) (string, error) { callData, err := contractABI.Pack(method) if err != nil { return "", fmt.Errorf("failed to pack %s call: %w", method, err) } result, err := s.client.CallContract(ctx, ethereum.CallMsg{ To: &contractAddr, Data: callData, }, nil) if err != nil { return "", fmt.Errorf("%s call failed: %w", method, err) } unpacked, err := contractABI.Unpack(method, result) if err != nil { return "", fmt.Errorf("failed to unpack %s result: %w", method, err) } if len(unpacked) == 0 { return "", fmt.Errorf("empty %s result", method) } if str, ok := unpacked[0].(string); ok { return str, nil } return "", fmt.Errorf("invalid %s result type: %T", method, unpacked[0]) } // callUint8Method calls a contract method that returns a uint8 func (s *TokenMetadataService) callUint8Method(ctx context.Context, contractAddr common.Address, contractABI abi.ABI, method string) (uint8, error) { callData, err := contractABI.Pack(method) if err != nil { return 0, fmt.Errorf("failed to pack %s call: %w", method, err) } result, err := s.client.CallContract(ctx, ethereum.CallMsg{ To: &contractAddr, Data: callData, }, nil) if err != nil { return 0, fmt.Errorf("%s call failed: %w", method, err) } unpacked, err := contractABI.Unpack(method, result) if err != nil { return 0, fmt.Errorf("failed to unpack %s result: %w", method, err) } if len(unpacked) == 0 { return 0, fmt.Errorf("empty %s result", method) } // Handle different possible return types switch v := unpacked[0].(type) { case uint8: return v, nil case *big.Int: val, err := security.SafeUint64FromBigInt(v) if err != nil { return 0, fmt.Errorf("invalid decimal value: %w", err) } return security.SafeUint8(val) default: return 0, fmt.Errorf("invalid %s result type: %T", method, unpacked[0]) } } // callBigIntMethod calls a contract method that returns a *big.Int func (s *TokenMetadataService) callBigIntMethod(ctx context.Context, contractAddr common.Address, contractABI abi.ABI, method string) (*big.Int, error) { callData, err := contractABI.Pack(method) if err != nil { return nil, fmt.Errorf("failed to pack %s call: %w", method, err) } result, err := s.client.CallContract(ctx, ethereum.CallMsg{ To: &contractAddr, Data: callData, }, nil) if err != nil { return nil, fmt.Errorf("%s call failed: %w", method, err) } unpacked, err := contractABI.Unpack(method, result) if err != nil { return nil, fmt.Errorf("failed to unpack %s result: %w", method, err) } if len(unpacked) == 0 { return nil, fmt.Errorf("empty %s result", method) } if bigInt, ok := unpacked[0].(*big.Int); ok { return bigInt, nil } return nil, fmt.Errorf("invalid %s result type: %T", method, unpacked[0]) } // categorizeToken determines the category of a token func (s *TokenMetadataService) categorizeToken(metadata *TokenMetadata) string { symbol := strings.ToUpper(metadata.Symbol) name := strings.ToUpper(metadata.Name) // Blue-chip tokens blueChip := []string{"WETH", "WBTC", "USDC", "USDT", "DAI", "ARB", "GMX", "GRT"} for _, token := range blueChip { if symbol == token { return "blue-chip" } } // DeFi tokens if strings.Contains(name, "DAO") || strings.Contains(name, "FINANCE") || strings.Contains(name, "PROTOCOL") || strings.Contains(symbol, "LP") { return "defi" } // Meme tokens (simple heuristics) memeKeywords := []string{"MEME", "DOGE", "SHIB", "PEPE", "FLOKI"} for _, keyword := range memeKeywords { if strings.Contains(symbol, keyword) || strings.Contains(name, keyword) { return "meme" } } return "unknown" } // assessRisk calculates a risk score for the token func (s *TokenMetadataService) assessRisk(metadata *TokenMetadata) float64 { risk := 0.5 // Base risk // Reduce risk for verified tokens if metadata.ContractVerified { risk -= 0.2 } // Reduce risk for blue-chip tokens if metadata.Category == "blue-chip" { risk -= 0.3 } // Increase risk for meme tokens if metadata.Category == "meme" { risk += 0.3 } // Reduce risk for stablecoins if metadata.IsStablecoin { risk -= 0.4 } // Increase risk for tokens with low total supply if metadata.TotalSupply != nil && metadata.TotalSupply.Cmp(big.NewInt(1e15)) < 0 { risk += 0.2 } // Ensure risk is between 0 and 1 if risk < 0 { risk = 0 } if risk > 1 { risk = 1 } return risk } // isStablecoin checks if a token is a stablecoin func (s *TokenMetadataService) isStablecoin(symbol, name string) bool { stablecoins := []string{"USDC", "USDT", "DAI", "FRAX", "LUSD", "MIM", "UST", "BUSD"} symbol = strings.ToUpper(symbol) name = strings.ToUpper(name) for _, stable := range stablecoins { if symbol == stable || strings.Contains(name, stable) { return true } } return strings.Contains(name, "USD") || strings.Contains(name, "DOLLAR") } // isWrappedToken checks if a token is a wrapped version func (s *TokenMetadataService) isWrappedToken(symbol, name string) bool { return strings.HasPrefix(strings.ToUpper(symbol), "W") || strings.Contains(strings.ToUpper(name), "WRAPPED") } // isVerifiedToken checks if a token is in the verified list func (s *TokenMetadataService) isVerifiedToken(addr common.Address) bool { _, exists := s.knownTokens[addr] return exists } // isContractVerified checks if the contract source code is verified func (s *TokenMetadataService) isContractVerified(ctx context.Context, addr common.Address) bool { // Check if contract has code code, err := s.client.CodeAt(ctx, addr, nil) if err != nil || len(code) == 0 { return false } // In a real implementation, you would check with a verification service like Etherscan // For now, we'll assume contracts with code are verified return len(code) > 0 } // getProxyImplementation gets the implementation address for proxy contracts func (s *TokenMetadataService) getProxyImplementation(ctx context.Context, proxyAddr common.Address) (common.Address, error) { // Try EIP-1967 standard storage slot slot := common.HexToHash("0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc") storage, err := s.client.StorageAt(ctx, proxyAddr, slot, nil) if err != nil { return common.Address{}, err } if len(storage) >= 20 { return common.BytesToAddress(storage[12:32]), nil } return common.Address{}, fmt.Errorf("no implementation found") } // getCachedMetadata retrieves cached metadata if available and not expired func (s *TokenMetadataService) getCachedMetadata(addr common.Address) *TokenMetadata { s.cacheMu.RLock() defer s.cacheMu.RUnlock() cached, exists := s.cache[addr] if !exists { return nil } // Check if cache is expired if time.Since(cached.LastUpdated) > s.cacheTTL { return nil } return cached } // cacheMetadata stores metadata in the cache func (s *TokenMetadataService) cacheMetadata(addr common.Address, metadata *TokenMetadata) { s.cacheMu.Lock() defer s.cacheMu.Unlock() // Create a copy to avoid race conditions cached := *metadata cached.LastUpdated = time.Now() s.cache[addr] = &cached } // getKnownArbitrumTokens returns a registry of known tokens on Arbitrum func getKnownArbitrumTokens() map[common.Address]*TokenMetadata { return map[common.Address]*TokenMetadata{ // WETH common.HexToAddress("0x82aF49447D8a07e3bd95BD0d56f35241523fBab1"): { Address: common.HexToAddress("0x82aF49447D8a07e3bd95BD0d56f35241523fBab1"), Symbol: "WETH", Name: "Wrapped Ether", Decimals: 18, IsWrapped: true, Category: "blue-chip", IsVerified: true, RiskScore: 0.1, IsStablecoin: false, }, // USDC common.HexToAddress("0xaf88d065e77c8cC2239327C5EDb3A432268e5831"): { Address: common.HexToAddress("0xaf88d065e77c8cC2239327C5EDb3A432268e5831"), Symbol: "USDC", Name: "USD Coin", Decimals: 6, Category: "blue-chip", IsVerified: true, RiskScore: 0.05, IsStablecoin: true, }, // ARB common.HexToAddress("0x912CE59144191C1204E64559FE8253a0e49E6548"): { Address: common.HexToAddress("0x912CE59144191C1204E64559FE8253a0e49E6548"), Symbol: "ARB", Name: "Arbitrum", Decimals: 18, Category: "blue-chip", IsVerified: true, RiskScore: 0.2, }, // USDT common.HexToAddress("0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9"): { Address: common.HexToAddress("0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9"), Symbol: "USDT", Name: "Tether USD", Decimals: 6, Category: "blue-chip", IsVerified: true, RiskScore: 0.1, IsStablecoin: true, }, // GMX common.HexToAddress("0xfc5A1A6EB076a2C7aD06eD22C90d7E710E35ad0a"): { Address: common.HexToAddress("0xfc5A1A6EB076a2C7aD06eD22C90d7E710E35ad0a"), Symbol: "GMX", Name: "GMX", Decimals: 18, Category: "defi", IsVerified: true, RiskScore: 0.3, }, } } // getERC20ABI returns the standard ERC20 ABI func getERC20ABI() string { return `[ { "constant": true, "inputs": [], "name": "name", "outputs": [{"name": "", "type": "string"}], "type": "function" }, { "constant": true, "inputs": [], "name": "symbol", "outputs": [{"name": "", "type": "string"}], "type": "function" }, { "constant": true, "inputs": [], "name": "decimals", "outputs": [{"name": "", "type": "uint8"}], "type": "function" }, { "constant": true, "inputs": [], "name": "totalSupply", "outputs": [{"name": "", "type": "uint256"}], "type": "function" } ]` } // getProxyABI returns a simple proxy ABI for implementation detection func getProxyABI() string { return `[ { "constant": true, "inputs": [], "name": "implementation", "outputs": [{"name": "", "type": "address"}], "type": "function" } ]` }