Security model
A covenant protocol cannot patch. The security argument therefore has to be made before the freeze, adversarially, and in writing. This chapter states the trust model, shows how each fatal failure class is closed, records the audit trail that got it there, and names the residual risks honestly.
1The trust model
- Oracles: at most 2 of the 5 keys Byzantine. Under that bound no under-backed mint and no cheap seizure is possible; two colluding keys can censor (a liveness loss), not extract.
- The deployer: sets the genesis configuration once - five pairwise-distinct oracle keys, the canonical NUMS, and every cross-covenant pin equal to its genuine compiled sibling. This is unfixable after deployment and not checkable from inside a covenant; it is guarded by a preflight (chapter Θʹ) and is the single most important thing to verify before shipping.
- Keepers: someone runs the liveness duties - POKE, REFRESH, and clearing the deep-tail bad debt. All are permissionless; none require trust in a specific party.
- Everything else is untrusted. Owners, keepers, redeemers, and arbitrary third parties are assumed adversarial, including in coalition.
2The five fatal classes
The audits organize covenant failure into five classes; the re-audit verdict is that all five are closed at the covenant level under the trust model above.
| Class | Attack shape | Closed by |
|---|---|---|
| Covenant bypass | move value without running the intended arm: key-path spend, smuggled second arm, aliased covenant instances, wrong arm behind a token gate | NUMS unspendable key path; the one-unit token recursed on every issuer arm;
current_index self-pins on every covenant (vault 0, pot inflow 1, reserve 3,
poke 0, attest 4); relative pot reads at current_index - 1 |
| Data-identity substitution | feed a lookalike UTXO (decoy pot, sybil reserve, wrong-asset coin) or a swapped state field into a covenant that reads a sibling | constant addresses pinned by flat scriptHash; script and asset and amount pinned on every value-carrying output; state bound into reconstructed addresses, so a wrong field yields a wrong address |
| Collateral theft | extract more than a rule permits: over-liquidate, seize a healthy vault, redeem par from an under-backed pool, skim on a no-oracle path, or lie about the price | directional MIN / MAX quotes; hard extraction caps with protocol-favoring truncation; the REPAY collateral-preservation assert; the strict per-vault ratchet plus permissionless REFRESH; the redemption backing floor |
| Bad-debt creation | manufacture a shortfall the reserve must cover: conjure a vault, fake the pot growth, or recapitalize an underwater vault from the reserve | issuer-attested bad debt (the genuineness check the reserve cannot make); the pinned full-debt burn; strict pot growth forcing the ATTEST arm; the 20% per-vault cap making conjured vaults a 4:1 self-loss |
| Griefing / halt | brick the mechanism: poison a ratchet, strand the reserve, block bad-debt clearing | ratchets only advance to quorum-verified heights and REFRESH demands health; the saturating reserve drain means every bad vault always closes; far-future heights are unsatisfiable by the lock-height jet; conjured out-of-range vaults self-grief only |
3The singletons cannot be burned
The pot, the issuer, and the reserve are single UTXOs that most operations must spend - a natural worry is whether a griefer can destroy one (spend it without recreating it) and brick the protocol. They cannot. Two locks close every spend path:
- No key path. Every singleton's taproot internal key is the NUMS point, so the only way to spend it is a covenant leaf. A "NUMS" with a known discrete log would break this - which is exactly why the deploy preflight asserts the canonical value (chapter Θʹ).
- Every arm recreates the singleton. There is no destroy arm and no arm with a missing recursion pin - each one requires the UTXO to reappear at its own address:
| Singleton | Arms | Recursion pin |
|---|---|---|
| Issuer | OPEN, DRAW, POKE, ATTEST | every arm ends in recurse_token: successor pinned to the issuer address,
asset == ISSUER_TOKEN_ID, amount == 1
issuer.simf:215-222 |
| Pot | inflow, outflow | both leaves pin current_script_hash == output_script_hash(1); inflow is
grow-only, outflow releases only the issuer-pinned amount
reserve_repay.simf:36, pot_outflow.simf:42 |
| Reserve | accumulate, bad-debt | accumulate recurses to output 3 (grow-only), bad-debt to output 2 with a downward-only self-defense; the shrink is authored by ATTEST stability.simf:75-115 |
So a singleton can only ever reappear at its own address, holding more (or, for the pot and
reserve, exactly the covenant-pinned delta). It cannot leave the covenant, split, or vanish. The
token additionally cannot duplicate - amount == 1 makes single-unit uniqueness an
invariant.
What a griefer can do falls short of destruction:
- Contend - race for a singleton's current outpoint. This slows honest users but destroys nothing; it is the throughput question, treated in chapter Ιʹ.
- Fragment - donate an extra UTXO to
POT_SPKorSTABILITY_SPK. The canonical singleton is untouched; the covenant cannot merge fragments, so the donated value simply strands - the griefer burns their own funds, not the protocol's. Honest builders track the canonical outpoint and ignore the rest (the decoy-pot attack is separately closed by the relativecurrent_index - 1read). - Poison an anchor - closed: a ratchet only advances to a quorum-verified height, the grow-only accumulate arm never advances it, and a far-future height is unsatisfiable (chapter Εʹ).
The un-burnability rests on the NUMS assumption and on deploy-config correctness, both preflight-checked. One economic edge is not griefing: if every unit of collateral is borrowed against, the pot can reach a zero-OBOL balance - the UTXO still exists, it is simply empty.
4The audit trail
Findings A and B - the internal exhaustive audit
A (stale-high mint). The mint gates had no upper staleness bound: a signed tick is
valid forever, and after a price fall an attacker could replay an old genuinely-signed high
tick to mint OBOL backed below 150% of the true price, then feed the under-backed vault to the
reserve. A was the motor, B the amplifier - one pump. Fixed by the global
last_mint_height anchor on the issuer with permissionless POKE
(chapter Εʹ).
B (bad-debt reserve drain). The reserve's bad-debt arm self-authorized its shrink,
trusting an unbound input 0: an attacker could stage a fake "vault" from a dust coin and burn
self-owned OBOL to fake the debt - near-neutral at peg, a real theft of
(1 - p) x debt under a de-peg to p, which is exactly when the backstop matters.
Fixed by the issuer ATTEST arm (chapter ΣΤʹ).
The fresh-context audit - the delegated-amount class
A second audit round with fresh context (2026-07-02; four parallel reviewers with on-node proof-of-concepts, then converging passes) found the protocol architecturally sound and identified one real root cause the earlier rounds had missed: amount delegation to a swappable branch (chapter Βʹ). Seven findings - four critical, two high, one low:
- Critical. REPAY had no collateral-preservation gate: a repay of zero with a dust
successor withdrew the full collateral. The proof-of-concept was accepted by elementsd.
Fix:
le_64(coll, output_amount(0)). - Critical. REFRESH-as-DRAW pot drain: the issuer's DRAW delegated the pot delta to the vault's DRAW branch, but a REFRESH produced the same successor shape without shrinking the pot. Fix: the issuer pins the pot delta itself.
- High. ATTEST force-close bypass: an underwater owner could co-run REPAY, keep a
zero-debt vault, and still collect the shortfall. Fix: ATTEST pins
pot_out == pot_in + debt. - High. A zero price collapses
divide_64to zero and with it the health gate - a free mint. Fix:price > 0per counted quote. - Low. Fees divertible to a sybil reserve via the witness-chosen reserve ratchet. Fix: the constant-address reserve.
- Critical. Reserve drain via a zero-amount DRAW: a d = 0 draw rode the grow-only
inflow leaf, freeing the token to sit at input 4 authoring nothing. Fix:
d > 0,principal > 0, pot read atcurrent_index - 1, and strict pot growth in the reserve arm. - Critical. Pot drain via a decoy pot at the hardcoded index the issuer read. Fix:
read the pot at
current_index - 1, adjacent to the token.
All seven were fixed and gold-standarded: for each, the guard was temporarily disabled to confirm the proof-of-concept passes, then restored to confirm it fails - isolating each assert as the sole block. After the fixes, two further adversarial rounds found no new covenant break; the class is judged exhausted. The results are carried forward as 27 on-node negative tests, each rejected for exactly one controlled reason.
The maturity audit - M-1 and M-2
M-1 (conjured-vault drain, critical, mitigated). A vault UTXO is byte-identical whether minted or conjured; ATTEST proves "currently underwater", not "ever minted". An attacker could conjure debt = D against dust collateral and pull ~1.05 x D from the reserve. No hard close exists - provenance is unobservable per-transaction, and a global debt accumulator would serialize every operation through one UTXO. Mitigated by the 20% per-vault cap: recover at most 20% of what you burn.
M-2 (dormant-vault stale-tick replay, high, mitigated). A passive owner's ratchet lags the tip, so an archived dip tick could seize a currently healthy vault - theft, not the accepted crash risk. A hard close is impossible (no chain-tip jet; a global freshness anchor would have to be spent by every removal, serializing them). Mitigated by moving REFRESH to the permissionless side of the witness sum: any keeper keeps any healthy vault fresh, and a refresh can neither harm the vault nor dodge liquidation (the health gate sits at the same quantile as the liquidation trigger).
The re-audit
Two rounds, ten lenses in total - reserve arithmetic, oracle freshness, bypass and identity, value conservation, griefing, harness fidelity, regression review of the hygiene edits, whole-transaction composition, Elements primitive footguns, and a from-scratch conservation check - plus verification of the lock-height jet against its C source. Result: zero covenant-code holes; all five classes confirmed closed; six hygiene items (tests, docs, defense-in-depth asserts, the preflight) closed on the way. One methodological lesson is recorded for future audits: single-reviewer all-clears were twice wrong in this project - one lens misjudged the conjured vault, and the L-1 lock-height question was only settled by reading the jet's C source, not the covenant comment.
5Accepted residuals
These are model boundaries, stated as such - not bugs:
- An honest sharp crash between ticks. Real market movement faster than the oracle cadence can create bad debt. This is the risk the reserve exists for.
- Keeper liveness. POKE, REFRESH of dormant vaults, and the deep-tail clearing are duties, not automatisms. An un-refreshed dormant vault remains exposed to M-2 replay; the defense is decentralized, not absolute.
- Oracle honesty and sourcing. The 2-Byzantine bound, the trust in
backing_k, and the keeper's freedom to pick which three quotes to present (the spread is the surface - chapter Εʹ). - Reserve economics. A systemic gap deeper than the reserve ends in the graceful soft de-peg, by explicit design choice.
- The interest-free put (E-7) and redemption force-close (E-8), both accepted with rationale in chapter Ζʹ.
- Deploy-configuration correctness. Preflight-checked, but unfixable after genesis; the deployer is trusted once.