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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 76 additions & 26 deletions docs/community-beasts-design.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ fully on-chain and are mintable through Loot Survivor dungeons.
| 15 | Art size caps | none on-chain — if the artist pays and the network accepts, it's valid |
| 16 | Death Mountain | not backwards compatible; DM gets a new interface version with `u64` entity IDs |
| 17 | Downstream consumers | games (Summit, tournaments, …) MUST allowlist species; self-declared tier makes unfiltered acceptance exploitable |
| 18 | Migration | current collection airdropped **1:1** into the new collection (holders keep their Beasts, re-encoded under the new token IDs); species 1–75 backfilled into the registry as read-only entries |
| 18 | Migration | holders move 1:1 into the new collection. Mainnet target is a player-initiated **`burn_and_mint`**: burn the V2 Beast, mint the V3 equivalent. Deferred — not built yet, and nothing in the design may foreclose it (see Migration) |
| 19 | Registry backfill of 1–75 | **deferred**, not required. Species 1–75 resolve from the baked-in `beast_definitions` tables; the registry is community-only (`FIRST_COMMUNITY_ID = 76`). Backfill stays possible later because no read path asserts a lower bound on registry keys |

## Architecture

Expand Down Expand Up @@ -548,14 +549,46 @@ costs 0 slots** — everything static rides in the token ID. `refresh_stats`
- Lost artist key = species config frozen as-is; minting continues through
the current minter unaffected.

## Migration: 1:1 airdrop from the current collection
## Migration: holders move 1:1 into the new collection

Every holder of the live collection receives the same Beasts in the new
Every holder of the live collection ends up with the same Beasts in the new
collection — same `(id, prefix, suffix, level, health, shiny, animated)`,
re-encoded under the 116-bit deterministic token IDs. Key property: **this
requires zero migration-specific code in `beasts_nft`.**

### Mechanism
re-encoded under the 116-bit deterministic token IDs.

### Mainnet target: player-initiated `burn_and_mint` (deferred)

The intended mainnet path is **not** an owner-run airdrop but a `burn_and_mint`
entrypoint players call themselves: present a V2 Beast, burn it, receive the
V3 equivalent. That keeps the migration permissionless, makes the V2 supply
verifiably retired, and removes the owner from the critical path.

**This is deferred and deliberately unbuilt.** The constraint carried through
every PR is that nothing may foreclose it. Concretely, what keeps the door
open:

- Mint validation lives in reusable helpers (`assert_can_mint`,
`MintingCoordinatorTrait::prepare_mint_with_traits`) rather than inlined in
the `mint` entrypoint, so a second entrypoint can reuse the whole chain
without relaxing any invariant.
- The `minted` uniqueness map starts empty except for the 75 genesis affix
slots, so every `(id, prefix, suffix)` a V2 holder could present is still
free to claim.
- Species 1–75 resolve tier/type/name from the baked-in tables with no
registry involvement, so burn-and-mint of an original Beast needs no
registry entry to exist.
- Token IDs are a pure function of beast attributes, so the V3 ID for any V2
Beast is computable off-chain before the contract exists.

One live hazard to respect at cutover: because V2 Beasts and V3 dungeon mints
draw from the same `(id, prefix, suffix)` space, a dungeon could mint a slot a
V2 holder still needs. Mainnet deploy must therefore leave `dungeon_address`
at zero (and community species paused, or registration closed) until
migration completes.

### Fallback mechanism: owner-run airdrop

Retained as the fallback if `burn_and_mint` slips. Requires zero
migration-specific code in `beasts_nft`.

- **Snapshot**: index the old collection (owners + `get_beast` per token) at a
stable block. If the old collection's `terminal_timestamp` has passed it is
Expand Down Expand Up @@ -595,14 +628,19 @@ writes for large species. Cost per beast ≈ the normal ~8 slot writes.
- **Rarity/uniqueness**: `(id, prefix, suffix)` entries repopulate through
the normal `minted` map.

### Registry backfill of species 1–75
### Registry backfill of species 1–75 — deferred, not required

Backfill was considered and **dropped from scope**. Species 1–75 resolve
name, tier, and type from `beast_definitions`, authorize against
`dungeon_address`, and render through the four legacy art contracts — all
without the registry. A backfill would only add a second, redundant lookup
surface for clients.

