From 1bdaf758119d15719988f5d70dbe80236c3ee933 Mon Sep 17 00:00:00 2001 From: Glory Matthew Date: Sat, 26 Sep 2026 22:49:24 +0100 Subject: [PATCH 1/2] docs: correct D5's mechanism per #74 review, cross-link D6 as the real residual risk MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Hillary's #74 approval flagged that D5's row, §7.2 and the archive header all said apply --mainnet "auto-selects" the plan file -- overstating the mechanism. She read clarinet 3.23.2's actual source (components/clarinet-cli/src/frontend/cli.rs, ApplyDeployment / load_deployment_if_exists) rather than trusting the naming convention. I re-verified the same source independently before writing anything down. Actual behavior: apply --mainnet always recomputes a plan from Clarinet.toml first and diffs it against the on-disk file, prompting Overwrite? [Y/n]. It falls back to the on-disk file SILENTLY only when that recomputation errors -- which happens on any checkout without settings/Mainnet.toml (gitignored, true of every fresh clone). That silent-fallback case, once all 13 paths resolved, is what #74 actually closed: one Enter from broadcast with no deployer key even present. It does NOT close the case where settings/Mainnet.toml exists (a real deploy machine) -- there clarinet always recomputes fresh from Clarinet.toml regardless of this file, so the archive changes nothing. The residual risk on that machine was never this YAML; it's whatever Clarinet.toml currently resolves the 14 funds-bearing contract names to, which today is the contracts/test/ localized copies -- D6, not D5. Corrected in three places: D5's table row, §7.2, and the archive file's own header comment. Added a cross-reference from D6 back to this finding, since D6 was previously framed only as a coverage gap and this makes explicit that it's also what a real mainnet deploy would publish today. No diff to the archived plan's content, no code change. Verified: suite 256/256, clarinet check 211/0, both unchanged; archive YAML still parses. Co-Authored-By: Claude Sonnet 5 --- .../archive/gen1-mainnet-plan-2026-09-22.yaml | 31 +++++++++++++------ docs/security/CONTRACT_INVENTORY.md | 25 +++++++++++---- 2 files changed, 40 insertions(+), 16 deletions(-) diff --git a/deployments/archive/gen1-mainnet-plan-2026-09-22.yaml b/deployments/archive/gen1-mainnet-plan-2026-09-22.yaml index a55bc69..1e45e70 100644 --- a/deployments/archive/gen1-mainnet-plan-2026-09-22.yaml +++ b/deployments/archive/gen1-mainnet-plan-2026-09-22.yaml @@ -6,16 +6,27 @@ # running it now would attempt to republish already-existing contract names # (which the chain would reject). # -# Moved here, out of deployments/, and renamed off "default.mainnet-plan.yaml" -# specifically to break `clarinet deployments apply --mainnet`'s no-flags -# auto-pickup convention. Until 2026-09-22 this file died early because two of -# its 13 paths (contracts/snp-flashstack-receiver.clar and -v3.clar) did not -# exist in the checkout — a missing-file error was accidentally doing safety -# work nobody designed for it. PR #70 committed those two files verbatim -# (closing D7), which made all 13 paths resolve and turned this into a -# genuinely runnable plan for the first time. It still needed a real mainnet -# deployer key and would still fail on-chain on duplicate names, but the -# "dies on a missing file" backstop was gone. See CONTRACT_INVENTORY.md D5. +# Moved here, out of deployments/, and renamed off "default.mainnet-plan.yaml". +# CORRECTED 2026-09-23, verified against clarinet 3.23.2's source rather than +# the filename convention alone: `apply --mainnet` does not blindly load this +# path -- it recomputes a plan from Clarinet.toml first and diffs it against +# the file, prompting Overwrite? [Y/n]. It falls back to this file SILENTLY +# only when that recomputation errors, which happens on any checkout without +# settings/Mainnet.toml (gitignored -- true of every fresh clone). Until +# 2026-09-22 this file died early anyway, because two of its 13 paths +# (contracts/snp-flashstack-receiver.clar and -v3.clar) did not exist in the +# checkout -- a missing-file error was accidentally doing safety work nobody +# designed for it. PR #70 committed those two files verbatim (closing D7), +# which made all 13 paths resolve, so the silent fallback became a real one +# `Enter` away from broadcast, with no deployer key even present. That is the +# case this archive closes: with the file gone, a missing plan now requires +# generating one from Clarinet.toml, which itself needs settings/Mainnet.toml +# and fails outright on a clean checkout. It does NOT close the case where +# settings/Mainnet.toml exists (a real deploy machine) -- there, clarinet +# always recomputes from Clarinet.toml regardless of this file, and the +# residual risk is what Clarinet.toml currently resolves the 14 funds-bearing +# contract names to (the contracts/test/ localized copies, D6), not this YAML. +# See CONTRACT_INVENTORY.md D5 and D6. # # Kept, not deleted, as the only record of what this key/era actually # published — nothing else in the repo documents it. diff --git a/docs/security/CONTRACT_INVENTORY.md b/docs/security/CONTRACT_INVENTORY.md index 5d6d062..9868ed9 100644 --- a/docs/security/CONTRACT_INVENTORY.md +++ b/docs/security/CONTRACT_INVENTORY.md @@ -108,8 +108,8 @@ Each is filed as a bead. None is silently resolved here. | D2 | README badge + Security section claim a **128**-test suite; ROADMAP claims **125**. Actual: **165 passing across 16 files** (`npm test`, 2026-09-15). | test run output | **FIXED 2026-09-16.** The D2 row itself had also gone stale: the true figure is now **176 passing across 17 files**, counted at runtime (`vitest --reporter=json`), not by grepping `it(` — several suites generate tests in a loop, so a static grep undercounts. README (badge, Security bullet, Quick start), ROADMAP (×2), `AUDIT_BRIEF.md` and `FLASH_LOAN_INVARIANT.md` all corrected. | `Flashstack-ajv.7.1` | | D3 | `docs/AUDIT_BRIEF.md` item 4 says v1 and v2 pools are "both listed as live in the README's mainnet contract table". No longer true — README now lists only v2 and calls v1 deprecated. My own brief is stale. | README §Mainnet contracts | **FIXED 2026-09-16.** `AUDIT_BRIEF.md` item 4 corrected in place, with a pointer to §3 and F-6 so the v1 surface stays visible to the auditor rather than disappearing. | `Flashstack-ajv.7.1` | | D4 | ROADMAP "Next" items 1 and 2 ask for the v2 pools and `flashstack-pool-oracle-v2` to be *deployed*. Both are already live at `SPR9PQAN…`. | Hiro contract API | **FIXED 2026-09-16.** Items 1 and 2 marked done with the live principal named. Item 3 (CI gating) deliberately left **unchecked**: the workflow exists (PR #44) but "required on every PR" is branch protection, which cannot be read from the repository — see §7. | `Flashstack-ajv.7.1` | -| D5 | `deployments/default.mainnet-plan.yaml` describes a gen-1 publish from `SP3TGRVG…`, and contains none of the live system. `/deployments/` is CODEOWNERS-protected, so it reads as authoritative. | file contents vs `find contracts` | **FIXED 2026-09-22 — archived.** After D7 closed (both SNP files committed verbatim), all 13 of this plan's paths resolved for the first time, turning a dead file into a genuinely runnable `clarinet deployments apply --mainnet` target — the filename `default.mainnet-plan.yaml` is the exact convention that flag auto-selects with no explicit path given. The "dies on a missing file" behavior had been doing safety work nobody designed for it; closing D7 removed it. Matt's disposition decision: **archive**, not delete — moved to `deployments/archive/gen1-mainnet-plan-2026-09-22.yaml` with a header explaining why, out of `deployments/` and off the auto-pickup filename, so the historical record of the gen-1 publish survives without being a live footgun. **Note:** `deployments/default.testnet-plan.yaml` has the identical all-paths-resolve shape (checked 2026-09-22) but is far lower-stakes — it targets the well-known public Clarinet deployer, a key nobody on this project holds, not a principal Matt controls. Left as-is; flagged here rather than acted on. | `Flashstack-ajv.7.2` | -| D6 | `clarinet check` covers 38 contracts; 70 `.clar` files exist. The 32 uncovered include `contracts/flashstack-pool-v3.clar` and the other v3/v2 successors — i.e. the audit/deploy targets are not type-checked by the CI gate. | `clarinet check` output vs `Clarinet.toml` registry | **PARTIALLY CLOSED (PR #48, 2026-09-18) — closes the receiver-library half, structural half still open.** PR #48 registered 20 previously-uncovered receiver contracts (17 land clean; 3 excluded with explicit comments for genuine `clarinet check` failures — arkadiko/usda read-only-vs-write mismatches, a zest-v2 type mismatch) plus the 46 external `requirements` those receivers call. FlashStack-owned registered contracts go **39 → 55**; the "209/210 contracts checked" figure some CI output shows is cache-state dependent and ~154 of it is third-party (ALEX/Zest/Arkadiko/Pyth/StackingDAO/…) — not a coverage metric for our own code, per Hillary's review on #48. **Structural half untouched:** all 14 funds-bearing contracts (both cores, all six pools, both oracles, `flashstack-pool-v3`, the v3 receiver trait) still resolve in `Clarinet.toml` to their `contracts/test/` localized copies, not the canonical `contracts/*.clar` an auditor would read and a deployment would publish. Clarinet keys contracts by name, so both can't be registered under one name — needs a second manifest/CI job or a restructure. See §7.3. `#53`'s drift guard is the current (weaker) substitute for real type-checking on the canonical sources. | `Flashstack-ajv.2.3` | +| D5 | `deployments/default.mainnet-plan.yaml` describes a gen-1 publish from `SP3TGRVG…`, and contains none of the live system. `/deployments/` is CODEOWNERS-protected, so it reads as authoritative. | file contents vs `find contracts` | **FIXED 2026-09-22 — archived.** After D7 closed (both SNP files committed verbatim), all 13 of this plan's paths resolved for the first time. **What that actually enabled, verified against clarinet 3.23.2's source** (`components/clarinet-cli/src/frontend/cli.rs`, `ApplyDeployment` / `load_deployment_if_exists` — *corrected 2026-09-23, this row previously said the flag "auto-selects" the file, which overstates it*): `apply --mainnet` recomputes a plan from `Clarinet.toml` and diffs it against this file when present, prompting `Overwrite? [Y/n]`; only if that recomputation **errors** does it fall back to the on-disk file silently — and it errors on any checkout without `settings/Mainnet.toml` (gitignored, so true of every fresh clone). With all 13 paths resolving, that fallback meant the stale gen-1 plan was one `Enter` away from broadcast on exactly that clean-checkout, no-deployer-key case. **This is the case #74 closes**: with the file gone, a missing plan now calls `generate_default_deployment`, which itself needs `settings/Mainnet.toml` and fails outright on a clean checkout. **It does not touch the case where `settings/Mainnet.toml` exists** — an operator's real deploy machine — where clarinet always recomputes fresh from `Clarinet.toml` regardless of this file. There, the risk was never this YAML; it's whatever `Clarinet.toml` currently resolves the 14 funds-bearing contract names to, which today is the `contracts/test/` localized copies (**D6**). Matt's disposition decision: **archive**, not delete — moved to `deployments/archive/gen1-mainnet-plan-2026-09-22.yaml` with a header explaining why, out of `deployments/` and off the default filename, so the historical record of the gen-1 publish survives without being live on the one path it could still reach. **Note:** `deployments/default.testnet-plan.yaml` has the identical all-paths-resolve shape (checked 2026-09-22) but is far lower-stakes — it targets the well-known public Clarinet deployer, a key nobody on this project holds, not a principal Matt controls. Left as-is; flagged here rather than acted on. | `Flashstack-ajv.7.2` | +| D6 | `clarinet check` covers 38 contracts; 70 `.clar` files exist. The 32 uncovered include `contracts/flashstack-pool-v3.clar` and the other v3/v2 successors — i.e. the audit/deploy targets are not type-checked by the CI gate. | `clarinet check` output vs `Clarinet.toml` registry | **PARTIALLY CLOSED (PR #48, 2026-09-18) — closes the receiver-library half, structural half still open.** PR #48 registered 20 previously-uncovered receiver contracts (17 land clean; 3 excluded with explicit comments for genuine `clarinet check` failures — arkadiko/usda read-only-vs-write mismatches, a zest-v2 type mismatch) plus the 46 external `requirements` those receivers call. FlashStack-owned registered contracts go **39 → 55**; the "209/210 contracts checked" figure some CI output shows is cache-state dependent and ~154 of it is third-party (ALEX/Zest/Arkadiko/Pyth/StackingDAO/…) — not a coverage metric for our own code, per Hillary's review on #48. **Structural half untouched:** all 14 funds-bearing contracts (both cores, all six pools, both oracles, `flashstack-pool-v3`, the v3 receiver trait) still resolve in `Clarinet.toml` to their `contracts/test/` localized copies, not the canonical `contracts/*.clar` an auditor would read and a deployment would publish. Clarinet keys contracts by name, so both can't be registered under one name — needs a second manifest/CI job or a restructure. See §7.3. `#53`'s drift guard is the current (weaker) substitute for real type-checking on the canonical sources. **This is not only a coverage gap** (*added 2026-09-23, per D5's correction*): on any machine with `settings/Mainnet.toml` present, `clarinet deployments apply --mainnet` recomputes its plan from `Clarinet.toml` — which is exactly this table of names — so a real mainnet deploy run today would publish these 14 contracts as their `contracts/test/` localized copies, not the canonical sources an auditor reviews. See D5. | `Flashstack-ajv.2.3` | | D7 | Source for the live `snp-flashstack-receiver` / `-v3` is absent from the repo. | contract live at `SP3TGRVG…`; no matching file | **FIXED 2026-09-21.** Both sources are now committed verbatim. Each is **byte-identical to the deployed contract**, checked against `GET /v2/contracts/source/SP3TGRVG7DKGFVRTTVGGS60S59R916FWB4DAB9STZ/`: `snp-flashstack-receiver` 3,270 bytes, sha256 `0722955560dbd791d5c3a29c84e1787e4a250df88db5ece6d27be768a0b920ea`; `snp-flashstack-receiver-v3` 5,396 bytes, sha256 `2560814d0e0103d2e8fcb337fc560cdbc5c39215828b81606712ab82ffb3fdec`. They had existed on the maintainer's machine but were gitignored as "stale experiment contracts", which is why the 2026-09-16 review found no source; they are not in `mattglory/snp-mvp` either (default branch checked). `tests/deployed-source-pins.test.ts` pins both hashes, so an edit to either file fails CI: an edit would be drift from an immutable contract, the F-7 shape. Re-verify with `curl -s \| jq -j .source \| shasum -a 256`. | `Flashstack-ajv.7.2` | @@ -179,10 +179,23 @@ from a Code Owner. Until then, treat only *required review* as established. **Decided: archive.** Moved to `deployments/archive/gen1-mainnet-plan-2026-09-22.yaml`, with a header recording what it was and why it moved. The decision sharpened once D7 closed: committing the two SNP receiver sources made all 13 of this plan's paths -resolve for the first time, turning a file that used to die on a missing path into a -genuinely runnable `clarinet deployments apply --mainnet` target, since that flag -auto-selects a file literally named `default.mainnet-plan.yaml` with no explicit path -given. Delete was rejected — it's the only record anywhere of what the gen-1 key +resolve for the first time. + +**The mechanism, verified against clarinet 3.23.2's source, not the naming convention +alone** (*corrected 2026-09-23*): `apply --mainnet` loads this path, but only as a +starting point — it first recomputes a plan from `Clarinet.toml` and diffs it against +the file, prompting `Overwrite? [Y/n]`. It falls back to the on-disk file **silently** +only when that recomputation errors, which happens on any checkout without +`settings/Mainnet.toml` (gitignored, so true of every fresh clone). That silent +fallback, on a clean checkout, is what #74 closes — once all 13 paths resolved, it was +one `Enter` away from broadcast with no deployer key even present. **Where +`settings/Mainnet.toml` exists (an operator's real deploy machine), clarinet always +recomputes fresh from `Clarinet.toml` regardless of this file** — the archive changes +nothing there. The residual risk on that machine was never this YAML; it's whatever +`Clarinet.toml` currently resolves the 14 funds-bearing contract names to, which today +is the `contracts/test/` localized copies. See **D6** and §7.3. + +Delete was rejected — it's the only record anywhere of what the gen-1 key actually published. A replacement plan describing the real live system was and is not possible from here: that system was published across two principals over multiple generations and no plan for it was ever committed. From 426e55ebf27e8e85694c111e940ae9ac1cc3a033 Mon Sep 17 00:00:00 2001 From: Glory Matthew Date: Mon, 28 Sep 2026 18:56:44 +0100 Subject: [PATCH 2/2] docs: rewrite D5/D6 per empirical findings, not source-reading (2nd correction) Requested changes from Hillary's #75 review. Two prior passes at this section relied on reading clarinet's source -- mine and hers, independently -- and both were wrong. She then actually ran clarinet 3.23.2 (the CI binary) in a network-isolated container against the real repo, before and after #74, with a mock node logging every broadcast attempt. I re-ran her test (#76, merged) and independently reproduced the core claim from scratch outside the harness before writing any of this down: 57 publishes, 26 from contracts/test/, matching exactly; the sbtc-token references she cited at lines 91/102 of the test sbtc-pool-v3 copy matched exactly; all five undeployed audit-track successor names re-confirmed 404 today. What actually changes: - D5: a clean checkout never broadcasts, before or after #74 (recompute fails, falls back, prompts, exits with zero requests either way). The gen-1 plan could only ever be signed by SP3TGRVG..., whose 13 names already exist on mainnet, so a real broadcast would be refused as duplicates regardless. #74 is hygiene -- it removed a route only that key could use, one the chain would have refused anyway -- not the risk reduction either earlier version of this row claimed. - D6: the real, still-live exposure, unaffected by #74. Any machine with settings/Mainnet.toml, any key, reaches the SAME plan clarinet computes fresh from Clarinet.toml (byte-identical before/after #74) -- 57 publishes, 26 from contracts/test/, sBTC resolving to a flash-mintable mock, 54 of 57 names free including all five successors. Now pinned by tests/mainnet-plan-guard.test.ts (#76). - Archive file header: same correction, so the historical record doesn't repeat either wrong prior explanation. - Dates: 2026-09-23 -> 2026-09-26 in all three places, per review. Verified: suite 258 passed / 1 expected fail (259) across 25 files (unchanged from post-#76 main), clarinet check 211/0 (unchanged), archive YAML still parses. Co-Authored-By: Claude Sonnet 5 --- .../archive/gen1-mainnet-plan-2026-09-22.yaml | 47 +++++++++++-------- docs/security/CONTRACT_INVENTORY.md | 45 +++++++++++------- 2 files changed, 57 insertions(+), 35 deletions(-) diff --git a/deployments/archive/gen1-mainnet-plan-2026-09-22.yaml b/deployments/archive/gen1-mainnet-plan-2026-09-22.yaml index 1e45e70..0406112 100644 --- a/deployments/archive/gen1-mainnet-plan-2026-09-22.yaml +++ b/deployments/archive/gen1-mainnet-plan-2026-09-22.yaml @@ -7,25 +7,34 @@ # (which the chain would reject). # # Moved here, out of deployments/, and renamed off "default.mainnet-plan.yaml". -# CORRECTED 2026-09-23, verified against clarinet 3.23.2's source rather than -# the filename convention alone: `apply --mainnet` does not blindly load this -# path -- it recomputes a plan from Clarinet.toml first and diffs it against -# the file, prompting Overwrite? [Y/n]. It falls back to this file SILENTLY -# only when that recomputation errors, which happens on any checkout without -# settings/Mainnet.toml (gitignored -- true of every fresh clone). Until -# 2026-09-22 this file died early anyway, because two of its 13 paths -# (contracts/snp-flashstack-receiver.clar and -v3.clar) did not exist in the -# checkout -- a missing-file error was accidentally doing safety work nobody -# designed for it. PR #70 committed those two files verbatim (closing D7), -# which made all 13 paths resolve, so the silent fallback became a real one -# `Enter` away from broadcast, with no deployer key even present. That is the -# case this archive closes: with the file gone, a missing plan now requires -# generating one from Clarinet.toml, which itself needs settings/Mainnet.toml -# and fails outright on a clean checkout. It does NOT close the case where -# settings/Mainnet.toml exists (a real deploy machine) -- there, clarinet -# always recomputes from Clarinet.toml regardless of this file, and the -# residual risk is what Clarinet.toml currently resolves the 14 funds-bearing -# contract names to (the contracts/test/ localized copies, D6), not this YAML. +# CORRECTED 2026-09-26 -- twice. First a source-reading pass said this flag +# "auto-selects" the file; a second source-reading pass said a clean checkout +# was one Enter from broadcast. Both were wrong, found by actually RUNNING +# clarinet 3.23.2 (the CI binary) in a network-isolated container, before and +# after this archive, with a mock node recording every broadcast attempt: +# +# - A clean checkout (no settings/Mainnet.toml) never broadcasts anything, +# before or after this archive. Recompute fails, falls back to this file, +# prompts Continue [Y/n]?, exits 0, ZERO requests sent -- before. Exits 1, +# still zero requests -- after. Neither path was ever a broadcast risk. +# - This plan could only ever be signed by SP3TGRVG... (any other key +# panics before signing), and all 13 of its names already exist there, so +# a real broadcast would have been refused as duplicates regardless. +# +# So archiving this file is hygiene -- it removed a route only the gen-1 key +# could use, one the chain would have refused anyway -- not risk reduction. +# +# The actual, still-live exposure was never this file and this archive does +# not touch it: on any machine with settings/Mainnet.toml, any key, the +# default `apply --mainnet` path (Enter, Enter) or -d both reach the plan +# clarinet computes FRESH from Clarinet.toml -- identical before and after +# this archive (sha256 217564c42bea...) -- and -d broadcasts it with no +# confirmation. That plan has 57 contract publishes, 26 of them from +# contracts/test/, including every funds-bearing localized copy (whose +# sbtc-token calls resolve, in that same plan, to the flash-mintable mock +# contracts/sbtc-token.clar) plus test fixtures. 54 of the 57 names are free +# at the current admin principal, including all five undeployed audit-track +# successors. Pinned by tests/mainnet-plan-guard.test.ts (#76). # See CONTRACT_INVENTORY.md D5 and D6. # # Kept, not deleted, as the only record of what this key/era actually diff --git a/docs/security/CONTRACT_INVENTORY.md b/docs/security/CONTRACT_INVENTORY.md index 9868ed9..e8ed877 100644 --- a/docs/security/CONTRACT_INVENTORY.md +++ b/docs/security/CONTRACT_INVENTORY.md @@ -108,8 +108,8 @@ Each is filed as a bead. None is silently resolved here. | D2 | README badge + Security section claim a **128**-test suite; ROADMAP claims **125**. Actual: **165 passing across 16 files** (`npm test`, 2026-09-15). | test run output | **FIXED 2026-09-16.** The D2 row itself had also gone stale: the true figure is now **176 passing across 17 files**, counted at runtime (`vitest --reporter=json`), not by grepping `it(` — several suites generate tests in a loop, so a static grep undercounts. README (badge, Security bullet, Quick start), ROADMAP (×2), `AUDIT_BRIEF.md` and `FLASH_LOAN_INVARIANT.md` all corrected. | `Flashstack-ajv.7.1` | | D3 | `docs/AUDIT_BRIEF.md` item 4 says v1 and v2 pools are "both listed as live in the README's mainnet contract table". No longer true — README now lists only v2 and calls v1 deprecated. My own brief is stale. | README §Mainnet contracts | **FIXED 2026-09-16.** `AUDIT_BRIEF.md` item 4 corrected in place, with a pointer to §3 and F-6 so the v1 surface stays visible to the auditor rather than disappearing. | `Flashstack-ajv.7.1` | | D4 | ROADMAP "Next" items 1 and 2 ask for the v2 pools and `flashstack-pool-oracle-v2` to be *deployed*. Both are already live at `SPR9PQAN…`. | Hiro contract API | **FIXED 2026-09-16.** Items 1 and 2 marked done with the live principal named. Item 3 (CI gating) deliberately left **unchecked**: the workflow exists (PR #44) but "required on every PR" is branch protection, which cannot be read from the repository — see §7. | `Flashstack-ajv.7.1` | -| D5 | `deployments/default.mainnet-plan.yaml` describes a gen-1 publish from `SP3TGRVG…`, and contains none of the live system. `/deployments/` is CODEOWNERS-protected, so it reads as authoritative. | file contents vs `find contracts` | **FIXED 2026-09-22 — archived.** After D7 closed (both SNP files committed verbatim), all 13 of this plan's paths resolved for the first time. **What that actually enabled, verified against clarinet 3.23.2's source** (`components/clarinet-cli/src/frontend/cli.rs`, `ApplyDeployment` / `load_deployment_if_exists` — *corrected 2026-09-23, this row previously said the flag "auto-selects" the file, which overstates it*): `apply --mainnet` recomputes a plan from `Clarinet.toml` and diffs it against this file when present, prompting `Overwrite? [Y/n]`; only if that recomputation **errors** does it fall back to the on-disk file silently — and it errors on any checkout without `settings/Mainnet.toml` (gitignored, so true of every fresh clone). With all 13 paths resolving, that fallback meant the stale gen-1 plan was one `Enter` away from broadcast on exactly that clean-checkout, no-deployer-key case. **This is the case #74 closes**: with the file gone, a missing plan now calls `generate_default_deployment`, which itself needs `settings/Mainnet.toml` and fails outright on a clean checkout. **It does not touch the case where `settings/Mainnet.toml` exists** — an operator's real deploy machine — where clarinet always recomputes fresh from `Clarinet.toml` regardless of this file. There, the risk was never this YAML; it's whatever `Clarinet.toml` currently resolves the 14 funds-bearing contract names to, which today is the `contracts/test/` localized copies (**D6**). Matt's disposition decision: **archive**, not delete — moved to `deployments/archive/gen1-mainnet-plan-2026-09-22.yaml` with a header explaining why, out of `deployments/` and off the default filename, so the historical record of the gen-1 publish survives without being live on the one path it could still reach. **Note:** `deployments/default.testnet-plan.yaml` has the identical all-paths-resolve shape (checked 2026-09-22) but is far lower-stakes — it targets the well-known public Clarinet deployer, a key nobody on this project holds, not a principal Matt controls. Left as-is; flagged here rather than acted on. | `Flashstack-ajv.7.2` | -| D6 | `clarinet check` covers 38 contracts; 70 `.clar` files exist. The 32 uncovered include `contracts/flashstack-pool-v3.clar` and the other v3/v2 successors — i.e. the audit/deploy targets are not type-checked by the CI gate. | `clarinet check` output vs `Clarinet.toml` registry | **PARTIALLY CLOSED (PR #48, 2026-09-18) — closes the receiver-library half, structural half still open.** PR #48 registered 20 previously-uncovered receiver contracts (17 land clean; 3 excluded with explicit comments for genuine `clarinet check` failures — arkadiko/usda read-only-vs-write mismatches, a zest-v2 type mismatch) plus the 46 external `requirements` those receivers call. FlashStack-owned registered contracts go **39 → 55**; the "209/210 contracts checked" figure some CI output shows is cache-state dependent and ~154 of it is third-party (ALEX/Zest/Arkadiko/Pyth/StackingDAO/…) — not a coverage metric for our own code, per Hillary's review on #48. **Structural half untouched:** all 14 funds-bearing contracts (both cores, all six pools, both oracles, `flashstack-pool-v3`, the v3 receiver trait) still resolve in `Clarinet.toml` to their `contracts/test/` localized copies, not the canonical `contracts/*.clar` an auditor would read and a deployment would publish. Clarinet keys contracts by name, so both can't be registered under one name — needs a second manifest/CI job or a restructure. See §7.3. `#53`'s drift guard is the current (weaker) substitute for real type-checking on the canonical sources. **This is not only a coverage gap** (*added 2026-09-23, per D5's correction*): on any machine with `settings/Mainnet.toml` present, `clarinet deployments apply --mainnet` recomputes its plan from `Clarinet.toml` — which is exactly this table of names — so a real mainnet deploy run today would publish these 14 contracts as their `contracts/test/` localized copies, not the canonical sources an auditor reviews. See D5. | `Flashstack-ajv.2.3` | +| D5 | `deployments/default.mainnet-plan.yaml` describes a gen-1 publish from `SP3TGRVG…`, and contains none of the live system. `/deployments/` is CODEOWNERS-protected, so it reads as authoritative. | file contents vs `find contracts` | **FIXED 2026-09-22 — archived, but not for the reason first recorded here** (*corrected 2026-09-26, superseding both this row's original text and its first correction — see below*). Two earlier passes at this row relied on reading clarinet's source, mine and Hillary's independently. Neither was trustworthy: the Security & Contract Lead then actually **ran clarinet 3.23.2** — the CI binary — inside a network-isolated container (`docker run --network none`), against a throwaway never-funded key, with a mock node logging every `POST /v2/transactions`, across the real repo at both `56a51b1` (before #74) and `4af27a6` (after). Findings: **(1)** a clean checkout (no `settings/Mainnet.toml`) could never broadcast, before or after #74 — recomputation fails, falls back to the gen-1 plan, prompts `Continue [Y/n]?`, then **exits 0 with zero requests sent**; after #74 it just exits 1, still zero requests. **(2)** The gen-1 plan could only ever have been signed by `SP3TGRVG…`: any other key panics before signing (`onchain/mod.rs:568`), and all 13 of its names already exist at `SP3TGRVG…`, so a real broadcast would in any case be refused as duplicates (that last step inferred, not run against a live node). **(3)** So #74 removed a route only the gen-1 key could use, one the chain would have refused anyway — **harmless hygiene, not risk reduction.** **The actual, still-live exposure, unaffected by #74**: on any machine with `settings/Mainnet.toml`, *any* key, the default path (`apply --mainnet`, Enter, Enter) or `-d` both reach the plan `clarinet` computes fresh from `Clarinet.toml` — same result before and after #74, byte-identical across both flows (sha256 `217564c42bea…`) — and broadcast it with **no confirmation under `-d`**. That plan is **D6**, not this file. Disposition (Matt): **archive**, not delete — moved to `deployments/archive/gen1-mainnet-plan-2026-09-22.yaml`, kept as the only record of what the gen-1 key actually published, off the default filename so it can't be mistaken for a live target. Guarded going forward by `tests/mainnet-plan-guard.test.ts` (#76), which pins the real exposure. **Note:** `deployments/default.testnet-plan.yaml` has a similar all-paths-resolve shape but is far lower-stakes — it targets the well-known public Clarinet deployer, a key nobody on this project holds. Left as-is. | `Flashstack-ajv.7.2` | +| D6 | `clarinet check` covers 38 contracts; 70 `.clar` files exist. The 32 uncovered include `contracts/flashstack-pool-v3.clar` and the other v3/v2 successors — i.e. the audit/deploy targets are not type-checked by the CI gate. | `clarinet check` output vs `Clarinet.toml` registry | **PARTIALLY CLOSED (PR #48, 2026-09-18) — closes the receiver-library half, structural half still open.** PR #48 registered 20 previously-uncovered receiver contracts (17 land clean; 3 excluded with explicit comments for genuine `clarinet check` failures — arkadiko/usda read-only-vs-write mismatches, a zest-v2 type mismatch) plus the 46 external `requirements` those receivers call. FlashStack-owned registered contracts go **39 → 55**; the "209/210 contracts checked" figure some CI output shows is cache-state dependent and ~154 of it is third-party (ALEX/Zest/Arkadiko/Pyth/StackingDAO/…) — not a coverage metric for our own code, per Hillary's review on #48. **Structural half untouched:** all 14 funds-bearing contracts (both cores, all six pools, both oracles, `flashstack-pool-v3`, the v3 receiver trait) still resolve in `Clarinet.toml` to their `contracts/test/` localized copies, not the canonical `contracts/*.clar` an auditor would read and a deployment would publish. Clarinet keys contracts by name, so both can't be registered under one name — needs a second manifest/CI job or a restructure. See §7.3. `#53`'s drift guard is the current (weaker) substitute for real type-checking on the canonical sources. **This is not only a coverage gap, and the scale is larger than "14 contracts"** (*rewritten 2026-09-26, per D5's correction — the earlier version of this sentence undercounted*): verified by actually running `clarinet deployments generate --mainnet` against this repo (clarinet 3.23.2, isolated container, no broadcast possible) — the plan it computes from `Clarinet.toml` today has **57 contract publishes, 26 of them from `contracts/test/`**, not only the 14 funds-bearing ones. The 26 include every localized copy (whose `sbtc-token` calls resolve, *inside that same plan*, to `contracts/sbtc-token.clar` — a flash-mintable mock, not canonical sBTC) plus test fixtures never meant to see mainnet: `malicious-token`, `mock-usdcx`, `test-receiver-bad`, `test-pool-v3-receiver-reentrant`, and others. At the current admin principal, 54 of the 57 names are free — including all five undeployed audit-track successors (`flashstack-pool-v3`, `flashstack-stx-pool-v3`, `flashstack-sbtc-pool-v3`, `flashstack-sbtc-core-v2`, `flashstack-stx-core-v2`, all re-confirmed 404 on 2026-09-28). Contract names can't be reused, so one such run would permanently occupy those names with test builds bound to a mock. Pinned by `tests/mainnet-plan-guard.test.ts` (#76): fails if the known 26 changes in either direction, and carries the target state (`it.fails`, no `contracts/test/` path at all) that flips green the moment this row's structural fix lands. See D5. | `Flashstack-ajv.2.3` | | D7 | Source for the live `snp-flashstack-receiver` / `-v3` is absent from the repo. | contract live at `SP3TGRVG…`; no matching file | **FIXED 2026-09-21.** Both sources are now committed verbatim. Each is **byte-identical to the deployed contract**, checked against `GET /v2/contracts/source/SP3TGRVG7DKGFVRTTVGGS60S59R916FWB4DAB9STZ/`: `snp-flashstack-receiver` 3,270 bytes, sha256 `0722955560dbd791d5c3a29c84e1787e4a250df88db5ece6d27be768a0b920ea`; `snp-flashstack-receiver-v3` 5,396 bytes, sha256 `2560814d0e0103d2e8fcb337fc560cdbc5c39215828b81606712ab82ffb3fdec`. They had existed on the maintainer's machine but were gitignored as "stale experiment contracts", which is why the 2026-09-16 review found no source; they are not in `mattglory/snp-mvp` either (default branch checked). `tests/deployed-source-pins.test.ts` pins both hashes, so an edit to either file fails CI: an edit would be drift from an immutable contract, the F-7 shape. Re-verify with `curl -s \| jq -j .source \| shasum -a 256`. | `Flashstack-ajv.7.2` | @@ -174,26 +174,39 @@ commits total. **Action for Matt:** confirm in Settings → Branches whether `ma requires the `Test Smart Contracts` check specifically, and whether review must come from a Code Owner. Until then, treat only *required review* as established. -### 7.2 Disposition of `deployments/default.mainnet-plan.yaml` (D5) — RESOLVED 2026-09-22 +### 7.2 Disposition of `deployments/default.mainnet-plan.yaml` (D5) — RESOLVED 2026-09-22, mechanism corrected 2026-09-26 **Decided: archive.** Moved to `deployments/archive/gen1-mainnet-plan-2026-09-22.yaml`, with a header recording what it was and why it moved. The decision sharpened once D7 closed: committing the two SNP receiver sources made all 13 of this plan's paths resolve for the first time. -**The mechanism, verified against clarinet 3.23.2's source, not the naming convention -alone** (*corrected 2026-09-23*): `apply --mainnet` loads this path, but only as a -starting point — it first recomputes a plan from `Clarinet.toml` and diffs it against -the file, prompting `Overwrite? [Y/n]`. It falls back to the on-disk file **silently** -only when that recomputation errors, which happens on any checkout without -`settings/Mainnet.toml` (gitignored, so true of every fresh clone). That silent -fallback, on a clean checkout, is what #74 closes — once all 13 paths resolved, it was -one `Enter` away from broadcast with no deployer key even present. **Where -`settings/Mainnet.toml` exists (an operator's real deploy machine), clarinet always -recomputes fresh from `Clarinet.toml` regardless of this file** — the archive changes -nothing there. The residual risk on that machine was never this YAML; it's whatever -`Clarinet.toml` currently resolves the 14 funds-bearing contract names to, which today -is the `contracts/test/` localized copies. See **D6** and §7.3. +**The mechanism — this section was wrong twice before it was tested, not just read.** +A first pass read clarinet's source and concluded the flag "auto-selects" this exact +filename. A second pass read the source more carefully and concluded the risk was a +clean checkout being one `Enter` from broadcast. **Both were source-reading, and both +were wrong** — confirmed 2026-09-26 by actually running clarinet 3.23.2 (the CI +binary) in a network-isolated container against the real repo, before and after #74, +with a mock node recording every broadcast attempt: + +- A clean checkout (no `settings/Mainnet.toml`) **never broadcasts anything**, before + or after #74. Recomputation fails, falls back to the gen-1 plan, prompts + `Continue [Y/n]?`, and **exits 0 with zero requests sent** — before #74. After #74 it + exits 1, still zero requests. Neither path was ever a broadcast risk. +- The gen-1 plan itself could only ever be signed by `SP3TGRVG…` — any other key + panics before signing — and all 13 of its names already exist at that principal, so a + real broadcast would have been refused as duplicates regardless. +- So #74 removed a route only the gen-1 key could use, one the chain would have + refused anyway. **Hygiene, correctly decided, but not the risk reduction either + earlier version of this section claimed.** + +**The actual, still-live exposure is unaffected by #74 and was never this file**: on +any machine with `settings/Mainnet.toml`, any key, the default `apply --mainnet` path +(Enter, Enter) or `-d` both reach the plan clarinet computes fresh from +`Clarinet.toml` — identical before and after #74 (byte-for-byte, sha256 +`217564c42bea…`) — and `-d` broadcasts it with no confirmation at all. That plan is +**D6**, not this file: see below, and `tests/mainnet-plan-guard.test.ts` (#76), which +now pins it. Delete was rejected — it's the only record anywhere of what the gen-1 key actually published. A replacement plan describing the real live system was and is