#!/usr/bin/env node /** * Validates config/deployment-status.json for "minimum viable deployed graph". * Use in CI so deployment-realistic sim cannot run with half-filled state. * * Rules: * - If bridgeAvailable === true on a chain, cwTokens must include at least cWUSDT and cWUSDC (phase 1). * - For each pmmPool / pmmPoolsVolatile[]: role in {defense, public_routing, truu_routing}, * feeBps and k present, base/quote (or tokenIn/tokenOut) exist in cwTokens or anchorAddresses. * TRUU must be listed under anchorAddresses when used as quote (e.g. mainnet chain 1). * - Any row marked live/routingVisible/publicRoutingEnabled must use a native protocol contract, * not a placeholder scaffold or aggregator-only/non-native lane. * * Exit code: 0 if valid, 1 if invalid (and prints errors to stderr). */ const fs = require('fs'); const path = require('path'); const CONFIG_DIR = path.join(__dirname, '..', 'config'); let DEPLOYMENT_STATUS_PATH = path.join(CONFIG_DIR, 'deployment-status.json'); const PHASE1_CW = ['cWUSDT', 'cWUSDC']; const VALID_ROLES = ['defense', 'public_routing', 'truu_routing']; const VALID_REFERENCE_PROTOCOLS = ['uniswap_v3', 'balancer', 'curve', '1inch']; const VALID_NATIVE_REFERENCE_PROTOCOLS = ['uniswap_v3', 'balancer', 'curve']; const HOME_CHAIN_CANONICAL_PREFIXES = ['c']; function looksPlaceholderAddress(address) { if (!address || typeof address !== 'string') return false; const normalized = address.toLowerCase(); if (!/^0x[0-9a-f]{40}$/.test(normalized)) return false; if (normalized === '0x0000000000000000000000000000000000000000') return true; const body = normalized.slice(2); const placeholderPrefixes = ['d0', '71', 'ba', 'c7']; const zeroCount = body.split('0').length - 1; return placeholderPrefixes.some((prefix) => body.startsWith(prefix)) && zeroCount >= 24; } function isLiveRow(row) { return row?.live === true || row?.routingVisible === true || row?.publicRoutingEnabled === true; } function loadJson(p) { return JSON.parse(fs.readFileSync(p, 'utf8')); } function validatePoolEntries(chainId, pools, listLabel, knownTokens, errors) { for (let i = 0; i < pools.length; i++) { const pool = pools[i]; const base = pool.base ?? pool.tokenIn; const quote = pool.quote ?? pool.tokenOut; if (!VALID_ROLES.includes(pool.role)) { errors.push(`Chain ${chainId} ${listLabel}[${i}]: role must be one of ${VALID_ROLES.join(', ')}`); } if (pool.feeBps == null || pool.k == null) { errors.push(`Chain ${chainId} ${listLabel}[${i}]: feeBps and k required`); } if (base && !knownTokens.has(base)) { errors.push(`Chain ${chainId} ${listLabel}[${i}]: base/tokenIn "${base}" not in cwTokens or anchorAddresses`); } if (quote && !knownTokens.has(quote)) { errors.push(`Chain ${chainId} ${listLabel}[${i}]: quote/tokenOut "${quote}" not in cwTokens or anchorAddresses`); } const addr = pool.poolAddress; if (addr != null && addr !== '') { const z = String(addr).toLowerCase(); if (z === '0x0000000000000000000000000000000000000000') { errors.push(`Chain ${chainId} ${listLabel}[${i}]: poolAddress must not be zero when set`); } if (pool.publicRoutingEnabled === true && looksPlaceholderAddress(z)) { errors.push(`Chain ${chainId} ${listLabel}[${i}]: live public routing poolAddress must use a native protocol contract, not a placeholder scaffold (${addr})`); } } if (pool.publicRoutingEnabled === true && pool.venue && pool.venue !== 'dodo_pmm') { errors.push(`Chain ${chainId} ${listLabel}[${i}]: public routing rows must use the native dodo_pmm venue, not "${pool.venue}"`); } } } function validateUniswapV2Entries(chainId, pools, knownTokens, errors) { for (let i = 0; i < pools.length; i++) { const pool = pools[i]; const base = pool.base; const quote = pool.quote; if (!base || !knownTokens.has(base)) { errors.push(`Chain ${chainId} uniswapV2Pools[${i}]: base "${base}" not in cwTokens or anchorAddresses`); } if (!quote || !knownTokens.has(quote)) { errors.push(`Chain ${chainId} uniswapV2Pools[${i}]: quote "${quote}" not in cwTokens or anchorAddresses`); } if (!pool.poolAddress || looksPlaceholderAddress(String(pool.poolAddress).toLowerCase())) { errors.push(`Chain ${chainId} uniswapV2Pools[${i}]: poolAddress must use a real deployed pair address`); } if (pool.venue && pool.venue !== 'uniswap_v2_pair') { errors.push(`Chain ${chainId} uniswapV2Pools[${i}]: venue must be "uniswap_v2_pair" when set`); } if (pool.publicRoutingEnabled === true && (!pool.factoryAddress || !pool.routerAddress)) { errors.push(`Chain ${chainId} uniswapV2Pools[${i}]: publicRoutingEnabled rows require factoryAddress and routerAddress`); } } } function main() { const status = loadJson(DEPLOYMENT_STATUS_PATH); const chains = status.chains || {}; const homeChainId = String(status.homeChainId || ''); const errors = []; for (const [chainId, chain] of Object.entries(chains)) { const cwTokens = chain.cwTokens || {}; const gasMirrors = chain.gasMirrors || {}; const anchorAddresses = chain.anchorAddresses || {}; const gasQuoteAddresses = chain.gasQuoteAddresses || {}; const pmmPools = chain.pmmPools || []; const uniswapV2Pools = chain.uniswapV2Pools || []; const pmmPoolsVolatile = chain.pmmPoolsVolatile || []; const gasPmmPools = chain.gasPmmPools || []; const gasReferenceVenues = chain.gasReferenceVenues || []; const bridgeAvailable = chain.bridgeAvailable; const isHomeChain = chainId === homeChainId; const skipPhase1Requirement = isHomeChain || chainId === '651940'; if (bridgeAvailable === true && !skipPhase1Requirement) { for (const sym of PHASE1_CW) { if (!cwTokens[sym] || typeof cwTokens[sym] !== 'string' || !cwTokens[sym].trim()) { errors.push(`Chain ${chainId} (${chain.name}): bridgeAvailable=true but cwTokens.${sym} missing or empty`); } } } const knownTokens = new Set([ ...Object.keys(cwTokens), ...Object.keys(gasMirrors), ...Object.keys(anchorAddresses), ...Object.keys(gasQuoteAddresses), ]); if (isHomeChain) { for (const pool of [...pmmPools, ...pmmPoolsVolatile]) { const base = pool.base ?? pool.tokenIn; const quote = pool.quote ?? pool.tokenOut; if (typeof base === 'string' && HOME_CHAIN_CANONICAL_PREFIXES.some((prefix) => base.startsWith(prefix))) { knownTokens.add(base); } if (typeof quote === 'string' && HOME_CHAIN_CANONICAL_PREFIXES.some((prefix) => quote.startsWith(prefix))) { knownTokens.add(quote); } } } validatePoolEntries(chainId, pmmPools, 'pmmPools', knownTokens, errors); validateUniswapV2Entries(chainId, uniswapV2Pools, knownTokens, errors); validatePoolEntries(chainId, pmmPoolsVolatile, 'pmmPoolsVolatile', knownTokens, errors); validatePoolEntries(chainId, gasPmmPools, 'gasPmmPools', knownTokens, errors); const gasPoolsByFamily = new Map(); for (const pool of gasPmmPools) { if (!pool.familyKey || typeof pool.familyKey !== 'string') { errors.push(`Chain ${chainId} gasPmmPools entry is missing familyKey`); continue; } if (!gasPoolsByFamily.has(pool.familyKey)) gasPoolsByFamily.set(pool.familyKey, []); gasPoolsByFamily.get(pool.familyKey).push(pool); } for (const [familyKey, pools] of gasPoolsByFamily.entries()) { const poolTypes = new Set(pools.map((pool) => pool.poolType)); if (!poolTypes.has('wrapped_native')) { errors.push(`Chain ${chainId} gas family ${familyKey}: missing wrapped_native DODO pool`); } if (!poolTypes.has('stable_quote')) { errors.push(`Chain ${chainId} gas family ${familyKey}: missing stable_quote DODO pool`); } } const referenceVenuesByFamily = new Map(); for (let i = 0; i < gasReferenceVenues.length; i++) { const venue = gasReferenceVenues[i]; if (!VALID_REFERENCE_PROTOCOLS.includes(venue.protocol)) { errors.push(`Chain ${chainId} gasReferenceVenues[${i}]: protocol must be one of ${VALID_REFERENCE_PROTOCOLS.join(', ')}`); } if (typeof venue.supported !== 'boolean') { errors.push(`Chain ${chainId} gasReferenceVenues[${i}]: supported must be set explicitly to true or false`); } if (!venue.familyKey || typeof venue.familyKey !== 'string') { errors.push(`Chain ${chainId} gasReferenceVenues[${i}]: familyKey required`); continue; } if (!referenceVenuesByFamily.has(venue.familyKey)) referenceVenuesByFamily.set(venue.familyKey, []); referenceVenuesByFamily.get(venue.familyKey).push(venue); if (venue.aggregatorOnly === true && venue.protocol !== '1inch') { errors.push(`Chain ${chainId} gasReferenceVenues[${i}]: aggregatorOnly rows must use protocol "1inch"`); } if (isLiveRow(venue)) { if (!VALID_NATIVE_REFERENCE_PROTOCOLS.includes(venue.protocol)) { errors.push(`Chain ${chainId} gasReferenceVenues[${i}]: live/routingVisible rows must use a native protocol contract, not "${venue.protocol}"`); } if (looksPlaceholderAddress(venue.venueAddress)) { errors.push(`Chain ${chainId} gasReferenceVenues[${i}]: live/routingVisible venueAddress must use a native protocol contract, not a placeholder scaffold (${venue.venueAddress})`); } } if (venue.supported === false && isLiveRow(venue)) { errors.push(`Chain ${chainId} gasReferenceVenues[${i}]: supported=false rows cannot be live/routingVisible`); } if (venue.supported === false && venue.protocol === '1inch' && venue.aggregatorOnly !== true) { errors.push(`Chain ${chainId} gasReferenceVenues[${i}]: unsupported 1inch rows must be marked aggregatorOnly=true`); } } for (const [familyKey, venues] of referenceVenuesByFamily.entries()) { const protocols = new Set(venues.map((venue) => venue.protocol)); if (!protocols.has('uniswap_v3')) { errors.push(`Chain ${chainId} gas family ${familyKey}: missing uniswap_v3 reference venue`); } const oneInch = venues.find((venue) => venue.protocol === '1inch'); if (oneInch?.routingVisible === true || oneInch?.live === true) { const hasUniswap = venues.some((venue) => venue.protocol === 'uniswap_v3' && venue.live === true); const hasDodo = (gasPoolsByFamily.get(familyKey) || []).some((pool) => pool.publicRoutingEnabled === true); if (!hasUniswap || !hasDodo) { errors.push(`Chain ${chainId} gas family ${familyKey}: 1inch cannot be live/routingVisible before DODO and Uniswap venues are live`); } } } } if (errors.length > 0) { errors.forEach((e) => process.stderr.write(e + '\n')); process.exit(1); } process.exit(0); } function resolveInputPath(argv) { const candidate = argv[2]; if (!candidate || candidate === '-h' || candidate === '--help') { return DEPLOYMENT_STATUS_PATH; } return path.isAbsolute(candidate) ? candidate : path.join(process.cwd(), candidate); } function printUsage() { process.stdout.write('Usage: node scripts/validate-deployment-status.cjs [path/to/deployment-status.json]\n'); } if (require.main === module) { const argv = process.argv; if (argv[2] === '-h' || argv[2] === '--help') { printUsage(); process.exit(0); } if (argv[2]) { DEPLOYMENT_STATUS_PATH = resolveInputPath(argv); } main(); } module.exports = { looksPlaceholderAddress, isLiveRow, validatePoolEntries, main, };