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}