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>
576 lines
26 KiB
JavaScript
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('');
|
|
|
|
|