Add permissionless BeastRegistry and StoredArtProvider factory - #19
Add permissionless BeastRegistry and StoredArtProvider factory#19loothero wants to merge 6 commits into
Conversation
|
Caution The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased. |
There was a problem hiding this comment.
Pull request overview
Adds the “permissionless surface” for Community Beasts by introducing a new on-chain registry that can register new species (including deterministic factory deployment of a canonical art provider) and by defining the interfaces needed for upcoming NFT-side integration via provenance minting and metadata refresh fan-out.
Changes:
- Introduces
BeastRegistryfor permissionless community species registration + per-species admin controls (minter/art/provider/locks) and metadata refresh cooldown. - Adds canonical
StoredArtProvidercontract deployed per-species by the registry, with registry-gated updates and variant selection by shiny/animated flags. - Extends
interfaces.cairowith community registry/art provider interfaces and anIBeastsProvenancesurface (covered by a recording mock in tests).
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| src/beast_registry.cairo | New permissionless community species registry contract + name guard + packed per-species metadata/flags. |
| src/stored_art_provider.cairo | New canonical per-species art provider contract with registry-gated set_art. |
| src/interfaces.cairo | Adds new registry/art/provenance interfaces and related types for community species. |
| src/beast_registry_tests.cairo | Adds tests with a mock provenance NFT to validate registry flows, locks, cooldown, and provider gating. |
| src/lib.cairo | Wires the new registry/provider modules (and test module under cfg(test)) into the crate. |
Comments suppressed due to low confidence (5)
src/beast_registry.cairo:320
get_artistcurrently reads from storage without checking that the species is registered, which can silently return 0 for unknown IDs. For consistency with other registry reads (and to avoid ambiguity), assert registration before returning.
fn get_artist(self: @ContractState, beast_id: u64) -> ContractAddress {
self.artists.entry(beast_id).read()
}
src/beast_registry.cairo:324
get_art_providerreturns the default 0 address for unregistered IDs, which can later cause confusing downstream failures (e.g., calling a zero provider). Consider asserting the species is registered here to fail fast with a clear message.
fn get_art_provider(self: @ContractState, beast_id: u64) -> ContractAddress {
self.art_providers.entry(beast_id).read()
}
src/beast_registry.cairo:328
get_stats_sourcecurrently returns 0 for unregistered species IDs, which is ambiguous with the intended “no kill stats” sentinel. To avoid treating unknown species as valid-but-disabled, assert registration before reading.
fn get_stats_source(self: @ContractState, beast_id: u64) -> ContractAddress {
self.stats_sources.entry(beast_id).read()
}
src/beast_registry.cairo:347
is_art_lockedcurrently returnsfalsefor unregistered IDs because unpacking an all-zeroSpeciesMetayieldsart_locked = false. This makes “unknown species” indistinguishable from “known but unlocked”. Consider asserting registration here for consistency with other species-level reads.
fn is_art_locked(self: @ContractState, beast_id: u64) -> bool {
self.metas.entry(beast_id).read().art_locked
}
src/beast_registry.cairo:351
is_minter_lockedcurrently returnsfalsefor unregistered IDs due to default storage unpacking. To avoid callers treating unknown species as “unlocked”, assert registration before reading.
fn is_minter_locked(self: @ContractState, beast_id: u64) -> bool {
self.metas.entry(beast_id).read().minter_locked
}
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| fn get_minter(self: @ContractState, beast_id: u64) -> ContractAddress { | ||
| self.minters.entry(beast_id).read() | ||
| } |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: f3f69d966a
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| meta.factory_provider = provider == self.factory_providers.entry(beast_id).read(); | ||
| self.metas.entry(beast_id).write(meta); | ||
| self.art_providers.entry(beast_id).write(provider); |
There was a problem hiding this comment.
Fan out metadata updates when swapping providers
When an artist swaps the provider after tokens have been indexed, this changes every token's rendered artwork but emits no NFT metadata-update fan-out, so ERC-4906-aware marketplaces can retain the old artwork. This can become permanent if the artist subsequently calls lock_art, because notify_art_updated is then blocked; trigger the same cooldown-governed NFT notification as other art changes before allowing the new pointer to be frozen.
Useful? React with 👍 / 👎.
GPT Code Reviewlgtm |
|
Addressed all three review findings in the latest commit:
New tests cover injection payloads in art, wrong/missing prefixes, swap cooldown sharing + fan-out counts, and compliant/non-compliant stats sources — 108 passing. |
|
On the remaining HIGH finding (custom providers can brick/inject Partially accepted-by-design, partially deferred to the NFT-integration PR — recorded in
|
|
Both findings fixed in 1. Locked custom providers keep their refresh path (Medium) — correct catch, and it was an inconsistency with the design doc's own honesty note: I documented that 2. GIF signature (Low) — confirmed: New tests: locked-custom can notify / locked-factory cannot; both GIF versions accepted; invalid version character and truncated magic rejected. 116 passing, formatting clean. |
PR 3 of the Community Beasts plan (docs/community-beasts-design.md): the permissionless surface. Anyone can register a new Beast species in one transaction; the NFT integration (mint auth, provenance mint, token_uri routing) lands next. - BeastRegistry: sequential u64 species IDs from 76; two registration paths (factory-deployed StoredArtProvider with salt = beast_id, or a custom IBeastArtProvider address); per-species artist admin with set_minter (zero = paused), one-way lock_minter and lock_art, update_art / notify_art_updated sharing a per-species refresh cooldown, stats_source setter, and transferable artist role. Tier, type, and flags pack into one SpeciesMeta slot. Registration reverts until the owner wires set_nft_address (one-time). - On-chain name guard: charset [A-Za-z0-9 ' -], no edge spaces, non-empty, <= 31 bytes - an injection defense for the unescaped JSON/SVG builders. Name uniqueness deliberately NOT enforced (squatting grief vector); species ID is the identity. - set_art_provider recomputes the factory flag against the species' canonical factory deploy, so update_art can never write through another species' provider; StoredArtProvider.set_art double-gates on registry caller + species ID match. - IBeastArtProvider takes the full PackableBeast so providers select variants themselves and can customize by prefix/suffix/tier. - New IBeastsProvenance interface (mint_provenance, emit_species_metadata_update) that beasts_nft implements in the next PR; covered here by a recording mock. - 39 new tests: meta packing, name-guard injection payloads, both registration paths, admin auth, both locks, cooldown sharing, factory-flag recompute, provider gating, artist transfer. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…s gate All three MEDIUM findings from the automated Cairo review: - StoredArtProvider now validates every URI in both the constructor and set_art: exact PNG/GIF data-URI prefix per slot plus a non-empty standard-base64 payload. The renderer embeds these verbatim in a single-quoted SVG attribute and factory providers carry the trusted "verified art" designation, so factory-stored content must be provably inert. - set_art_provider now shares the per-species refresh cooldown and fans out MetadataUpdate atomically with the pointer change - a swap changes every token's rendered art, and without the fan-out a subsequent lock_art would leave marketplaces permanently stale. - set_stats_source now performs the design-specified set-time check: a non-zero source must be a deployed contract answering SRC5 for the new IBEAST_STATS_ID (IBeastStats interface defined for the PR 4 cached-stats flow; zero clears without any call). Tests: art payload/prefix/markup rejection, swap cooldown + fan-out, compliant/non-compliant stats sources. 108 passing. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Follow-up to the Codex re-review: the charset check alone admitted structurally malformed base64 (odd length, mid-string padding), which could be locked permanently as broken factory art. The validator now requires payload length % 4 == 0, '=' only in the final two positions (never '=X'), and the encoded image signature - PNG's magic bytes always base64-encode to the "iVBORw0KGgo" prefix and GIF87a/89a to "R0lGOD", so magic verification is a prefix comparison with no on-chain decoding. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Response to the final Codex review finding on custom art providers: the species-scoped brick risk is accepted by design (self-inflicted, contained, and the reason the factory path carries the verified badge), but the injection half gets a concrete mitigation - token_uri will validate custom provider output (base64 image data URI, wider mime allowlist) before embedding, landing with the render path in the NFT integration PR. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two review findings: - notify_art_updated now gates on art_locked only for factory providers. lock_art freezes a factory provider's output completely (its sole mutator is registry-gated set_art), so a refresh there could never carry new art. For a custom provider the lock freezes only the pointer - the provider contract may still change what it returns - so blocking notification would strand ERC-4906 consumers on permanently stale metadata for that species. The shared cooldown still applies. - GIF magic validation now requires a complete encoded signature: R0lGODdh (GIF87a) or R0lGODlh (GIF89a). The previous 6-character "R0lGOD" prefix admitted payloads such as R0lGODAAAAAA, which decodes to the invalid header GIF80. PNG unchanged (iVBORw0KGgo is already a complete signature plus the implied IHDR length byte). Tests: locked custom provider can still notify, locked factory cannot; GIF87a/GIF89a both accepted, invalid version and truncated magic rejected. 116 passing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
3d517d5 to
9078668
Compare
The registry stored an artists map alongside a transferable Genesis Beast, which is two sources of truth for one thing. Sell the creator token and an invisible role stayed behind; every UI then has to explain which one actually governs. Now there is one. Permissioned entrypoints resolve the artist as owner_of(genesis_token_id(beast_id)), so control moves with the token on any marketplace and the two can never diverge. transfer_artist_role is deleted — the ERC721 transfer is the transfer. genesis_token_id is derived rather than stored: encode_token_id over the canonical (id, 0, 0) shape with the species' registered traits. That shape now has a single definition in beast_manager::genesis_beast, because the registry and the NFT must compute the same token or the role would point at nothing. Costs taken knowingly: each permissioned write makes one owner_of call to the NFT (owner-set and write-once, so not an untrusted call), and mis-sending a Genesis Beast freezes that species' admin. Burning cannot cause that — the enumeration component rejects burns — but a bad transfer can, same as any NFT. Saves a storage slot per species and removes an entrypoint. Also unlocks client-side lookup: token_of_owner_by_index plus a local decode_token_id tells a wallet which species it controls, with no registry reads and no event scanning. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Summary
PR 3 of the Community Beasts plan (
docs/community-beasts-design.md): the permissionless surface. Anyone can add a new Beast species — art (4 variants), name, type, tier, and a minter/dungeon address — in a single transaction. The NFT-side integration (per-species mint auth, provenance mint,token_urirouting, fan-out) lands in PR 4; the registry already drives it through a newIBeastsProvenanceinterface, covered here by a recording mock.Changes
src/beast_registry.cairo— the registry contract:u64species IDs starting at 76;is_registered/species_count/get_species_traitsreads for the NFT and clients.register_beast_with_art(factory-deploys the canonicalStoredArtProviderviadeploy_syscallwithsalt = beast_id— deterministic address per species) andregister_beast(customIBeastArtProvideraddress, non-zero required).set_minter(zero = paused), one-waylock_minterandlock_art,update_art(factory providers only) andnotify_art_updated(custom providers) sharing one per-species refresh cooldown (1h constant, tunable pre-deploy),set_stats_source(wired for PR 4's cached kill stats), andtransfer_artist_role(non-zero only).SpeciesMetafelt252 slot (customStorePacking), keeping registration at 5 registry slots + 1 factory-address slot.mint_provenancecall (the NFT reads tier/type back to encode the Genesis token ID); registration reverts until the owner's one-timeset_nft_address.Security-review items implemented:
assert_valid_name): charset[A-Za-z0-9 ' -], no leading/trailing space, non-empty, ≤31 bytes — the injection defense for the unescaped JSON/SVG builders. No uniqueness check by design (name-squatting grief vector); species ID is the identity.set_art_providerrecomputesfactory_provideragainst the species' canonical factory deploy on every swap, soupdate_artcan never write through another species' provider.StoredArtProvider.set_artrequires registry caller AND species-ID match;get_data_uriasserts the decoded beast's species.src/stored_art_provider.cairo— canonical per-species art provider: four stored data URIs, variant selected from the decoded beast's shiny/animated flags. Not upgradable; factory +lock_art= provably frozen art.src/interfaces.cairo—IBeastRegistry,IBeastArtProvider(takes the fullPackableBeastper design decision),IStoredArtProvider,IBeastsProvenance,BeastType,BeastDefinition. The legacyu8provider interface is untouched (genesis data contracts).Testing
scarb build— clean;scarb fmt --check --workspace— cleansnforge test --max-n-steps 4294967295— 102 passed, 0 failed, 305 ignored (+39 vs main)",\,<, control bytes, comma, edge spaces, empty); both registration paths incl. zero-provider/tier-bounds rejection; duplicate names allowed; sequential IDs; pre-wiring reverts; one-timeset_nft_address; artist-only auth; both locks one-way and blocking; shared cooldown across both art mutators; factory-flag recompute round trip; provider set_art/render gating; artist transfer incl. rights loss and zero-target rejection; provenance-mint args verified via mock.@codex review
🤖 Generated with Claude Code