Species 1–75 get read-only registry entries at deploy (name/tier/type from
the genesis tables, artist = contract owner, minter mirroring
`dungeon_address`, art provider = a thin adapter over the legacy data
contracts or zero with the NFT's legacy branch as the renderer). Clients get
one lookup surface for all species; the NFT's auth and rendering branches for
`id <= 75` remain the on-chain source of truth.
It stays available later: `FIRST_COMMUNITY_ID = 76` gates *registration*
only, and no registry read path asserts a lower bound on a stored key, so a
future admin entrypoint could write entries for 1–75 without touching the
existing logic. `is_registered` and `assert_registered` would need their
range check widened at that point; nothing else would.

### Cutover sequence

Expand All @@ -617,13 +655,22 @@ one lookup surface for all species; the NFT's auth and rendering branches for

## Deployment sequence (fresh deploy, per repo policy)

1. Declare `StoredArtProvider` class.
2. Deploy `BeastRegistry(owner, stored_art_class_hash)`.
3. Deploy `beasts_nft(..., registry_address)` — constructor mints the 75
genesis beasts (entered into `minted`) and backfills species 1–75 into
the registry. No `terminal_timestamp` param.
4. `registry.set_nft_address(nft)` — one-time; registry reverts all
1. Declare and deploy the four genesis art data contracts.
2. Declare `StoredArtProvider` class (never deployed directly — the registry
deploys instances per species).
3. Deploy `BeastRegistry(owner, stored_art_class_hash)`.
4. Deploy `beasts_nft(name, symbol, owner, royalty_receiver,
royalty_fraction, 4 art providers, death_mountain)` — constructor mints
the 75 genesis beasts (entered into `minted`). No `terminal_timestamp`
param, and no registry param: the two contracts are wired after the fact
because each needs the other's address.
5. `registry.set_nft_address(nft)` — one-time; registry reverts all
registration until this is called.
6. `nft.set_registry_address(registry)` — one-time; community-species mints
and renders revert until this is called.

Steps 5 and 6 are both write-once and both required; a stack missing either
accepts no community species at all, which is the intended fail-closed state.

## Web app flow

Expand Down Expand Up @@ -683,15 +730,18 @@ Curation is UI/indexer-level only; the contract layer stays permissionless.
branch first, which includes it).
3. **PR 3 — registry + provider**: `BeastRegistry`, `StoredArtProvider`,
factory, `IBeastArtProvider`, name charset guard, registry tests.
4. **PR 4 — NFT integration**: mint auth, provenance mint, `token_uri`
routing, fan-out fixes (genesis token + bookmark min), cached
`refresh_stats`, integration tests.
4. **PR 4 — NFT integration**: per-species mint auth, provenance mint,
`token_uri` art/name/stats routing, render-time output validation for
untrusted providers, fan-out fix (genesis token), cached `refresh_stats`,
end-to-end integration tests. Stacked on PR 3 rather than on `main` —
the registry is not merged to `main` until the whole stack has been
exercised on Sepolia.
5. **PR 5 — SDK + web app**: TS SDK encode/decode + renderer; self-service
app (likely separate repo). Death Mountain `u64` interface update tracked
in the DM repo.
6. **PR 6 — migration tooling**: snapshot script, per-species
descending-power sort, `Migrator` contract + batch runner, cutover
runbook. Needed before mainnet deploy, independent of PRs 3–5.
6. **PR 6 — migration**: `burn_and_mint` entrypoint (see Migration), plus
snapshot/verification tooling and the cutover runbook. Needed before
mainnet, independent of PRs 3–5.

## Remaining open items

Expand Down
94 changes: 94 additions & 0 deletions docs/sepolia-v3-deployment.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
# Beasts V3 — Sepolia deployment

