Chapter ΙΒʹ · the frozen source

Covenant source

The five frozen Simplicity programs, verbatim. Every file.simf:line cross-reference in this spec links here, to the exact line. This is the artifact the golden CMRs in chapter Θʹ pin.

vault.simf the per-position covenant

Holds L-BTC collateral; commits (debt, owner, last_height) in an unspendable data leaf; owner and permissionless ops.

1/* 2 * STYX v1 - vault covenant. The per-position CDP. 3 * 4 * NOTE: the `match` below is a Simplicity `case` node, so the program must 5 * PRUNE against the spend env (satisfy_with_env) before encoding, else 6 * Elements' Anti-DOS CHECK_CASE rejects the unpruned program. 7 * 8 * data leaf = OP_RETURN || debt(8 BE) || owner(32) || last_height(4 BE) (45 B, unspendable) 9 * vault addr = taproot(NUMS, tapbranch( leaf(this CMR), dataleaf(debt, owner, last_height) )) 10 * 11 * `last_height` is the freshness ratchet (the most recent oracle height the vault acknowledged): 12 * every collateral-removing op (liquidate/redeem/bad-debt) and ratchet-advancing op (draw/refresh) 13 * requires a tick STRICTLY NEWER than it, so a stale tick from a past dip cannot be replayed. The 14 * owner advances it via DRAW or a health-gated REFRESH (proving CR >= 130%); permissionless ops 15 * respect it but never advance it (so a self-redeem cannot dodge liquidation). 16 * 17 * `debt` is static (no interest); it changes only by recursing to a new vault address. 18 * Ops: CLOSE / REPAY / DRAW (owner-signed); partial LIQUIDATE (heal-to-target), FULL-LIQ, 19 * BAD-DEBT liquidation, and REDEEM (the peg floor), all permissionless. Liquidation is a fast 20 * partial heal-to-target with no challenge window or delay. 21 * Partial liquidation heals the residual to CR [132%, 137%] and caps the owner's loss at 22 * 1.15 * dd; bad-debt liquidation (CR < 100%) repays the full debt and lets the stability 23 * reserve cover the shortfall; redeem swaps X OBOL for X worth of collateral at the peg floor. 24 * The 3-of-5 directional quorum bounds a lying oracle. 25 * 26 * Witness OP (nested Either sum; the arm map is below): 27 * owner op = Left( (owner_sig, kind) ) kind = close / repay / draw 28 * partial = Right( Left( (dd, tick) ) ) permissionless, heal-to-target 29 * full-liq = Right( Right( Left( tick ) ) ) permissionless, full (CR in [100%,115%]) 30 * bad-debt = Right( Right( Right( Left( tick ) ) ) ) permissionless, full (CR < 100%) 31 * redeem = Right( Right( Right( Right( Left( (X, tick) ) ) ) ) ) permissionless, peg floor 32 * refresh = Right( Right( Right( Right( Right( tick ) ) ) ) ) PERMISSIONLESS ratchet advance (M-2) 33 * (kind = Left(()) close | Right(Left(r)) repay | Right(Right((d,tick))) draw) 34 * REFRESH is permissionless (M-2): any keeper can advance a HEALTHY vault's last_height toward the tip, 35 * so a stale dip tick cannot be replayed to seize a since-recovered vault's collateral. It only ADVANCES 36 * last_height and preserves debt+collateral, and an unhealthy vault cannot be refreshed - so no owner 37 * signature is needed and it can never dodge a genuine liquidation. Per-vault analog of the issuer POKE. 38 * The oracle tick is (height, (backing_k, [slot; 5])) - a 3-of-5 quorum of optional oracle quotes 39 * at a shared height plus a co-signed backing_k (the global backing ratio, used only by REDEEM's 40 * floor); verify_quorum verifies exactly three, timelocks to the height, and returns (min, max). 41 * 42 * Params: NUMS, TAPLEAF_TAG, LEAF_VC (= 0xc4 << 8 | 0x2d), OBOL_ID, ORACLE_PK_1..5, POLICY, 43 * POT_SPK, STABILITY_SPK (the constant-address stability reserve's flat scriptHash, pinned on the 44 * liquidate / redeem fee). Witness: DEBT, OWNER, LAST_HEIGHT, OP. 45 */ 46fn dataleaf_hash(debt: u64, owner: u256, last_height: u32) -> u256 { 47 let tag: u256 = param::TAPLEAF_TAG; 48 let h: Ctx8 = jet::sha_256_ctx_8_init(); 49 let h: Ctx8 = jet::sha_256_ctx_8_add_32(h, tag); 50 let h: Ctx8 = jet::sha_256_ctx_8_add_32(h, tag); 51 let h: Ctx8 = jet::sha_256_ctx_8_add_2(h, param::LEAF_VC); // leaf version || compact_size(45) 52 let h: Ctx8 = jet::sha_256_ctx_8_add_1(h, 106); // 0x6a OP_RETURN (script byte 0) 53 let h: Ctx8 = jet::sha_256_ctx_8_add_8(h, debt); 54 let h: Ctx8 = jet::sha_256_ctx_8_add_32(h, owner); 55 let h: Ctx8 = jet::sha_256_ctx_8_add_4(h, last_height); // freshness ratchet: the last oracle 56 jet::sha_256_ctx_8_finalize(h) // height this vault acknowledged 57} 58 59// sha256 of this vault's scriptPubKey if it commits to (debt, owner, last_height). 60fn own_spk_hash_for(debt: u64, owner: u256, last_height: u32) -> u256 { 61 let leaf0: u256 = jet::build_tapleaf_simplicity(jet::script_cmr()); 62 let leaf1: u256 = dataleaf_hash(debt, owner, last_height); 63 let merkle: u256 = jet::build_tapbranch(leaf0, leaf1); 64 let outkey: u256 = jet::build_taptweak(jet::internal_key(), merkle); 65 let h: Ctx8 = jet::sha_256_ctx_8_init(); 66 let prefix: u16 = 0x5120; 67 let h: Ctx8 = jet::sha_256_ctx_8_add_2(h, prefix); 68 let h: Ctx8 = jet::sha_256_ctx_8_add_32(h, outkey); 69 jet::sha_256_ctx_8_finalize(h) 70} 71 72// One oracle quote: verify a signature over sha256(height(4 BE) || price(4 BE)); return price. 73fn check_quote(pk: u256, height: u32, backing_k: u32, quote: (u32, Signature)) -> u32 { 74 let (price, sig): (u32, Signature) = quote; 75 let h: Ctx8 = jet::sha_256_ctx_8_init(); 76 let h: Ctx8 = jet::sha_256_ctx_8_add_4(h, height); 77 let h: Ctx8 = jet::sha_256_ctx_8_add_4(h, price); 78 let h: Ctx8 = jet::sha_256_ctx_8_add_4(h, backing_k); // oracles co-sign the backing ratio in the same tick 79 let msg: u256 = jet::sha_256_ctx_8_finalize(h); 80 jet::bip_0340_verify((pk, msg), sig); 81 // reject a zero price: coll_at_cr / required_sats divide by price*200 and divide_64(n,0)=0, so a 82 // single oracle signing price=0 would collapse the MIN-quote health gate to 0 (mint against no 83 // collateral) - breaking the 1-of-3 fault model. A valid quote is strictly positive. 84 assert!(jet::lt_32(0, price)); 85 price 86} 87 88// One quote SLOT for a fixed oracle key: Left(()) = this oracle did not sign this tick; Right(quote) 89// = it did. process_slot folds (count, lo, hi) over the five slots and verifies the sig ONLY on the 90// Right branch (so after pruning exactly `count` bip340 checks run). check_quote asserts price > 0. 91fn process_slot(pk: u256, height: u32, backing_k: u32, slot: Either<(), (u32, Signature)>, acc: (u32, u32, u32)) -> (u32, u32, u32) { 92 let (count, lo, hi): (u32, u32, u32) = acc; 93 match slot { 94 Left(_absent: ()) => (count, lo, hi), 95 Right(quote: (u32, Signature)) => { 96 let price: u32 = check_quote(pk, height, backing_k, quote); 97 let (of, c): (bool, u32) = jet::add_32(count, 1); 98 assert!(jet::eq_8(jet::left_pad_low_1_8(<bool>::into(of)), 0)); 99 (c, jet::min_32(lo, price), jet::max_32(hi, price)) 100 } 101 } 102} 103 104// 3-of-5 quorum at a shared height: five oracle keys, each with an OPTIONAL signed quote; EXACTLY 105// three must sign (the threshold). Verifies those three, timelocks the spend to the height, returns 106// (min, max). With <= 2 Byzantine oracles any three DISTINCT keys contain >= 1 honest, so the honest 107// quote floors MIN/MAX (value safety in BOTH directions); and any three of five online suffice 108// (tolerates 2 offline - the 3-of-3 liveness single-point-of-failure is gone). Callers pick the 109// quantile that makes their action harder to abuse: MIN for health (open/draw), MAX for removal. 110fn verify_quorum(height: u32, backing_k: u32, quotes: [Either<(), (u32, Signature)>; 5]) -> (u32, u32) { 111 let [s1, s2, s3, s4, s5]: [Either<(), (u32, Signature)>; 5] = quotes; 112 let a0: (u32, u32, u32) = (0, 4294967295, 0); // (count, lo = u32::MAX sentinel, hi = 0 sentinel) 113 let a1: (u32, u32, u32) = process_slot(param::ORACLE_PK_1, height, backing_k, s1, a0); 114 let a2: (u32, u32, u32) = process_slot(param::ORACLE_PK_2, height, backing_k, s2, a1); 115 let a3: (u32, u32, u32) = process_slot(param::ORACLE_PK_3, height, backing_k, s3, a2); 116 let a4: (u32, u32, u32) = process_slot(param::ORACLE_PK_4, height, backing_k, s4, a3); 117 let a5: (u32, u32, u32) = process_slot(param::ORACLE_PK_5, height, backing_k, s5, a4); 118 let (count, lo, hi): (u32, u32, u32) = a5; 119 assert!(jet::eq_32(count, 3)); // exactly three distinct oracles signed (the 3-of-5 threshold) 120 // height must be a BLOCK height, not a BIP65 timestamp. check_lock_height already makes any 121 // height >= 500000000 unsatisfiable (lockHeight is forced to 0 in timestamp mode), so this is 122 // defense in depth / local visibility: it states the ratchet-bound invariant here rather than 123 // resting it on the jet's timestamp semantics, so last_height can never be advanced past a 124 // reachable block (no far-future brick of the removal/refresh arms). 125 assert!(jet::lt_32(height, 500000000)); 126 jet::check_lock_height(height); 127 (lo, hi) 128} 129 130// Collateral sats needed for `debt_cents` of debt at collateral ratio CR = k / 2_000_000, 131// at `price` (USD/BTC). k = CR_percent * 2_000_000: 300000000 = 150%, 260000000 = 130%, 132// 230000000 = 115%, 264000000 = 132%, 274000000 = 137%. divide_64 truncates; the gate (< 133// threshold) and the extraction cap (<= threshold) both truncate in the protocol-favoring 134// direction, and the residual band [132%,137%] absorbs the sub-satoshi slack on the heal bounds. 135fn coll_at_cr(debt_cents: u32, price: u32, k: u32) -> u64 { 136 let numerator: u64 = jet::multiply_32(debt_cents, k); 137 let denom: u64 = jet::multiply_32(price, 200); 138 jet::divide_64(numerator, denom) 139} 140 141fn explicit_output_amount(idx: u32) -> u64 { 142 let pair: (Asset1, Amount1) = unwrap(jet::output_amount(idx)); 143 let (_asset, amount): (Asset1, Amount1) = pair; 144 unwrap_right::<(u1, u256)>(amount) 145} 146 147fn explicit_output_asset(idx: u32) -> u256 { 148 let pair: (Asset1, Amount1) = unwrap(jet::output_amount(idx)); 149 let (asset, _amount): (Asset1, Amount1) = pair; 150 unwrap_right::<(u1, u256)>(asset) 151} 152 153// The OBOL pot successor (output 1) IS the genuine pot (scriptHash == POT_SPK, a flat param), 154// is OBOL, and equals pot_in (input 1) + delta. Pinning POT_SPK closes the reserve-identity gap: 155// without it a spender could route the returned OBOL to a fake "reserve" they control and lower 156// the debt for free. POT_SPK is vault-independent (the pot reconstructs nobody - open/draw, the 157// only vault-reconstructing leaves, live in the separate token-gated issuer), so V -> POT_SPK 158// does not cycle with the issuer's I -> V. See ../README.md and ../../v1-SCOPE.md. 159fn pot_grows_by(delta: u64) { 160 // pin BOTH ends of the pot to POT_SPK, symmetric with pot_shrinks_by. Input 1 is the pot on every 161 // inflow path (repay / close / liquidate / redeem / bad-debt; reserve_repay pins current_index==1). 162 // Without the input pin, value conservation still forces `delta` fresh OBOL into a POT_SPK output, 163 // but a spend could leave the genuine pot UTXO unspent and mint delta into a NEW POT_SPK UTXO from 164 // an arbitrary OBOL coin; pinning the input closes that so the release comes from the real pot. 165 assert!(jet::eq_256(param::POT_SPK, unwrap(jet::input_script_hash(1)))); 166 assert!(jet::eq_256(param::POT_SPK, unwrap(jet::output_script_hash(1)))); 167 assert!(jet::eq_256(param::OBOL_ID, explicit_output_asset(1))); 168 let reserve_out: u64 = explicit_output_amount(1); 169 let (r_asset, r_amount): (Asset1, Amount1) = unwrap(jet::input_amount(1)); 170 assert!(jet::eq_256(param::OBOL_ID, unwrap_right::<(u1, u256)>(r_asset))); 171 let reserve_in: u64 = unwrap_right::<(u1, u256)>(r_amount); 172 let (overflow, expected): (bool, u64) = jet::add_64(reserve_in, delta); 173 assert!(jet::eq_8(jet::left_pad_low_1_8(<bool>::into(overflow)), 0)); 174 assert!(jet::eq_64(reserve_out, expected)); 175} 176 177// DRAW counterpart: the pot successor (output 1) is the genuine pot, OBOL, and equals pot_in - d. 178fn pot_shrinks_by(d: u64) { 179 // pin BOTH ends of the pot to POT_SPK (H1): the released OBOL must leave the genuine pot, not a 180 // plain OBOL coin (which would let a DRAW fragment OBOL to the real pot without pot_outflow ever 181 // running). OPEN already pins its input pot; this mirrors it on DRAW. 182 assert!(jet::eq_256(param::POT_SPK, unwrap(jet::input_script_hash(1)))); 183 assert!(jet::eq_256(param::POT_SPK, unwrap(jet::output_script_hash(1)))); 184 assert!(jet::eq_256(param::OBOL_ID, explicit_output_asset(1))); 185 let reserve_out: u64 = explicit_output_amount(1); 186 let (r_asset, r_amount): (Asset1, Amount1) = unwrap(jet::input_amount(1)); 187 assert!(jet::eq_256(param::OBOL_ID, unwrap_right::<(u1, u256)>(r_asset))); 188 let reserve_in: u64 = unwrap_right::<(u1, u256)>(r_amount); 189 let (underflow, expected): (bool, u64) = jet::subtract_64(reserve_in, d); 190 assert!(jet::eq_8(jet::left_pad_low_1_8(<bool>::into(underflow)), 0)); // d <= pot_in 191 assert!(jet::eq_64(reserve_out, expected)); 192} 193 194// output 0 is a vault at (new_debt, owner, new_last_height) holding L-BTC collateral (POLICY). 195fn recurse_to(new_debt: u64, owner: u256, new_last_height: u32) { 196 assert!(jet::eq_256(own_spk_hash_for(new_debt, owner, new_last_height), unwrap(jet::output_script_hash(0)))); 197 assert!(jet::eq_256(param::POLICY, explicit_output_asset(0))); 198} 199 200fn current_collateral() -> u64 { 201 let (c_asset, c_amount): (Asset1, Amount1) = jet::current_amount(); 202 // the vault's own collateral must be L-BTC (H2), so the CR math compares like with like - a 203 // junk-asset vault input cannot be passed off as collateral against the L-BTC-sat thresholds. 204 assert!(jet::eq_256(param::POLICY, unwrap_right::<(u1, u256)>(c_asset))); 205 unwrap_right::<(u1, u256)>(c_amount) 206} 207 208fn explicit_input_amount(idx: u32) -> u64 { 209 let pair: (Asset1, Amount1) = unwrap(jet::input_amount(idx)); 210 let (_asset, amount): (Asset1, Amount1) = pair; 211 unwrap_right::<(u1, u256)>(amount) 212} 213fn explicit_input_asset(idx: u32) -> u256 { 214 let pair: (Asset1, Amount1) = unwrap(jet::input_amount(idx)); 215 let (asset, _amount): (Asset1, Amount1) = pair; 216 unwrap_right::<(u1, u256)>(asset) 217} 218 219// stability (input/output 3) is the genuine reserve, pinned by the flat STABILITY_SPK param. The 220// reserve is now a CONSTANT-address covenant (like the pot), so the vault pins it directly - no 221// reconstruction, no witnessed last_height. This closes the sybil-reserve fee diversion (a keeper 222// could previously route the fee to a reserve UTXO at an arbitrary/bad-debt-proof last_height) and 223// removes the V -> S CMR edge (V now depends on S only through a flat scriptHash param). 224fn stability_grows_by(share: u64) { 225 assert!(jet::eq_256(param::STABILITY_SPK, unwrap(jet::input_script_hash(3)))); // genuine reserve in 226 assert!(jet::eq_256(param::POLICY, explicit_input_asset(3))); // reserve input is L-BTC (explicit) 227 assert!(jet::eq_256(param::STABILITY_SPK, unwrap(jet::output_script_hash(3)))); // same address out 228 assert!(jet::eq_256(param::POLICY, explicit_output_asset(3))); 229 let stab_in: u64 = explicit_input_amount(3); 230 let stab_out: u64 = explicit_output_amount(3); 231 let (overflow, expected): (bool, u64) = jet::add_64(stab_in, share); 232 assert!(jet::eq_8(jet::left_pad_low_1_8(<bool>::into(overflow)), 0)); 233 assert!(jet::eq_64(stab_out, expected)); 234} 235 236fn main() { 237 let debt: u64 = witness::DEBT; 238 let owner: u256 = witness::OWNER; 239 let last_height: u32 = witness::LAST_HEIGHT; 240 // bind (debt, owner, last_height) to this address. `last_height` is the freshness ratchet: 241 // the height of the most recent oracle tick this vault has acknowledged. Any op that removes 242 // collateral (liquidate / redeem / bad-debt) or advances the ratchet (draw / refresh) must use 243 // a tick STRICTLY NEWER than last_height, so a stale tick (from a past dip) cannot be replayed. 244 assert!(jet::eq_256(own_spk_hash_for(debt, owner, last_height), jet::current_script_hash())); 245 246 // debt fits in u32 cents, so every `rightmost_64_32(debt)` (liquidate / redeem / bad-debt pricing) 247 // is lossless. OPEN/DRAW already enforce this at mint time, but a vault UTXO can be CONJURED by 248 // funding the reconstructed address directly with an out-of-range debt (no OBOL minted); asserting 249 // it locally here (every arm) makes the bound explicit instead of resting on the emergent 250 // "full debt must be burned into the pot, and supply < 2^32" argument. 251 assert!(jet::lt_64(debt, 4294967296)); 252 253 // One vault per tx: the vault sits at input 0 in every flow (owner ops, liquidate, bad-debt, 254 // redeem), so requiring it forbids co-spending two vaults to ALIAS the single shared pot 255 // output. Without this, two equal-debt vaults could each assert `pot_out == pot_in + debt` 256 // against the same output 1 and both pass, returning ONE debt of OBOL while extinguishing TWO 257 // (a backing drain). current_index is the spend's own input position. 258 assert!(jet::eq_32(jet::current_index(), 0)); 259 260 // The oracle tick is (height, (backing_k, [slot; 5])). Owner kinds: close / repay / draw; 261 // permissionless: partial-liquidate / full-liq / bad-debt / redeem / refresh (M-2). REFRESH moved to 262 // the permissionless side so any keeper can keep a healthy vault's ratchet fresh (see the header). 263 let op: Either< 264 (Signature, Either<(), Either<u64, (u64, (u32, (u32, [Either<(), (u32, Signature)>; 5])))>>), 265 Either<(u64, (u32, (u32, [Either<(), (u32, Signature)>; 5]))), Either<(u32, (u32, [Either<(), (u32, Signature)>; 5])), Either<(u32, (u32, [Either<(), (u32, Signature)>; 5])), Either<(u64, (u32, (u32, [Either<(), (u32, Signature)>; 5]))), (u32, (u32, [Either<(), (u32, Signature)>; 5]))>>>> 266 > = witness::OP; 267 match op { 268 Left(owner_op: (Signature, Either<(), Either<u64, (u64, (u32, (u32, [Either<(), (u32, Signature)>; 5])))>>)) => { 269 let (owner_sig, kind): (Signature, Either<(), Either<u64, (u64, (u32, (u32, [Either<(), (u32, Signature)>; 5])))>>) = owner_op; 270 jet::bip_0340_verify((owner, jet::sig_all_hash()), owner_sig); 271 match kind { 272 Left(_close: ()) => { 273 // CLOSE: return the full debt; collateral is freed to the owner. 274 pot_grows_by(debt); 275 } 276 Right(rest: Either<u64, (u64, (u32, (u32, [Either<(), (u32, Signature)>; 5])))>) => { 277 match rest { 278 Left(r: u64) => { 279 // REPAY r: return r, recurse to (debt - r); last_height carries forward. 280 // REPAY carries no oracle tick, so it cannot gate health - the collateral 281 // must therefore NOT shrink. Without this an owner REPAYs r=0 and routes the 282 // whole collateral to themselves while keeping the debt, leaving the minted 283 // OBOL unbacked (a confirmed collateralization break). Same preservation as 284 // REFRESH; the tx fee comes from a separate coin, not the locked collateral. 285 let (underflow, new_debt): (bool, u64) = jet::subtract_64(debt, r); 286 assert!(jet::eq_8(jet::left_pad_low_1_8(<bool>::into(underflow)), 0)); 287 let coll: u64 = current_collateral(); 288 pot_grows_by(r); 289 recurse_to(new_debt, owner, last_height); 290 assert!(jet::le_64(coll, explicit_output_amount(0))); // collateral not reduced 291 } 292 Right(draw: (u64, (u32, (u32, [Either<(), (u32, Signature)>; 5])))) => { 293 // DRAW d: borrow d more; debt -> debt + d, 150% at the LOWEST quote. 294 // Advances the ratchet to this tick's height (must be > last_height). 295 let (d, tick): (u64, (u32, (u32, [Either<(), (u32, Signature)>; 5]))) = draw; 296 let (height, (backing_k, quotes)): (u32, (u32, [Either<(), (u32, Signature)>; 5])) = tick; 297 let (lo, _hi): (u32, u32) = verify_quorum(height, backing_k, quotes); 298 assert!(jet::lt_32(last_height, height)); // strictly fresher 299 let (overflow, new_debt): (bool, u64) = jet::add_64(debt, d); 300 assert!(jet::eq_8(jet::left_pad_low_1_8(<bool>::into(overflow)), 0)); 301 assert!(jet::lt_64(new_debt, 4294967296)); // debt fits in u32 cents (no truncation) 302 pot_shrinks_by(d); 303 recurse_to(new_debt, owner, height); 304 let new_debt_cents: u32 = jet::rightmost_64_32(new_debt); 305 assert!(jet::le_64(coll_at_cr(new_debt_cents, lo, 300000000), explicit_output_amount(0))); 306 } 307 } 308 } 309 } 310 } 311 Right(perm: Either<(u64, (u32, (u32, [Either<(), (u32, Signature)>; 5]))), Either<(u32, (u32, [Either<(), (u32, Signature)>; 5])), Either<(u32, (u32, [Either<(), (u32, Signature)>; 5])), Either<(u64, (u32, (u32, [Either<(), (u32, Signature)>; 5]))), (u32, (u32, [Either<(), (u32, Signature)>; 5]))>>>>) => { 312 match perm { 313 Left(liq: (u64, (u32, (u32, [Either<(), (u32, Signature)>; 5])))) => { 314 // LIQUIDATE (permissionless, partial, heal-to-target). All checks use the HIGHEST 315 // of the three quotes. The keeper repays `dd`; the residual vault keeps the old 316 // last_height (liquidation proves the OPPOSITE of health, so it must not advance 317 // the ratchet); the tick must be STRICTLY NEWER than last_height (no stale replay). 318 let (dd, tick): (u64, (u32, (u32, [Either<(), (u32, Signature)>; 5]))) = liq; 319 let (height, (backing_k, quotes)): (u32, (u32, [Either<(), (u32, Signature)>; 5])) = tick; 320 let (_lo, hi): (u32, u32) = verify_quorum(height, backing_k, quotes); 321 assert!(jet::lt_32(last_height, height)); // fresher than the vault's last ack 322 323 // dd in (0, debt): a partial repayment, residual vault remains. 324 assert!(jet::lt_64(0, dd)); 325 let (uf_rd, residual_debt): (bool, u64) = jet::subtract_64(debt, dd); 326 assert!(jet::eq_8(jet::left_pad_low_1_8(<bool>::into(uf_rd)), 0)); // dd <= debt 327 assert!(jet::lt_64(0, residual_debt)); // dd < debt (partial) 328 329 // pre-state gate: the vault is unhealthy, CR < 130% at the MAX quote. 330 let coll: u64 = current_collateral(); 331 let debt_cents: u32 = jet::rightmost_64_32(debt); 332 assert!(jet::lt_64(coll, coll_at_cr(debt_cents, hi, 260000000))); 333 334 // `dd` repaid to the reserve; the vault recurses to (debt - dd, owner), same last_height. 335 pot_grows_by(dd); 336 recurse_to(residual_debt, owner, last_height); 337 let residual_coll: u64 = explicit_output_amount(0); 338 339 // heal-to-target: the residual vault lands in CR [132%, 137%] at the MAX quote. The 340 // target sits just above the 130% trigger (was [150%,155%]) so the owner keeps more 341 // equity and the keeper fronts a smaller `dd` (a smaller per-step collateral dump, less 342 // liquidation reflexivity); the tradeoff is residuals re-trigger sooner. The 115% 343 // full-liq boundary is UNCHANGED - it is set by the 1.15 extraction cap below, not the 344 // heal target - so lowering the target does not reopen the E-3 dead-zone. 345 let rd_cents: u32 = jet::rightmost_64_32(residual_debt); 346 assert!(jet::le_64(coll_at_cr(rd_cents, hi, 264000000), residual_coll)); // >= 132% 347 assert!(jet::le_64(residual_coll, coll_at_cr(rd_cents, hi, 274000000))); // <= 137% 348 349 // owner protection: the collateral extracted is at most 1.15 * dd worth at the MAX quote. 350 let (uf_ex, extraction): (bool, u64) = jet::subtract_64(coll, residual_coll); 351 assert!(jet::eq_8(jet::left_pad_low_1_8(<bool>::into(uf_ex)), 0)); // residual_coll <= coll 352 let dd_cents: u32 = jet::rightmost_64_32(dd); 353 assert!(jet::le_64(extraction, coll_at_cr(dd_cents, hi, 230000000))); // <= 1.15 * dd 354 355 // pin the 5% stability fee into the genuine reserve (V -> S; this moved off the 356 // pot so the pot reconstructs nobody and the reserve can pin POT_SPK on bad-debt). 357 stability_grows_by(coll_at_cr(dd_cents, hi, 10000000)); 358 } 359 Right(rest2: Either<(u32, (u32, [Either<(), (u32, Signature)>; 5])), Either<(u32, (u32, [Either<(), (u32, Signature)>; 5])), Either<(u64, (u32, (u32, [Either<(), (u32, Signature)>; 5]))), (u32, (u32, [Either<(), (u32, Signature)>; 5]))>>>) => { 360 match rest2 { 361 Left(fulliq: (u32, (u32, [Either<(), (u32, Signature)>; 5]))) => { 362 // FULL LIQUIDATION (permissionless, CR in [100%, 115%] at the MAX quote). This band 363 // is the partial-liquidation dead-zone (economic finding E-3): a partial heal to 364 // 132% must extract 1.15x collateral per unit debt, but below CR 115% that ratio 365 // EXCEEDS the CR, so any partial liquidation pushes the CR DOWN - no heal exists at 366 // any heal target. Instead the keeper repays the FULL debt and seizes the 367 // collateral; the vault CLOSES (no successor). Owner's whole excess buffer (coll - 368 // debt_sats, in [0, 15% of debt]) is the penalty; ONE THIRD goes to the reserve and 369 // the keeper keeps two thirds - the exact analog of partial-liq's 10:5 split. 370 let (height, (backing_k, quotes)): (u32, (u32, [Either<(), (u32, Signature)>; 5])) = fulliq; 371 let (_lo, hi): (u32, u32) = verify_quorum(height, backing_k, quotes); 372 assert!(jet::lt_32(last_height, height)); // fresher than the vault's last ack (no stale replay) 373 let coll: u64 = current_collateral(); 374 let debt_cents: u32 = jet::rightmost_64_32(debt); 375 // CR in [100%, 115%]: debt_sats <= coll <= 1.15*debt_sats. Below 100% is bad-debt 376 // (reserve-backed); above 115% the partial heal is feasible and takes over. 377 let debt_sats: u64 = coll_at_cr(debt_cents, hi, 200000000); 378 assert!(jet::le_64(debt_sats, coll)); // CR >= 100% (not bad-debt) 379 assert!(jet::le_64(coll, coll_at_cr(debt_cents, hi, 230000000))); // CR <= 115% (partial infeasible) 380 // the keeper burns the FULL debt into the genuine pot; the vault closes (no recurse). 381 pot_grows_by(debt); 382 // route ONE THIRD of the excess (coll - debt_sats) to the reserve, mirroring the 383 // partial-liq 10:5 keeper:reserve penalty split. excess is in [0, 15% of debt] (CR in 384 // [100,115]), so reserve = excess/3 in [0, 5% of debt] and the keeper's seizure (coll 385 // - reserve_fee, unpinned output 0, forced by L-BTC conservation) keeps the other 386 // 2/3. At CR 115% this is keeper 10% / reserve 5% of debt - CONTINUOUS with a partial 387 // just above 115% - and it funds the reserve on the one liquidation band that paid it 388 // nothing before. reserve_fee <= excess, so the keeper's seizure stays >= 0. 389 let (uf_x, excess): (bool, u64) = jet::subtract_64(coll, debt_sats); 390 assert!(jet::eq_8(jet::left_pad_low_1_8(<bool>::into(uf_x)), 0)); // coll >= debt_sats (holds by the CR>=100 gate) 391 stability_grows_by(jet::divide_64(excess, 3)); 392 } 393 Right(bad_or_rest: Either<(u32, (u32, [Either<(), (u32, Signature)>; 5])), Either<(u64, (u32, (u32, [Either<(), (u32, Signature)>; 5]))), (u32, (u32, [Either<(), (u32, Signature)>; 5]))>>) => { 394 match bad_or_rest { 395 Left(bad: (u32, (u32, [Either<(), (u32, Signature)>; 5]))) => { 396 // BAD-DEBT LIQUIDATION (permissionless, full, CR < 100% at the MAX quote). 397 // Fresher than last_height (no stale replay); vault closes (no successor). 398 let (height, (backing_k, quotes)): (u32, (u32, [Either<(), (u32, Signature)>; 5])) = bad; 399 let (_lo, hi): (u32, u32) = verify_quorum(height, backing_k, quotes); 400 assert!(jet::lt_32(last_height, height)); 401 let debt_cents: u32 = jet::rightmost_64_32(debt); 402 assert!(jet::lt_64(current_collateral(), coll_at_cr(debt_cents, hi, 200000000))); 403 pot_grows_by(debt); 404 } 405 Right(redeem_or_refresh: Either<(u64, (u32, (u32, [Either<(), (u32, Signature)>; 5]))), (u32, (u32, [Either<(), (u32, Signature)>; 5]))>) => { 406 match redeem_or_refresh { 407 Left(redeem: (u64, (u32, (u32, [Either<(), (u32, Signature)>; 5])))) => { 408 // REDEEM (permissionless, the peg floor). Swaps X OBOL for X worth of 409 // collateral at the MAX quote; recurse to (debt - X, owner) with the same 410 // last_height (redeem must not advance the ratchet - else a self-redeem 411 // would dodge liquidation); the tick must be STRICTLY NEWER than last_height. 412 let (x, tick): (u64, (u32, (u32, [Either<(), (u32, Signature)>; 5]))) = redeem; 413 let (height, (backing_k, quotes)): (u32, (u32, [Either<(), (u32, Signature)>; 5])) = tick; 414 let (_lo, hi): (u32, u32) = verify_quorum(height, backing_k, quotes); 415 assert!(jet::lt_32(last_height, height)); 416 417 // X in (0, debt]. 418 assert!(jet::lt_64(0, x)); 419 let (uf_nd, new_debt): (bool, u64) = jet::subtract_64(debt, x); 420 assert!(jet::eq_8(jet::left_pad_low_1_8(<bool>::into(uf_nd)), 0)); // x <= debt 421 assert!(jet::lt_64(x, 4294967296)); // x fits in u32 cents so rightmost_64_32 is lossless (H3) 422 423 pot_grows_by(x); 424 recurse_to(new_debt, owner, last_height); 425 let residual_coll: u64 = explicit_output_amount(0); 426 427 // owner protection: at most X worth of collateral leaves the vault. 428 let coll: u64 = current_collateral(); 429 let (uf_ex, extraction): (bool, u64) = jet::subtract_64(coll, residual_coll); 430 assert!(jet::eq_8(jet::left_pad_low_1_8(<bool>::into(uf_ex)), 0)); // residual_coll <= coll 431 let x_cents: u32 = jet::rightmost_64_32(x); 432 // backing-ratio floor: value the claim at min(par, backing_ratio). When OBOL is 433 // under-backed (backing_k < par) the redeemer may extract only its PRO-RATA share 434 // of collateral, not a full $1 - so redeeming cannot strip par collateral out of an 435 // under-backed pool and worsen the backing for everyone else. backing_k is co-signed 436 // by the oracle quorum in the shared tick (it is GLOBAL off-chain data the covenant 437 // cannot compute per-tx); the same <= 2 Byzantine model that bounds price bounds it. 438 let floor_k: u32 = jet::min_32(200000000, backing_k); 439 assert!(jet::le_64(extraction, coll_at_cr(x_cents, hi, floor_k))); // <= X * min(par, backing_ratio) 440 441 // pin the 0.5% redemption fee into the genuine reserve (V -> S). 442 stability_grows_by(coll_at_cr(x_cents, hi, 1000000)); 443 } 444 Right(tick: (u32, (u32, [Either<(), (u32, Signature)>; 5]))) => { 445 // PERMISSIONLESS REFRESH (M-2): any keeper advances a HEALTHY vault's 446 // ratchet to a fresher tick (CR >= 130% at the MAX quote), so a dormant 447 // vault's last_height tracks the tip and a stale dip tick (height <= 448 // last_height) can no longer be replayed to seize its recovered collateral. 449 // No owner sig: refresh only ADVANCES last_height and preserves debt+collateral 450 // (recurse_to pins debt/owner; the collateral may not shrink), and an UNHEALTHY 451 // vault cannot be refreshed - so a third party can neither harm the vault nor 452 // let it dodge a genuine liquidation. MAX quote is the exact dual of LIQUIDATE. 453 let (height, (backing_k, quotes)): (u32, (u32, [Either<(), (u32, Signature)>; 5])) = tick; 454 let (_lo, hi): (u32, u32) = verify_quorum(height, backing_k, quotes); 455 assert!(jet::lt_32(last_height, height)); // strictly fresher 456 let coll: u64 = current_collateral(); 457 let debt_cents: u32 = jet::rightmost_64_32(debt); 458 assert!(jet::le_64(coll_at_cr(debt_cents, hi, 260000000), coll)); // CR >= 130% at MAX 459 recurse_to(debt, owner, height); 460 assert!(jet::le_64(coll, explicit_output_amount(0))); // collateral not reduced 461 } 462 } 463 } 464 } 465 } 466 } 467 } 468 } 469 } 470 } 471}

reserve_repay.simf the pot inflow leaf

OBOL returning to the pot on repay, close, liquidate, redeem, and bad-debt. Grow-only, pinned to input 1.

1/* 2 * STYX MULTIVAULT - reserve REPAY leaf. 3 * 4 * OBOL flows back into the shared reserve when a vault is liquidated (the keeper 5 * returns the vault's debt). The reserve may only grow or stay on this leaf, so it 6 * can never be drained here; the vault's LIQUIDATE leaf is what pins the exact amount 7 * returned (reserve_in + that vault's debt). Recurses to output 1. 8 * 9 * The pot sits at input 1 on every inflow path (repay / close / liquidate / redeem / bad-debt), so 10 * this leaf pins current_index == 1 (H4, defense in depth): a second same-address pot input cannot be 11 * co-spent to alias the single output-1 recursion. (Safe even without it - the recursion is grow-only 12 * and the protocol keeps exactly one pot UTXO - but the pin removes the aliasing surface entirely.) 13 * 14 * Param: OBOL_ID. 15 */ 16fn explicit_output_amount(idx: u32) -> u64 { 17 let pair: (Asset1, Amount1) = unwrap(jet::output_amount(idx)); 18 let (_asset, amount): (Asset1, Amount1) = pair; 19 unwrap_right::<(u1, u256)>(amount) 20} 21 22fn explicit_output_asset(idx: u32) -> u256 { 23 let pair: (Asset1, Amount1) = unwrap(jet::output_amount(idx)); 24 let (asset, _amount): (Asset1, Amount1) = pair; 25 unwrap_right::<(u1, u256)>(asset) 26} 27 28fn main() { 29 let obol: u256 = param::OBOL_ID; 30 31 // the pot is always at input 1 on inflow paths; pin it so a second same-address input cannot 32 // alias the single output-1 recursion (H4). 33 assert!(jet::eq_32(jet::current_index(), 1)); 34 35 // recurse: the reserve successor is output 1, and must be OBOL 36 assert!(jet::eq_256(jet::current_script_hash(), unwrap(jet::output_script_hash(1)))); 37 assert!(jet::eq_256(obol, explicit_output_asset(1))); 38 let reserve_out: u64 = explicit_output_amount(1); 39 40 // reserve input (this input) must be OBOL 41 let (in_asset, in_amount): (Asset1, Amount1) = jet::current_amount(); 42 assert!(jet::eq_256(obol, unwrap_right::<(u1, u256)>(in_asset))); 43 let reserve_in: u64 = unwrap_right::<(u1, u256)>(in_amount); 44 45 // inflow only: the reserve may grow or stay, never shrink on this leaf 46 assert!(jet::le_64(reserve_in, reserve_out)); 47}

pot_outflow.simf the pot outflow leaf

OBOL leaving the pot on OPEN and DRAW, gated on the issuer token co-spent at the adjacent input.

1/* 2 * STYX v1 - OBOL pot OUTFLOW leaf. 3 * 4 * The OBOL pot holds the supply and is vault-independent (it reconstructs nobody), so the vault 5 * can pin it by a flat POT_SPK param without a CMR cycle. OBOL leaves the pot only on OPEN and 6 * DRAW, and only when authorized by the issuer - the covenant that reconstructs the vault and 7 * enforces 150% health. This leaf is the gate: it recurses the pot (output 1) and requires the 8 * issuer's unique token to be co-spent at the next input. The issuer (input pot_index + 1) does 9 * the real work (vault genuineness, the released amount, health); this leaf only ensures a 10 * genuine issuer authorized the release, so the pot cannot be drained by a bare spend. 11 * 12 * Tx layout (OPEN): inputs [pot(0), issuer(1), collateral(2)]; (DRAW): [vault(0), pot(1), issuer(2)]. 13 * In both the pot successor is output 1 and the issuer is at the input right after the pot. 14 * 15 * Params: OBOL_ID, ISSUER_TOKEN_ID (a 1-unit genesis asset held only by the issuer - a flat 16 * asset id, not a CMR, so this gate adds no covenant dependency). 17 */ 18fn explicit_output_asset(idx: u32) -> u256 { 19 let pair: (Asset1, Amount1) = unwrap(jet::output_amount(idx)); 20 let (asset, _amount): (Asset1, Amount1) = pair; 21 unwrap_right::<(u1, u256)>(asset) 22} 23fn explicit_input_asset(idx: u32) -> u256 { 24 let pair: (Asset1, Amount1) = unwrap(jet::input_amount(idx)); 25 let (asset, _amount): (Asset1, Amount1) = pair; 26 unwrap_right::<(u1, u256)>(asset) 27} 28 29fn main() { 30 // this pot input holds OBOL (explicit local check - the issuer reads this input's amount by index 31 // and pins the release against it, so making the asset explicit here removes the reliance on 32 // per-asset conservation to catch a non-OBOL UTXO funded at the public POT_SPK address). 33 let idx: u32 = jet::current_index(); 34 assert!(jet::eq_256(param::OBOL_ID, explicit_input_asset(idx))); 35 36 // the issuer (its unique token) must be co-spent at the input right after the pot. 37 let (of, next): (bool, u32) = jet::add_32(idx, 1); 38 assert!(jet::eq_8(jet::left_pad_low_1_8(<bool>::into(of)), 0)); 39 assert!(jet::eq_256(param::ISSUER_TOKEN_ID, explicit_input_asset(next))); 40 41 // recurse: the pot successor is output 1, in OBOL. The issuer pins the released amount. 42 assert!(jet::eq_256(jet::current_script_hash(), unwrap(jet::output_script_hash(1)))); 43 assert!(jet::eq_256(param::OBOL_ID, explicit_output_asset(1))); 44}

issuer.simf the mint authority

Holds the 1-unit identity token; the only vault reconstructor; global mint anchor; bad-debt attester.

1/* 2 * STYX v1 - issuer covenant. The mint authority, with a global mint-recency ratchet. 3 * 4 * STATUS: the three ops dispatch on a `match` (Simplicity `case` nodes), so the runner MUST 5 * PRUNE the program against the spend env (satisfy_with_env) before encoding, else Elements' 6 * Anti-DOS CHECK_CASE rejects the unpruned program. 7 * 8 * The issuer holds a unique 1-unit token and is the only covenant that reconstructs vaults (the 9 * I -> V edge); the pot's outflow leaf gates every OBOL release on this token being co-spent, so 10 * every mint is already serialized through this one UTXO. This covenant merges the old two-leaf 11 * {open, draw} tree into ONE match-dispatching covenant plus an unspendable data leaf carrying a 12 * `last_mint_height` freshness ratchet - the same shape as the vault and the stability reserve: 13 * 14 * data leaf = OP_RETURN || last_mint_height(4 BE) (5 B, unspendable) 15 * issuer addr = taproot(NUMS, tapbranch( leaf(this CMR), dataleaf(last_mint_height) )) 16 * 17 * `last_mint_height` is the highest oracle height the mint authority has acknowledged. The mint 18 * paths (OPEN, DRAW) price the 150% health gate at an oracle tick whose height has NO upper 19 * staleness bound (check_lock_height is a lower bound only, the covenant cannot read the chain 20 * tip, and three honest oracles genuinely signed the same historical high price so the directional 21 * MIN does not help). So when the real price has fallen, an attacker replays an old, still-validly 22 * signed high tick and mints OBOL backed far below 150% of the TRUE price (audit finding A). The 23 * ratchet closes that: every OPEN/DRAW must use a tick at height >= last_mint_height and advances 24 * it; a stale tick has height < the anchor, so the floor rejects exactly those ticks while leaving 25 * legitimate mints (a fresh, near-tip tick) untouched. POKE advances the anchor permissionlessly 26 * with a fresh tick and no token movement, so keepers (or organic mint traffic) keep it near the 27 * tip ahead of the mint paths. 28 * 29 * The floor is NON-STRICT (last_mint_height <= height), unlike the vault's and stability's strict 30 * `lt_32`. The global anchor only needs to reject OLDER ticks; it must NOT serialize two legitimate 31 * mints that share one fresh oracle height (several borrowers at the same tick). A stale tick still 32 * fails (height < anchor), and there is no per-position value to over-extract by same-height replay 33 * here (contrast stability bad-debt), so strict monotonicity is unnecessary and harmful to liveness. 34 * 35 * Residual (documented, same model as the stability reserve and dormant-vault liquidation): a 36 * stale-high mint is possible only with a tick newer than the issuer's last poke/mint, so keepers 37 * poke the issuer toward the tip; in an active market organic mint traffic keeps the anchor fresh 38 * for free. No new trust assumption beyond the keeper-poke-liveness already accepted in v1-SCOPE.md. 39 * 40 * The issuer is also the bad-debt ATTESTER (closes audit finding B). The reserve's bad-debt release 41 * cannot verify on its own that input 0 is a genuine underwater vault (the reserve cannot reconstruct 42 * a vault - a CMR cycle, since V already reconstructs S). The issuer is the only covenant compiled 43 * after V (so it CAN reconstruct vaults) that the reserve recognizes by a flat id (its token). The 44 * ATTEST arm reconstructs the vault, sizes the shortfall, and authors the reserve shrink (the new 45 * ISSUER -> S edge); the reserve's bad-debt arm gates on the issuer token and defers the amount. 46 * 47 * Ops (witness OP, a nested Either sum; the arms are below): 48 * open = Left( (principal, (owner, (height, [(price, sig); 3]))) ) mint against a NEW vault 49 * draw = Right(Left( (old_debt, (owner, (old_last_height, (new_debt, draw_height)))) )) borrow more 50 * poke = Right(Right(Left( (height, [(price, sig); 3]) ))) advance the anchor, no movement 51 * attest = Right(Right(Right( (debt, (owner, (lh, tick))) ))) bad-debt attest 52 * The oracle tick is (height, (backing_k, [slot; 5])); verify_quorum verifies exactly three of five, 53 * timelocks to the height, and returns (min, max). backing_k is co-signed but only REDEEM (in the vault) 54 * reads it - the issuer threads it through the signed message so mint ticks commit to it too. POKE is 55 * PINNED to input 0 so it can never co-spend a pot 56 * outflow or a reserve shrink (which need the token at an input >= 1); see the poke arm. 57 * 58 * Tx layouts: 59 * OPEN inputs [pot(0), issuer(this, 1), collateral(2)]; 60 * outputs [vault(0, L-BTC), pot - principal(1, OBOL), borrower(2, OBOL), issuer(3, token), fee]. 61 * DRAW inputs [vault(0), pot(1), issuer(this, 2)]; 62 * outputs [vault successor(0, L-BTC), pot - d(1, OBOL), borrower(2, OBOL), issuer(3, token), fee]. 63 * POKE inputs [issuer(this, 0), fee coin(1)]; outputs [issuer(0, token), change, fee]. 64 * ATTEST inputs [vault(0), pot(1), keeper OBOL(2), reserve(3), issuer(this, 4), fee(5)]; 65 * outputs [keeper coll+shortfall(0, L-BTC), pot + debt(1, OBOL), reserve successor(2, L-BTC), 66 * issuer(3, token), changes, fee]. 67 * 68 * Params: NUMS, TAPLEAF_TAG, LEAF_VC (= 0xc4 << 8 | 0x2d, the 45-byte vault data leaf), 69 * ISSUER_LEAF_VC (= 0xc4 << 8 | 0x05, this covenant's 5-byte data leaf), VAULT_CMR, STABILITY_SPK 70 * (the constant-address reserve's flat scriptHash; the ISSUER -> S pin on attest), OBOL_ID, 71 * POLICY, ORACLE_PK_1..5, POT_SPK, ISSUER_TOKEN_ID. Witness: LAST_MINT_HEIGHT, OP. 72 */ 73 74// this issuer's own data leaf: OP_RETURN || last_mint_height(4 BE) (5 bytes). 75fn issuer_dataleaf_hash(last_mint_height: u32) -> u256 { 76 let tag: u256 = param::TAPLEAF_TAG; 77 let h: Ctx8 = jet::sha_256_ctx_8_init(); 78 let h: Ctx8 = jet::sha_256_ctx_8_add_32(h, tag); 79 let h: Ctx8 = jet::sha_256_ctx_8_add_32(h, tag); 80 let h: Ctx8 = jet::sha_256_ctx_8_add_2(h, param::ISSUER_LEAF_VC); // leaf version || compact_size(5) 81 let h: Ctx8 = jet::sha_256_ctx_8_add_1(h, 106); // 0x6a OP_RETURN (script byte 0) 82 let h: Ctx8 = jet::sha_256_ctx_8_add_4(h, last_mint_height); // the mint-recency ratchet 83 jet::sha_256_ctx_8_finalize(h) 84} 85// sha256 of this issuer's scriptPubKey if it commits to `last_mint_height` (self-reconstruction). 86fn own_spk_hash_for(last_mint_height: u32) -> u256 { 87 let leaf0: u256 = jet::build_tapleaf_simplicity(jet::script_cmr()); 88 let leaf1: u256 = issuer_dataleaf_hash(last_mint_height); 89 let merkle: u256 = jet::build_tapbranch(leaf0, leaf1); 90 let outkey: u256 = jet::build_taptweak(jet::internal_key(), merkle); 91 let h: Ctx8 = jet::sha_256_ctx_8_init(); 92 let prefix: u16 = 0x5120; 93 let h: Ctx8 = jet::sha_256_ctx_8_add_2(h, prefix); 94 let h: Ctx8 = jet::sha_256_ctx_8_add_32(h, outkey); 95 jet::sha_256_ctx_8_finalize(h) 96} 97 98// the vault data leaf and address (the I -> V edge), mirroring issuer_open/issuer_draw: a 45-byte 99// OP_RETURN || debt(8) || owner(32) || last_height(4), finalized over NUMS with VAULT_CMR. 100fn vault_dataleaf_hash(debt: u64, owner: u256, last_height: u32) -> u256 { 101 let tag: u256 = param::TAPLEAF_TAG; 102 let h: Ctx8 = jet::sha_256_ctx_8_init(); 103 let h: Ctx8 = jet::sha_256_ctx_8_add_32(h, tag); 104 let h: Ctx8 = jet::sha_256_ctx_8_add_32(h, tag); 105 let h: Ctx8 = jet::sha_256_ctx_8_add_2(h, param::LEAF_VC); 106 let h: Ctx8 = jet::sha_256_ctx_8_add_1(h, 106); 107 let h: Ctx8 = jet::sha_256_ctx_8_add_8(h, debt); 108 let h: Ctx8 = jet::sha_256_ctx_8_add_32(h, owner); 109 let h: Ctx8 = jet::sha_256_ctx_8_add_4(h, last_height); 110 jet::sha_256_ctx_8_finalize(h) 111} 112fn vault_spk_hash_for(debt: u64, owner: u256, last_height: u32) -> u256 { 113 let leaf0: u256 = jet::build_tapleaf_simplicity(param::VAULT_CMR); 114 let leaf1: u256 = vault_dataleaf_hash(debt, owner, last_height); 115 let merkle: u256 = jet::build_tapbranch(leaf0, leaf1); 116 let outkey: u256 = jet::build_taptweak(param::NUMS, merkle); 117 let h: Ctx8 = jet::sha_256_ctx_8_init(); 118 let prefix: u16 = 0x5120; 119 let h: Ctx8 = jet::sha_256_ctx_8_add_2(h, prefix); 120 let h: Ctx8 = jet::sha_256_ctx_8_add_32(h, outkey); 121 jet::sha_256_ctx_8_finalize(h) 122} 123 124 125// Collateral sats whose value equals `debt_cents` of debt at CR = k / 2_000_000 at `price` (the same 126// integer math as the vault / stability coll_at_cr). k = 200000000 (100%) values the debt in sats - 127// the bad-debt threshold and the shortfall basis. 128fn coll_at_cr(debt_cents: u32, price: u32, k: u32) -> u64 { 129 let numerator: u64 = jet::multiply_32(debt_cents, k); 130 let denom: u64 = jet::multiply_32(price, 200); 131 jet::divide_64(numerator, denom) 132} 133 134fn check_quote(pk: u256, height: u32, backing_k: u32, quote: (u32, Signature)) -> u32 { 135 let (price, sig): (u32, Signature) = quote; 136 let h: Ctx8 = jet::sha_256_ctx_8_init(); 137 let h: Ctx8 = jet::sha_256_ctx_8_add_4(h, height); 138 let h: Ctx8 = jet::sha_256_ctx_8_add_4(h, price); 139 let h: Ctx8 = jet::sha_256_ctx_8_add_4(h, backing_k); // oracles co-sign the backing ratio in the same tick 140 let msg: u256 = jet::sha_256_ctx_8_finalize(h); 141 jet::bip_0340_verify((pk, msg), sig); 142 // reject a zero price: coll_at_cr / required_sats divide by price*200 and divide_64(n,0)=0, so a 143 // single oracle signing price=0 would collapse the MIN-quote health gate to 0 (mint against no 144 // collateral) - breaking the 1-of-3 fault model. A valid quote is strictly positive. 145 assert!(jet::lt_32(0, price)); 146 price 147} 148// One quote SLOT for a fixed oracle key (see vault.simf for the full rationale): verifies the sig 149// only on the Right branch, folds (count, lo, hi). check_quote asserts price > 0. 150fn process_slot(pk: u256, height: u32, backing_k: u32, slot: Either<(), (u32, Signature)>, acc: (u32, u32, u32)) -> (u32, u32, u32) { 151 let (count, lo, hi): (u32, u32, u32) = acc; 152 match slot { 153 Left(_absent: ()) => (count, lo, hi), 154 Right(quote: (u32, Signature)) => { 155 let price: u32 = check_quote(pk, height, backing_k, quote); 156 let (of, c): (bool, u32) = jet::add_32(count, 1); 157 assert!(jet::eq_8(jet::left_pad_low_1_8(<bool>::into(of)), 0)); 158 (c, jet::min_32(lo, price), jet::max_32(hi, price)) 159 } 160 } 161} 162// 3-of-5 quorum: five keys, each with an OPTIONAL signed quote; EXACTLY three sign (threshold). 163// Verifies those three, timelocks to the height, returns (min, max). Any three distinct keys contain 164// >= 1 honest with <= 2 Byzantine, so the honest quote floors the quantile; any three of five online 165// suffice. The mint health gate uses the MIN (a single high quote cannot fake health). 166fn verify_quorum(height: u32, backing_k: u32, quotes: [Either<(), (u32, Signature)>; 5]) -> (u32, u32) { 167 let [s1, s2, s3, s4, s5]: [Either<(), (u32, Signature)>; 5] = quotes; 168 let a0: (u32, u32, u32) = (0, 4294967295, 0); 169 let a1: (u32, u32, u32) = process_slot(param::ORACLE_PK_1, height, backing_k, s1, a0); 170 let a2: (u32, u32, u32) = process_slot(param::ORACLE_PK_2, height, backing_k, s2, a1); 171 let a3: (u32, u32, u32) = process_slot(param::ORACLE_PK_3, height, backing_k, s3, a2); 172 let a4: (u32, u32, u32) = process_slot(param::ORACLE_PK_4, height, backing_k, s4, a3); 173 let a5: (u32, u32, u32) = process_slot(param::ORACLE_PK_5, height, backing_k, s5, a4); 174 let (count, lo, hi): (u32, u32, u32) = a5; 175 assert!(jet::eq_32(count, 3)); // exactly three distinct oracles signed (the 3-of-5 threshold) 176 // height must be a BLOCK height, not a BIP65 timestamp. check_lock_height already makes any 177 // height >= 500000000 unsatisfiable (lockHeight is forced to 0 in timestamp mode), so this is 178 // defense in depth / local visibility: it states the anchor-bound invariant here rather than 179 // resting it on the jet's timestamp semantics, so last_mint_height can never be advanced past a 180 // reachable block (no far-future POKE/OPEN/DRAW brick of the global mint floor). 181 assert!(jet::lt_32(height, 500000000)); 182 jet::check_lock_height(height); 183 (lo, hi) 184} 185 186fn required_sats(debt_cents: u32, price: u32) -> u64 { 187 let numerator: u64 = jet::multiply_32(debt_cents, 300000000); 188 let denom: u64 = jet::multiply_32(price, 200); 189 jet::divide_64(numerator, denom) 190} 191 192fn explicit_output_amount(idx: u32) -> u64 { 193 let pair: (Asset1, Amount1) = unwrap(jet::output_amount(idx)); 194 let (_asset, amount): (Asset1, Amount1) = pair; 195 unwrap_right::<(u1, u256)>(amount) 196} 197fn explicit_output_asset(idx: u32) -> u256 { 198 let pair: (Asset1, Amount1) = unwrap(jet::output_amount(idx)); 199 let (asset, _amount): (Asset1, Amount1) = pair; 200 unwrap_right::<(u1, u256)>(asset) 201} 202fn explicit_input_amount(idx: u32) -> u64 { 203 let pair: (Asset1, Amount1) = unwrap(jet::input_amount(idx)); 204 let (_asset, amount): (Asset1, Amount1) = pair; 205 unwrap_right::<(u1, u256)>(amount) 206} 207fn explicit_input_asset(idx: u32) -> u256 { 208 let pair: (Asset1, Amount1) = unwrap(jet::input_amount(idx)); 209 let (asset, _amount): (Asset1, Amount1) = pair; 210 unwrap_right::<(u1, u256)>(asset) 211} 212 213// recurse the issuer's 1-unit token to its ADVANCED address (committing `mint_height`) at `out_idx`. 214// Pinning the amount to 1 (not just "unchanged") makes single-unit uniqueness a covenant invariant. 215fn recurse_token(mint_height: u32, out_idx: u32) { 216 let (t_asset, t_amount): (Asset1, Amount1) = jet::current_amount(); 217 assert!(jet::eq_256(param::ISSUER_TOKEN_ID, unwrap_right::<(u1, u256)>(t_asset))); 218 assert!(jet::eq_64(unwrap_right::<(u1, u256)>(t_amount), 1)); 219 assert!(jet::eq_256(own_spk_hash_for(mint_height), unwrap(jet::output_script_hash(out_idx)))); 220 assert!(jet::eq_256(param::ISSUER_TOKEN_ID, explicit_output_asset(out_idx))); 221 assert!(jet::eq_64(explicit_output_amount(out_idx), 1)); 222} 223 224fn main() { 225 let last_mint_height: u32 = witness::LAST_MINT_HEIGHT; 226 // bind `last_mint_height` to this address. OPEN/DRAW require a tick at height >= it (no stale 227 // replay) and advance it; POKE advances it permissionlessly with no token movement. 228 assert!(jet::eq_256(own_spk_hash_for(last_mint_height), jet::current_script_hash())); 229 230 // OP = open | (draw | (poke | attest)). tick = (height, [(price, sig); 3]). 231 // attest = (debt, (owner, (lh, tick))) 232 let op: Either< 233 (u64, (u256, (u32, (u32, [Either<(), (u32, Signature)>; 5])))), 234 Either< 235 (u64, (u256, (u32, (u64, u32)))), 236 Either<(u32, (u32, [Either<(), (u32, Signature)>; 5])), (u64, (u256, (u32, (u32, (u32, [Either<(), (u32, Signature)>; 5])))))> 237 > 238 > = witness::OP; 239 match op { 240 Left(open: (u64, (u256, (u32, (u32, [Either<(), (u32, Signature)>; 5]))))) => { 241 // OPEN: release `principal` OBOL from the pot to a borrower against a new vault. The 242 // tick prices the 150% gate; its height must clear the global floor and becomes the new 243 // anchor (and the new vault's own last_height). 244 let (principal, rest): (u64, (u256, (u32, (u32, [Either<(), (u32, Signature)>; 5])))) = open; 245 let (owner, tick): (u256, (u32, (u32, [Either<(), (u32, Signature)>; 5]))) = rest; 246 let (open_height, (backing_k, quotes)): (u32, (u32, [Either<(), (u32, Signature)>; 5])) = tick; 247 let (price, _hi): (u32, u32) = verify_quorum(open_height, backing_k, quotes); 248 assert!(jet::le_32(last_mint_height, open_height)); // global mint floor (non-strict) 249 250 // debt = principal (no fold): the 0.5% borrow fee is now paid as L-BTC INTO THE RESERVE at 251 // open (see the reserve pin below), not folded into the debt as OBOL surplus in the pot. This 252 // funds the bad-debt backstop in proportion to origination FLOW, independent of liquidation 253 // activity (reserve-stage E-2 funding). debt (cents) must fit u32 so rightmost_64_32 is lossless. 254 let debt: u64 = principal; 255 assert!(jet::lt_64(debt, 4294967296)); 256 let fee_cents: u32 = jet::rightmost_64_32(principal); 257 let fee_sats: u64 = coll_at_cr(fee_cents, price, 1000000); // 0.5% of principal valued at the MIN quote 258 259 // a mint must MOVE OBOL: principal > 0. A principal==0 open leaves the pot unchanged, so it 260 // would spend the pot via the grow-only INFLOW leaf (which has no token gate) instead of the 261 // token-gated OUTFLOW leaf - freeing the unique issuer token to sit at a reserve's bad-debt 262 // input (input 4) and drain it while this arm authors nothing. Forbidding it forces OUTFLOW. 263 assert!(jet::lt_64(0, principal)); 264 265 // recurse the token to the issuer's advanced address (output 4). The reserve now sits at 266 // output 3 (the borrow-fee inflow, below), so the token moved from output 3 to output 4. 267 recurse_token(open_height, 4); 268 269 // the pot releases exactly `principal`. Read it at current_index-1 (the input immediately 270 // BEFORE this issuer). pot_outflow token-gates the issuer at pot_index+1, so the releasing 271 // pot is always at (this issuer's index - 1); reading it THERE - not at a hardcoded index - 272 // binds this amount pin to the SAME pot pot_outflow released, closing the decoy-pot drain (a 273 // second pot at another index spent via outflow while the issuer pinned a decoy). output 1 274 // is the pot successor (pot_outflow recurses there). 275 let (uf_pi, pot_idx): (bool, u32) = jet::subtract_32(jet::current_index(), 1); 276 assert!(jet::eq_8(jet::left_pad_low_1_8(<bool>::into(uf_pi)), 0)); // issuer sits after the pot (index >= 1) 277 assert!(jet::eq_256(param::POT_SPK, unwrap(jet::input_script_hash(pot_idx)))); 278 assert!(jet::eq_256(param::OBOL_ID, explicit_input_asset(pot_idx))); // pot input is OBOL (explicit, not via conservation) 279 assert!(jet::eq_256(param::POT_SPK, unwrap(jet::output_script_hash(1)))); 280 assert!(jet::eq_256(param::OBOL_ID, explicit_output_asset(1))); 281 let pot_in: u64 = explicit_input_amount(pot_idx); 282 let pot_out: u64 = explicit_output_amount(1); 283 let (uf, expected_pot): (bool, u64) = jet::subtract_64(pot_in, principal); 284 assert!(jet::eq_8(jet::left_pad_low_1_8(<bool>::into(uf)), 0)); 285 assert!(jet::eq_64(pot_out, expected_pot)); 286 287 // the borrower (output 2) receives exactly `principal` OBOL. 288 assert!(jet::eq_256(param::OBOL_ID, explicit_output_asset(2))); 289 assert!(jet::eq_64(explicit_output_amount(2), principal)); 290 291 // the borrow fee funds the reserve: the reserve is co-spent at input 3 (its grow-only 292 // accumulate arm, current_index == 3, recursing to output 3) and grows by EXACTLY fee_sats. 293 // input 3 / output 3 are the constant STABILITY_SPK, in L-BTC. The reserve accumulate arm 294 // self-defends grow-only; the issuer pins the exact amount (else the borrower could pay 0). 295 assert!(jet::eq_256(param::STABILITY_SPK, unwrap(jet::input_script_hash(3)))); 296 assert!(jet::eq_256(param::POLICY, explicit_input_asset(3))); 297 assert!(jet::eq_256(param::STABILITY_SPK, unwrap(jet::output_script_hash(3)))); 298 assert!(jet::eq_256(param::POLICY, explicit_output_asset(3))); 299 let resv_in: u64 = explicit_input_amount(3); 300 let (of_f, resv_exp): (bool, u64) = jet::add_64(resv_in, fee_sats); 301 assert!(jet::eq_8(jet::left_pad_low_1_8(<bool>::into(of_f)), 0)); 302 assert!(jet::eq_64(explicit_output_amount(3), resv_exp)); 303 304 // the new vault (output 0) sits at the address committing to (debt, owner, open_height), 305 // in L-BTC, and the collateral covers the full debt at 150% at the LOWEST oracle quote. 306 // open_height >= the anchor keeps the new vault's own ratchet healthy too. 307 assert!(jet::eq_256(vault_spk_hash_for(debt, owner, open_height), unwrap(jet::output_script_hash(0)))); 308 assert!(jet::eq_256(param::POLICY, explicit_output_asset(0))); 309 let coll_sats: u64 = explicit_output_amount(0); 310 let debt_cents: u32 = jet::rightmost_64_32(debt); 311 assert!(jet::le_64(required_sats(debt_cents, price), coll_sats)); 312 } 313 Right(rest: Either<(u64, (u256, (u32, (u64, u32)))), Either<(u32, (u32, [Either<(), (u32, Signature)>; 5])), (u64, (u256, (u32, (u32, (u32, [Either<(), (u32, Signature)>; 5])))))>>) => { 314 match rest { 315 Left(draw: (u64, (u256, (u32, (u64, u32))))) => { 316 // DRAW: authorize OBOL outflow for an owner borrowing more against an existing 317 // vault. Input 0 must be the genuine OLD vault (so a real vault's DRAW branch 318 // pins the pot outflow, the new debt and 150% health); output 0 is its successor. 319 let (old_debt, d1): (u64, (u256, (u32, (u64, u32)))) = draw; 320 let (owner, d2): (u256, (u32, (u64, u32))) = d1; 321 let (old_last_height, d3): (u32, (u64, u32)) = d2; 322 let (new_debt, draw_height): (u64, u32) = d3; 323 324 // input 0 is the genuine vault at (old_debt, owner, old_last_height). 325 assert!(jet::eq_256(vault_spk_hash_for(old_debt, owner, old_last_height), unwrap(jet::input_script_hash(0)))); 326 327 // output 0 is the vault successor at (new_debt, owner, draw_height). The vault's 328 // DRAW branch enforces output 0's last_height == the tick height it verified and 329 // priced, so binding output 0 here makes `draw_height` trustworthy without 330 // re-running the quorum in the issuer. 331 assert!(jet::eq_256(vault_spk_hash_for(new_debt, owner, draw_height), unwrap(jet::output_script_hash(0)))); 332 assert!(jet::le_32(last_mint_height, draw_height)); // global mint floor (non-strict) 333 334 // the released OBOL leaves the GENUINE pot (output 1 == POT_SPK, OBOL). Read the pot 335 // at current_index-1 (the input immediately BEFORE this issuer). pot_outflow 336 // token-gates the issuer at pot_index+1, so the releasing pot is at (this issuer's 337 // index - 1); reading it THERE binds this amount pin to the SAME pot pot_outflow 338 // released (closing the decoy-pot drain), AND in a bad-debt layout (issuer at input 339 // 4) it makes this arm read input 3 = the RESERVE (not POT_SPK) and reject - so a 340 // DRAW cannot masquerade as the reserve authorizer; only ATTEST can sit at input 4. 341 assert!(jet::eq_256(param::POT_SPK, unwrap(jet::output_script_hash(1)))); 342 assert!(jet::eq_256(param::OBOL_ID, explicit_output_asset(1))); 343 let (uf_pi, pot_idx): (bool, u32) = jet::subtract_32(jet::current_index(), 1); 344 assert!(jet::eq_8(jet::left_pad_low_1_8(<bool>::into(uf_pi)), 0)); // issuer sits after the pot 345 assert!(jet::eq_256(param::POT_SPK, unwrap(jet::input_script_hash(pot_idx)))); 346 assert!(jet::eq_256(param::OBOL_ID, explicit_input_asset(pot_idx))); // pot input is OBOL (explicit, not via conservation) 347 348 // Pin the pot outflow amount to EXACTLY d = new_debt - old_debt, and require d > 0 (a 349 // draw MUST move OBOL, so it uses the token-gated OUTFLOW leaf; a d==0 draw would ride 350 // the grow-only INFLOW leaf and free the token to drain a reserve). Releasing d > 0 351 // forces output 0 == vault(old_debt + d), which ONLY the vault's own DRAW branch 352 // produces (and DRAW enforces 150% health) - REPAY/REFRESH/etc. cannot match it. 353 let (uf_d, d): (bool, u64) = jet::subtract_64(new_debt, old_debt); 354 assert!(jet::eq_8(jet::left_pad_low_1_8(<bool>::into(uf_d)), 0)); // new_debt >= old_debt 355 assert!(jet::lt_64(0, d)); // a draw grows debt (d > 0) 356 let pot_in: u64 = explicit_input_amount(pot_idx); 357 let pot_out: u64 = explicit_output_amount(1); 358 let (uf_p, expect_pot): (bool, u64) = jet::subtract_64(pot_in, d); 359 assert!(jet::eq_8(jet::left_pad_low_1_8(<bool>::into(uf_p)), 0)); 360 assert!(jet::eq_64(pot_out, expect_pot)); 361 362 // recurse the token to the issuer's advanced address (output 3). 363 recurse_token(draw_height, 3); 364 } 365 Right(poke_or_attest: Either<(u32, (u32, [Either<(), (u32, Signature)>; 5])), (u64, (u256, (u32, (u32, (u32, [Either<(), (u32, Signature)>; 5])))))>) => { 366 match poke_or_attest { 367 Left(poke: (u32, (u32, [Either<(), (u32, Signature)>; 5]))) => { 368 // POKE (permissionless): advance the anchor to a fresher tick with no token 369 // movement, so the mint floor tracks the tip ahead of OPEN/DRAW. The tx fee 370 // comes from a separate coin; the token recurses to output 0. 371 // 372 // SECURITY: poke is PINNED to input 0 (current_index == 0). Any value 373 // covenant that gates on "the issuer token is co-spent" (the pot's outflow 374 // leaf wants it at pot_index+1 >= 1; the reserve's bad-debt arm wants it at 375 // a fixed index >= 1) can never be satisfied by a poke, because the unique 376 // token then sits at input 0. Without this pin a poke could be smuggled in 377 // to authorize a pot release (or a reserve shrink) without pinning its 378 // amount - a drain. So poke is forced to be a standalone, value-neutral op. 379 let (height, (backing_k, quotes)): (u32, (u32, [Either<(), (u32, Signature)>; 5])) = poke; 380 let (_lo, _hi): (u32, u32) = verify_quorum(height, backing_k, quotes); 381 assert!(jet::le_32(last_mint_height, height)); // never move the anchor backward 382 assert!(jet::eq_32(jet::current_index(), 0)); // standalone: token at input 0 383 recurse_token(height, 0); 384 } 385 Right(attest: (u64, (u256, (u32, (u32, (u32, [Either<(), (u32, Signature)>; 5])))))) => { 386 // BAD-DEBT-ATTEST (closes audit finding B). The stability reserve cannot 387 // reconstruct a vault (a CMR cycle: V already reconstructs S), so it cannot 388 // verify on its own that a bad-debt release is backed by a genuine underwater 389 // vault. The issuer is the only covenant compiled after V (so it CAN 390 // reconstruct vaults) that the reserve can recognize by a flat id (its 391 // token). This arm attests the bad-debt and AUTHORS the reserve shrink; the 392 // reserve's bad-debt arm gates on the issuer token and defers the amount. 393 // 394 // Tx layout: inputs [vault(0), pot(1), keeper OBOL(2), reserve(3), issuer(this,4), fee(5)]; 395 // outputs [keeper coll+shortfall(0, L-BTC), pot+debt(1, OBOL), 396 // reserve successor(2, L-BTC), issuer successor(3, token), changes, fee]. 397 let (debt, a1): (u64, (u256, (u32, (u32, (u32, [Either<(), (u32, Signature)>; 5]))))) = attest; 398 let (owner, a2): (u256, (u32, (u32, (u32, [Either<(), (u32, Signature)>; 5])))) = a1; 399 let (lh, tick): (u32, (u32, (u32, [Either<(), (u32, Signature)>; 5]))) = a2; 400 let (height, (backing_k, quotes)): (u32, (u32, [Either<(), (u32, Signature)>; 5])) = tick; 401 402 // 0. the issuer sits at input 4 (defense in depth), mirroring POKE's input-0 pin. 403 // The tx layout fixes vault@0, pot@1, keeper@2, reserve@3, issuer@4; the reserve's 404 // bad-debt arm already requires the token at input 4 (stability.simf), so this is 405 // forced today via token uniqueness - but pinning it locally means ATTEST does not 406 // rely on a sibling covenant to know its own position (matters for an immutable 407 // artifact). It also makes every index this arm reads (input 0/1/3) absolute. 408 assert!(jet::eq_32(jet::current_index(), 4)); 409 410 // 1. input 0 is a GENUINE vault at (debt, owner, lh). This is the genuineness 411 // gate the reserve cannot do itself: input 0 MUST be a real vault, so its 412 // own bad-debt branch runs (enforcing CR<100%, the fresh tick, and burning 413 // the full `debt` OBOL into the genuine pot). `debt` is thereby the REAL 414 // committed debt (the reconstruction binds it). 415 assert!(jet::eq_256(vault_spk_hash_for(debt, owner, lh), unwrap(jet::input_script_hash(0)))); 416 417 // 2. price the shortfall at the MAX quote of a fresh tick. Recency for the 418 // reserve release now rests SOLELY on the global mint anchor: le_32( 419 // last_mint_height, height). The anchor is monotone and advanced by every 420 // mint / poke / attest, so it subsumes the reserve's old per-reserve ratchet 421 // (which is why the reserve's last_height data leaf could be removed). This 422 // also never moves the anchor backward: a bad-debted vault's own last_height 423 // can sit below the anchor, so le_32 stops an attest resetting it (finding A). 424 let (_lo, hi): (u32, u32) = verify_quorum(height, backing_k, quotes); 425 assert!(jet::le_32(last_mint_height, height)); // global recency + anchor only advances 426 assert!(jet::lt_64(debt, 4294967296)); // debt fits u32 cents so rightmost_64_32 is lossless (explicit, not emergent) 427 let debt_cents: u32 = jet::rightmost_64_32(debt); 428 let debt_sats: u64 = coll_at_cr(debt_cents, hi, 200000000); // debt valued in sats 429 assert!(jet::eq_256(param::POLICY, explicit_input_asset(0))); // collateral is L-BTC (not a junk asset) 430 let coll: u64 = explicit_input_amount(0); // the vault's collateral 431 assert!(jet::lt_64(coll, debt_sats)); // CR < 100% (a real deficit) 432 let (uf_s, shortfall): (bool, u64) = jet::subtract_64(debt_sats, coll); 433 assert!(jet::eq_8(jet::left_pad_low_1_8(<bool>::into(uf_s)), 0)); 434 435 // read the reserve identity + balance EARLY: both the keeper seizure and the shrink 436 // are now capped by what the reserve holds (partial bad-debt, E-2). 437 assert!(jet::eq_256(param::STABILITY_SPK, unwrap(jet::input_script_hash(3)))); 438 assert!(jet::eq_256(param::POLICY, explicit_input_asset(3))); // reserve input is L-BTC 439 let stab_in: u64 = explicit_input_amount(3); 440 441 // BAD-DEBT BOUNTY (E-4) + PARTIAL RESERVE (E-2). The keeper is owed shortfall + a 442 // 5%-of-debt bounty at the MAX quote. The reserve pays what it CAN: 443 // reserve_pay = min(shortfall + bounty, stab_in). 444 // This turns an under-funded reserve from a HARD CLIFF (the old subtract_64 underflow 445 // made the bad-debt tx UNCONSTRUCTIBLE -> the underwater vault froze, its OBOL stayed 446 // unbacked, and every later bad vault was stranded) into a GRACEFUL DRAIN: the reserve 447 // empties, EVERY bad vault still closes (full debt burned, OBOL retired), and the 448 // residual is an explicit accounting deficit surfaced as a soft de-peg (the honest 449 // terminal state of a fixed-supply claim). The full-debt burn (3b) is UNCHANGED, so the 450 // force-close / finding-B pin still holds regardless of how little the reserve paid. 451 let bounty: u64 = coll_at_cr(debt_cents, hi, 10000000); // 5% of debt at MAX 452 let (of_z, full_pay): (bool, u64) = jet::add_64(shortfall, bounty); 453 assert!(jet::eq_8(jet::left_pad_low_1_8(<bool>::into(of_z)), 0)); 454 // M-1 PER-VAULT EXPOSURE CAP: the reserve covers at most 20% of the debt (k = 455 // 20% * 2e6) for any single vault. A genuine single-block gap past the 130% 456 // trigger lands a vault around CR 85% (shortfall ~15%), so 20% covers it with 457 // margin. The cap moves the keeper's break-even from CR 100% to CR 80%: bad-debt 458 // profit = reserve_pay - shortfall, so once shortfall >= 20% of debt (CR <= 80%) 459 // no rational keeper calls this arm. A genuine FAST gap that skips the 460 // 130/115/100 windows and lands below CR 80% is therefore left un-force-closed 461 // and surfaces as the accepted soft de-peg (its collateral is still redeemable 462 // via REDEEM, other vaults unaffected) - size the reserve against the CR-80% 463 // break-even, not CR 100%. But a CONJURED vault - funded directly at the vault address with dust 464 // collateral and NO mint (a vault UTXO is byte-identical whether it minted or 465 // not; provenance is not observable in this tx) - is CR<100% by construction, so 466 // its shortfall is ~100% of debt and the uncapped path would release 467 // reserve_pay ~= 1.05*debt_sats for FREE, draining the whole reserve at a +5% 468 // profit. Capping the per-vault draw makes that strictly loss-making (burn ~100% 469 // of debt in OBOL, recover <= 20% from the reserve) without a mint-provenance 470 // proof. Deeper GENUINE gaps fall to the soft-de-peg tail (v1-RESERVE-BACKSTOP). 471 let cap: u64 = coll_at_cr(debt_cents, hi, 40000000); // 20% of debt at MAX 472 let capped: u64 = jet::min_64(full_pay, cap); // bound the per-vault draw 473 let reserve_pay: u64 = jet::min_64(capped, stab_in); // then never > the balance 474 let (of_s, seizure): (bool, u64) = jet::add_64(coll, reserve_pay); // keeper gets coll + what the reserve paid 475 assert!(jet::eq_8(jet::left_pad_low_1_8(<bool>::into(of_s)), 0)); 476 477 // 3. Pin the keeper's seizure: output 0 = coll + reserve_pay, L-BTC. When the reserve is 478 // full this is debt_sats + bounty; when partial it is smaller (the keeper eats the 479 // uncovered slice). The FULL-debt burn (3b) is what forces the vault to close. 480 assert!(jet::eq_256(param::POLICY, explicit_output_asset(0))); 481 assert!(jet::eq_64(explicit_output_amount(0), seizure)); 482 483 // 3b. FORCE a genuine full repayment: the pot (input/output 1) must grow by the 484 // FULL `debt` OBOL. This is the real close condition - NOT the output-0 amount 485 // alone. `recurse_to` pins only output 0's script + asset, never its amount, 486 // and the REPAY branch merely forbids SHRINKING the collateral, so an 487 // underwater owner could co-spend the vault via REPAY(r=1): burn ~nothing, 488 // recurse to a vault successor holding exactly debt_sats (which GREW the 489 // collateral, so REPAY accepts), and still collect the reserve's shortfall - 490 // recapitalizing the bad position for free (reopens audit finding B). Pinning 491 // pot_out == pot_in + debt forces the whole debt to be retired into the 492 // genuine pot regardless of which vault branch runs, so the shortfall payout 493 // is always matched by an equal OBOL burn (a bad-debt is then value-neutral 494 // at peg, the reserve only absorbs the genuine deficit). 495 assert!(jet::eq_256(param::POT_SPK, unwrap(jet::input_script_hash(1)))); 496 // pin input 1's asset locally (defense in depth): the pot-growth math below reads 497 // explicit_input_amount(1), so the input asset must be OBOL. This holds today 498 // via the co-spent reserve arm (stability.simf pins input1==OBOL) and the vault's 499 // pot_grows_by, but an immutable covenant should not rely on a sibling for a value 500 // it reads itself - mirror the local input-asset pins at OPEN (input pot) / DRAW. 501 assert!(jet::eq_256(param::OBOL_ID, explicit_input_asset(1))); 502 assert!(jet::eq_256(param::POT_SPK, unwrap(jet::output_script_hash(1)))); 503 assert!(jet::eq_256(param::OBOL_ID, explicit_output_asset(1))); 504 let pot_in: u64 = explicit_input_amount(1); 505 let pot_out: u64 = explicit_output_amount(1); 506 let (uf_pb, expect_pot): (bool, u64) = jet::add_64(pot_in, debt); 507 assert!(jet::eq_8(jet::left_pad_low_1_8(<bool>::into(uf_pb)), 0)); 508 assert!(jet::eq_64(pot_out, expect_pot)); 509 510 // 4. pin the reserve shrink (ISSUER -> S, flat): output 2 is the reserve successor at 511 // the constant STABILITY_SPK, shrunk by exactly reserve_pay. reserve_pay <= stab_in 512 // by the min above, so subtract_64 never underflows - the reserve drains to (at worst) 513 // zero instead of reverting the whole tx. 514 assert!(jet::eq_256(param::STABILITY_SPK, unwrap(jet::output_script_hash(2)))); 515 assert!(jet::eq_256(param::POLICY, explicit_output_asset(2))); 516 let (uf_o, stab_out): (bool, u64) = jet::subtract_64(stab_in, reserve_pay); 517 assert!(jet::eq_8(jet::left_pad_low_1_8(<bool>::into(uf_o)), 0)); // reserve_pay <= stab_in (holds by min_64) 518 assert!(jet::eq_64(explicit_output_amount(2), stab_out)); 519 520 // 5. recurse the token to output 3, advancing last_mint_height to `height` 521 // (a bad-debt carries a fresh tick, so it harmlessly doubles as a poke). 522 recurse_token(height, 3); 523 } 524 } 525 } 526 } 527 } 528 } 529}

stability.simf the reserve

Constant-address L-BTC backstop. Accumulate (grow-only) and issuer-attested bad-debt shrink.

1/* 2 * STYX v1 - stability reserve covenant (constant-address, issuer-anchored recency). 3 * 4 * The two ops dispatch on a `match` (Simplicity `case` nodes), so the program MUST PRUNE 5 * against the spend env (satisfy_with_env) before encoding, else Elements' Anti-DOS 6 * CHECK_CASE rejects the unpruned program. 7 * 8 * A single covenant holding L-BTC (POLICY) at a CONSTANT taproot address (a single Simplicity leaf, 9 * no data leaf), so every peer can pin it by a flat scriptHash - exactly like the OBOL pot. It has 10 * TWO ops: 11 * accumulate = Left(()) the liquidate / redeem 5% / 0.5% fee inflow, grow-only, recurse to self 12 * bad-debt = Right(()) release the issuer-authored shortfall (CR < 100%), recurse to self 13 * 14 * WHY NO PER-RESERVE RECENCY RATCHET (the old `last_height` data leaf is gone): bad-debt is 15 * ISSUER-ATTESTED (audit finding B). The reserve cannot reconstruct a vault (a CMR cycle, since V 16 * already reconstructs S), so it GATES on the issuer's unique token at input 4 and DEFERS the shrink 17 * amount to the issuer's BAD-DEBT-ATTEST arm, which reconstructs the genuine underwater vault, sizes 18 * the shortfall at the MAX quote, and authors output 2. The issuer arm ALSO carries the recency: it 19 * verifies the tick and gates `le_32(last_mint_height, height)` against its GLOBAL mint anchor 20 * (advanced on every open / draw / poke / attest). That global anchor subsumes the reserve's old 21 * per-reserve ratchet (it is advanced by all mint traffic, so it is weakly fresher, and monotone), so 22 * a stale-tick bad-debt drain is blocked by the SAME keeper-poke-liveness model - now keepers poke the 23 * ISSUER (via its poke) rather than the reserve. Making the address constant also closes the fee 24 * griefing where a keeper routed the 5% / 0.5% fee into a sybil reserve UTXO at an arbitrary (even 25 * u32::MAX, bad-debt-proof) last_height: there is now exactly one reserve scriptHash. 26 * 27 * The fee inflow needs no freshness of its own: it is grow-only, and the 5% / 0.5% AMOUNT is priced 28 * by the VAULT's liquidate / redeem branch under the VAULT's own last_height ratchet. 29 * 30 * Params: POT_SPK (the genuine pot's scriptHash - bad-debt requires the repaid debt to land there), 31 * ISSUER_TOKEN_ID (the bad-debt issuer-attest gate, a flat asset id), OBOL_ID, POLICY. Witness: OP. 32 */ 33fn explicit_output_amount(idx: u32) -> u64 { 34 let pair: (Asset1, Amount1) = unwrap(jet::output_amount(idx)); 35 let (_asset, amount): (Asset1, Amount1) = pair; 36 unwrap_right::<(u1, u256)>(amount) 37} 38fn explicit_output_asset(idx: u32) -> u256 { 39 let pair: (Asset1, Amount1) = unwrap(jet::output_amount(idx)); 40 let (asset, _amount): (Asset1, Amount1) = pair; 41 unwrap_right::<(u1, u256)>(asset) 42} 43fn explicit_input_asset(idx: u32) -> u256 { 44 let pair: (Asset1, Amount1) = unwrap(jet::input_amount(idx)); 45 let (asset, _amount): (Asset1, Amount1) = pair; 46 unwrap_right::<(u1, u256)>(asset) 47} 48fn explicit_input_amount(idx: u32) -> u64 { 49 let pair: (Asset1, Amount1) = unwrap(jet::input_amount(idx)); 50 let (_asset, amount): (Asset1, Amount1) = pair; 51 unwrap_right::<(u1, u256)>(amount) 52} 53fn current_balance() -> u64 { 54 let (_c_asset, c_amount): (Asset1, Amount1) = jet::current_amount(); 55 unwrap_right::<(u1, u256)>(c_amount) 56} 57 58fn main() { 59 // this reserve input holds L-BTC (explicit local check - both arms read current_balance by amount 60 // and peers pin this input by index, so making the asset explicit here removes the reliance on 61 // per-asset conservation to catch a non-POLICY UTXO funded at the public STABILITY_SPK address). 62 let (c_asset, _c_amount): (Asset1, Amount1) = jet::current_amount(); 63 assert!(jet::eq_256(param::POLICY, unwrap_right::<(u1, u256)>(c_asset))); 64 65 // OP = accumulate | bad-debt. The address is constant (no data leaf), so the reserve recurses to 66 // its OWN scriptHash (jet::current_script_hash) on both arms - no self-reconstruction needed. 67 let op: Either<(), ()> = witness::OP; 68 match op { 69 Left(_accumulate: ()) => { 70 // ACCUMULATE: the liquidate / redeem fee inflow. Grow-only, recurse to output 3 at the 71 // SAME (constant) address; the exact 5% / 0.5% share is pinned by the vault. The reserve 72 // sits at input 3 on both fee paths, so pin current_index == 3 (H4) so a second 73 // same-address input cannot alias the single output-3 recursion. 74 assert!(jet::eq_32(jet::current_index(), 3)); 75 assert!(jet::eq_256(jet::current_script_hash(), unwrap(jet::output_script_hash(3)))); 76 assert!(jet::eq_256(param::POLICY, explicit_output_asset(3))); 77 assert!(jet::le_64(current_balance(), explicit_output_amount(3))); // grow-only 78 } 79 Right(_baddebt: ()) => { 80 // BAD-DEBT (issuer-attested; closes audit finding B). The reserve GATES on the issuer's 81 // unique token being co-spent (input 4) and DEFERS the shrink amount to the issuer's 82 // ATTEST arm, which reconstructs the genuine underwater vault, sizes the shortfall, and 83 // authors output 2. Here the reserve only self-defends: a fixed input index, the POT_SPK 84 // pins (defense in depth), own-address recursion, and a downward balance check. Recency 85 // lives on the issuer's global mint anchor (le_32(last_mint_height, height)). 86 // Tx layout: inputs [vault(0), pot(1), keeper OBOL(2), reserve(this, 3), issuer(4), fee(5)]. 87 88 // the reserve sits at a FIXED input index, so two reserve inputs cannot alias one shrink 89 // output (the latent aliasing gap the audit flagged on the reserve arms). 90 assert!(jet::eq_32(jet::current_index(), 3)); 91 92 // the issuer token MUST be co-spent at input 4. Together with the pot-GROWTH check below 93 // this forces the issuer's ATTEST arm into the tx: poke is pinned to input 0 (cannot sit at 94 // 4); open/draw require the pot to move OBOL out (principal/d > 0), and their pot pin reads 95 // current_index-1, so at input 4 they would read input 3 = THIS reserve (not POT_SPK) and 96 // reject. Only ATTEST reconstructs the genuine vault, GROWS the pot by the full debt, and 97 // authors this reserve's output-2 amount. 98 assert!(jet::eq_256(param::ISSUER_TOKEN_ID, explicit_input_asset(4))); 99 100 // the repaid debt is genuinely burned into the real pot (input1/output1 == POT_SPK + OBOL), 101 // and the pot must STRICTLY GROW. Among the issuer arms only ATTEST grows the pot (open/draw 102 // shrink or keep it, poke never touches it), so requiring growth independently forces ATTEST 103 // and rejects a non-authoring issuer arm (e.g. a d==0 draw) that leaves the pot unchanged - 104 // an amount-unpinned reserve drain. This does not rely on the index-adjacency argument. 105 assert!(jet::eq_256(param::POT_SPK, unwrap(jet::input_script_hash(1)))); 106 assert!(jet::eq_256(param::POT_SPK, unwrap(jet::output_script_hash(1)))); 107 assert!(jet::eq_256(param::OBOL_ID, explicit_input_asset(1))); 108 assert!(jet::eq_256(param::OBOL_ID, explicit_output_asset(1))); 109 assert!(jet::lt_64(explicit_input_amount(1), explicit_output_amount(1))); // pot strictly grows (only ATTEST does) 110 111 // recurse to output 2 at the SAME (constant) address; the EXACT shrink is authored by the 112 // issuer. Here we only self-defend that the balance does not grow. 113 assert!(jet::eq_256(jet::current_script_hash(), unwrap(jet::output_script_hash(2)))); 114 assert!(jet::eq_256(param::POLICY, explicit_output_asset(2))); 115 assert!(jet::le_64(explicit_output_amount(2), current_balance())); // downward self-defense 116 } 117 } 118}