@artblocks/abx-cli 0.1.0-alpha.20 → 0.1.0-alpha.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/CHANGELOG.md +1081 -0
  2. package/assets/renderer-scaffold/README.md +2 -2
  3. package/assets/renderer-scaffold/src/MyRenderer.sol +2 -2
  4. package/assets/renderer-scaffold/src/interfaces/IAbxFieldRenderer.sol +1 -1
  5. package/assets/renderer-scaffold/src/interfaces/IAbxParams.sol +2 -2
  6. package/assets/renderer-scaffold/test/MyRenderer.t.sol +1 -1
  7. package/dist/commands/deploy.d.ts +30 -17
  8. package/dist/commands/deploy.d.ts.map +1 -1
  9. package/dist/commands/deploy.js +523 -117
  10. package/dist/commands/deploy.js.map +1 -1
  11. package/dist/commands/project.d.ts +16 -0
  12. package/dist/commands/project.d.ts.map +1 -1
  13. package/dist/commands/project.js +171 -15
  14. package/dist/commands/project.js.map +1 -1
  15. package/dist/commands/reads.d.ts.map +1 -1
  16. package/dist/commands/reads.js +35 -5
  17. package/dist/commands/reads.js.map +1 -1
  18. package/dist/commands/scaffold.d.ts +3 -1
  19. package/dist/commands/scaffold.d.ts.map +1 -1
  20. package/dist/commands/scaffold.js +81 -29
  21. package/dist/commands/scaffold.js.map +1 -1
  22. package/dist/commands/service.js +1 -1
  23. package/dist/commands/service.js.map +1 -1
  24. package/dist/commands/storage.d.ts.map +1 -1
  25. package/dist/commands/storage.js +11 -3
  26. package/dist/commands/storage.js.map +1 -1
  27. package/dist/commands/submit-app.d.ts +58 -0
  28. package/dist/commands/submit-app.d.ts.map +1 -0
  29. package/dist/commands/submit-app.js +512 -0
  30. package/dist/commands/submit-app.js.map +1 -0
  31. package/dist/config.d.ts +1 -15
  32. package/dist/config.d.ts.map +1 -1
  33. package/dist/config.js +18 -1
  34. package/dist/config.js.map +1 -1
  35. package/dist/flag-allowlists.d.ts +53 -0
  36. package/dist/flag-allowlists.d.ts.map +1 -0
  37. package/dist/flag-allowlists.js +152 -0
  38. package/dist/flag-allowlists.js.map +1 -0
  39. package/dist/flags.d.ts +6 -2
  40. package/dist/flags.d.ts.map +1 -1
  41. package/dist/flags.js +23 -0
  42. package/dist/flags.js.map +1 -1
  43. package/dist/kind.d.ts +8 -3
  44. package/dist/kind.d.ts.map +1 -1
  45. package/dist/kind.js +25 -0
  46. package/dist/kind.js.map +1 -1
  47. package/dist/main.js +166 -53
  48. package/dist/main.js.map +1 -1
  49. package/dist/mintpage.d.ts.map +1 -1
  50. package/dist/mintpage.js +29 -4
  51. package/dist/mintpage.js.map +1 -1
  52. package/dist/output.d.ts +32 -1
  53. package/dist/output.d.ts.map +1 -1
  54. package/dist/output.js +74 -9
  55. package/dist/output.js.map +1 -1
  56. package/dist/ownerops.d.ts +101 -13
  57. package/dist/ownerops.d.ts.map +1 -1
  58. package/dist/ownerops.js +421 -71
  59. package/dist/ownerops.js.map +1 -1
  60. package/dist/preview.d.ts +1 -1
  61. package/dist/preview.js +1 -1
  62. package/dist/remote.d.ts.map +1 -1
  63. package/dist/remote.js +8 -2
  64. package/dist/remote.js.map +1 -1
  65. package/dist/schema.d.ts +18 -0
  66. package/dist/schema.d.ts.map +1 -1
  67. package/dist/schema.js +37 -2
  68. package/dist/schema.js.map +1 -1
  69. package/dist/served.js +1 -1
  70. package/dist/update-check.d.ts +25 -0
  71. package/dist/update-check.d.ts.map +1 -1
  72. package/dist/update-check.js +49 -9
  73. package/dist/update-check.js.map +1 -1
  74. package/package.json +6 -6
  75. package/skill/SKILL.md +64 -32
  76. package/skill/reference/code-projects.md +98 -22
  77. package/skill/reference/creator-token.md +16 -3
  78. package/skill/reference/decisions.md +83 -11
  79. package/skill/reference/hosting.md +1 -1
  80. package/skill/reference/operating.md +35 -12
  81. package/skill/reference/setup.md +1 -1
  82. package/skill/reference/troubleshooting.md +16 -6
package/CHANGELOG.md CHANGED
@@ -1,5 +1,1086 @@
1
1
  # @artblocks/abx-cli
2
2
 
