diff --git a/deployments/archive/gen1-mainnet-plan-2026-09-22.yaml b/deployments/archive/gen1-mainnet-plan-2026-09-22.yaml index a55bc69..e0c47e2 100644 --- a/deployments/archive/gen1-mainnet-plan-2026-09-22.yaml +++ b/deployments/archive/gen1-mainnet-plan-2026-09-22.yaml @@ -6,16 +6,40 @@ # 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-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) reaches the plan clarinet +# computes FRESH from Clarinet.toml -- identical before and after this +# archive (sha256 217564c42bea...). -d is the one place #74 changed anything: +# it panicked before signing pre-#74 (any key but SP3TGRVG...), and since #74 +# it reaches that same fresh plan and broadcasts it with no confirmation. At +# the 2026-09-26 measurement, that plan had 57 contract publishes, 26 of them +# from contracts/test/ (28 as of 2026-10-05's merges -- see +# tests/mainnet-plan-guard.test.ts's KNOWN_TEST_PATHS for the live count, not +# this snapshot), 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 were 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 # published — nothing else in the repo documents it. diff --git a/docs/security/CONTRACT_INVENTORY.md b/docs/security/CONTRACT_INVENTORY.md index 5d6d062..f7b36dd 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, 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) reached the fresh `Clarinet.toml` plan **both before and after #74** — same plan, sha256 `217564c42bea…` — unchanged by it. `-d` is the one place #74 changed behavior: pre-#74, `-d` **panicked before signing** for any key other than `SP3TGRVG…` (the gen-1 plan's `expected-sender`); since #74, `-d` reaches that same fresh plan and broadcasts it with **no confirmation**. (This doesn't argue for reverting #74 — the panic was protecting `-d` only by accident, and the default Enter-Enter path reached the fresh plan before #74 regardless.) 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 set changes in either direction (28 as of 2026-10-05's merges, up from 26 at the 2026-09-26 measurement above — the guard's `KNOWN_TEST_PATHS` is the live count, this row's 57/26/54 figures are a dated snapshot, not a maintained total), 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,15 +174,42 @@ 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, 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 — 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) reaches the plan clarinet computes fresh from `Clarinet.toml` — +identical before and after #74 (byte-for-byte, sha256 `217564c42bea…`). `-d` is where +#74 changed behavior: it panicked before signing pre-#74 (for any key but +`SP3TGRVG…`), and since #74 it reaches that same fresh plan and 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 not possible from here: that system was published across two principals over multiple generations and no plan for it was ever committed.