Deployed 2026-07-31 from branch `feat/nft-registry-integration` (PR #20,
stacked on #19). Tooling: **sncast 0.60.0** (`--network sepolia`). Not
starkli — it is unsupported and absent from this environment.

Deployer / owner / royalty receiver: sncast account `commit-reveal-sepolia`
`0x736faa0dca6a4569bf22471b574ddf42107f5af81d67e2cb9e1aa9bba7de76b`

Total cost: ~10 STRK.

## Deployed contracts

| Contract | Address |
|---|---|
| **beasts_nft** | `0x01dac77837c6751777d917051a6e405967c5c75f46df5ab7c635e52819634bfd` |
| **beast_registry** | `0x06d46c98087a1246182c6cd8ef144ee0a67da6e6cc9e44e39aef08cf92d30045` |
| beast_png_regular_data | `0x045f6cf8249ebee56f699a46cb66f02cbd23419f1c2cd3e62a3dfdedaf894279` |
| beast_png_shiny_data | `0x0291ad81a428262fd709f0075dedf69814173bf5b60f989cf7095f5efa72c670` |
| beast_gif_regular_data | `0x04a15db02fc7c991f2080e349cfbfb8f96f5fd61dd92c1cbe60ed7dbd4d49bfe` |
| beast_gif_shiny_data | `0x07bf05b8aa73d7fe6cc3bfb67efae46daa1260bd71634f08c43fd555cdf17631` |

## Declared class hashes

| Contract | Class hash |
|---|---|
| beasts_nft | `0x350e97a3244fecad9f850d84843a0effc26a364c392c2e8c4379cb5de0193ea` |
| beast_registry | `0x2afeefe9818b1c3fa839cef077cad5c6767bda41e6737e31ed44ed1a3fd6a97` |
| stored_art_provider | `0x2e3011cf968bbea8b72e75efdfe120318ccf61fe711d2f7f927114e2d8da56e` |
| beast_png_regular_data | `0x15d5742d2e7804531ac456b7ba82e9dc961ba154cbaaad631e2e7b4e887b68b` |
| beast_png_shiny_data | `0x3a1bfcae2737a12df248675d57c3a1a94eeceb5a696f14fa6f23fd99bf3d247` |
| beast_gif_regular_data | `0x1597fbb34f42f6b49fa2944943c74396ff7bdec4028743e02290b7c6714c900` |
| beast_gif_shiny_data | `0x44f9108354ce2a7699348ae13cb6c2dc595c8eff342c936bd9e0f3c79252939` |

`stored_art_provider` is declared but never deployed directly — the registry
deploys one instance per species with `salt = beast_id`.

## Wiring

Both pointers are write-once and both are required; a stack missing either
accepts no community species at all.

```
registry.set_nft_address(0x01dac778...) tx 0x06a0634be77503b1e32507406b046d320098f2f17a0142cc56dba07639e22fd5
nft.set_registry_address(0x06d46c98...) tx 0x02af8798debaea3fbd5949bafe2b506e048a8fed8cde08101e63149fa4236ded
```

`dungeon_address` is **unset (zero)**, so genesis species 1–75 cannot be
minted yet. That is deliberate: it is also the state mainnet must launch in
until the `burn_and_mint` migration completes, or a dungeon could claim an
`(id, prefix, suffix)` slot a V2 holder still needs.

## Verification performed

| Check | Result |
|---|---|
| Constructor genesis mint | `total_supply() == 75` |
| Genesis token ownership | `owner_of(0x7006400010000000000000000001)` → deployer |
| Genesis render (legacy art path) | `token_uri` → `"Warlock"`, new bestiary description |
| Permissionless registration | `register_beast_with_art('Gloomfang', Hunter, 3, ...)` → species **76** |
| Factory art provider auto-deploy | `0x4b11caad7b2b29949f957c0854d2fa3a37fc40519bfee2eb844d8f319ac19b9`, `factory_provider: true` |
| Provenance mint | `total_supply()` 75 → 76 |
| Per-species mint auth | `mint(..., 76, 1, 1, 10, 100, 0, 1)` from the registered minter succeeded |
| Community render | `"Agony Bane" Gloomfang`, Rank 1, 20,970-byte SVG |

Decoded attributes of the community mint — every value routed correctly:

```
Beast ID 76 Beast Gloomfang Type Hunter Tier 3 (from registry)
Prefix Agony Suffix Bane (shared tables)
Level 10 Health 100 Power 30 Rank 1
Shiny 0 Animated 1 Genesis 0
Adventurers Killed 0 Last Killed By 0 Last Death Timestamp 0
```

`animated = 1` selected the GIF variant (`R0lGODdh...`) from the factory
provider, confirming variant routing. Power 30 = level 10 × (6 − tier 3).
Stats are zero because no `stats_source` is set — the cache is read, never a
live call.

## Not done here

- **Registering the original 75 into the registry** — dropped from scope.
Species 1–75 resolve name/tier/type from `beast_definitions` and render
through the legacy art contracts without any registry entry. See the
design doc; a backfill remains possible later.
- `set_dungeon_address` — left zero, see above.
- `set_death_mountain_address` — zero; genesis stats read as 0.

## Reproducing

Declares must be run **one at a time with `--wait`**. Firing them
back-to-back produces `Invalid transaction nonce`, because sncast reads the
account nonce before the previous declare is accepted.
Loading
Loading