3
+ ## 0.1.0-alpha.22
4
+
5
+ ### Minor Changes
6
+
7
+ - 1b6b741: Rename "artist" to "creator" throughout, including the protocol vocabulary
8
+
9
+ ABX is a tool for creators generally. Art remains a first-class use case, but it should not be baked
10
+ into the product's framing — and it was, down to the on-chain identifiers.
11
+
12
+ **Breaking, and deliberately a clean break** (pre-launch, folded into the one redeploy that four
13
+ rounds of security remediation already required — deferring would have meant either living with the
14
+ old vocabulary permanently or spending a second deployment on a word):
15
+
16
+ - `AuthOption` is now `Creator`, `TokenOwner`, `Address`, `CreatorOrTokenOwner`, `CreatorOrAddress`,
17
+ `TokenOwnerOrAddress`, `CreatorOrTokenOwnerOrAddress`. Order is unchanged, so the `uint8` values
18
+ are identical — only the names moved. Users type these: `--schema key:Type:Creator`.
19
+ - The on-chain collection metadata field keys `artist` / `artist_links` are now `creator` /
20
+ `creator_links`. These are `bytes32` keys written on-chain and emitted verbatim into every
21
+ collection's `contractURI` JSON.
22
+ - CLI flags `--artist` / `--artist-links` are now `--creator` / `--creator-links`. No aliases.
23
+ - `@artblocks/abx-token-api` renames its `art` module to `content`: `generateArt` →
24
+ `generateContent`, `artContentHash` → `contentHash`.
25
+ - `AbxMetadataRenderer.SPEC_VERSION` is **7**, and `isCurrentRenderer` gates on it. A v6 renderer
26
+ emits a different member key, so it is genuinely behind, not cosmetically so.
27
+
28
+ Package CHANGELOGs are left alone on purpose: they record what past alpha versions actually shipped,
29
+ and rewriting them would make the release history lie about flag names that really were `--artist`.
30
+
31
+ - 1b6b741: A purchase now carries the terms the buyer accepted — closing the one path by which a project owner
32
+ could take more than the buyer agreed to.
33
+
34
+ Both shared fixed-price minters take a mandatory **terms guard** — not a slippage allowance; see the note
35
+ below — and revert a new `SaleTermsChanged()` when the live sale doesn't match:
36
+
37
+ ```solidity
38
+ purchase(address token, address expectedPaymentToken, uint256 maxPrice)
39
+ purchaseTo(address token, address to, address expectedPaymentToken, uint256 maxPrice)
40
+ // 1155 — the bound is the TOTAL, since qty multiplies it
41
+ purchase(address token, uint256 id, uint256 qty, address expectedPaymentToken, uint256 maxTotalPrice)
42
+ purchaseTo(address token, uint256 id, uint256 qty, address to, address expectedPaymentToken, uint256 maxTotalPrice)
43
+ ```
44
+
45
+ `configure` takes effect immediately and has no timelock. On the ETH lane that was largely contained
46
+ (`msg.value` must equal `price`), but an ERC-20 sale settles `safeTransferFrom(buyer, payee, price)`
47
+ against a standing allowance — so an owner who front-ran a pending purchase with a huge price spent the
48
+ buyer's whole approval, and one who switched the sale to a different ERC-20 reached an allowance granted
49
+ somewhere else. `address(0)` as `expectedPaymentToken` means ETH. There is deliberately no "no maximum"
50
+ sentinel: a bound that can be defaulted away is the bug being fixed.
51
+
52
+ SDK (breaking): `preparePurchase` / `preparePurchase1155` take the live terms — `sale`, exactly what
53
+ `readSaleConfig` / `readSaleConfig1155` returns — instead of a hand-computed `value`. They derive the
54
+ ETH to attach (`price`, or `price × quantity`) and the bound from those terms, so the payment and the
55
+ guard cannot desync. Pass `maxPrice` / `maxTotalPrice` to state a wider ceiling (a UI that tolerates a
56
+ small move rather than making the buyer re-sign); it widens the guard only, never the payment. New
57
+ exported type `PurchaseTerms`.
58
+
59
+ **`maxPrice` is not slippage tolerance on the ETH lane, and the docs no longer imply it is.** The minter
60
+ requires `msg.value == price` (an equality) and V1 has no refund path, so an in-flight ETH purchase
61
+ reverts if the price moves in _either_ direction, however wide the ceiling. Real tolerance exists only on
62
+ the ERC-20 lane, where the live `price` is pulled from the buyer's allowance and anything at or below the
63
+ ceiling settles.
64
+
65
+ CLI: `abx minter buy` hands the terms it just read to the SDK (the price × quantity math now lives in
66
+ one place), and the scaffolded `abx mint-page` binds each mint to the terms the page is displaying —
67
+ if the owner re-prices mid-click the mint reverts, nothing is charged, and the page tells the buyer to
68
+ review the new price. Re-pricing a live sale therefore fails the buys already in flight, by design:
69
+ pause first if you want a clean cutover.
70
+
71
+ **Deploy note:** this is a runtime change to two shared singletons, so both need a redeploy on every
72
+ supported chain (CREATE2 + canonical salts, so the new addresses are identical cross-chain) and the new
73
+ addresses recorded in `packages/sdk/src/deployments.ts` +
74
+ `site/content/docs/reference/deployments.mdx`. Until then the CREATE2-prediction-vs-manifest tests fail
75
+ on purpose — that is the drift detector doing its job, not a broken test.
76
+
77
+ - 1b6b741: The configure hook can see a blob's size and contents — so creators can police it instead of the protocol
78
+
79
+ `IAbxConfigureHook.onParamConfigured` gains two arguments:
80
+
81
+ ```solidity
82
+ function onParamConfigured(
83
+ uint256 tokenId,
84
+ bytes32 key,
85
+ bytes32 value, // the literal on the scalar path; keccak256(data) on the blob path
86
+ address updatedBy,
87
+ uint256 dataLength, // 0 on the scalar path; data.length on the blob path (never 0 there)
88
+ address dataBlobAddress // address(0) on the scalar path; the live SSTORE2 pointer otherwise
89
+ ) external;
90
+ ```
91
+
92
+ **Why.** On a multi-copy edition params belong to the id, so a holder-writable `String`/`Bytes` key is
93
+ shared: one holder could store large values and make the shared id's document expensive for every
94
+ co-holder. An independent audit asked for a protocol byte ceiling. A ceiling would cap every project
95
+ to police a configuration almost nobody wants — but the alternative we had documented, "the creator
96
+ constrains it in their own hook", was not actually possible: the hook only ever saw `keccak256(data)`.
97
+ It could not learn the length, could not read the content, and could not read the new value from
98
+ storage either, because nothing had persisted yet. Now it can, and no protocol limit is imposed.
99
+
100
+ **The bytes are not forwarded** — a blob can be nearly a full contract's worth of data, and putting it
101
+ in calldata would make every configure call pay for it whether the hook looks or not. A hook enforcing
102
+ a size ceiling reads `dataLength`; a hook that needs content calls `SSTORE2.read(dataBlobAddress)` and
103
+ pays for exactly what it asked for. Guaranteed: `keccak256(SSTORE2.read(dataBlobAddress)) == value`.
104
+
105
+ **Two things to know when writing one.** `tokenParamData(tokenId, key)` still returns the OLD value
106
+ inside the hook (that is what "before the value persists" means — and it lets you veto a regression).
107
+ And the blob is written _before_ the hook, because an address is meaningless until the contract behind
108
+ it exists; so a veto happens after the writer paid to store the bytes. The accepted path costs the
109
+ same as before, only rejection wastes, and it wastes only the rejecting writer's own gas.
110
+
111
+ `ConfigurableParams` is extension **v3** — the beacon's `extensionVersion` is how an integrator tells
112
+ which calling convention a deployed token uses. A hook written for the 4-argument form will revert
113
+ against a v3 token, and vice versa.
114
+
115
+ Addresses: `AbxParamsLib` moved, `AbxEditionLib` with it (it links it), and the four token types that
116
+ compose `ConfigurableParams` moved with their factories — `SeriesCode`, `EditionCode`,
117
+ `OneOfOneEdition`, `EditionImage`. The two ERC-721 image types and their factory, `AbxMetadataLib`,
118
+ `AbxCodeLib`, the renderer, chunk store, seed source and both minters keep their addresses.
119
+
120
+ - 1b6b741: Renderer spec v10 projects four long-reserved keys; edition trust anchors move
121
+
122
+ **Renderer spec v10.** Four keys the spec reserved from the start were projected on chain by nothing:
123
+ `background_color` and `youtube_url` (token), `banner_image` and `featured_image` (collection). A
124
+ documented reserved key that no surface emits is a hole rather than a saved byte — a creator sets it,
125
+ observes nothing, and cannot tell whether the tool or the marketplace is at fault. All four now project
126
+ when their representation is chain-reachable, and are omitted when unset. `isCurrentRenderer` gates on
127
+ 10; a v9 renderer silently drops them.
128
+
129
+ **`collaborators` is no longer a reserved key.** It was in the registry, projected by neither plane, and
130
+ because reserved keys are excluded from the resolver's `artifacts` listing, setting it on chain did
131
+ nothing observable anywhere. It is now an ordinary creator key — which means it **does** list in
132
+ `artifacts`. Put a collaborator list in a named artifact (`abx attach`) or in `description`.
133
+
134
+ **The resolver's two collection image keys are now symmetric with the renderer.** `featured_image` is a
135
+ top-level `contractURI` key instead of artifacts-only, and `banner_image` projects for every
136
+ chain-reachable representation instead of only `url` (an `inline` or `reader` banner used to vanish).
137
+
138
+ **Docs now carry the three-list split** — which keys both planes project, which are the off-chain
139
+ resolver's alone by design (`abx_provenance.status`, operator overlay, effects/live-view, the
140
+ `artifacts` listing, collection `image` courtesy, `image_data`, off-chain-decode values), and which are
141
+ not reserved at all. That split is the thing to read before adding a key to either surface: drift on a
142
+ key both planes _can_ emit is what produced three earlier remediations.
143
+
144
+ **Addresses.** The three edition factories move (trust anchors) along with their implementations, and
145
+ the renderer moves. `AbxEditionLib`, `AbxMetadataLib`, `AbxCodeLib`, `AbxParamsLib`, all three ERC-721
146
+ factories, the chunk store, the seed source, both minters and both generators keep their addresses.
147
+ Existing clones keep running their frozen implementations; re-point a live collection at the new
148
+ renderer with `abx set-renderer` rather than redeploying it.
149
+
150
+ - f64a31f: Expose `lockScript` above the contract layer — the lock that actually freezes a code project's work.
151
+
152
+ `SeriesCode`/`EditionCode` have always had `lockScript()` (and `scriptLocked()`), but nothing above
153
+ the ABI surfaced it: the SDK had no `prepareLockScript`, the CLI had no `lock-script` command, and the
154
+ skill's immutability guidance told creators that `lock-field` + `lock-uri` made a project "provably
155
+ immutable". For a generative/code drop that is false — those freeze only the _metadata_; the owner can
156
+ keep rewriting the on-chain program (`setScriptChunk`/`removeLastScriptChunk`) until the script is
157
+ locked. Agents building on ABX repeatedly missed that the program could be frozen at all.
158
+
159
+ - **SDK:** new `prepareLockScript({contract, chainId})` (mirrors `prepareLockDependencies`).
160
+ - **CLI:** new `abx lock-script <address>` command (any signing lane), plus `abx verify` now reports
161
+ the script and dependency lock state for code projects (`script: locked/UNLOCKED`, with a nudge to
162
+ `lock-script` when the program is still mutable) — read from the already-indexed state, no extra RPC.
163
+ - **Skill:** the immutability guidance now names the full freeze for a code drop
164
+ (`lock-script` + `lock-dependencies` + `lock-field`/`lock-uri`, plus `set-schema … lock=now` for a
165
+ param value), corrects the "provably immutable" claim, and surfaces PostParams as a first-class
166
+ `deploy-code` capability (not only a generative feature) in the `SKILL.md` description and decision
167
+ tree so collector-settable on-chain parameters are discoverable.
168
+
169
+ - 1b6b741: Independent-audit remediation: `abx_provenance` reshaped, `abx_params` removed, hook ABI widened
170
+
171
+ An independent adversarial audit of the contracts found no Critical or High issue, no theft path and
172
+ no cross-project reach — but four Mediums, eight Lows and five architectural notes. Everything is
173
+ addressed. Consumer-visible changes:
174
+
175
+ **`abx_provenance` entries are now `{field, source, note}`.** They lost `onChain`, which was wrong in
176
+ both directions — a `url` reported `false` although the URL string is stored on chain, while an
177
+ `inline` value of `"https://…"` reported `true` for a pure pointer — and `verifiedAgainstChain`,
178
+ which was hardcoded `null` on every entry. `source` says where the bytes came from; whether a value
179
+ resolves on chain is visible in the value, and that judgment belongs to the reader. The off-chain
180
+ resolver drops `onChain` for the same reason (it was exactly `status === 'on-chain'`) and keeps
181
+ `status`, which it can genuinely compute.
182
+
183
+ **`abx_params` no longer appears in `tokenURI`.** It was emitted by the on-chain renderer AND the
184
+ resolver and parsed back by nobody. Params enumerate directly from the contract — `tokenParamKeys`,
185
+ `tokenParam`, `paramSchemaKeys` — which is canonical, needs no indexer, and is what a chain-only
186
+ reader should use; a code project's script still receives them through `tokenData`. Traits for
187
+ marketplaces belong in `attributes`, unchanged.
188
+
189
+ **A computed `image` locator now lands verbatim.** A field renderer returning
190
+ `("text/uri-list", "ipfs://…")` was being data-wrapped into `data:text/uri-list;base64,…`, which no
191
+ marketplace dereferences. `image` and `animation_url` now share one implementation of the rule.
192
+
193
+ **`IAbxTransferHook.onTokenTransfer` gains `operator` and `amount`.** Without them a hook on a
194
+ shared-supply ERC-1155 cannot tell a real transfer from a zero-amount no-op — which let any address
195
+ fire the param lifecycle for an id it held no copy of. The token also refuses to notify on
196
+ zero-amount and self-transfers.
197
+
198
+ **`pingURI` is owner-only**, and `paramSchemaHead` / `selectOption` are new: resolving one selected
199
+ option no longer copies the whole option table on every render.
200
+
201
+ `AbxMetadataRenderer.SPEC_VERSION` is 8. Every canonical address moves — every token type is now
202
+ library-linked, including the two ERC-721 image factories, because the metadata field store was
203
+ externalized into the new `AbxMetadataLib` to restore EIP-170 headroom before any fix was stacked on
204
+ top of 187 bytes of margin.
205
+
206
+ The CLI now warns when `--copies` meets a holder-writable `--schema`: on an edition, params belong to
207
+ the id, so every holder shares one value and the last writer wins.
208
+
209
+ - 1b6b741: The custom seed source is now a real choice, not a documented one
210
+
211
+ We tell creators — in the docs, in `AbxSeedSource`'s own NatSpec, and in `deploy-code --help` — that the
212
+ canonical mint seed is pseudorandom and that the answer for anything lottery-like is to point
213
+ `seedSource` at their own `IAbxSeedSource` over commit-reveal or a VRF oracle. On-chain that was always
214
+ true. In the toolkit it was not: there was no flag, no owner command, and the only route was an
215
+ `ABX_SEED_SOURCE` env var validated as nothing more than "has code at this address". A doc/code
216
+ contradiction of our own making, now closed at both ends.
217
+
218
+ **Deploy:** `abx deploy-code --seed-source <0x…|canonical>` (and the `--copies` edition lane). Omitting
219
+ it is unchanged — the canonical `AbxSeedSource` from the manifest, bootstrapped on a chain that lacks
220
+ one — and `--no-seed` still means no mint-time seed at all. Passing both is refused rather than resolved
221
+ in someone's favour. `ABX_SEED_SOURCE` keeps working and is no longer the unchecked lane.
222
+
223
+ **After deploy:** `abx set-seed-source <address> <0x…|canonical|none>` — owner-only, same positional
224
+ grammar as `abx set-transfer-validator`. It surfaces the on-chain `setSeedSource(address)` that nothing
225
+ reached before. **Future mints only**: a seed settles the instant it is assigned, so nothing already
226
+ minted changes. The command says so, names the count of tokens already carrying a seed from the old
227
+ source, and points out that a part-sold drop then spans two sources — public on the spine
228
+ (`SeedSourceSet`) but not something an early buyer is told, so pause and say so.
229
+
230
+ **`abx state` now prints the seed source**, labelled: `none`, the canonical singleton, or `CUSTOM`. It is
231
+ the one setting an owner can re-point mid-sale that changes what a later buyer receives, and it was
232
+ readable nowhere.
233
+
234
+ **Both writes probe the address first, and `code.length > 0` is not the check.** The SDK gained
235
+ `probeSeedSource` — one `eth_call` to `seed(uint256,address)`, which must return 32 bytes — plus
236
+ `readSeedSource`, `prepareSetSeedSource`, `assertSeedSourceUsable`, and a `SeedSourceUnusableError`
237
+ carrying a structured verdict. Four shapes are refused, each with its own message: **no code**;
238
+ **empty return** (the permissive-fallback shape — a Safe, an uninitialised proxy, an EIP-7702-delegated
239
+ EOA; pasting your own wallet is the common way in); **a return under 32 bytes**; and **a revert** (either
240
+ the wrong address entirely, or a real source not yet armed — `seed()` is called synchronously inside the
241
+ mint and its revert bubbles, so a source that cannot answer now cannot answer at mint either). The
242
+ interface is non-`view` on purpose, but an `eth_call` simulates a state-keeping source fine, so
243
+ commit-reveal and oracle-fed sources probe without sending anything. This matters because a
244
+ misconfigured seed source is **completely silent**: the write succeeds, `seedSource()` reads back what
245
+ you set, the event fires, `abx state` shows it — and then every mint of the collection reverts in the
246
+ token's `bytes32` decode. Same spirit as `CreatorToken._requireHasCode`.
247
+
248
+ **Naming fix: a `seed` param schema is not a "re-roll", and no longer described as one.** The governed
249
+ path is `configureTokenParam(tokenId, "seed", value)` — **the caller supplies the value**. Nothing is
250
+ re-randomized and the seed source is never consulted again; with an unbounded `Uint256Range` the
251
+ authorized party may set any 32-byte value, repeatedly, until they like the output. "Re-roll" promises a
252
+ buyer a fresh random draw they could reasonably expect to be fair, which is not what they get. It is a
253
+ collector-chosen (or creator-chosen) seed, and the docs, the specs, and the skill now say that. The
254
+ substance that was correct is unchanged: the schema is a pre-sale commitment (declarable only before the
255
+ collection's first seed exists), `paramSchema("seed")` is the buyer's read, and the type must be a
256
+ literal/scalar one.
257
+
258
+ **Dropped a stale warning** in the skill about a `seed` schema changing how the seed decodes off-chain
259
+ while the on-chain generator injects raw hex — `tokendata.ts` returns the raw hex for `seed`
260
+ unconditionally, ahead of the schema branch, so the wire format is invariant across both surfaces and
261
+ there is nothing left to warn about.
262
+
263
+ **Also corrected: `maxPrice` is a terms assertion, not slippage tolerance.** On the ETH lane the minter
264
+ requires `msg.value == price` (an equality) and V1 has no refund path, so an in-flight ETH purchase
265
+ reverts if the price moves in _either_ direction, however wide the ceiling — widening `maxPrice` on an
266
+ ETH sale buys the buyer nothing. Real tolerance exists only on the ERC-20 lane, where the live price is
267
+ pulled from the buyer's allowance. `specs/protocol/minter-spine.md` and
268
+ `site/content/docs/protocol/minting.mdx` said "a price cut still settles" without that distinction.
269
+
270
+ - 1b6b741: Renderer spec v9: a computed image is carried once, not twice
271
+
272
+ A field-renderer-produced image was emitted as the reserved `image` key **and again** inside the
273
+ `artifacts` manifest, then the whole document was base64-encoded around both copies. On the
274
+ on-chain-SVG lane that roughly doubled the inner payload of `tokenURI` — the one read this protocol
275
+ most wants to stay within ordinary RPC limits.
276
+
277
+ The data-plane spec always allowed an on-chain renderer to omit entries duplicating reserved keys it
278
+ already emits ("an EVM-efficiency reduction, never a semantic one"). The renderer now takes that for
279
+ every reserved key, so on a fully-on-chain project `artifacts` is absent rather than empty. Nothing is
280
+ lost, because the entry's only unique contribution was a `mimeType` and a `data:` URI states its own.
281
+ The off-chain resolver still emits the complete listing, duplicates included — that asymmetry is the
282
+ spec's, and it is now documented on both surfaces.
283
+
284
+ Also removed: three orphaned param getters on the renderer (`paramKeysOf`, `paramSchemaOf`,
285
+ `paramDataOf`), stranded when v8 took params out of `tokenURI`, whose NatSpec still claimed `tokenURI`
286
+ called them.
287
+
288
+ `AbxMetadataRenderer.SPEC_VERSION` is **9** and `isCurrentRenderer` gates on 9. Only the renderer
289
+ moves — nothing links it, so no factory or implementation address changes. Existing on-chain-URI
290
+ projects keep resolving through the v8 renderer until re-pointed with `abx set-renderer`.
291
+
292
+ - 1b6b741: Upgrading from alpha.12/alpha.13 — what breaks, in one place
293
+
294
+ This release lands the contract-audit branch. It is a **compatibility break with earlier alphas**, which
295
+ is fine at this stage (greenfield alpha, no backwards-compatibility promise) but is worth having in one
296
+ list rather than spread across a dozen changesets. Nothing here is a deprecation with a migration
297
+ window; the old shapes are gone.
298
+
299
+ **Every canonical address moved**, and four manifest fields are new (`metadataLib`, `paramsLib`,
300
+ `codeLib`, `editionLib` — the delegatecalled write-path libraries). Resolve through `getDeployment` and
301
+ the upgrade is transparent; anything you have _stored_ keyed by a factory address is not. A project
302
+ indexed from a superseded anchor reads `isCanonical: false` against the new manifest — correct, since it
303
+ is what a creator sees if they don't redeploy, but it is a data question, not a version bump.
304
+
305
+ **The metadata document changed: renderer spec v4 → v9.** If you assemble or assert on it:
306
+
307
+ - `abx_provenance` entries lost `onChain` (it was exactly `status === 'on-chain'`, and wrong in both
308
+ directions) and `verifiedAgainstChain` (hardcoded `null` on every entry). `source` and `status` stay.
309
+ - The `abx_params` block is gone from **both** lanes. Params enumerate from the contract —
310
+ `tokenParamKeys` / `tokenParam` / `paramSchemaKeys` — which is canonical and needs no indexer; a code
311
+ project's script still receives them through `tokenData`.
312
+ - `artist` / `artist_links` are now `creator` / `creator_links` (reserved on-chain collection field
313
+ keys), and `AuthOption.Artist*` is `AuthOption.Creator*` (numeric values unchanged).
314
+ - A computed `image` locator (`text/uri-list`) lands **verbatim** instead of being wrapped into a
315
+ `data:` URI no marketplace dereferences.
316
+ - The on-chain renderer no longer duplicates a computed image into `artifacts`. The off-chain resolver
317
+ still emits the complete listing — that asymmetry is specified, not a bug.
318
+ - Four long-reserved keys now project on chain: `background_color`, `youtube_url` (token) and
319
+ `banner_image`, `featured_image` (collection). `featured_image` also becomes a top-level resolver key
320
+ instead of artifacts-only, and `banner_image` projects for every chain-reachable representation
321
+ rather than `url` alone.
322
+ - `collaborators` is **no longer reserved** — projected by neither plane and excluded from `artifacts`
323
+ by its own reserved-ness, so it did nothing. It is now an ordinary creator key that does list.
324
+
325
+ **`tokenData` divergences fixed, so rendered output can change:** a contract-scope `seed` no longer
326
+ leaks into a token's `tokenData` (the on-chain generator never read one), `seed`'s wire format is always
327
+ raw 32-byte hex regardless of schema, and `</script` escaping is case-insensitive to match the on-chain
328
+ generator. Cached renders for affected code projects should be re-derived.
329
+
330
+ **Removed:** `paramKeysOf`, `paramSchemaOf`, `paramDataOf` on `AbxMetadataRenderer` — orphaned when
331
+ params left `tokenURI`. Read the token directly instead.
332
+
333
+ **Two hook interfaces widened** (`ConfigurableParams` is extension **v3**; a hook built for the old
334
+ signature reverts against a v3 token): `IAbxTransferHook.onTokenTransfer` gained `operator` + `amount`;
335
+ `IAbxConfigureHook.onParamConfigured` gained `dataLength` + `dataBlobAddress`.
336
+
337
+ **Additive:** `ParamHooksFrozen` folds into `ParamHooks.locked`; `paramSchemaHead` + `selectOption`
338
+ replace pulling a whole option table to resolve one index; `readParamHooks`; `abxJs()` / `gunzipScript()`
339
+ on the generator for piecewise document assembly; and the three ERC-1155 ABIs now carry
340
+ `MetadataUpdate` / `BatchMetadataUpdate` / `DefaultMaxSupplySet`, which were emitted but not decodable.
341
+
342
+ - 1b6b741: Transfer-validator authorization has one home, and the five affected trust anchors move
343
+
344
+ `CreatorToken1155.setTransferValidator` performed no authorization at all, and the library it
345
+ delegates to performed none either. What protected the three edition token types was that each one
346
+ _overrode_ the setter to run the caller check first — one guard in three copies, sitting in front of
347
+ an unguarded mixin whose own comment claimed the tokens carried no override. Nothing deployed was
348
+ vulnerable, but a fourth edition type that simply inherited the mixin would have shipped an
349
+ unauthenticated `setTransferValidator`, which decides whether collectors' tokens can move at all.
350
+
351
+ The guard now runs in the mixin, the overrides are gone, and the function is no longer `virtual` —
352
+ so the arrangement is enforced by the compiler rather than by a comment. A dead
353
+ `_requireMinterOrOwner` in `ExternalMinter`, whose NatSpec wrongly advertised it as the mint-time auth
354
+ check (the real one also enforces `paused()`), is deleted in the same pass.
355
+
356
+ **Both changes are source-only** — the compiled runtime bodies are byte-identical before and after,
357
+ measured across all ten affected artifacts. Only the metadata hash differs, and since that is part of
358
+ the initcode, the CREATE2 addresses move. Five factories and their implementations are redeployed and
359
+ re-verified on Sepolia and Base Sepolia: `SeriesImageFactory`, `SeriesCodeFactory`,
360
+ `OneOfOneEditionFactory`, `EditionImageFactory`, `EditionCodeFactory`. Everything else — the 1/1 image
361
+ factory, all four libraries, the renderer, chunk store, seed source, both minters, both generators —
362
+ keeps its address.
363
+
364
+ Projects deployed from the superseded factories keep working, but `abx` will report their anchor as
365
+ an older version. Redeploy from the anchors in the refreshed manifest.
366
+
367
+ ### Patch Changes
368
+
369
+ - 1b6b741: `ABX_CHAIN` in a `.env` now actually selects the chain
370
+
371
+ Found by a cold sandbox agent: the creator asked for Sepolia, put `ABX_CHAIN=sepolia` in `.env`, and
372
+ got a **Base Sepolia** preview with no error. The agent scored it 3/5 and called it "a serious trust
373
+ problem for a tool whose entire pitch is 'here's exactly what would be deployed'". It was right.
374
+
375
+ `config.ts` derives `CHAIN` as a module-level const, so it evaluated while the import graph loaded —
376
+ before `main()` called `loadDotEnv()`. Every other setting escaped this by being read lazily inside a
377
+ function (`storageOptions()`, the signing key at send time), by which point `.env` was loaded.
378
+ `ABX_CHAIN` was the one eager read, and the worst one to lose: on a funded send it is a wrong-chain
379
+ deploy with real artifacts at an address nobody meant.
380
+
381
+ The same ordering hid a second symptom: `assertKnownChainEnv()` also runs at module scope, so a typo
382
+ like `ABX_CHAIN=mainnet` in `.env` read as "unset" and the CLI quietly used the default instead of
383
+ printing its "not a chain this toolkit ships / mainnet is not supported yet" refusal. Both are fixed
384
+ by loading `.env` before the chain is derived, and both are covered by tests.
385
+
386
+ - 1b6b741: Externalize the code-custody READ surfaces into `AbxCodeLib` — B22 step 2, buying back the bytes the ERC-4906 audit fix spent
387
+
388
+ The audit remediation that made `OnChainMetadata`'s field writes emit ERC-4906 (closing a real finding:
389
+ that surface mutated `tokenURI` silently, so a correct indexer served the pre-edit document forever) cost
390
+ ~96 B per token, and `SeriesCode` and `EditionCode` had no room for it — both fell under the 150 B EIP-170
391
+ floor `contracts/test/CodeSize.t.sol` guards. That is precisely the trigger condition `docs/10-backlog.md`
392
+ B22 was written for, and step 2 was the prescribed answer.
393
+
394
+ The `IAbxOnChainScript` and `IAbxDependencies` view bodies now live in `AbxCodeLib`, next to the write
395
+ paths that were already there — `scriptChunkCount`/`scriptChunk`/`scriptLocked` and
396
+ `dependencyCount`/`dependencyByIndex`/`dependencyRegistry`/`dependenciesLocked`. The mixins keep the
397
+ functions (same signatures, same ABI, same ERC-165 ids) as **raw-calldata passthroughs** through a new
398
+ shared `AbxCodeDelegate` base, which forwards the call's exact calldata to the library and returns the
399
+ return data untouched. Storage still resolves in the token's own ERC-7201 namespaces and events still log
400
+ from the token — that is what `delegatecall` buys.
401
+
402
+ Raw, not typed, on purpose: B22 step 1 measured a _typed_ shell (`return AbxCodeLib.f(...)`) **growing**
403
+ the token by ~855 B, because decoding the library's return value and re-encoding it at the call site costs
404
+ more than the extracted body saves. The same mistake was made again during this remediation, on
405
+ `setTokenField`/`setContractField`, and reverted. The price of the raw form is that `AbxCodeLib`'s read
406
+ signatures are now part of every composing token's external ABI — same signature ⇒ same selector is the
407
+ whole mechanism — so they must track the interfaces verbatim; both the library and the mixins now say so.
408
+
409
+ EIP-170 margins, before → after:
410
+
411
+ | contract | before | after | floor |
412
+ | ------------- | ------ | ------- | ----- |
413
+ | `SeriesCode` | 125 | **529** | 150 |
414
+ | `EditionCode` | 132 | **507** | 150 |
415
+
416
+ `AbxCodeLib` grew 3,905 → 4,961 B and still has ~19.6 KB to spare. Nothing else moved: `OnChainScript` and
417
+ `Dependencies` are composed **only** by `SeriesCode` and `EditionCode`, and both already linked
418
+ `AbxCodeLib`, so no contract gained a library dependency and the other four tokens are byte-identical.
419
+
420
+ No behavior change and no ABI change — the read signatures, return types, mutability, ERC-165 ids and the
421
+ `ScriptIndexOutOfRange`/`DependencyIndexOutOfRange` error selectors are all preserved (the errors stay
422
+ declared on the mixins so they remain in each token's ABI, and now revert from the library's
423
+ identically-named, identically-selectored twin). This does move the implementation bytecode, so it rides
424
+ the redeploy this remediation already requires: `AbxCodeLib`, both code tokens, and their factories (and
425
+ therefore the factory trust anchors) all get new addresses.
426
+
427
+ - 1b6b741: CREATE2 for library deployments, as the standard process — and delete the false belief that said it was impossible
428
+
429
+ An audit found that two ERC-1155 token types were held library-free, and an EIP-170 size floor relaxed, on
430
+ the recorded belief that linking a delegatecalled library costs a factory its CREATE2-deterministic address.
431
+ That belief is false, and the committed broadcast artifacts always refuted it: every recorded library deploy
432
+ went through the keyless CREATE2 proxy, `AbxParamsLib` landed at ONE address on Sepolia and Base Sepolia
433
+ from deployer nonces **215 apart** (a nonce is not an input to a CREATE2 address), and both library-linked
434
+ factories have held chain-identical addresses across four deploy generations.
435
+
436
+ What was true was narrower and self-inflicted: `deploy.ts` deployed its libraries with `send({to: null})` —
437
+ a plain EOA `CREATE` — **by its own choice**, so _it_ could not predict them. `create2.ts` and both code
438
+ factory deploy functions then wrote that local limitation up as a property of libraries in general, and the
439
+ repo proceeded to contradict itself (`deployments.ts` and `reference/deployments.mdx` describe the correct
440
+ mechanism a few files away).
441
+
442
+ The SDK now does what the docs already claimed:
443
+
444
+ - Both bootstraps (`deploySeriesCodeFactory`, `deployEditionCodeFactory`) deploy every write-path library
445
+ through the keyless proxy at the canonical `AbxSalts` salts — `abx.lib.params.v1`, `abx.lib.code.v1`,
446
+ `abx.lib.edition.v1`, mirrored in `ABX_SALT` and pinned against `AbxSalts.sol` by a new test — and then
447
+ CREATE2 the linked factory at its own salt. **An SDK-bootstrapped chain and a forge-bootstrapped chain now
448
+ produce identical addresses** (verified: the SDK's predictions equal what
449
+ `DeployLibraries.s.sol --sig 'predict()'` prints).
450
+ - Idempotent, out of necessity rather than politeness: the proxy _reverts_ on an occupied address, so a
451
+ library already on-chain is linked against instead of redeployed — which is also what makes a partially
452
+ bootstrapped chain recoverable by re-running.
453
+ - New: `predictParamsLib`, `predictCodeLib`, `predictEditionLib`, `predictSeriesCodeFactory`,
454
+ `predictEditionCodeFactory`, plus exported `libPlaceholder` / `linkLibraries` / `LIB_FQN`. Both code
455
+ factories therefore gained the CREATE2 self-heal every other canonical anchor has — and need no version
456
+ probe for it, because code at a CREATE2 address is bytecode-bound to the build that predicted it.
457
+
458
+ **Fixes a real crash on the way past.** `deployEditionCodeFactory` could not succeed at all: it deployed
459
+ `abxEditionLibBytecode` raw, but `AbxEditionLib` itself delegatecalls `AbxParamsLib`, so its shipped
460
+ bytecode carries an unlinked `__$…$__` placeholder — not hex, so the RPC rejected it with an opaque
461
+ `Invalid byte sequence`. The old guard only checked the _factory's_ bytecode, never the library's. The
462
+ library is now linked before it is deployed, and `linkLibraries` throws with the offending placeholder named.
463
+
464
+ **The trap that IS real, now written down** (`create2.ts`, `reference/deployments.mdx`): a library's address
465
+ is a function of its creation bytecode, so build settings move it. Linking _after_ compilation — substituting
466
+ into solc's placeholder, what forge's automatic linking and the SDK both do — cannot change that bytecode.
467
+ Handing solc the addresses at build time (`--libraries src/…:Lib:0x…`) writes the map into
468
+ `settings.libraries` in every artifact's metadata, and the metadata hash is appended to the creation
469
+ bytecode: measured here, that moves the library's own address and the linked factory's initcode. Optimizer
470
+ profile does the same (the code tokens compile at 200 runs). Both failures are invisible until two chains
471
+ disagree, so the docs now say to compare `predict()` against the SDK's predictions rather than assume.
472
+
473
+ Docs and specs swept for the old claim: `reference/deployments.mdx` (the salts, and a "libraries go first"
474
+ bootstrap step naming `DeployLibraries.s.sol` and its `predict()` dry run), `reference/contracts.mdx`
475
+ (`AbxEditionLib` was missing from the library taxonomy entirely), `reference/sdk.mdx`,
476
+ `specs/self-host-toolkit/deployment.md`, and the `abx-self-host` skill's code-projects reference. No
477
+ addresses were invented: nothing is redeployed yet, and the two manifest-drift tests still stand as the
478
+ detectors waiting on it.
479
+
480
+ - 1b6b741: `abx doctor` stops calling a dust balance "funded", and its provenance line stops guessing how you invoked it
481
+
482
+ Both found by cold agents in the clean room, one of them twice.
483
+
484
+ **Funding had two states where it needed three.** A wallet holding `0.000000000848940602 ETH` — 848
485
+ gwei, enough for nothing — reported as `funded`, in full wei precision, with no faucet link, because the
486
+ test was `balance > 0n`. An agent then spent its whole run preparing a deploy it could not send and
487
+ named the quiet funding signal as its top friction. Now: zero is `empty`, below 0.001 ETH is
488
+ `too little to deploy` with a faucet link, and anything above reads `funded` at six decimals instead of
489
+ eighteen. The floor is a readout threshold, **not** a refusal — we don't estimate anyone's gas, and a
490
+ cheap L2 deploy below it is the caller's call.
491
+
492
+ **The `binary` line said `source checkout (pnpm abx)`** even when the caller had typed a bare `abx` that
493
+ happened to resolve to a checkout. `source` was right; the parenthetical named an invocation that never
494
+ happened. It now says what is running rather than how you typed it. A provenance line that misstates the
495
+ invocation undermines every readout sitting next to it.
496
+
497
+ Not changed, deliberately: `deploy --dry-run` still refuses to print an address without `--salt`. An
498
+ agent asked for a non-final example address to skip the two-step dance, but the salt is the only thing
499
+ that pins the address, and printing a real-looking one that won't be the deployed address is the exact
500
+ failure that refusal exists to prevent.
501
+
502
+ - 1b6b741: Editions: the buyer picks the work, and the skill stops promising a freeze we withdrew
503
+
504
+ **Say what the edition mint flow actually is.** The 721 Series lane and the edition lane answer "who
505
+ picks the work?" in opposite ways, and only the 721 half was written down. `IAbxSequentialMint` takes a
506
+ recipient and mints `nextTokenId` — nothing is selectable. The edition minter is keyed `(token, id)`, so
507
+ `purchase(token, id, …)` _names the work_, and on an `EditionCode` project each id carries its own
508
+ `seed`, drawn at that id's **first** mint because the seed belongs to the work rather than to a copy.
509
+ Choosing the id therefore chooses the seed. That is the product, not a leak: an edition is copies of a
510
+ known work, and seeing what you are buying is the normal case.
511
+
512
+ Two consequences are now documented rather than left to be discovered:
513
+
514
+ - Buying an **already-minted** id is choosing among settled, publicly readable works.
515
+ - Buying an **unminted** id draws its seed with the buyer's chosen `id` in the preimage — a real
516
+ selection surface (compute each candidate id's seed in a view loop, buy the best), bounded by which ids
517
+ have a configured sale and by `maxInvocations`. This is the same grinding surface the recipient `to` was
518
+ removed from the preimage to avoid, and `id` cannot be removed the same way, because it _is_ the
519
+ work. So `AbxSeedSource`'s "accept-or-decline, not choose" is now stated as the **721-lane** property
520
+ it is, everywhere it appeared (`protocol/code-projects`, `specs/protocol/code-projects`,
521
+ `specs/protocol/event-spine`, the skill).
522
+
523
+ **Creator mints the supply, then lists on secondary** is documented as a first-class way to run an
524
+ edition: no minter, no allocation, no pause to manage, and every seed settles under the creator's own
525
+ transactions. It needs nothing new — the owner may always mint, paused or not, up to the id-space and
526
+ per-id caps — and it composes with a sale (reserve to self, price the rest).
527
+
528
+ **The shipped skill's frontmatter no longer advertises "freeze/lock a project so it can never change"** —
529
+ the exact promise `SKILL.md`'s own body walks back. It now names what a lock actually freezes (fields, URI
530
+ config, script, dependencies) plus the new param-hook freeze, so the sentence that gets the skill _picked_
531
+ agrees with the sentence that tells the creator the truth. The router stayed a router: the seed paragraph
532
+ and the Solidity-lane paragraph were compressed to pay for the new material, with the depth in
533
+ `reference/code-projects.md`.
534
+
535
+ - 1b6b741: Stop promising immutability the locks don't deliver — and name the residual owner powers in one place
536
+
537
+ **The claim that was wrong.** With the field lock, URI lock, contract-URI lock, script lock and
538
+ dependency lock _all_ engaged, the rendered output can still change, two ways. **Params have no lock at
539
+ all** — they aren't fields, so no field lock reaches them, and the renderer projects the whole enumerated
540
+ set into `tokenURI` as `abx_params` (a program reads the same set as `tokenData`). And a
541
+ **`Resolution.Registry` dependency is re-fetched from the registry contract on every read** —
542
+ `lockDependencies` freezes the ref and the registry pointer, not another contract's storage. `abx verify`
543
+ reported `chain-complete` on both sides of a registry swapping its library bytes, and the CLI was telling
544
+ people "a fully immutable code drop is lock-script + lock-dependencies + lock-field/lock-uri".
545
+
546
+ Every unqualified verdict is now qualified rather than deleted, because locking a pointer _is_ part of the
547
+ story: `lock-uri`, `lock-script` and `lock-dependencies` narration, their `--help` entries, `abx verify`'s
548
+ chain-complete pass (which now says the flag describes **where** bytes come from, not that they're frozen),
549
+ `GeneratorStatus.chainComplete`'s doc comment, the `lockScript` SDK docs, `protocol/metadata`,
550
+ `protocol/params`, `protocol/code-projects`, `reference/cli`, the operate guide, `specs/protocol/*`, and the
551
+ skill's SKILL.md · `reference/decisions.md` · `reference/code-projects.md` · `reference/operating.md`. The
552
+ line the toolkit now gives a creator is **"your metadata is locked"**, with the stronger claim reserved for
553
+ a project whose graph is on-chain `0x…` deps plus locked fields. Stated once per place the decision is made,
554
+ not restated everywhere.
555
+
556
+ Framed as a capability as well as a caveat: a token that _deliberately_ live-adapts to on-chain conditions
557
+ is a legitimate and interesting thing to build, and it's the same mechanism that makes it possible.
558
+
559
+ **New page: [What a project owner can do](https://abx.docs.artblocks.io/protocol/owner-powers/).** There was
560
+ nowhere a collector could read what powers the creator retains. It states both halves plainly, with the read
561
+ for each. What an owner **can** do: re-point or suspend an enrolled 721C/1155C transfer validator (and block
562
+ transfers entirely with one that reverts — though if they renounce ownership, anyone may suspend it and
563
+ nobody can re-arm it); change price, payee, allocation and pause on a live sale with no timelock; keep
564
+ receiving primary-sale proceeds after selling the project, because `primaryPayee` doesn't move on
565
+ `transferOwnership`; mint reserves outside the minter's allocation, since assigning a minter grants the whole
566
+ remaining cap rather than a slice; change royalty receiver and bps forever; re-point metadata subject to the
567
+ locks above; swap the seed source; reassign a token's seed _iff_ a `seed` schema was declared before the
568
+ collection's first seed existed; and walk away. What they provably **cannot** do: take a buyer's funds beyond
569
+ the terms that buyer signed for (`purchase`/`purchaseTo` require `expectedPaymentToken` and a maximum, and
570
+ revert `SaleTermsChanged`); exceed a supply or per-id edition cap; rewrite or clear a settled seed; reach
571
+ another project through any shared singleton (the renderer and chunk store hold no storage, the generator's
572
+ wiring is `immutable` with no setters, the seed source is a stateless view namespaced by caller, and the
573
+ minters key everything per token and hold no funds); or take a token out of a collector's wallet — there is no
574
+ owner-only transfer, burn, or clawback anywhere in the protocol.
575
+
576
+ Framing, deliberately: ABX is self-serve, the creator is a privileged role, the protocol's job is to keep
577
+ that role away from a collector's funds and tokens, and the residual powers are disclosed so a collector can
578
+ price them. Not an accusation — a creator who destroys their own collection has destroyed what they were paid
579
+ for. The point is that "trust me" should be optional.
580
+
581
+ - 1b6b741: `minter configure` no longer tells an edition its minter is unassigned when it isn't
582
+
583
+ The readiness footer `abx minter configure` prints after configuring an edition sale read
584
+ `minter()`, `primaryPayee()` and `paused()` through the ERC-721 1/1 ABI, which has none of them. The
585
+ call threw client-side, the `.catch` swallowed it into a zero address, and a correctly-configured
586
+ edition was told to go run `set-minter` and `set-primary-payee` — both of which it had already done.
587
+
588
+ This is the same bug `minter show` had, fixed on 2026-08-05; only the show path was corrected then.
589
+ The footer now reads through the edition ABI, and a live regression test covers it, so the two paths
590
+ can't drift apart again.
591
+
592
+ - 1b6b741: Gate on-chain content by what a node will READ, not by what it costs to write
593
+
594
+ The documented on-chain envelope — ≲24 KB/file, ≲256 KB/project — was a _storage-cost_ figure sold as a
595
+ support envelope, and the CLI's only loud warning fired on the project total. Measurement says the
596
+ binding constraint is somewhere else entirely: **`tokenURI` reassembles the whole document on every
597
+ call**, and that cost is **superlinear** — EVM memory expansion is quadratic, so the per-KB rate climbs
598
+ with size. Across the 10–100 KB range that decides most projects it measures **~360,000–405,000 gas per
599
+ KB**, rising to ~460,000/KB at 187 KB and ~510,000/KB at 256 KB. `inline` and `reader` land within ~1% of
600
+ each other up to 75 KB (the cost is the renderer's string building, not the storage mechanism) and
601
+ diverge above it (~5% at 187 KB, ~10% at 256 KB, as the chunk store's quadratic read loop takes over), so
602
+ `--compress fastlz` makes a field cheaper to write and not one gas cheaper to read.
603
+
604
+ Measured with `forge` (`OneOfOneImage` + `AbxMetadataRenderer`, callee execution gas for one `tokenURI`
605
+ call, `reader` staging at 22,000-byte chunks):
606
+
607
+ | on-chain content | `tokenURI` gas | per KB | | on-chain content | `tokenURI` gas | per KB |
608
+ | ---------------- | -------------- | ------- | --- | ---------------- | -------------- | ------- |
609
+ | 3 KB | 1,123,327 | 374,000 | | 100 KB | 40,254,159 | 403,000 |
610
+ | 10 KB | 3,588,993 | 359,000 | | 128 KB | 53,559,735 | 418,000 |
611
+ | 23 KB | 8,346,220 | 363,000 | | 187 KB | 86,021,071 | 460,000 |
612
+ | 40 KB | 14,740,366 | 369,000 | | 256 KB | 131,269,134 | 513,000 |
613
+ | 50 KB | 18,759,333 | 375,000 | | | | |
614
+ | 75 KB | 29,137,215 | 388,000 | | | | |
615
+ | 90 KB | 35,868,124 | 399,000 | | | | |
616
+
617
+ geth's `--rpc.gascap` defaults to 50M, hosted providers commonly cap lower, and L1 block limits sit in
618
+ the mid-30-millions — so a project at the _documented_ ceiling could not be read by a normal node, and
619
+ from ~90 KB (~36M gas) up no contract can read it inside a transaction at all. Note what the 100 KB
620
+ refusal is: a **margin**, not the wall. 100 KB reads at ~40M, and geth's own 50M default is not
621
+ exhausted until ~120 KB; the refusal sits early because hosted providers cap well below geth.
622
+
623
+ The gate now sits on that axis, per token (each `tokenURI` assembles only its own content, so a
624
+ 300-piece collection of 5 KB works is fine while one 256 KB work is not):
625
+
626
+ - **from ~40 KB — warn.** Reads need a deliberately high-gas RPC.
627
+ - **past ~100 KB — refuse**, with `--allow-unreadable-onchain` as the override. The refusal names the
628
+ three ways forward and says plainly what the override accepts: rendering needs a high-gas endpoint and
629
+ most marketplaces and indexers will show nothing. It fires on the **dry run** too, so a creator meets it
630
+ before spending gas rather than after the first staging transaction.
631
+
632
+ Live on every path that puts bytes on-chain: `deploy --onchain-image`, `deploy-series --onchain-image`,
633
+ both `--copies` edition twins, and `set-field --file`. New in the SDK: `classifyOnchainReadSize`,
634
+ `tokenUriGasEstimate` (fitted to the measurements, so it tracks the superlinearity instead of
635
+ multiplying by a flat rate), `TOKEN_URI_GAS_PER_KB_LOW` / `TOKEN_URI_GAS_PER_KB_HIGH` (the measured
636
+ 10–100 KB envelope — there is deliberately no single flat `TOKEN_URI_GAS_PER_KB`, because there is no
637
+ single rate), `ONCHAIN_READ_WARN_BYTES`, `ONCHAIN_READ_REFUSE_BYTES`, `ONCHAIN_READ_GETH_CAP_BYTES`
638
+ (where geth's 50M default actually runs out, so the refusal can be described as the margin it is)
639
+ (`exceedsOnchainSoftLimit` stays, now documented as the _write_-cost predicate it always was). The
640
+ whole-collection 256 KB warning stays as well, relabelled as write cost only, since it never said
641
+ anything about readability.
642
+
643
+ Two things deliberately not done. **No on-chain guard and no gas check** — a read happens off-chain, a
644
+ read too large for one node is an RPC-capability problem, and bricking a contract is worse than needing a
645
+ capable endpoint. And the **code lane still only warns**, at the same 40 KB threshold and now quoting the
646
+ gas figure: `abx inspect`'s document size is an estimate that undercounts unknown dependencies, and a
647
+ registry-hosted library like p5 is legitimately ~200 KB, so the honest move there is to name the cost and
648
+ point at the generator's piecewise getters (`document`, `tokenDataJson`, `dependencyTag`, `abxJs`,
649
+ `gunzipScript`, `registryScriptChunk`) rather than block a shipped lane on a heuristic. Those getters are
650
+ now in the SDK's generator ABI, which claimed to carry "the piecewise reads" while omitting three of them.
651
+
652
+ The docs and the `abx-self-host` skill were swept in the same pass: `protocol/onchain-storage` carries the
653
+ measured table and the corrected envelope, `protocol/code-projects` gains a piecewise-read section, and
654
+ the skill's Quick start and `reference/decisions.md` lead with "40 KB is the number, not 256 KB" plus the
655
+ rule that the override is a creator's deliberate choice and never a way to clear a warning.
656
+
657
+ - 1b6b741: Round-3 audit remediation: link the two ERC-1155 image anchors, and stop guarding by name
658
+
659
+ `EditionImage` and `OneOfOneEdition` became library-linked in the previous round, but neither the
660
+ SDK nor the forge scripts were updated. `predictEditionFactory()` and
661
+ `predictOneOfOneEditionFactory()` hashed **unlinked** bytecode — and viem does not reject a
662
+ `__$…$__` placeholder, it UTF-8-encodes it, so both returned a confident hash of ASCII garbage for
663
+ a trust anchor, and `ops.ts` would have broadcast that bytecode as initcode. Both now link
664
+ `AbxEditionLib` (itself linked against `AbxParamsLib`) and ensure the libraries exist first.
665
+
666
+ Two addresses move as a result: `EditionImageFactory` and `OneOfOneEditionFactory`. Both are
667
+ pre-launch and pending the redeploy gate.
668
+
669
+ This was the third instance of one bug class, and the existing guard missed it because it checked
670
+ two factories **by name**. The replacement is exhaustive by construction: it reverse-looks-up which
671
+ initcode each `predict*` actually hashed, drives every `prepareDeploy*` and every `deploy*`
672
+ orchestrator against a stub chain, and asserts no `__$` placeholder survives anywhere — so the next
673
+ library-linked contract is covered without anyone remembering to add it.
674
+
675
+ Also: every SDK deploy now goes through CREATE2 at a canonical salt (the 1/1 and Series anchors were
676
+ still plain `CREATE`, landing at nonce-dependent addresses that could never match the manifest), and
677
+ the CLI no longer tells creators an ERC-1155 edition emits no ERC-4906 — both lanes emit it now.
678
+
679
+ - 1b6b741: Warn when an explicit `--salt` has an all-zero guard prefix
680
+
681
+ The factory reads a salt's leading 20 bytes as an access guard: the caller's own address means only
682
+ that signer may deploy with it, all-zero means anyone may. Omitting `--salt` gives you the pinned
683
+ form. But only `abx predict` ever explained this — the deploy commands took an explicit `--salt`
684
+ verbatim and said nothing.
685
+
686
+ So a creator who pinned a vanity salt with a zero prefix and announced the predicted address was
687
+ handing it to whoever deployed there first, and the front-runner ends up as `owner()`, because
688
+ ownership is set by `initialize` rather than by the prediction. The 2026-08 security review
689
+ reproduced that.
690
+
691
+ It warns rather than refuses: a zero prefix is the right choice for a deliberately shared,
692
+ caller-independent deployment. The check lives in the flag parser, which is the one place every
693
+ command passes through.
694
+
695
+ - 1b6b741: Say the honest thing about the mint seed: pseudorandom, not lottery-grade
696
+
697
+ The canonical `AbxSeedSource` stopped hashing the recipient (`to` is accepted and ignored) because a
698
+ buyer names the recipient in `purchaseTo`, which turned a caller-supplied field into a search space —
699
+ grind candidates in a view loop, buy once at the winner. Dropping it removes cheap _targeting_, and
700
+ nothing more: the seed is still derived entirely from on-chain values, so it is **deterministic after
701
+ the fact** (replayable from the block — the property that makes generative output verifiable) and
702
+ **not secret before it** (a contract minting in the same transaction can compute the seed it would
703
+ receive and revert unless it likes the result).
704
+
705
+ The docs and the toolkit now say that where a creator decides, instead of implying more than the
706
+ contract delivers:
707
+
708
+ - `deploy-code --help` spells out what `--no-seed` opts out of, and the deploy plan prints the same
709
+ note beside the `seed source` line it already showed — on the 721 and edition lanes both.
710
+ - The `abx-self-host` skill gained a one-paragraph router entry in **Code projects** and a
711
+ **Seeds — pseudorandom, not lottery-grade** section in `reference/code-projects.md`: fine where the
712
+ seed diversifies output and the distribution is the product, not fine for a raffle, a prize draw,
713
+ or any drop where one rare outcome is worth materially more than mint price. The escape hatch is
714
+ named rather than hinted at — the creator's own `IAbxSeedSource` over commit-reveal or a VRF oracle,
715
+ which is a per-project swap, not a fork of ABX (and is now reachable as `--seed-source` /
716
+ `abx set-seed-source` — see the companion entry).
717
+ - Both also document the seed's new governance rule: a `seed` param schema can only be declared
718
+ **before the collection's first seed exists**, so whether a seed's value may be re-set — and by
719
+ whom — is a pre-sale commitment a buyer reads with `paramSchema("seed")` (or `abx state`), not
720
+ something added after a sellout. That schema is deliberately **not** described as a "re-roll":
721
+ the governed write is `configureTokenParam(tokenId, "seed", value)` and the caller supplies the
722
+ value, so it is a chosen seed, not a fresh random draw.
723
+
724
+ The public docs (`site/content/docs`), the protocol specs, and `contracts/README.md` were swept in the
725
+ same pass; no claim that the seed is "unpredictable" survives anywhere.
726
+
727
+ - 1b6b741: Fix `abx skill install` and `abx scaffold-renderer` on a published npm install
728
+
729
+ Reported from the field on v0.1.0-alpha.21: `abx skill install` — the canonical way to install the
730
+ agent skill, and the first thing the quickstart tells you to run — failed on every fresh global npm
731
+ install with `✗ bundled skill not found`. The reporter's diagnosis was exactly right.
732
+
733
+ `resolveBundledSkill()` computed the package root as
734
+ `resolve(fileURLToPath(import.meta.url), '..', '..')`. That is correct for a module compiled to
735
+ `dist/main.js` and off by one for `dist/commands/scaffold.js`, which is a level deeper — so it landed
736
+ on `dist/` and looked for `<pkg>/dist/skill`. The assumption was written when the function lived one
737
+ level up and quietly broke when it moved into `commands/`.
738
+
739
+ Two more instances of the same root cause came out with it:
740
+
741
+ - **`abx scaffold-renderer` was broken the same way** and had not been reported — it looked for
742
+ `<pkg>/dist/assets/renderer-scaffold`.
743
+ - **The "am I running from source?" guard** matched `/src/<one-segment>$`, so it read false for
744
+ anything under `src/commands/`. On its own that was harmless; together with the root fix it would
745
+ have made a dev run prefer `<pkg>/skill` — gitignored prepack output — over the canonical skill,
746
+ which is how the canonical skill once got overwritten by a copy eight versions behind.
747
+
748
+ Nothing caught any of it because a dev run never has the published layout: running from source, both
749
+ resolvers fall back to the repo copies and work regardless of the arithmetic. So the fix is structural
750
+ rather than a third `..`: one `packageRootFrom()` that walks up to the CLI's own name-checked
751
+ `package.json`, depth-independent by construction, plus `packaging.test.ts` — which builds a published
752
+ layout in a temp dir and walks it from several depths, and fails the build if counted-`..` root math
753
+ comes back in any spelling.
754
+
755
+ Workaround on alpha.21, if you're on it:
756
+ `cp -r $(npm root -g)/@artblocks/abx-cli/skill ~/.claude/skills/abx-self-host`
757
+
758
+ - 133ecbc: Add `abx submit-app` — optional post-deploy listing in the ABX App Store.
759
+
760
+ Not folded into deploy. After a collection exists, the agent offers it, confirms catalog copy with the creator (name / summary / description — not the collection's NFT metadata), then mints through the gated minter and writes params in gas-capped batches. Already-claimed collections skip the mint (same as `/update`). Shipped Base Sepolia registry + minter; override with `--registry` / `--minter`.
761
+
762
+ - 1b6b741: The param transfer hook is a veto, not best-effort — say so everywhere, and ship the lock that answers it
763
+
764
+ The transfer hook used to swallow its own revert, and every surface promised that the parameter
765
+ lifecycle "must never block a transfer". That promise could not be kept, so it is withdrawn rather than
766
+ restated: Solady runs the ERC-721/1155 **receiver acceptance check after the hook**, so on the `safe*`
767
+ variants — which marketplace fills wrap — a hook that is cheap when a wallet estimates gas and expensive
768
+ when the transfer lands starves the work that comes after it, with or without a gas cap. Swallowing
769
+ bought a guarantee that was false in exactly the cases people use, while making an honest hook's failure
770
+ invisible.
771
+
772
+ So the contract now calls the hook plainly (no swallow, no gas budget, no assembly), **its revert fails
773
+ the transfer**, and — because a mint and a burn are transfers from/to `0x0` — a reverting hook also stops
774
+ minting for that project, including through the shared minter. That is deliberate: a hook is always the
775
+ creator's own contract, which is why it may block issuance while the transfer _validator_, usually a
776
+ third party's, never sees mint or burn.
777
+
778
+ The mitigation is a lock, and it is now reachable end to end:
779
+
780
+ - **SDK** — `prepareLockParamHooks({contract, chainId})`, the sibling of `prepareLockScript` /
781
+ `prepareLockDependencies`. Plus two reads: `readParamHooks` (the trio) and `readParamHooksLocked`,
782
+ which establishes the freeze without a getter by simulating `setParamHooks` with the current trio and
783
+ watching for `ParamHooksLocked` — and returns `undefined`, never a cheerful `false`, when a node won't
784
+ answer.
785
+ - **CLI** — `abx lock-param-hooks <address>`. Owner-only, one-way, every signing lane, `--dry-run`
786
+ guarded. It prints the exact three addresses it will freeze, states what is given up (arming a
787
+ transfer veto, arming a configure veto, re-pointing or clearing the augment hook) and that there is no
788
+ way back, and distinguishes the two cases: freezing an **empty** set is how a project _proves_ it can
789
+ never arm a transfer veto, while freezing a set that already contains a hook pins which contracts can
790
+ run — it does not disarm them.
791
+ - **`abx state`** now prints the three hooks and whether they are frozen, next to the transfer validator,
792
+ because that pair is the read a buyer performs. `abx verify` warns on an armed-but-unfrozen transfer
793
+ hook. `abx set-param-hooks` states the veto at the moment a transfer hook is armed.
794
+ - **Reconstruction** — `ParamHooksFrozen` folds into `ProjectState.paramHooks.locked`. The event is
795
+ declared only by `AbxParamsLib`, so `spineEventAbi` now unions that ABI (its other events dedup away):
796
+ without it the one-way lock on a hook that can veto a transfer was invisible to every consumer that
797
+ folds the spine.
798
+
799
+ Swept for the withdrawn promise: `protocol/params` (which gained a section on why it died),
800
+ `protocol/owner-powers` (the hook is now on **both** lists — an unlocked hook set as a live power, a
801
+ frozen one as a guarantee), `specs/protocol/event-spine`, `specs/protocol/user-stories`, the spine event
802
+ doc strings, and the `abx-self-host` skill (`SKILL.md`'s lock bullet plus the operating and code-project
803
+ references). No surface still says "best-effort", "reverts are swallowed", or "never blocks a transfer",
804
+ and a CLI test now fails if one comes back.
805
+
806
+ - 1b6b741: Transfer validator: preflight what the chain actually refuses, and stop calling `setTransferValidator` owner-only
807
+
808
+ Two corrections, both in the direction that was costing someone something.
809
+
810
+ **The preflight was a bare `getCode`, and the chain's check is not.** `validateTransfer` returns nothing,
811
+ so there is no ABI decode to fail — which means any address whose fallback succeeds for an unknown
812
+ selector passes a has-code check and then waves **every** transfer through, while ERC-165,
813
+ `getTransferValidator()` and the extension beacon all report enforcement as ON. A Safe does exactly this
814
+ (`FallbackManager.fallback()` returns empty when no handler is set), as do an uninitialised proxy and a
815
+ 7702-delegated EOA, and a creator pasting their own Safe is the likely real case. The contract's guard was
816
+ rewritten to probe with a selector no validator implements and require it to **fail**; the toolkit now
817
+ asks the same question, with the same selector, through the new SDK `probeTransferValidator` (verdicts:
818
+ `ok` · `no-code` · `permissive-fallback` · `unreachable`). Both call sites use it — `abx
819
+ set-transfer-validator` and `deploy --721c` — so a creator gets a refusal that names what is actually
820
+ wrong instead of passing the CLI and reverting on chain against an error whose documented meaning ("no
821
+ code on this chain") was false for their address.
822
+
823
+ **`setTransferValidator` is owner-only only while there is an owner.** Once `owner() == address(0)`
824
+ **anyone** may suspend enforcement by passing `address(0)`, and nobody may ever arm a validator again —
825
+ the dead-man release for a validator that reverts every transfer on a collection with nobody left to
826
+ re-point it, which would otherwise strand every collector's token permanently. Four surfaces still said
827
+ owner-only, and the SDK's was wrong in the direction that matters: an integrator reading it would refuse
828
+ to build the suspension a stranded holder needs. Corrected in `creator-token.ts` (module header plus
829
+ `prepareSetTransferValidator`), `protocol/royalty-enforcement`, `protocol/interfaces`, and
830
+ `specs/protocol/interfaces`, each stating the asymmetry: an ownerless collection can only ever be moved
831
+ _toward_ transferability, so this hands a stranger no power over a live project.
832
+
833
+ - Updated dependencies [1b6b741]
834
+ - Updated dependencies [1b6b741]
835
+ - Updated dependencies [1b6b741]
836
+ - Updated dependencies [1b6b741]
837
+ - Updated dependencies [1b6b741]
838
+ - Updated dependencies [1b6b741]
839
+ - Updated dependencies [f64a31f]
840
+ - Updated dependencies [1b6b741]
841
+ - Updated dependencies [1b6b741]
842
+ - Updated dependencies [1b6b741]
843
+ - Updated dependencies [1b6b741]
844
+ - Updated dependencies [1b6b741]
845
+ - Updated dependencies [1b6b741]
846
+ - Updated dependencies [1b6b741]
847
+ - Updated dependencies [1b6b741]
848
+ - Updated dependencies [1b6b741]
849
+ - Updated dependencies [1b6b741]
850
+ - @artblocks/abx-sdk@0.1.0-alpha.14
851
+ - @artblocks/abx-token-api@0.1.0-alpha.17
852
+ - @artblocks/abx-indexer@0.1.0-alpha.15
853
+ - @artblocks/abx-storage@0.1.0-alpha.14
854
+
855
+ ## 0.1.0-alpha.21
856
+
857
+ ### Patch Changes
858
+
859
+ - 528c6c6: Close most of the ERC-1155 edition CLI parity gaps (B28), give every command one unknown-flag posture
860
+ (B30), and add the per-holder edition balance read (B31).
861
+
862
+ Each B28 item was checked against the tree before any code was written: **none needed a Solidity
863
+ change.** The contracts and SDK ops were already per-id and standard-neutral, so all of this is CLI
864
+ plumbing and **no redeploy, address churn, or Etherscan re-verification is involved.**
865
+
866
+ **Edition custody is now symmetric with the 721 side** — each lane verified live on testnet, not just
867
+ by dry run:
868
+
869
+ - **`deploy-series --copies` gains pattern 2: off-chain image, on-chain JSON, nothing to run.** The 721
870
+ Series had the whole branch; `EditionImage` never got it, so a multi-artwork **raster** edition had no
871
+ no-server option at all. A uniform file extension uploads the folder as ONE directory and bakes a
872
+ single `{id}`-substituting collection field, so the collection costs one field instead of N; mixed
873
+ extensions fall back to per-id `url` fields. Verified at
874
+ `0x4e7e2d1aA951d99046323a620a2a5891b1195452`, where `uri(0|1|2)` resolve to `…/0.png`, `…/1.png`,
875
+ `…/2.png` off one template and all three fetch at their source byte sizes. This gap had real design
876
+ consequences: a cold agent asked for "3 artworks × 25 copies each", hit the SVG-only refusal, and
877
+ split the collection into **three separate single-artwork contracts** to get the permanence it wanted.
878
+ - **`--onchain-image` works on both edition lanes, on the hot AND wallet lanes.** It was refused
879
+ outright on `deploy-series --copies` and hot-lane-only on `deploy --copies`; one sign session now
880
+ covers the chunk writes plus the deploy, and the staging txs are counted in the `approvals` line the
881
+ session's own total is derived from. Verified at `0x0A71382207a980c23E26a54F2b815072da41761A`:
882
+ `uri(id)` returns a `data:image/svg+xml;base64` image with zero `http(s)` anywhere in the document.
883
+ `EditionImage` also now treats `--onchain-image` as implying on-chain resolution — every sibling lane
884
+ already did, and without it the flag would have demanded a resolver URL for a drop whose bytes are
885
+ already on-chain.
886
+ - **`deploy-code --copies` accepts on-chain `--dep`/`--dep-registry`.** `EditionCode` already inherited
887
+ the `Dependencies` extension and called `_initDependencies()`; the legs come from the SDK's shared
888
+ `dependencySetupCalls`, so the two lanes cannot encode `setDependency` differently. So "an edition of
889
+ my p5 sketch, with p5 coming from the chain" is a real lane now — verified on Sepolia at
890
+ `0xC403CB9B956Ca814F1934d8d2A44fc1D2023EFB3`, where `abx verify` reports **chain-complete — every
891
+ dependency resolves to proven on-chain bytes; no server, gateway, or CDN in the graph**, with a 233 KB
892
+ inline `animation_url`.
893
+ - **The cold lane stays refused for `--onchain-image`**, deliberately and identically on all four paths:
894
+ staging is a sequence where each chunk tx's receipt feeds the next, so it cannot be signed offline.
895
+ This replaces an edition-specific "tracked as a follow-up" note with the real, shared reason.
896
+
897
+ Still open on the edition lanes, and recorded in B28: `deploy-code --copies` has no `--code-dir` or
898
+ field renderers, and `deploy-code --resume` has no edition path (that one is genuinely new per-id diff
899
+ logic, not a port).
900
+
901
+ **Every command now has one unknown-flag posture.** Roughly 35 commands — `state`, `add`, `index`,
902
+ `migrate`, `configure-param`, `set-field`, `set-royalty`, `set-minter`, `pause`/`unpause`, `verify`, … —
903
+ silently ignored an unrecognized flag, so `abx set-royalty --bsp 500` did something quietly different
904
+ from what was asked. Allowlists now live in one file and are applied once in the dispatcher; it warns
905
+ rather than refuses (a false refusal would break a working script, and `deploy*` still refuses via its
906
+ own exhaustive sets).
907
+
908
+ The obvious implementation — deriving each allowlist from the command's own `usage` string — was tried
909
+ and **rejected on evidence**: shared helpers read flags that appear in no usage string anywhere
910
+ (`storageOverrides()` reads twelve, pulled in by six commands; `--remote-token` by five), so deriving
911
+ would have warned on valid flags. That is worse than the silence it replaces, because a notice people
912
+ learn to distrust stops working for the real cases too. The sets are explicit instead, composed from
913
+ named groups, and a test reads each command's `--help` to assert every flag it **documents** is accepted
914
+ — the direction that would produce false warnings.
915
+
916
+ **`abx tokens <addr> --holder 0x..`** adds a `held` column — `balanceOf(holder, id)` per id, a live head
917
+ read. It answers the one question supply/cap cannot, and the one a funded agent had to leave blank after
918
+ transferring a copy (it correctly declined to fabricate the recipient's balance rather than guess).
919
+ Refused on a 721, where the owner column already answers it. Also: `deploy-series`' `[2] On-chain
920
+ renderer` step now prints the renderer address instead of an empty header, and `abx verify` no longer
921
+ ends in two ⚠ on a healthy fully-on-chain **image** drop (the code-project checks are scoped to the code
922
+ twins; a static drop has no program and no `animation_url` by design).
923
+
924
+ - 528c6c6: ERC-1155 editions: the trust-anchor and on-chain-URI checks told the truth about 721s only, and four
925
+ membrane gaps around them (2026-08-05 wave-4 agent sweep — 12 cold agents, one funded end-to-end run
926
+ on Base Sepolia).
927
+
928
+ The 1155 contracts themselves were fine: a funded run took an edition from deploy through
929
+ `tokenuri` → mint more copies → transfer one copy → lower the per-id cap, verifying every step
930
+ against the chain. What shipped wrong was everything that _describes_ an edition.
931
+
932
+ - **`abx verify` reported EVERY canonical edition as `canonical: NO`.** `detectCanonicalFactory`
933
+ probed only the three 721 anchors and, on no match, fell back to the 721 1/1 factory — whose
934
+ `isAbxClone` answers **false**, not "unknown". So the one signal platforms allowlist against was
935
+ confidently inverted for the entire edition line (the function's own docstring promised
936
+ "canonicity just shows unverified, never wrong"). All six anchors are now probed. **Editions
937
+ registered before this fix keep the wrong factory in the local projection** — `abx forget <addr>`
938
+ then `abx add <addr>` re-detects it.
939
+ - **`abx verify`'s on-chain-URI probe called the ERC-721 `tokenURI` selector on editions**, which
940
+ exposes `uri(id)`. It reverted for every edition, so verify could never confirm that a
941
+ fully-on-chain edition resolves — and it printed the raw multi-line viem dump (Contract Call /
942
+ args / Docs / Version) into a creator-facing readout. The probe now switches on `contractType`
943
+ (matching `reconstruct.ts`, which already did), reports which accessor it read so an edition is
944
+ never described in 721 terms, and readout errors are trimmed to their first line.
945
+ - **An address with no contract was reported as a specific type: `OneOfOneImage`.**
946
+ `detectTokenKind`'s three probes are each `try/catch → false`, so nothing-deployed and
947
+ no-extensions-composed were indistinguishable and the ladder fell through to its `1of1` default.
948
+ `abx set-max-supply` and `abx minter buy` then told edition owners _"…is a OneOfOneImage (721) —
949
+ drop your edition flags"_ for a mistyped address or, far more often, the wrong `ABX_CHAIN` (the
950
+ default is base-sepolia, so any Sepolia contract hit this immediately) — advice that removes the
951
+ _correct_ flags. The existence check now lives inside `detectTokenKind`, so all 13 call sites
952
+ inherit it; it fails **open** on an unreadable `getCode`, so an RPC blip never becomes a
953
+ "no contract" claim.
954
+ - **`abx transfer --token-id <n>` silently transferred id 0.** `transfer` spells the id `--token`
955
+ while every sibling command (`mint`, `set-max-supply`, `minter …`) spells it `--token-id`, and an
956
+ unrecognized flag was simply ignored — so an agent that learned the name from `mint` moved the
957
+ **wrong artwork** with no warning. `--token-id` is now an accepted alias; disagreeing values are
958
+ refused rather than silently preferring one.
959
+ - **The edition deploy previews dropped lines their 721 twins print.** All three edition lanes
960
+ omitted `approvals N wallet approval(s)` from the dry-run readout — which the skill promises "every
961
+ preview" prints and tells the agent to state up front, so on an edition the agent had nothing to
962
+ tell the creator about how many wallet prompts were coming (`deploy-code --copies` had it only in
963
+ the `--confirm` sentence, which is off by default). `deploy --copies` and `deploy-series --copies`
964
+ also omitted the "resolves ON-CHAIN via the renderer — no resolver, no server" line, which is
965
+ precisely the guarantee an edition creator is asking about, and `deploy-series --copies` never
966
+ showed `paused`. All six lanes now print the approval count; the 1155 paths say `uri()`.
967
+ - **Owner-op writes now surface a stray flag instead of swallowing it.** `mint`, `transfer`,
968
+ `set-max-supply`, and `minter configure|show|buy` warn on an unrecognized flag (warn, not refuse —
969
+ per `unknownFlags`' contract, a false warning must never break a script). These are the commands
970
+ where the 1155 semantics live in _optional_ flags that default rather than fail: a typo'd
971
+ `--amount 50` minted 1 copy and a typo'd `--quantity 5` bought 1 and paid 1×, both in total
972
+ silence. The broader gap — ~35 commands with no unknown-flag notice at all — is filed as B30.
973
+
974
+ A second verification sweep (six more cold agents, one funded) confirmed each fix above from a fresh
975
+ start and turned up four more, all fixed here:
976
+
977
+ - **A supply cap could be raised in `--dry-run` and only revert on send.** Both twins —
978
+ `set-max-invocations` (721) and `set-max-supply` (1155) — printed the new cap as fact for a value
979
+ the chain forbids, then reverted for real. A clean dry run is read as permission to send. Both now
980
+ read the current cap first and refuse up front; the 1155 side also refuses a cap **below** live
981
+ supply (the second way it reverted). Both guards fail **open** on an unreadable getter, since the
982
+ chain enforces the invariant anyway. (The 721 guard needed the Series ABI — reading `maxInvocations`
983
+ through the 1/1 ABI that `read()` uses throws, which would have made the guard silently never fire.)
984
+ - **`abx refresh` told edition owners ERC-4906 had already pinged marketplaces.** An ERC-1155 edition
985
+ emits no ERC-4906 at all — that is precisely why `ping-uri` exists — so the one sentence a creator
986
+ reads after editing metadata said "already handled" when nothing had been, and `ping-uri --help`
987
+ said the opposite. Two commands contradicting each other on the new lane. `refresh` is now
988
+ kind-aware and names `ping-uri` for editions.
989
+ - **`abx verify` ended in two ⚠ on a perfectly healthy fully-on-chain image drop** — "generator
990
+ reports NO code" and "no animation_url" — because the code-project lane runs whenever a tokenURI
991
+ renderer is set, which is true for every on-chain project, image or code. A static image has no
992
+ program and no `animation_url` by design. Three separate agents named this as the worst thing about
993
+ `verify`, and one said plainly it would make a creator distrust future real warnings. The code-lane
994
+ checks are now scoped to the code twins; a static drop gets one informational line instead. (This
995
+ was pre-existing on the 721 path too, not an editions regression.)
996
+ - **`EditionImage`'s SVG-only refusal was a thinner copy of its 721 twin's** — the Series version names
997
+ three routes (`--onchain-image`, `--backend ipfs|arweave`, or host off-chain); the edition version
998
+ said only "drop `--onchain-uri`". A cold agent asked for "3 artworks × 25 copies each" read that,
999
+ concluded there was no no-server option for one contract, and **fanned the collection out into three
1000
+ separate single-artwork contracts**. The refusal now states the real options for the lane and says
1001
+ explicitly that the off-chain-image-URL-in-on-chain-JSON route exists only per-artwork via
1002
+ `abx deploy --copies` — one contract per artwork, not one collection. The underlying capability gap
1003
+ is recorded in B28.
1004
+
1005
+ One knock-on from the first fix: the `false` branch of the canonical readout said _"not a clone of the
1006
+ **configured** factory"_ (singular), which now understates the check and misdirects the reader — six
1007
+ anchors are probed, so a `false` means no trust anchor the CLI knows deployed this contract, and the
1008
+ usual real cause is a **superseded** factory or a hand-deployed contract, not a misconfiguration. It
1009
+ now says so, and points at `abx doctor` for the current anchors.
1010
+
1011
+ Also: `deploy --help` no longer calls the edition sale stack (`--minter`/`--primary-payee`/
1012
+ `--unpaused`) "required" — it is optional, and an agent handed a creator hardcoded addresses as
1013
+ mandatory because of it; `set-field --help` now lists all eleven `--representation` values (it showed
1014
+ seven, omitting `url-template`, `renderer`, `sha256`, `inline-gzip`); and `abx doctor`'s label column
1015
+ widened so `edition factory` no longer runs into its address.
1016
+
1017
+ The skill-drift ✗ now names **which** copy is stale and prescribes the command that actually clears
1018
+ it — `abx skill install` writes the project-local copy only, so a stale **global** copy produced a ✗
1019
+ whose own remedy could not fix it. Six agents hit it in one sweep; several re-ran the install
1020
+ repeatedly and one left its sandbox to read `~/.claude/skills` to work out what the tool meant.
1021
+
1022
+ - 528c6c6: Wave-6 validation sweep: stop three help strings from denying features that work, make `storage show`
1023
+ honor the flags it advertises, and name the byte custody on both Series lanes.
1024
+
1025
+ Nine cold agents re-drove the edition surface after the B28/B30/B31 work. **Every new lane is reachable
1026
+ and no re-run regressed** — the situation that previously fanned "3 artworks × 25 copies" out into three
1027
+ separate contracts now reports _"Contracts deployed: 1, not 3"_; `--onchain-image` + edition + browser
1028
+ wallet went 4/5 at Haiku after being refused outright; the on-chain-dep edition lane went 3/5 → 4/5; the
1029
+ wrong-chain diagnosis went 2/5 → 3/5 on the strength of the new monotonic-cap guard. **B30 produced zero
1030
+ false warnings across all nine**, including a run that control-tested the mechanism with a deliberate
1031
+ bogus flag, and a funded run that exercised ~10 commands with real writes.
1032
+
1033
+ What the sweep found was mostly **documentation still describing behavior the same branch had replaced** —
1034
+ the failure mode a test suite cannot catch, because tests assert what the code does and none of them read
1035
+ the prose that tells an agent what to try:
1036
+
1037
+ - **`deploy --help`** said `--onchain-image` is "hot lane only on an edition (not yet wired for `--sign`)".
1038
+ It works on the wallet lane. Now states the real rule: hot and `--sign` both work; `--unsigned` is
1039
+ refused everywhere, 721 and edition alike, because each chunk tx's receipt feeds the next.
1040
+ - **`deploy-code --help`** listed `--dep`/`--dep-registry` among flags "refused in code" for `--copies`.
1041
+ They work. An agent verified all four flags directly and called this out as _"the dangerous direction:
1042
+ it would talk a creator out of a feature that actually works."_ Now says they work, and names the
1043
+ Sepolia-only registry constraint.
1044
+ - **The `EditionImage` SVG-only refusal** — a message rewritten earlier in the same branch — still said
1045
+ `--onchain-image` "is not wired on the edition-series lane yet", after that branch wired it. It now
1046
+ lists all four real routes for raster art in one contract.
1047
+ - **`abx refresh`'s ERC-4906 claim had no edition caveat** in the skill or `site/using-abx/guides/operate`,
1048
+ even though the CLI itself was made kind-aware earlier in the branch. The skill's _"`abx refresh` is the
1049
+ only nudge"_ was flatly false for an edition, where `ping-uri` is required.
1050
+
1051
+ Two real product fixes, both surfaced by the new warning doing its job:
1052
+
1053
+ - **`abx storage show` now honors `--backend`/`--gateway`/the rest of the storage override set.** It read
1054
+ env-only while its own output said _"choose per command: `--backend` … `--gateway`"_ — advertising flags
1055
+ it then discarded and always reporting `backend: fs`. This was invisible until every command grew an
1056
+ unknown-flag notice, which correctly flagged it (the flags really were ignored). A resolution preview
1057
+ that can't preview a resolution is the one thing it exists to do. Provenance now reports `--backend` as
1058
+ a source, alongside env and default.
1059
+ - **Both `deploy-series` lanes now name the byte custody** (`storage: ipfs (--backend) — byte custody for
1060
+ the image(s)`), as the 1/1 lane always has. A creator asking for "images on IPFS" had no way to confirm
1061
+ from the preview that their collection would pin there; the flag was accepted in silence.
1062
+
1063
+ Also documented, not changed: the ids × copies arithmetic. `deploy-code --copies 100` takes its id space
1064
+ from `--max`, which **defaults to 16** — so "an edition of 100" without `--max 1` means up to 16 different
1065
+ variations × 100 copies each. The dry-run states the real shape (`up to N id(s), M copies each`) and always
1066
+ did; nothing taught the translation, so the skill now does, for all three commands.
1067
+
1068
+ Three legibility nits were routed to B32 rather than fixed: `abx state` naming a contract "Series" where a
1069
+ refusal calls it "SeriesCode"; the Base-Sepolia default appearing only in a subordinate clause of the
1070
+ "no contract" error (three straight waves have lost time to it); and `--onchain-image` on a folder of large
1071
+ rasters honestly reporting 35 wallet approvals with no nudge toward the cheaper lane.
1072
+
1073
+ Two agent claims were checked and **rejected** so they don't get "fixed" into regressions: the skill does
1074
+ _not_ say `--onchain-image` is refused on an edition series (it documents the `--unsigned`-only refusal
1075
+ correctly), and `abx predict` does _not_ silently pick the 721 factory without `--copies` — it labels the
1076
+ lane, yields a different address, and echoes `--copies` in its reproduce line.
1077
+
1078
+ - Updated dependencies [528c6c6]
1079
+ - @artblocks/abx-sdk@0.1.0-alpha.13
1080
+ - @artblocks/abx-indexer@0.1.0-alpha.14
1081
+ - @artblocks/abx-storage@0.1.0-alpha.13
1082
+ - @artblocks/abx-token-api@0.1.0-alpha.16
1083
+
3
1084
  ## 0.1.0-alpha.20
4
1085
 
5
1086
  ### Minor Changes