Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 30 additions & 10 deletions deployments/archive/gen1-mainnet-plan-2026-09-22.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,36 @@
# 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) 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
# published — nothing else in the repo documents it.
Expand Down
40 changes: 33 additions & 7 deletions docs/security/CONTRACT_INVENTORY.md
Original file line number Diff line number Diff line change
Expand Up @@ -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) 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/<name>`: `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 <url> \| jq -j .source \| shasum -a 256`. | `Flashstack-ajv.7.2` |


Expand Down Expand Up @@ -174,15 +174,41 @@ 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) 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
not possible from here: that system was published across two principals over multiple
generations and no plan for it was ever committed.
Expand Down
Loading