#!/usr/bin/env node /** * Inventory sizing tool: reads simulation-params.json, optional per-chain V_epoch, * outputs I_T^* (inventory target), suggested D_0, and assumptions per chain per cW token. * Keeps configs honest and PRs reviewable. * * Usage: * node scripts/size-inventory.cjs # use scenario defaults for V_epoch * node scripts/size-inventory.cjs --sigma 1.8 # override sigma (default 1.5) * node scripts/size-inventory.cjs --refill-ratio 0.33 # T_refill/T_epoch (default 0.33) * node scripts/size-inventory.cjs --v-epoch '{"1":100000,"56":80000,"137":60000}' # per-chain V_epoch * EUR tokens use eurDefaults from simulation-params (higher sigma, k). */ const fs = require('fs'); const path = require('path'); const CONFIG_DIR = path.join(__dirname, '..', 'config'); const PARAMS_PATH = path.join(CONFIG_DIR, 'simulation-params.json'); const TOKEN_MAP_PATH = path.join(CONFIG_DIR, 'token-map.json'); const CW_USD = ['cWUSDT', 'cWUSDC', 'cWAUSDT', 'cWUSDW']; const CW_EUR = ['cWEURC', 'cWEURT']; const CW_ALL = [...CW_USD, ...CW_EUR]; function loadJson(p) { return JSON.parse(fs.readFileSync(p, 'utf8')); } function parseArg(name, def) { const i = process.argv.indexOf(name); if (i === -1) return def; const v = process.argv[i + 1]; if (name === '--v-epoch') return v ? JSON.parse(v) : null; if (name === '--sigma' || name === '--refill-ratio' || name === '--depth-mult') return v ? Number(v) : def; return def; } function main() { const params = loadJson(PARAMS_PATH); const tokenMap = fs.existsSync(TOKEN_MAP_PATH) ? loadJson(TOKEN_MAP_PATH) : { bridgedSymbols: CW_ALL }; const cwTokens = tokenMap.bridgedSymbols || CW_ALL; const chains = params.chains || {}; const eurDefaults = params.eurDefaults || { sigma: 2, k: 0.2, feeBps: 35 }; const sigmaUsd = parseArg('--sigma', 1.5); const refillRatio = parseArg('--refill-ratio', 0.33); const depthMult = parseArg('--depth-mult', 0.75); // D_0 = depthMult * I_T^* const vEpochOverride = parseArg('--v-epoch', null); const assumptions = { sigma_usd: sigmaUsd, sigma_eur: eurDefaults.sigma, T_refill_T_epoch: refillRatio, depth_multiplier: depthMult, formula: 'I_T^* >= V_epoch * sigma * (1 + T_refill/T_epoch) / (1 - beta) + gamma_buffer', depth_rule: `D_0 = ${depthMult} * I_T^*`, }; const out = { assumptions, chains: {}, generatedAt: new Date().toISOString() }; for (const [chainId, chain] of Object.entries(chains)) { const beta = chain.bridgeBeta ?? 0.001; const gamma = Number(chain.bridgeGammaUnits || 0) || 0; const gammaBuffer = gamma * 2; // optional: 2 refill batches const currentTarget = Number(chain.inventoryTargetUnits || 0) || 0; // V_epoch: override > env per chain > 10% of current target as scenario default let vEpoch = vEpochOverride && vEpochOverride[chainId] != null ? Number(vEpochOverride[chainId]) : (process.env[`V_EPOCH_${chainId}`] ? Number(process.env[`V_EPOCH_${chainId}`]) : null); if (vEpoch == null || isNaN(vEpoch)) { vEpoch = currentTarget > 0 ? Math.round(currentTarget * 0.1) : 100000; } out.chains[chainId] = { name: chain.name, hubStable: chain.hubStable, tokens: {} }; for (const symbol of cwTokens) { const isEur = CW_EUR.includes(symbol); const sigma = isEur ? eurDefaults.sigma : sigmaUsd; const denom = 1 - beta; if (denom <= 0) throw new Error(`Chain ${chainId}: invalid beta ${beta}`); const iTStar = Math.ceil((vEpoch * sigma * (1 + refillRatio)) / denom + gammaBuffer); const d0 = Math.ceil(depthMult * iTStar); const k = isEur ? (eurDefaults.k ?? 0.2) : (chain.k ?? 0.1); const feeBps = isEur ? (eurDefaults.feeBps ?? 35) : (chain.feeBps ?? 25); out.chains[chainId].tokens[symbol] = { I_T_star: iTStar, D_0: d0, V_epoch: vEpoch, sigma, beta, gamma, gamma_buffer: gammaBuffer, k, feeBps, stress_band_eur: isEur ? 'wider band; use higher sigma' : null, }; } } console.log(JSON.stringify(out, null, 2)); return out; } main();