#!/usr/bin/env node /** * sim-usdm-controller.mjs — simulator for the five-layer USDm controller * (snapshot 0147, §12 of usdm_complete-design_FOR-design-record.md). * * Walks the controller through 5 scenarios so the dynamics become concrete: * 1. Calm growth — flat BCH, steady minting * 2. Sustained rally — BCH +100% over 6 months * 3. Sustained crash — BCH -50% over 6 months * 4. Flash crash + recovery — BCH -50% then back in 1 week * 5. Adoption boom — BCH stable, rapid minting (many catalyst events) * * For each scenario it prints the controller's response across the timeline * and a summary showing how the five layers interact. * * Run: node scripts/sim-usdm-controller.mjs */ // ── Controller constants ──────────────────────────────────────────────── const BLOCK_MIN = 10; // one block ≈ 10 minutes const RETARGET_BLOCKS = 2016; // ~2 weeks const TIME_DOUBLING_BLOCKS = 210_000; // ~4 years per doubling const SUPPLY_CATALYST = 210_000; // 210k cumulative minted per catalyst // Per-direction bounds (asymptotic targets reached via catalysts) const N_FLOOR = 1; const N_CEILING = 144; const D_FLOOR = 0.05; const D_CEILING = 0.20; // Catalyst step sizes const N_STEP = 1; const D_STEP = 0.005; // DART time-trigger weights and max step const W_PRICE = 1.0; const W_FLOW = 0.5; const W_SUPPLY = 0.5; const K_MAX = 5; // Deadband + cap (Layer 5, simplified — real controller has more) const DEADBAND_BPS = 50; // 0.5% const CAP_BPS = 1000; // 10% max single step // ── Controller state ──────────────────────────────────────────────────── class Controller { constructor() { this.rate = 200_000; // sats per $1 USDm (BCH = $500) this.N_up = 1; // cadence multiplier rally side this.N_down = 1; // cadence multiplier crash side this.d_up = 0.10; this.d_down = 0.10; this.doubling_epoch = 0; // Layer 3 this.catalyst_count = 0; // Layer 2 sub-trigger count this.last_recal_block = 0; this.last_retarget_block = 0; this.cumulative_minted = 0; // For DART time-trigger signals collected over window: this.window_mints = 0; this.window_redeems = 0; this.window_rate_moves = 0; // net direction over window this.window_supply_start = 0; this.prev_window_growth = 0; // for velocity comparison } // Effective cadence (in blocks) for a direction, accounting for time-doubling cadenceBlocks(direction) { const N = direction === 'down' ? this.N_down : this.N_up; return N * Math.pow(2, this.doubling_epoch); // each N already represents 10-min units (= 1 block); doubling adds 2^epoch } // Step: process one block step(block, bch_price_usd, mints, redeems) { this.cumulative_minted += mints; this.window_mints += mints; this.window_redeems += redeems; // Layer 3 — time-doubling check const expected_epoch = Math.floor(block / TIME_DOUBLING_BLOCKS); if (expected_epoch > this.doubling_epoch) this.doubling_epoch = expected_epoch; // Layer 2 sub-trigger — supply catalyst (every 210k minted) const expected_catalysts = Math.floor(this.cumulative_minted / SUPPLY_CATALYST); while (expected_catalysts > this.catalyst_count) { this.catalyst_count += 1; this.applyCatalyst(); } // Layer 2 main — DART time trigger (every 2016 blocks) if (block - this.last_retarget_block >= RETARGET_BLOCKS) { this.applyRetarget(); this.last_retarget_block = block; this.window_mints = 0; this.window_redeems = 0; this.window_rate_moves = 0; this.prev_window_growth = this.cumulative_minted - this.window_supply_start; this.window_supply_start = this.cumulative_minted; } // Recalibration check const target = oracleTarget(bch_price_usd); const direction = target > this.rate ? 'down' : 'up'; const cadence = this.cadenceBlocks(direction); if (block - this.last_recal_block >= cadence) { const gap = target - this.rate; const gapBps = Math.abs((gap / this.rate) * 10000); if (gapBps >= DEADBAND_BPS) { const d = direction === 'down' ? this.d_down : this.d_up; let delta = gap * d; // Layer 5 — cap const max_delta = (this.rate * CAP_BPS) / 10000; delta = Math.sign(delta) * Math.min(Math.abs(delta), max_delta); this.rate = Math.round(this.rate + delta); this.last_recal_block = block; this.window_rate_moves += direction === 'down' ? 1 : -1; } } } // Layer 2 sub — supply catalyst applied applyCatalyst() { this.N_down = Math.min(this.N_down + N_STEP, N_CEILING); this.N_up = Math.max(this.N_up - N_STEP, N_FLOOR); this.d_down = Math.max(this.d_down - D_STEP, D_FLOOR); this.d_up = Math.min(this.d_up + D_STEP, D_CEILING); } // Layer 2 main — DART time-trigger applyRetarget() { // Compute signals from window observations const net_dir = Math.sign(this.window_rate_moves); // -1 = predominantly rally, +1 = predominantly crash, 0 = neutral const flow_ratio = this.window_mints / Math.max(this.window_redeems, 1); const window_growth = this.cumulative_minted - this.window_supply_start; const growth_accel = this.prev_window_growth > 0 ? window_growth / this.prev_window_growth : 1.0; // Quantize each signal to ±{0,1,2,3} based on strength const ds = (raw, t1, t2, t3) => raw === 0 ? 0 : Math.sign(raw) * (Math.abs(raw) >= t3 ? 3 : Math.abs(raw) >= t2 ? 2 : Math.abs(raw) >= t1 ? 1 : 0); const dprice = ds(net_dir * 3, 1, 2, 3); // net_dir already ±1 const dflow = ds(flow_ratio - 1, 0.2, 1.0, 5.0) * (flow_ratio > 1 ? 1 : -1); const dsupply = ds(growth_accel - 1, 0.1, 1.0, 4.0) * (growth_accel > 1 ? 1 : -1); // Per the design record §12a.1 sign rules per direction // Sustained crash (dprice > 0) → increase N_down (slower crash; positive) // Sustained rally (dprice < 0) → decrease N_up (faster rally; negative) // Net flow: mint>redeem → both N's increase; redeem>mint → both N's decrease // Supply weak → N_up up, N_down down; supply strong → both N's down let sign_up_price = dprice < 0 ? dprice : 0; // rally (negative dprice) → reduce N_up (also negative) let sign_down_price = dprice > 0 ? dprice : 0; // crash (positive dprice) → increase N_down (positive) let sign_up_flow = dflow; // direct let sign_down_flow = dflow; // direct let sign_up_supply = -dsupply; // weak (negative dsupply) → positive (raise N_up); strong → negative (reduce N_up) let sign_down_supply = -Math.abs(dsupply); // weak and strong both reduce N_down (signal magnitude only, always negative) // Sum with weights + clamp const sum_up = W_PRICE * sign_up_price + W_FLOW * sign_up_flow + W_SUPPLY * sign_up_supply; const sum_down = W_PRICE * sign_down_price + W_FLOW * sign_down_flow + W_SUPPLY * sign_down_supply; const delta_up = Math.max(-K_MAX, Math.min(K_MAX, Math.round(sum_up))); const delta_down = Math.max(-K_MAX, Math.min(K_MAX, Math.round(sum_down))); // Apply this.N_up = Math.max(N_FLOOR, Math.min(N_CEILING, this.N_up + delta_up)); this.N_down = Math.max(N_FLOOR, Math.min(N_CEILING, this.N_down + delta_down)); } } // Oracle target: sats per $1 from BCH price in USD function oracleTarget(bch_usd) { return Math.round((100_000_000 / bch_usd)); } // Format helpers const fmt = { rate: (r) => `${r.toLocaleString().padStart(7)} sats/$`, pct: (n) => `${(n * 100).toFixed(1)}%`, bch: (p) => `$${p.toFixed(0)}`, }; // ── Scenario runner ───────────────────────────────────────────────────── function runScenario(name, description, priceFn, mintFn, redeemFn, blocks) { console.log(`\n${'═'.repeat(72)}`); console.log(` ${name}`); console.log(` ${description}`); console.log('═'.repeat(72)); const c = new Controller(); const snapshots = []; const checkpoints = [0, 0.05, 0.15, 0.30, 0.50, 0.75, 1.0].map((f) => Math.floor(f * blocks)); for (let block = 0; block < blocks; block++) { const bch_price = priceFn(block, blocks); const mints = mintFn(block, blocks); const redeems = redeemFn(block, blocks); c.step(block, bch_price, mints, redeems); if (checkpoints.includes(block)) { snapshots.push({ block, bch_price, rate: c.rate, target: oracleTarget(bch_price), N_up: c.N_up, N_down: c.N_down, d_up: c.d_up, d_down: c.d_down, catalysts: c.catalyst_count, doubling: c.doubling_epoch, minted: c.cumulative_minted, }); } } // Final snapshot const finalPrice = priceFn(blocks - 1, blocks); snapshots.push({ block: blocks - 1, bch_price: finalPrice, rate: c.rate, target: oracleTarget(finalPrice), N_up: c.N_up, N_down: c.N_down, d_up: c.d_up, d_down: c.d_down, catalysts: c.catalyst_count, doubling: c.doubling_epoch, minted: c.cumulative_minted, }); // Print trajectory console.log(`\n ${'Block'.padEnd(8)} ${'BCH'.padStart(6)} ${'oracle$/rate'.padStart(14)} ${'controller rate'.padStart(16)} ${'gap%'.padStart(7)} ${'N↑'.padStart(4)} ${'N↓'.padStart(4)} ${'d↑'.padStart(5)} ${'d↓'.padStart(5)} ${'catalysts'.padStart(10)}`); console.log(` ${'-'.repeat(105)}`); for (const s of snapshots) { const gap = ((s.rate - s.target) / s.target) * 100; console.log( ` ${String(s.block).padEnd(8)} ${fmt.bch(s.bch_price).padStart(6)} ${s.target.toLocaleString().padStart(14)} ${s.rate.toLocaleString().padStart(16)} ${gap.toFixed(1).padStart(6)}% ${String(s.N_up).padStart(4)} ${String(s.N_down).padStart(4)} ${(s.d_up * 100).toFixed(1).padStart(4)}% ${(s.d_down * 100).toFixed(1).padStart(4)}% ${String(s.catalysts).padStart(10)}` ); } const final = snapshots[snapshots.length - 1]; const finalGap = ((final.rate - final.target) / final.target) * 100; console.log(`\n Summary: rate vs oracle target gap at end = ${finalGap >= 0 ? '+' : ''}${finalGap.toFixed(1)}%`); console.log(` Asymmetric bias: N_down/N_up = ${(final.N_down / final.N_up).toFixed(1)}x · d_up/d_down = ${(final.d_up / final.d_down).toFixed(2)}x`); console.log(` Total catalysts fired: ${final.catalysts} · doubling regime: ${final.doubling} · cumulative minted: ${final.minted.toLocaleString()}`); } // ── Scenarios ─────────────────────────────────────────────────────────── const ONE_DAY = 144; // blocks const ONE_WEEK = 7 * ONE_DAY; const SIX_MONTHS = 26 * ONE_WEEK; const TWO_YEARS = 104 * ONE_WEEK; runScenario( 'SCENARIO 1 — Calm growth', 'BCH stable at $500; steady minting (50 units/block ≈ 7,200/day)', () => 500, () => 50, () => 5, SIX_MONTHS, ); runScenario( 'SCENARIO 2 — Sustained rally', 'BCH climbs $500 → $1000 over 6 months; moderate minting + redemption pickup', (b, total) => 500 + (500 * b) / total, (b, total) => 30 + (50 * b) / total, // minting picks up toward end (b, total) => 5 + (15 * b) / total, // redemption also rises SIX_MONTHS, ); runScenario( 'SCENARIO 3 — Sustained crash', 'BCH falls $500 → $250 over 6 months; mints climb (bargain buying), redeems flat', (b, total) => 500 - (250 * b) / total, (b, total) => 30 + (40 * b) / total, () => 5, SIX_MONTHS, ); runScenario( 'SCENARIO 4 — Flash crash + recovery', 'BCH at $500, drops to $250 over 3 days, recovers over 4 days; normal flows', (b) => { if (b < 3 * ONE_DAY) return 500 - (250 * b) / (3 * ONE_DAY); if (b < 7 * ONE_DAY) return 250 + (250 * (b - 3 * ONE_DAY)) / (4 * ONE_DAY); return 500; }, () => 50, () => 5, ONE_WEEK * 2, // 2 weeks to see the recovery + 1 week post ); runScenario( 'SCENARIO 5 — Adoption boom (many catalysts)', 'BCH stable at $500; minting rapidly increases (50 → 500 units/block); many 210k catalysts fire', () => 500, (b, total) => 50 + (450 * b) / total, () => 5, SIX_MONTHS, ); console.log(`\n${'═'.repeat(72)}`); console.log(' All scenarios complete. Observe:'); console.log(' - Scenario 1: controller stays calm; no catalysts due to low minting'); console.log(' - Scenario 2: rate moves up gradually; up-side cadence reduces from DART signals'); console.log(' - Scenario 3: rate lag widens; mint-favored arbitrage opportunity emerges'); console.log(' - Scenario 4: rate barely moves during flash event; full recovery without trace'); console.log(' - Scenario 5: many catalysts fire; asymmetric bias accumulates rapidly'); console.log('═'.repeat(72));