#!/usr/bin/env node /** * On-Chain Verification Script * Verifies token list entries against on-chain contracts using RPC calls * * RPC endpoints (fallback order): * 1. https://rpc-http-pub.d-bis.org (primary) * 2. https://rpc-core.d-bis.org (fallback) */ import { readFileSync } from 'fs'; import { fileURLToPath } from 'url'; import { dirname, resolve } from 'path'; import { ethers } from 'ethers'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); const RPC_ENDPOINTS = [ 'https://rpc-http-pub.d-bis.org', 'https://rpc-core.d-bis.org' ]; const REQUIRED_CHAIN_ID = 138; const REQUIRED_CHAIN_ID_HEX = '0x8a'; // ERC-20 ABI (minimal) const ERC20_ABI = [ 'function decimals() view returns (uint8)', 'function symbol() view returns (string)', 'function name() view returns (string)', 'function totalSupply() view returns (uint256)' ]; // Oracle ABI (Chainlink-compatible) const ORACLE_ABI = [ 'function latestRoundData() view returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound)', 'function decimals() view returns (uint8)', 'function description() view returns (string)' ]; async function getProvider() { let lastError; for (const rpcUrl of RPC_ENDPOINTS) { try { const provider = new ethers.JsonRpcProvider(rpcUrl); // Verify chain ID const network = await provider.getNetwork(); const chainId = Number(network.chainId); if (chainId !== REQUIRED_CHAIN_ID) { throw new Error(`Chain ID mismatch: expected ${REQUIRED_CHAIN_ID}, got ${chainId}`); } // Test connection await provider.getBlockNumber(); console.log(`āœ… Connected to RPC: ${rpcUrl} (Chain ID: ${chainId})\n`); return provider; } catch (error) { lastError = error; console.log(`āš ļø Failed to connect to ${rpcUrl}: ${error.message}`); continue; } } throw new Error(`Failed to connect to any RPC endpoint. Last error: ${lastError?.message}`); } async function verifyERC20Token(provider, token, index) { const results = []; const prefix = `Token[${index}] ${token.symbol || token.name}`; try { // Check if contract exists const code = await provider.getCode(token.address); if (code === '0x') { results.push({ type: 'error', message: `${prefix}: No contract code at address ${token.address}` }); return results; } const contract = new ethers.Contract(token.address, ERC20_ABI, provider); // Verify decimals try { const onChainDecimals = await contract.decimals(); if (Number(onChainDecimals) !== token.decimals) { results.push({ type: 'error', message: `${prefix}: Decimals mismatch - list: ${token.decimals}, on-chain: ${onChainDecimals}` }); } else { results.push({ type: 'success', message: `${prefix}: Decimals verified (${token.decimals})` }); } } catch (error) { results.push({ type: 'warning', message: `${prefix}: Failed to read decimals: ${error.message}` }); } // Verify symbol (warn if different) try { const onChainSymbol = await contract.symbol(); if (onChainSymbol !== token.symbol) { results.push({ type: 'warning', message: `${prefix}: Symbol mismatch - list: "${token.symbol}", on-chain: "${onChainSymbol}"` }); } } catch (error) { results.push({ type: 'warning', message: `${prefix}: Failed to read symbol: ${error.message}` }); } // Verify name (warn if different) try { const onChainName = await contract.name(); if (onChainName !== token.name) { results.push({ type: 'warning', message: `${prefix}: Name mismatch - list: "${token.name}", on-chain: "${onChainName}"` }); } } catch (error) { results.push({ type: 'warning', message: `${prefix}: Failed to read name: ${error.message}` }); } // Verify totalSupply (optional) try { await contract.totalSupply(); results.push({ type: 'success', message: `${prefix}: totalSupply() callable` }); } catch (error) { results.push({ type: 'warning', message: `${prefix}: totalSupply() failed: ${error.message}` }); } } catch (error) { results.push({ type: 'error', message: `${prefix}: Verification failed: ${error.message}` }); } return results; } async function verifyOracleToken(provider, token, index) { const results = []; const prefix = `Token[${index}] ${token.symbol || token.name} (Oracle)`; try { // Check if contract exists const code = await provider.getCode(token.address); if (code === '0x') { results.push({ type: 'error', message: `${prefix}: No contract code at address ${token.address}` }); return results; } const contract = new ethers.Contract(token.address, ORACLE_ABI, provider); // Verify latestRoundData try { await contract.latestRoundData(); results.push({ type: 'success', message: `${prefix}: latestRoundData() callable` }); } catch (error) { results.push({ type: 'error', message: `${prefix}: latestRoundData() failed: ${error.message}` }); } // Verify decimals try { const onChainDecimals = await contract.decimals(); if (Number(onChainDecimals) !== token.decimals) { results.push({ type: 'error', message: `${prefix}: Decimals mismatch - list: ${token.decimals}, on-chain: ${onChainDecimals}` }); } else { results.push({ type: 'success', message: `${prefix}: Decimals verified (${token.decimals})` }); } } catch (error) { results.push({ type: 'warning', message: `${prefix}: Failed to read decimals: ${error.message}` }); } } catch (error) { results.push({ type: 'error', message: `${prefix}: Verification failed: ${error.message}` }); } return results; } function isOracleToken(token) { return token.tags && (token.tags.includes('oracle') || token.tags.includes('pricefeed')); } async function verifyOnChain(filePath, required = false) { console.log(`\nšŸ”— Verifying on-chain contracts: ${filePath}\n`); // Read token list file let tokenList; try { const fileContent = readFileSync(filePath, 'utf-8'); tokenList = JSON.parse(fileContent); } catch (error) { console.error('āŒ Error reading or parsing token list file:'); console.error(` ${error.message}`); process.exit(1); } let provider; try { provider = await getProvider(); } catch (error) { if (required) { console.error(`āŒ ${error.message}`); console.error('On-chain verification is required but RPC connection failed.'); process.exit(1); } else { console.log(`āš ļø ${error.message}`); console.log('Skipping on-chain verification (optional mode)\n'); return 0; } } if (!tokenList.tokens || !Array.isArray(tokenList.tokens)) { console.log('No tokens to verify.\n'); return 0; } const allResults = []; for (const [index, token] of tokenList.tokens.entries()) { let results; if (isOracleToken(token)) { results = await verifyOracleToken(provider, token, index); } else { results = await verifyERC20Token(provider, token, index); } allResults.push(...results); } // Report results const errors = allResults.filter(r => r.type === 'error'); const warnings = allResults.filter(r => r.type === 'warning'); const successes = allResults.filter(r => r.type === 'success'); if (errors.length > 0) { console.log('āŒ Errors:'); errors.forEach(r => console.log(` ${r.message}`)); console.log(''); } if (warnings.length > 0) { console.log('āš ļø Warnings:'); warnings.forEach(r => console.log(` ${r.message}`)); console.log(''); } if (successes.length > 0) { console.log('āœ… Verified:'); successes.forEach(r => console.log(` ${r.message}`)); console.log(''); } if (errors.length > 0) { console.log(`āŒ Verification failed with ${errors.length} error(s)\n`); return 1; } console.log('āœ… All on-chain verifications passed!\n'); return 0; } // Main const args = process.argv.slice(2); const filePath = args.find(arg => !arg.startsWith('--')) || resolve(__dirname, '../lists/dbis-138.tokenlist.json'); const required = args.includes('--required'); if (!filePath) { console.error('Usage: node verify-on-chain.js [path/to/token-list.json] [--required]'); process.exit(1); } verifyOnChain(filePath, required).then(exitCode => { process.exit(exitCode); }).catch(error => { console.error('Unexpected error:', error); process.exit(1); });