deviant/scripts/sim-cdp-hedge-bch-frame.mjs
walerikus 518a3ce174 chore: initial project baseline — full Deviant codebase under version control
Brings the whole project into git (previously only the on-ramp feature and
a few parallel files were tracked). Includes: wallet core (src/), CashScript
contracts/, the Capacitor shell (shells/), scripts/, tests, and the docs
corpus (wiki/, snapshots/, Parameters/, Archive/, KnowledgeBase text).

Excluded via .gitignore: node_modules/, dist/, .env (secrets), testnet WIF
key files (.chipnet-*.json, devWallets.ts), KnowledgeBase/RAW media (~9.6 GB),
transcripts/ (session logs), and machine-local config (.claude/*.local,
*.lock). Secret-scanned before commit — no credentials in the tree.

Note: src/swapMath.ts, src/swapBuilder.ts, contracts/OpenMintToken.cash are
legacy (OP_RETURN era) but still imported; committed as-is, flagged for a
separate removal once de-referenced.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 20:38:07 +02:00

576 lines
26 KiB
JavaScript

#!/usr/bin/env node
/**
* sim-cdp-hedge-bch-frame.mjs
*
* Numerical simulation for ADR-0031 / ADR-0033 / `wiki/usd-rail.md` §4.4.
*
* Validates the claim from the 2026-05-28 design conversation: that a 50/50
* BCH-allocated split between a CDP-borrower-side position and an AnyHedge
* hedge-leg position is structurally **offsetting in the BCH frame** (not just
* diversification in the USD frame), and that two wallet-side triggers can
* exploit that offsetting to defend BCH holdings through volatility.
*
* NOT a chipnet broadcast. NOT a contract test. Pure math against the published
* mechanics of CDP liquidation and AnyHedge settlement.
*
* v2 (snapshot 0085, 2026-05-31): rescaled to canonical ParyonUSD parameters
* after Turn 0 (snapshot 0084) revealed the actual contract surface:
*
* - Min debt 100.00 PUSD (Borrowing.cash:56)
* - Min collateral ratio 110% (Borrowing.cash:64, `collateral * 10 / 11`)
* - Liquity-style StabilityPool liquidation: ALL collateral seized; borrower
* KEEPS the borrowed stable in their hand (liquidate.cash) — distinct from
* Maker-style "penalty + residual returned." Adds `liquidationModel`.
*
* v1 'maker' model (textbook keeper + 13% penalty + residual returned) is
* kept for reference. `liquidationModel: 'paryon'` is the canonical default.
*
* Run:
* node scripts/sim-cdp-hedge-bch-frame.mjs
*
* Cross-refs: ADR-0031 (hedge mechanism), ADR-0033 (CDP taxonomy + volatility
* paradox), wiki/usd-rail.md §4.4 (basket-stable / hybrid composition),
* snapshot 0084 (Paryon contract surface verified).
*/
// ---------- pure math primitives -----------------------------------------
const SATS_PER_BCH = 100_000_000n;
/** USD value of a sat amount at a given BCH/USD price. */
const satsToUsd = (sats, bchUsd) =>
Number(sats) / Number(SATS_PER_BCH) * bchUsd;
/** Sats needed to represent a USD amount at a given BCH/USD price. */
const usdToSats = (usd, bchUsd) =>
BigInt(Math.round(usd / bchUsd * Number(SATS_PER_BCH)));
/** Format sats with thousand separators + BCH-equivalent. */
const fmtSats = (s) =>
`${s.toLocaleString()} sats (${(Number(s) / Number(SATS_PER_BCH)).toFixed(4)} BCH)`;
/** Format USD with 2 decimals. */
const fmtUsd = (u) => `$${u.toFixed(2)}`;
// ---------- CDP side -----------------------------------------------------
/**
* Open a CDP at BCH = openPrice, locking `collateralSats` and minting stable
* at the chosen initial collateralization.
*
* Returns the CDP state. We track:
* - collateralSats (immutable until close/liquidation)
* - debtUsd (immutable until repaid)
* - initialCollatRatio (for liquidation threshold comparison)
* - liqThreshold (e.g. 1.5 means 150%)
* - liqPenalty (e.g. 0.13 = 13%)
*/
function openCDP({ collateralSats, openPriceBchUsd, initCollatRatio, liqThreshold, liqPenalty, liquidationModel = 'maker', debtUsdExplicit }) {
// Canonical Paryon: debt set explicitly (>= $100), liqThreshold = 1.10. 'maker' default: debt derived from initCollatRatio.
const collateralUsdAtOpen = satsToUsd(collateralSats, openPriceBchUsd);
const debtUsd = debtUsdExplicit ?? (collateralUsdAtOpen / initCollatRatio);
return {
collateralSats,
debtUsd,
liqThreshold,
liqPenalty,
liquidationModel,
openPriceBchUsd,
mintedStableUsd: debtUsd,
liquidationPriceBchUsd: (debtUsd * liqThreshold * Number(SATS_PER_BCH)) / Number(collateralSats),
};
}
/**
* Close a CDP cleanly at currentPrice (no liquidation). Repays debt using
* the held stable, returns full collateral.
* Returns the borrower's resulting sats + remaining stable.
*/
function closeCDPClean(cdp, currentPriceBchUsd, stableHeldUsd) {
// Repay debt with stable; collateral returned in full.
return {
satsReturned: cdp.collateralSats,
stableRemaining: stableHeldUsd - cdp.debtUsd, // typically 0 if minted == held
liquidated: false,
};
}
/**
* Liquidate a CDP at currentPrice. Behavior depends on liquidationModel:
* - 'maker' → keeper seizes (debt + penalty); borrower retains remainder
* - 'paryon' → StabilityPool seizes ALL collateral; borrower keeps the
* borrowed stable (the pool already repaid the debt)
*/
function liquidateCDP(cdp, currentPriceBchUsd) {
if (cdp.liquidationModel === 'paryon') {
// Liquity-style: pool takes ALL collateral; borrower retains stable.
return {
satsReturned: 0n,
stableRemaining: cdp.mintedStableUsd,
liquidated: true,
liquidationPriceBchUsd: cdp.liquidationPriceBchUsd,
liquidationModel: 'paryon',
};
}
// 'maker' and 'paryon-protected' share the same borrower-side math:
// seize (debt + penalty) worth at current price; return residual; borrower keeps stable.
// The protocols differ only in who *receives* the seized BCH (keeper auction vs
// stability pool) — invisible from the borrower's perspective, which is what
// the BCH-frame analysis tracks. 'paryon-protected' is the dUSD candidate
// (snapshot 0085 deferred section) that swaps Paryon's "take all" rule for
// Maker's penalty-and-return rule while keeping Paryon's atomic-pool mechanism.
const collateralUsd = satsToUsd(cdp.collateralSats, currentPriceBchUsd);
const seizedUsd = cdp.debtUsd * (1 + cdp.liqPenalty);
let residualSats;
if (collateralUsd <= seizedUsd) {
residualSats = 0n;
} else {
const seizedSats = usdToSats(seizedUsd, currentPriceBchUsd);
residualSats = cdp.collateralSats - seizedSats;
}
return {
satsReturned: residualSats,
stableRemaining: cdp.mintedStableUsd,
liquidated: true,
liquidationPriceBchUsd: cdp.liquidationPriceBchUsd,
liquidationModel: cdp.liquidationModel, // 'maker' or 'paryon-protected'
};
}
// ---------- AnyHedge side ------------------------------------------------
/**
* Open an AnyHedge hedge leg at BCH = openPrice. Hedge holder contributes
* hedgeContribSats; long contributes longContribSats; total box = sum.
* Hedge claim is the USD value of the hedge's contribution at open (claim
* preserved across settlement).
*
* Returns the hedge state.
*/
function openHedge({ hedgeContribSats, longContribSats, openPriceBchUsd }) {
const hedgeClaimUsd = satsToUsd(hedgeContribSats, openPriceBchUsd);
const boxSats = hedgeContribSats + longContribSats;
// The price at which the hedge's $X claim equals exactly the whole box
// (= the floor price; below this the peg breaks):
const floorPriceBchUsd = (hedgeClaimUsd * Number(SATS_PER_BCH)) / Number(boxSats);
return {
hedgeContribSats,
longContribSats,
boxSats,
hedgeClaimUsd,
openPriceBchUsd,
floorPriceBchUsd,
};
}
/**
* Settle the hedge at currentPrice. Hedge gets min($X/price, box) in sats.
*/
function settleHedge(hedge, currentPriceBchUsd) {
const wantedSats = usdToSats(hedge.hedgeClaimUsd, currentPriceBchUsd);
const hedgeGetsSats = wantedSats > hedge.boxSats ? hedge.boxSats : wantedSats;
return {
hedgeGetsSats,
longGetsSats: hedge.boxSats - hedgeGetsSats,
floorBreached: wantedSats > hedge.boxSats,
};
}
// ---------- combined scenario runner -------------------------------------
/**
* Run one scenario: open CDP + hedge, walk through a price path, decide
* whether liquidation triggers, decide whether trigger-defense activates,
* report final BCH and USD outcomes.
*/
function runScenario(name, params) {
const { openPriceBchUsd, closePriceBchUsd, returnPriceBchUsd, useTriggers } = params;
const cdp = openCDP(params.cdp);
const hedge = openHedge(params.hedge);
const startSats = cdp.collateralSats + hedge.hedgeContribSats;
const startUsd =
cdp.mintedStableUsd + // stable in hand
satsToUsd(cdp.collateralSats, openPriceBchUsd) - // collateral
cdp.debtUsd + // owed
hedge.hedgeClaimUsd; // hedge claim
// Walk price to closePrice. Decide if liquidation happens en route.
// (Simplified: we assume monotonic move; liquidation triggers iff closePrice
// crosses cdp.liquidationPriceBchUsd from above.)
const crossesLiquidation =
openPriceBchUsd > cdp.liquidationPriceBchUsd &&
closePriceBchUsd <= cdp.liquidationPriceBchUsd;
// Trigger-defense: if we cross the liquidation threshold, use the stable to
// repay debt BEFORE liquidation fires. Stable is consumed; collateral freed.
const triggerDefendedCDP = useTriggers && crossesLiquidation;
let cdpResult;
if (triggerDefendedCDP) {
// Repay with stable; collateral returned. Stable destroyed.
cdpResult = {
satsReturned: cdp.collateralSats,
stableRemaining: 0,
liquidated: false,
defended: true,
};
} else if (crossesLiquidation) {
cdpResult = liquidateCDP(cdp, cdp.liquidationPriceBchUsd);
} else {
cdpResult = closeCDPClean(cdp, closePriceBchUsd, cdp.mintedStableUsd);
}
// Hedge: trigger-defense closes at closePrice (lock the BCH windfall);
// no trigger means we wait for "settlement" at returnPriceBchUsd.
const hedgeSettlePrice = useTriggers ? closePriceBchUsd : returnPriceBchUsd;
const hedgeResult = settleHedge(hedge, hedgeSettlePrice);
const finalSats = cdpResult.satsReturned + hedgeResult.hedgeGetsSats;
const finalUsd =
satsToUsd(finalSats, returnPriceBchUsd) +
(cdpResult.stableRemaining || 0);
const deltaSatsPct = ((Number(finalSats) - Number(startSats)) / Number(startSats)) * 100;
const deltaUsdPct = ((finalUsd - startUsd) / startUsd) * 100;
return {
name,
startSats,
startUsd,
finalSats,
finalUsd,
deltaSatsPct,
deltaUsdPct,
cdpResult,
hedgeResult,
crossesLiquidation,
cdp,
hedge,
};
}
// ---------- formatting ---------------------------------------------------
function printResult(r) {
const line = (label, val) => console.log(` ${label.padEnd(28)} ${val}`);
console.log('');
console.log(`━━ ${r.name} ━━`);
line('Start (sats):', fmtSats(r.startSats));
line('Start (USD):', fmtUsd(r.startUsd));
line('CDP liquidation price:', fmtUsd(r.cdp.liquidationPriceBchUsd));
line('Hedge floor price:', fmtUsd(r.hedge.floorPriceBchUsd));
line('Liquidation crossed?:',
r.crossesLiquidation
? (r.cdpResult.defended ? 'yes (defended by trigger)' : 'yes (LIQUIDATED)')
: 'no');
line('Hedge settle:',
r.hedgeResult.floorBreached
? `floor breached → hedge got whole box (${fmtSats(r.hedgeResult.hedgeGetsSats)})`
: `within range → ${fmtSats(r.hedgeResult.hedgeGetsSats)}`);
line('Stable remaining:', r.cdpResult.stableRemaining ? fmtUsd(r.cdpResult.stableRemaining) : 'spent / none');
line('Final BCH:', fmtSats(r.finalSats));
line('Δ BCH:', `${r.deltaSatsPct >= 0 ? '+' : ''}${r.deltaSatsPct.toFixed(2)}%`);
line('Final USD:', fmtUsd(r.finalUsd));
line('Δ USD:', `${r.deltaUsdPct >= 0 ? '+' : ''}${r.deltaUsdPct.toFixed(2)}%`);
}
// ---------- scenarios ---------------------------------------------------
// Canonical ParyonUSD parameters (per Borrowing.cash + liquidate.cash, snapshot 0084):
// - debt explicit ≥ $100 PUSD
// - liqThreshold = 1.10 (110%); below this, StabilityPool may liquidate
// - liquidationModel = 'paryon' (pool seizes all collateral; borrower keeps stable)
// Position size: 40M sats CDP collateral (200% init ratio at BCH=$500) + 20M sats hedge contribution.
// Liquidation triggers at BCH ≈ $275 (collateral = $110 = 110% of $100 debt).
const COMMON = {
openPriceBchUsd: 500,
cdp: {
collateralSats: 40_000_000n, // 0.4 BCH = $200 at open
openPriceBchUsd: 500,
debtUsdExplicit: 100, // canonical Paryon minimum
initCollatRatio: 2.0, // 200% — reported; debtUsdExplicit overrides
liqThreshold: 1.10, // 110% Paryon threshold (Borrowing.cash line 64: collateral*10/11)
liqPenalty: 0.0, // not used in paryon model; kept for maker comparison
liquidationModel: 'paryon', // Liquity-style stability pool
},
hedge: {
hedgeContribSats: 20_000_000n, // 0.2 BCH = $100 at open ⇒ $100 hedge claim
longContribSats: 20_000_000n, // 1x leverage; box = 40M sats; floor = $250
openPriceBchUsd: 500,
},
};
const SCENARIOS = [
{
name: '(1) Small drop -20% to $400 (no liq, no triggers)',
...COMMON,
closePriceBchUsd: 400,
returnPriceBchUsd: 400,
useTriggers: false,
},
{
name: '(2) Liquidation threshold at $275 (no triggers) → all collateral seized',
...COMMON,
closePriceBchUsd: 275, // exactly Paryon 110% threshold
returnPriceBchUsd: 275,
useTriggers: false,
},
{
name: '(3) Deep crash -50% to $250 (no triggers) → hedge at box cap',
...COMMON,
closePriceBchUsd: 250, // hedge floor: $100 claim = whole 40M-sat box
returnPriceBchUsd: 250,
useTriggers: false,
},
{
name: '(4) Catastrophe -90% to $50 (no triggers) → floor breached',
...COMMON,
closePriceBchUsd: 50,
returnPriceBchUsd: 50,
useTriggers: false,
},
{
name: '(5) Round-trip -50%→$500 (no triggers) → permanent CDP loss',
...COMMON,
closePriceBchUsd: 250,
returnPriceBchUsd: 500,
useTriggers: false,
},
{
name: '(6) Round-trip -50%→$500 (WITH triggers) → defend + lock',
...COMMON,
closePriceBchUsd: 250,
returnPriceBchUsd: 500,
useTriggers: true,
},
{
name: '(7) Rise +50% to $750 (no triggers) → hedge pays the long',
...COMMON,
closePriceBchUsd: 750,
returnPriceBchUsd: 750,
useTriggers: false,
},
];
// ---------- run ---------------------------------------------------------
console.log('═══════════════════════════════════════════════════════════════════');
console.log(' CDP + AnyHedge BCH-frame simulation (ADR-0031 / ADR-0033 / usd-rail.md §4.4)');
console.log('═══════════════════════════════════════════════════════════════════');
console.log('');
console.log(' CANONICAL ParyonUSD parameters (snapshot 0084 rescale):');
console.log(' - 40M sats ($200 at BCH=$500) → CDP collateral; mints $100 PUSD');
console.log(' - 20M sats ($100 at BCH=$500) → AnyHedge contribution; $100 hedge claim');
console.log(' - Liquidation: 110% threshold (triggers at BCH≈$275); Liquity-style pool seizure');
console.log(' - Hedge floor: $250 (where $100 claim = whole 40M-sat box; 1x long)');
console.log(' - On liquidation borrower KEEPS the $100 PUSD (pool repays debt)');
console.log('');
const results = SCENARIOS.map((s) => runScenario(s.name, s));
results.forEach(printResult);
// Summary table
console.log('');
console.log('═══════════════════════════════════════════════════════════════════');
console.log(' Summary');
console.log('═══════════════════════════════════════════════════════════════════');
console.log('');
console.log(' Scenario'.padEnd(58) + 'Δ BCH'.padEnd(12) + 'Δ USD');
console.log(' ' + '─'.repeat(82));
for (const r of results) {
const sign = (v) => (v >= 0 ? '+' : '');
console.log(
' ' +
r.name.slice(0, 56).padEnd(58) +
`${sign(r.deltaSatsPct)}${r.deltaSatsPct.toFixed(1)}%`.padEnd(12) +
`${sign(r.deltaUsdPct)}${r.deltaUsdPct.toFixed(1)}%`,
);
}
console.log('');
console.log(' Key findings:');
console.log(' - Small / large unidirectional drops: BCH frame is approximately maintained');
console.log(' or positive (hedge transfers long\'s collateral on the way down).');
console.log(' - Liquidation crossing without triggers: permanent BCH loss to keeper penalty.');
console.log(' - Round-trip volatility WITHOUT triggers: worst case — CDP loss is permanent,');
console.log(' hedge BCH gain reverses on recovery.');
console.log(' - Round-trip volatility WITH triggers (repay-debt-before-liquidation + close-');
console.log(' hedge-at-trough): BCH preserved across the round trip.');
console.log(' - Rises: hedge loses sats to the long; this is the structural cost of holding');
console.log(' the SHORT side via the hedge leg.');
console.log('');
console.log(' Triggers are wallet-side rules in this simulation. Making them on-chain-');
console.log(' enforceable would require a covenant that holds both legs and acts on an');
console.log(' oracle attestation (recorded in usd-rail.md §4.4.4 as a buildable but');
console.log(' oracle-trust-introducing primitive).');
console.log('');
// =====================================================================
// THREE-WAY MODEL COMPARISON @ 150% initial collateralization
// (added 2026-05-31 per design conversation; snapshot 0086)
//
// Apples-to-apples comparison: same 150% initial, same 110% liquidation
// threshold, same hedge, same scenarios — only the post-liquidation
// mechanism varies. The Maker / paryon-protected variants will produce
// identical numbers from the borrower's perspective (only the recipient
// of seized BCH differs — keeper vs pool — invisible to the borrower).
// That equality is itself the insight: dUSD-protected can deliver
// Maker-equivalent borrower outcomes while keeping Paryon's atomic-pool
// back-end (no auction, no keeper bot dependency).
// =====================================================================
// Note on the penalty / threshold pairing chosen here:
// At 110% liquidation threshold (collateral = 110% of debt at trigger), a 13%
// penalty needs $113 of seized value but only $110 is available — vault is
// "underwater" by $3 — so Maker / paryon-protected also take all collateral,
// making them indistinguishable from paryon-canonical. Two ways to make the
// difference visible:
// (a) higher threshold (e.g. 125%) → real Maker convention, but Paryon's
// 110% threshold is what's deployed today and we want to keep
// apples-to-apples.
// (b) lower penalty (5%) at the 110% threshold → represents the
// borrower-friendly dUSD-protected design choice. Pool gets
// smaller per-liquidation reward but borrower keeps residual.
// Going with (b). 13% Maker reference shown separately (next block).
const COMPARE_COMMON = {
openPriceBchUsd: 500,
cdp: {
collateralSats: 30_000_000n, // 0.3 BCH = $150 at open → 150% init for $100 debt
openPriceBchUsd: 500,
debtUsdExplicit: 100,
initCollatRatio: 1.5, // reporting; debtUsdExplicit overrides
liqThreshold: 1.10, // same 110% trigger across all three for apples-to-apples
liqPenalty: 0.05, // 5% — fits within 110% threshold (dUSD-protected default)
// liquidationModel set per variant below
},
hedge: {
hedgeContribSats: 20_000_000n,
longContribSats: 20_000_000n,
openPriceBchUsd: 500,
},
};
const MODELS = [
{ key: 'paryon-canonical', label: 'Paryon (take all)', liquidationModel: 'paryon' },
{ key: 'maker', label: 'Maker (87%-residual)', liquidationModel: 'maker' },
{ key: 'paryon-protected', label: 'dUSD-protected (87% w/ residual + pool)', liquidationModel: 'paryon-protected' },
];
const COMPARE_SCENARIOS = [
{ name: '-20% to $400 (no liq)', closePriceBchUsd: 400, returnPriceBchUsd: 400, useTriggers: false },
{ name: '-26.6% to $367 (liquidation @ 110%)', closePriceBchUsd: 367, returnPriceBchUsd: 367, useTriggers: false },
{ name: '-50% to $250 (deep, post-liq)', closePriceBchUsd: 250, returnPriceBchUsd: 250, useTriggers: false },
{ name: '-90% to $50 (catastrophe)', closePriceBchUsd: 50, returnPriceBchUsd: 50, useTriggers: false },
{ name: 'Round-trip -50%→$500 (no triggers)', closePriceBchUsd: 250, returnPriceBchUsd: 500, useTriggers: false },
{ name: 'Round-trip -50%→$500 (WITH triggers)', closePriceBchUsd: 250, returnPriceBchUsd: 500, useTriggers: true },
{ name: '+50% to $750 (no triggers)', closePriceBchUsd: 750, returnPriceBchUsd: 750, useTriggers: false },
];
console.log('═══════════════════════════════════════════════════════════════════════════════');
console.log(' THREE-WAY MODEL COMPARISON @ 150% initial collateralization (50M-sat position)');
console.log('═══════════════════════════════════════════════════════════════════════════════');
console.log('');
console.log(' Setup: 30M sats CDP collateral ($150) + 20M sats hedge contribution ($100).');
console.log(' Same 110% liquidation threshold across all three models (triggers at BCH≈$367).');
console.log(' Maker and dUSD-protected share borrower-side math (only recipient of seized');
console.log(' BCH differs); they appear identical in this table. The equality IS the insight.');
console.log('');
// Header
const colWidth = 14;
const headerCells = ['Scenario'.padEnd(42)].concat(
MODELS.flatMap((m) => [`${m.key.slice(0, 12)} ΔBCH`.padStart(colWidth)]),
);
console.log(' ' + headerCells.join(''));
console.log(' ' + '─'.repeat(42 + colWidth * MODELS.length));
for (const sc of COMPARE_SCENARIOS) {
const cells = [sc.name.padEnd(42)];
for (const m of MODELS) {
const params = {
...sc,
openPriceBchUsd: COMPARE_COMMON.openPriceBchUsd,
cdp: { ...COMPARE_COMMON.cdp, liquidationModel: m.liquidationModel },
hedge: { ...COMPARE_COMMON.hedge },
};
const r = runScenario(`${sc.name} [${m.key}]`, params);
const sign = r.deltaSatsPct >= 0 ? '+' : '';
cells.push(`${sign}${r.deltaSatsPct.toFixed(1)}%`.padStart(colWidth));
}
console.log(' ' + cells.join(''));
}
console.log('');
console.log(' Observations (5% penalty / 110% threshold):');
console.log(' - Maker and dUSD-protected columns are identical from borrower perspective.');
console.log(' The only protocol-level difference (auction-keeper vs stability-pool) is on');
console.log(' the RECEIVING side of the seized BCH, not on the borrower\'s side.');
console.log(' - Paryon-canonical is harshest on liquidation (full seizure) but identical to');
console.log(' the others when no liquidation triggers (scenarios 1, 6 with triggers, 7).');
console.log(' - dUSD-protected saves ~2.7% BCH per liquidation event vs canonical at the 5%');
console.log(' penalty setting. Tuning the penalty trades borrower-friendliness vs pool reward.');
console.log(' - The TRIGGER STRATEGY equalizes all three models when applied (scenario 6:');
console.log(' use stable to repay debt before liquidation → identical outcome regardless');
console.log(' of which liquidation mechanic the protocol uses).');
console.log('');
// ─── Maker-realistic variant: 125% threshold + 13% penalty ────────────────
// Real-world Maker has historically used 150% threshold and 13% penalty. At
// 150% threshold the penalty fits cleanly (collateral=$150, need=$113, residual=$37
// for $100 debt). Use 125% threshold here as a middle ground (still gives the 13%
// penalty room to fit). Paryon-canonical can't change its threshold without losing
// its identity, so this block keeps it at 110% — meaning Paryon liquidates LATER
// in the price drop than this Maker would.
console.log('═══════════════════════════════════════════════════════════════════════════════');
console.log(' COMPARISON v2: Maker @ 125% threshold + 13% penalty (realistic) vs others');
console.log('═══════════════════════════════════════════════════════════════════════════════');
console.log('');
console.log(' At 125% threshold a $100 debt triggers liquidation when collateral = $125');
console.log(' (BCH = $417 — only -16.6% drop, much earlier than 110% threshold\'s $367).');
console.log(' Penalty fits cleanly: seize $113, return $12 residual = ~2.88M sats at $417.');
console.log(' Paryon-canonical (still 110% threshold) only liquidates at $367 = later.');
console.log(' This shows the REAL trade: earlier-but-softer vs later-but-harsher.');
console.log('');
const MODELS_V2 = [
{ key: 'paryon-canon-110%', label: 'Paryon (110%, take all)',
cdp: { liquidationModel: 'paryon', liqThreshold: 1.10, liqPenalty: 0.0 } },
{ key: 'maker-125%/13%', label: 'Maker (125%, 13% penalty)',
cdp: { liquidationModel: 'maker', liqThreshold: 1.25, liqPenalty: 0.13 } },
{ key: 'dusd-prot-110%/5%', label: 'dUSD-protected (110%, 5% penalty)',
cdp: { liquidationModel: 'paryon-protected', liqThreshold: 1.10, liqPenalty: 0.05 } },
];
console.log(' ' + 'Scenario'.padEnd(42) + MODELS_V2.map((m) => m.key.padStart(colWidth + 4)).join(''));
console.log(' ' + '─'.repeat(42 + (colWidth + 4) * MODELS_V2.length));
for (const sc of COMPARE_SCENARIOS) {
const cells = [sc.name.padEnd(42)];
for (const m of MODELS_V2) {
const params = {
...sc,
openPriceBchUsd: COMPARE_COMMON.openPriceBchUsd,
cdp: { ...COMPARE_COMMON.cdp, ...m.cdp },
hedge: { ...COMPARE_COMMON.hedge },
};
const r = runScenario(`${sc.name} [${m.key}]`, params);
const sign = r.deltaSatsPct >= 0 ? '+' : '';
cells.push(`${sign}${r.deltaSatsPct.toFixed(1)}%`.padStart(colWidth + 4));
}
console.log(' ' + cells.join(''));
}
console.log('');
console.log(' Observations:');
console.log(' - Realistic Maker liquidates SOONER (125% vs 110%) on price drops — kicks in');
console.log(' on smaller moves but leaves bigger residual. Borrower loses some BCH earlier');
console.log(' but never gets fully wiped.');
console.log(' - Paryon-canonical absorbs more drop before triggering but takes everything when');
console.log(' it does. Two opposite strategies; no "right answer."');
console.log(' - dUSD-protected can choose either threshold. Same-as-Paryon threshold + small');
console.log(' penalty is the "least-change" path that adds borrower protection without');
console.log(' changing trigger behavior.');
console.log('');