@bannynet/core-v6 0.0.16 → 0.0.18

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.
package/README.md CHANGED
@@ -1,115 +1,40 @@
1
1
  # Banny Retail
2
2
 
3
- On-chain composable avatar system for Juicebox 721 collections -- manages Banny character bodies, backgrounds, and outfit NFTs with layered SVG rendering. Bodies can be dressed with outfits and placed on backgrounds, all composed into fully on-chain SVG images with base64-encoded JSON metadata.
3
+ Banny Retail is an on-chain avatar system for Juicebox 721 collections. A body NFT can wear outfit NFTs, sit on a background NFT, and render the full composition as an on-chain SVG and base64 metadata payload.
4
4
 
5
- [Docs](https://docs.juicebox.money) | [Discord](https://discord.gg/juicebox)
5
+ Docs: <https://docs.juicebox.money>
6
+ Architecture: [ARCHITECTURE.md](./ARCHITECTURE.md)
6
7
 
7
- ## Conceptual Overview
8
+ ## Overview
8
9
 
9
- Banny is a composable NFT character system built on top of Juicebox 721 hooks. Each Banny is a body NFT (Alien, Pink, Orange, or Original) that can wear outfit NFTs and sit on a background NFT. The resolver composes these layers into a single SVG image, fully on-chain.
10
+ This is a resolver-centric application built on top of [`@bananapus/721-hook-v6`](https://www.npmjs.com/package/@bananapus/721-hook-v6). The resolver owns attached outfit and background NFTs while a body is decorated, then composes the active layers into a single token URI response.
10
11
 
11
- Outfit changes can be locked for 7 days via `lockOutfitChangesFor`. This proves that a Banny's appearance is stable -- useful for PFPs, social displays, or any context where others rely on a Banny looking the way it does right now. While locked, neither the owner nor anyone else can change or remove the body's outfits or background.
12
+ The main user flows are:
12
13
 
13
- ### How It Works
14
+ - mint body, outfit, and background NFTs through a Juicebox 721 hook
15
+ - attach accessories to a body with `decorateBannyWith`
16
+ - optionally freeze the current look for seven days with `lockOutfitChangesFor`
17
+ - upload SVG payloads lazily after an owner registers their content hashes
14
18
 
15
- ```
16
- 1. A Juicebox 721 hook registers Banny721TokenUriResolver as its token URI resolver
17
- → All tokenURI() calls are forwarded to the resolver
18
- |
19
- 2. Users mint Banny body NFTs + outfit/background NFTs via the 721 hook
20
- → Bodies are the "base" layer; outfits and backgrounds are accessories
21
- |
22
- 3. Body owner calls decorateBannyWith(hook, bodyId, backgroundId, outfitIds)
23
- → Outfit and background NFTs are transferred to the resolver contract
24
- → Resolver tracks which body wears which outfits
25
- → Body's tokenURI now renders the full dressed composition
26
- |
27
- 4. Outfit lock (optional): lockOutfitChangesFor(hook, bodyId)
28
- → Freezes outfit and background changes for 7 days
29
- → Prevents moving currently equipped assets away through another body's decoration call
30
- → Proves the Banny's look is stable (useful for PFPs, displays)
31
- |
32
- 5. SVG content is stored on-chain via a two-step process:
33
- → Owner registers content hashes: setSvgHashesOf(upcs, hashes)
34
- → Anyone uploads matching content: setSvgContentsOf(upcs, contents)
35
- → Falls back to IPFS base URI if on-chain content not yet stored
36
- ```
19
+ Use this repo when you need collection-specific, fully on-chain metadata composition on top of Juicebox NFTs. Do not use it as a generic 721 hook; it is an application-layer resolver, not a protocol NFT primitive.
37
20
 
38
- ### Decoration Flow
39
-
40
- ```mermaid
41
- sequenceDiagram
42
- participant Owner as Body Owner
43
- participant Resolver as Banny721TokenUriResolver
44
- participant Hook as 721 Hook (NFT Contract)
45
-
46
- Owner->>Resolver: decorateBannyWith(hook, bodyId, bgId, outfitIds)
47
- Resolver->>Hook: ownerOf(bodyId) -- verify caller owns body
48
- Hook-->>Resolver: owner address
49
-
50
- Note over Resolver: Check body is not locked
51
-
52
- alt Background is changing
53
- Resolver->>Hook: transferFrom(owner, resolver, bgId)
54
- Note over Resolver: Store bgId as body's background
55
- opt Previous background exists
56
- Resolver->>Hook: transferFrom(resolver, owner, prevBgId)
57
- end
58
- end
59
-
60
- loop Each outfit in outfitIds
61
- Resolver->>Hook: transferFrom(owner, resolver, outfitId)
62
- Note over Resolver: Track outfitId as worn by body
63
- opt Previous outfit in same category
64
- Resolver->>Hook: transferFrom(resolver, owner, prevOutfitId)
65
- end
66
- end
67
-
68
- Note over Resolver: tokenURI(bodyId) now renders composed SVG
69
- Owner->>Resolver: tokenURI(bodyId)
70
- Resolver-->>Owner: base64-encoded JSON with layered SVG
71
- ```
21
+ If a bug changes tier pricing, mint eligibility, or treasury flow, it is probably not here first. Start in the 721 hook repo and only come here once the issue is clearly in attachment, custody, or rendering behavior.
22
+
23
+ ## Key Contract
24
+
25
+ | Contract | Role |
26
+ | --- | --- |
27
+ | `Banny721TokenUriResolver` | Resolves token metadata, stores equipped accessories, enforces outfit locks, and renders layered SVG output for Banny collections. |
28
+
29
+ ## Mental Model
72
30
 
73
- ### Asset Categories
74
-
75
- | Category ID | Name | Slot Rules |
76
- |-------------|------|------------|
77
- | 0 | Body | Base character. One of four types: Alien, Pink, Orange, Original. |
78
- | 1 | Background | Scene behind the Banny. One per body. |
79
- | 2 | Backside | Layer rendered behind the body. |
80
- | 3 | Necklace | Accessory (default provided if none attached). |
81
- | 4 | Head | Full head accessory. Blocks eyes, glasses, mouth, and headtop. |
82
- | 5 | Eyes | Eye style (defaults: alien or standard based on body type). |
83
- | 6 | Glasses | Eyewear layer. Blocked by head. |
84
- | 7 | Mouth | Mouth expression (default provided). Blocked by head. |
85
- | 8 | Legs | Lower body clothing. |
86
- | 9 | Suit | Full body one-piece. Blocks suit top and suit bottom. |
87
- | 10 | Suit Bottom | Lower suit piece. Blocked by full suit. |
88
- | 11 | Suit Top | Upper suit piece. Blocked by full suit. |
89
- | 12 | Headtop | Top-of-head accessory. Blocked by head. |
90
- | 13 | Hand | Held item layer. |
91
- | 14-17 | Special | Special suit, legs, head, and body overlays. |
92
-
93
- ### Body Types
94
-
95
- | UPC | Type | Color Palette |
96
- |-----|------|--------------|
97
- | 1 | Alien | Green tones (`67d757`, `30a220`, `217a15`, `09490f`) with purple accents |
98
- | 2 | Pink | Pink tones (`ffd8c5`, `ff96a9`, `fe588b`, `c92f45`) |
99
- | 3 | Orange | Orange tones (`f3a603`, `ff7c02`, `fd3600`, `c32e0d`) |
100
- | 4 | Original | Yellow tones (`ffe900`, `ffc700`, `f3a603`, `965a1a`) |
101
-
102
- ## Architecture
103
-
104
- | Contract | Description |
105
- |----------|-------------|
106
- | `Banny721TokenUriResolver` | The sole contract. Implements `IJB721TokenUriResolver` to serve fully on-chain SVG token URIs for any Juicebox 721 hook. Manages outfit attachment, background assignment, outfit locking, on-chain SVG storage, and layered SVG rendering. Inherits `Ownable`, `ReentrancyGuard`, `ERC2771Context`, `IERC721Receiver`. |
107
-
108
- ### Interface
109
-
110
- | Interface | Description |
111
- |-----------|-------------|
112
- | `IBanny721TokenUriResolver` | Public API: `tokenUriOf`, `svgOf`, `decorateBannyWith`, `lockOutfitChangesFor`, `assetIdsOf`, `namesOf`, `userOf`, `wearerOf`, SVG management, metadata management, plus all events. |
31
+ This repo owns three things:
32
+
33
+ 1. custody of attached outfit and background NFTs while equipped
34
+ 2. rules around what a body can wear and when that can change
35
+ 3. rendering of the final token metadata payload
36
+
37
+ It does not own mint pricing, tier issuance, or project accounting.
113
38
 
114
39
  ## Install
115
40
 
@@ -117,84 +42,45 @@ sequenceDiagram
117
42
  npm install @bannynet/core-v6
118
43
  ```
119
44
 
120
- If using Forge directly:
45
+ ## Development
46
+
47
+ The contract stack relies on `via_ir = true` in `foundry.toml`.
121
48
 
122
49
  ```bash
123
- forge install
50
+ npm install
51
+ forge build
52
+ forge test
124
53
  ```
125
54
 
126
- ## Develop
55
+ Useful scripts:
127
56
 
128
- Requires `via_ir = true` in foundry.toml due to stack depth in SVG composition.
57
+ - `npm run deploy:mainnets`
58
+ - `npm run deploy:testnets`
59
+ - `npm run deploy:mainnets:drop:1`
60
+ - `npm run deploy:testnets:drop:1`
129
61
 
130
- | Command | Description |
131
- |---------|-------------|
132
- | `forge build` | Compile contracts (requires via-IR) |
133
- | `forge test` | Run all tests (14 test files: functionality, attacks, decoration flows, fork integration, transfer lifecycle, audit gaps, QA, regressions) |
134
- | `forge test -vvv` | Run tests with full trace |
62
+ ## Deployment Notes
63
+
64
+ Deployments are handled through Sphinx using the environments configured in `script/Deploy.s.sol`. The resolver is intended to be plugged into a Juicebox 721 hook as that hook's token URI resolver.
135
65
 
136
66
  ## Repository Layout
137
67
 
138
- ```
68
+ ```text
139
69
  src/
140
- Banny721TokenUriResolver.sol # Sole contract (~1,428 lines)
70
+ Banny721TokenUriResolver.sol
141
71
  interfaces/
142
- IBanny721TokenUriResolver.sol # Public interface + events
143
72
  test/
144
- Banny721TokenUriResolver.t.sol # Unit tests
145
- BannyAttacks.t.sol # Security/adversarial tests
146
- DecorateFlow.t.sol # Decoration flow tests
147
- Fork.t.sol # Fork integration tests
148
- OutfitTransferLifecycle.t.sol # Transfer lifecycle tests
149
- TestAuditGaps.sol # Audit gap coverage (meta-tx, SVG edge cases)
150
- TestQALastMile.t.sol # QA tests (gas, round-trip, fallback)
151
- regression/
152
- ArrayLengthValidation.t.sol # Input validation regression
153
- BodyCategoryValidation.t.sol # Body category regression
154
- BurnedTokenCheck.t.sol # Burned token regression
155
- CEIReorder.t.sol # CEI ordering regression
156
- ClearMetadata.t.sol # Metadata clearing regression
157
- MsgSenderEvents.t.sol # Event emission regression
158
- RemovedTierDesync.t.sol # Removed tier desync regression
73
+ unit, attack, fork, audit, QA, and regression coverage
159
74
  script/
160
- Deploy.s.sol # Sphinx multi-chain deployment
161
- Drop1.s.sol # Outfit drop deployment
162
- Add.Denver.s.sol # Denver-specific deployment
75
+ Deploy.s.sol
76
+ Drop1.s.sol
77
+ Add.Denver.s.sol
163
78
  helpers/
164
- BannyverseDeploymentLib.sol # Deployment artifact reader
165
- MigrationHelper.sol # Migration utilities
166
79
  ```
167
80
 
168
- ## Permissions
169
-
170
- | Action | Who Can Do It |
171
- |--------|--------------|
172
- | `decorateBannyWith` | Body owner (must also own or have worn-by-body access to outfits/backgrounds) |
173
- | `lockOutfitChangesFor` | Body owner |
174
- | `setSvgHashesOf` | Contract owner only |
175
- | `setSvgContentsOf` | Anyone (content validated against registered hash) |
176
- | `setProductNames` | Contract owner only |
177
- | `setMetadata` | Contract owner only |
178
-
179
- ## Supported Chains
180
-
181
- Deployed via Sphinx deterministic deployment (`script/Deploy.s.sol`).
182
-
183
- | Network | Chain ID |
184
- |---------|----------|
185
- | Ethereum | 1 |
186
- | Optimism | 10 |
187
- | Base | 8453 |
188
- | Arbitrum | 42161 |
189
- | Ethereum Sepolia | 11155111 |
190
- | Optimism Sepolia | 11155420 |
191
- | Base Sepolia | 84532 |
192
- | Arbitrum Sepolia | 421614 |
193
-
194
- ## Risks
195
-
196
- - **Outfit custody:** Attached outfits and backgrounds are held by the resolver contract. If a return transfer fails (e.g., owner is a non-receiver contract), the asset is retained in the attached list rather than stranded — the owner can retry once the issue is resolved. Permanently unrecoverable assets (burned NFTs) create phantom entries in SVG rendering.
197
- - **7-day lock is fixed.** Cannot be shortened or cancelled once set. The lock duration is hardcoded.
198
- - **SVG immutability.** Once SVG content is stored on-chain for a UPC, it cannot be changed. A mistake in the content requires deploying a new resolver.
199
- - **Hash registration is owner-only, but content upload is not.** Anyone can call `setSvgContentsOf` as long as the content matches the registered hash. This is by design for lazy uploads.
200
- - **Single resolver per hook.** The resolver is set on the 721 hook and applies to all tiers. Different collections would need different resolver instances.
81
+ ## Risks And Notes
82
+
83
+ - attached outfits and backgrounds are custodied by the resolver while equipped
84
+ - outfit locks are fixed-duration and cannot be shortened once set
85
+ - on-chain SVG content is immutable once uploaded for a given registered hash
86
+ - rendering quality and metadata correctness depend on the integrity of uploaded SVG assets
package/RISKS.md CHANGED
@@ -1,4 +1,21 @@
1
- # RISKS.md -- banny-retail-v6
1
+ # Banny Retail Risk Register
2
+
3
+ This file focuses on failure modes that can break NFT custody, let untrusted hook integrations bypass assumptions, or leave a rendered Banny in a state that does not match the assets users think they own.
4
+
5
+ ## How to use this file
6
+
7
+ - Read `Priority risks` first; those are the highest-signal failure modes for operators, auditors, and integrators.
8
+ - Use `Accepted Behaviors` to separate intentional tradeoffs from genuine bugs.
9
+ - Treat `Invariants to Verify` as required test and audit targets.
10
+
11
+ ## Priority risks
12
+
13
+ | Priority | Risk | Why it matters | Primary controls |
14
+ |----------|------|----------------|------------------|
15
+ | P0 | Untrusted `hook` or store integration | The caller chooses the hook, and the resolver trusts it for ownership checks, tier metadata, and transfers. A bad hook can fake authority or trap assets. | Operationally restrict supported hooks, scrutinize sections 1, 3, and 5, and test with hostile hook behavior. |
16
+ | P1 | Silent transfer failure retention | Failed returns intentionally keep attachment records to avoid stranding NFTs, but this can leave phantom render state if the underlying asset is gone forever. | Explicit accepted-behavior rules, retained-item handling, and invariants around custody/state correspondence. |
17
+ | P1 | Sale-time outfit lock griefing | A seller can transfer a locked body and force the buyer to wait up to 7 days before changing outfits. | Fixed-duration lock, marketplace disclosure, and user education before secondary sales. |
18
+
2
19
 
3
20
  ## 1. Trust Assumptions
4
21
 
package/SKILLS.md CHANGED
@@ -1,256 +1,40 @@
1
1
  # Banny Retail
2
2
 
3
- ## Purpose
4
-
5
- On-chain composable NFT avatar system that renders Banny characters with layered SVG outfits and backgrounds for Juicebox 721 collections. Bodies can be dressed, locked, and rendered entirely on-chain.
6
-
7
- ## Contracts
8
-
9
- | Contract | Role |
10
- |----------|------|
11
- | `Banny721TokenUriResolver` | Resolves token URIs for any Juicebox 721 hook by composing layered SVGs from registered asset content. Manages outfit attachment, background assignment, outfit locking, on-chain SVG storage, and metadata generation. Inherits `Ownable`, `ReentrancyGuard`, `ERC2771Context`, `IERC721Receiver`. (~1,331 lines) |
12
-
13
- ## Key Functions
14
-
15
- ### Token URI & Rendering
16
-
17
- | Function | What it does |
18
- |----------|-------------|
19
- | `tokenUriOf(hook, tokenId)` | Returns base64-encoded JSON metadata URI with on-chain SVG image. For bodies: composes dressed Banny with attached outfits and background. For outfit/background tokens: renders the item on a grayscale mannequin Banny. |
20
- | `svgOf(hook, tokenId, shouldDressBannyBody, shouldIncludeBackgroundOnBannyBody)` | Returns the composed SVG string. Layers (in order): background, backside, body, necklace, eyes, mouth, outfits by category. Falls back to IPFS base URI if on-chain content not stored. |
21
-
22
- ### Decoration
23
-
24
- | Function | What it does |
25
- |----------|-------------|
26
- | `decorateBannyWith(hook, bannyBodyId, backgroundId, outfitIds)` | Attaches background and outfits to a body. Transfers assets to contract custody. Validates ownership, lock status, category ordering, slot conflicts. Attempts to return previously attached items to caller — if a return fails, the item is retained (not stranded). Emits `DecorateBanny`. |
27
- | `lockOutfitChangesFor(hook, bannyBodyId)` | Locks a body's outfit for 7 days. Cannot accelerate an existing lock. Caller must own the body NFT. |
28
-
29
- ### Views
30
-
31
- | Function | What it does |
32
- |----------|-------------|
33
- | `assetIdsOf(hook, bannyBodyId)` | Returns `(backgroundId, outfitIds[])` currently attached to body. Filters out stale entries where `_wearerOf` no longer matches. |
34
- | `namesOf(hook, tokenId)` | Returns `(fullName, categoryName, productName)`. Full name includes inventory count (e.g., "42/100"). |
35
- | `userOf(hook, backgroundId)` | Returns the body ID using a background, or 0 if unused. Validates consistency. |
36
- | `wearerOf(hook, outfitId)` | Returns the body ID wearing an outfit, or 0 if unworn. Validates outfit is in body's attached array. |
37
-
38
- ### SVG Content Management
39
-
40
- | Function | Who | What it does |
41
- |----------|-----|-------------|
42
- | `setSvgHashesOf(upcs, svgHashes)` | Owner only | Registers expected SVG content hashes for UPCs. Immutable once set. |
43
- | `setSvgContentsOf(upcs, svgContents)` | **Anyone** | Stores on-chain SVG content. Must match previously registered hash. Cannot overwrite existing content. |
44
- | `setProductNames(upcs, names)` | Owner only | Sets human-readable names for product UPCs. |
45
- | `setMetadata(description, url, baseUri)` | Owner only | Updates token metadata description, external URL, and SVG base URI. Empty strings clear the field. |
46
-
47
- ### Token Receipt
48
-
49
- | Function | What it does |
50
- |----------|-------------|
51
- | `onERC721Received(operator, from, tokenId, data)` | Validates token receipt. Reverts unless `operator == address(this)`. Called when contract takes custody of outfits/backgrounds during decoration. |
52
-
53
- ## Integration Points
54
-
55
- | Dependency | Import | Used For |
56
- |------------|--------|----------|
57
- | `@bananapus/721-hook-v6` | `IJB721TiersHook`, `IJB721TiersHookStore`, `IJB721TokenUriResolver`, `JB721Tier`, `JBIpfsDecoder`, `IERC721` | Token ownership checks, tier/product data resolution, IPFS URI decoding, hook store queries, safe transfers |
58
- | `@openzeppelin/contracts` | `Ownable`, `ReentrancyGuard`, `ERC2771Context`, `IERC721Receiver`, `Strings` | Access control, reentrancy protection, meta-transactions, safe NFT receipt, string utilities |
59
- | `lib/base64` | `Base64` | Base64 encoding for on-chain SVG and JSON metadata |
60
-
61
- ### Deployment / Setup Sequence
62
-
63
- The resolver is connected to a 721 hook in one of two ways:
64
-
65
- 1. **At hook deployment**: Pass the resolver address as `tokenUriResolver` in `JBDeploy721TiersHookConfig` when calling `IJB721TiersHookDeployer.deployHookFor(...)`. The hook's `initialize()` stores it in the hook store via `recordSetTokenUriResolver`.
66
- 2. **After deployment**: Call `hook.setMetadata(...)` with the resolver address as the `tokenUriResolver` parameter. Requires `SET_721_METADATA` permission. Pass `IJB721TokenUriResolver(address(this))` as a sentinel value to leave it unchanged, since `address(0)` clears the resolver.
67
-
68
- Once set, the hook's store maps `tokenUriResolverOf[hook]` to the resolver address. Any `tokenURI(tokenId)` call on the hook delegates to `resolver.tokenUriOf(hook, tokenId)`, which composes the on-chain SVG.
69
-
70
- ## Rendering Order
3
+ ## Use This File For
71
4
 
72
- The composed SVG layers elements back-to-front. Later layers are drawn on top of earlier layers. The full rendering order for a dressed body is:
5
+ - Use this file when the task involves Banny outfit attachment, layered SVG rendering, token URI composition, or asset custody and lock behavior.
6
+ - Start here, then open the resolver, scripts, or tests that match the exact rendering or attachment path in question.
73
7
 
74
- 1. **Background** (category 1) -- if attached and `shouldIncludeBackgroundOnBannyBody` is true
75
- 2. **Body** (category 0) -- the base Banny character with color-fill styles (`b1`-`b4`, `a1`-`a3`)
76
- 3. **Backside** (category 2) -- rendered behind the body in the SVG source but layered by the SVG's internal z-ordering
77
- 4. **Necklace** (category 3) -- default injected if none attached; custom necklaces are deferred and rendered after suit top (category 11)
78
- 5. **Head** (category 4) -- if present, suppresses default injection of eyes, mouth
79
- 6. **Eyes** (category 5) -- default alien or standard eyes injected if no head and no explicit eyes
80
- 7. **Glasses** (category 6)
81
- 8. **Mouth** (category 7) -- default mouth injected if no head and no explicit mouth
82
- 9. **Legs** (category 8)
83
- 10. **Suit** (category 9)
84
- 11. **Suit Bottom** (category 10)
85
- 12. **Suit Top** (category 11)
86
- 13. **Custom Necklace** -- rendered here (after suit top) if a custom necklace was provided, so it layers on top of clothing
87
- 14. **Headtop** (category 12)
88
- 15. **Hand** (category 13)
89
- 16. **Special overlays** (categories 14-17) -- suit, legs, head, body specials
8
+ ## Read This Next
90
9
 
91
- For non-body tokens (outfits, backgrounds), the item is rendered on a grayscale mannequin Banny (`fill:#808080`) for preview.
10
+ | If you need... | Open this next |
11
+ |---|---|
12
+ | Repo overview and user-facing behavior | [`README.md`](./README.md), [`ARCHITECTURE.md`](./ARCHITECTURE.md) |
13
+ | Resolver implementation | [`src/Banny721TokenUriResolver.sol`](./src/Banny721TokenUriResolver.sol) |
14
+ | Deployment or scripted drops | [`script/Deploy.s.sol`](./script/Deploy.s.sol), [`script/Drop1.s.sol`](./script/Drop1.s.sol), [`script/Add.Denver.s.sol`](./script/Add.Denver.s.sol) |
15
+ | Decoration lifecycle and regressions | [`test/DecorateFlow.t.sol`](./test/DecorateFlow.t.sol), [`test/OutfitTransferLifecycle.t.sol`](./test/OutfitTransferLifecycle.t.sol), [`test/regression/`](./test/regression/) |
16
+ | Adversarial or QA coverage | [`test/BannyAttacks.t.sol`](./test/BannyAttacks.t.sol), [`test/TestQALastMile.t.sol`](./test/TestQALastMile.t.sol), [`test/audit/`](./test/audit/) |
92
17
 
93
- ## SVG Format Requirements
18
+ ## Repo Map
94
19
 
95
- - **Canvas**: All SVGs are rendered within a `400x400` viewport: `<svg width="400" height="400" viewBox="0 0 400 400">`.
96
- - **Fragment, not document**: SVG content stored via `setSvgContentsOf` must NOT include the parent `<svg>` element. The resolver wraps all fragments in the outer SVG element with shared styles (`.o{fill:#050505;}.w{fill:#f9f9f9;}`).
97
- - **Hash verification only**: The contract validates uploaded SVG content against a pre-registered `keccak256` hash but performs no structural validation (no dimension checks, no viewBox enforcement). Ensuring content renders correctly at 400x400 is the uploader's responsibility.
98
- - **Body color classes**: Body SVGs use CSS classes `b1`-`b4` (body fills) and `a1`-`a3` (accent fills) which are injected as `<style>` elements based on the body's UPC. Outfit SVGs can reference `.o` (dark, #050505) and `.w` (light, #f9f9f9) classes provided by the wrapper.
99
- - **Immutability**: Once stored, SVG content cannot be changed. A content or hash error requires deploying a new resolver instance.
20
+ | Area | Where to look |
21
+ |---|---|
22
+ | Main contract | [`src/Banny721TokenUriResolver.sol`](./src/Banny721TokenUriResolver.sol) |
23
+ | Scripts | [`script/`](./script/) |
24
+ | Tests | [`test/`](./test/) |
100
25
 
101
- ## Key Types
102
-
103
- ### Asset Categories
104
-
105
- | ID | Name | Slot Rules |
106
- |----|------|------------|
107
- | 0 | Body | Base character. Owns outfits and backgrounds. |
108
- | 1 | Background | One per body. |
109
- | 2 | Backside | Behind body layer. |
110
- | 3 | Necklace | Default provided if none attached. |
111
- | 4 | Head | Blocks eyes, glasses, mouth, headtop. |
112
- | 5 | Eyes | Defaults: alien eyes (UPC 1) or standard eyes (UPC 2-4). |
113
- | 6 | Glasses | Blocked by head. |
114
- | 7 | Mouth | Default provided. Blocked by head. |
115
- | 8 | Legs | Lower body clothing. |
116
- | 9 | Suit | Full one-piece. Blocks suit top and suit bottom. |
117
- | 10 | Suit Bottom | Blocked by full suit. |
118
- | 11 | Suit Top | Blocked by full suit. |
119
- | 12 | Headtop | Blocked by head. |
120
- | 13 | Hand | Held item. |
121
- | 14-17 | Special | Special suit, legs, head, body overlays. |
122
-
123
- ### Body Types (by UPC)
124
-
125
- | UPC | Type | Default Eyes |
126
- |-----|------|-------------|
127
- | 1 | Alien | `DEFAULT_ALIEN_EYES` (purple) |
128
- | 2 | Pink | `DEFAULT_STANDARD_EYES` |
129
- | 3 | Orange | `DEFAULT_STANDARD_EYES` |
130
- | 4 | Original | `DEFAULT_STANDARD_EYES` |
131
-
132
- ## Events
133
-
134
- | Event | When |
135
- |-------|------|
136
- | `DecorateBanny(hook, bannyBodyId, backgroundId, outfitIds, caller)` | Body decorated with new outfits/background |
137
- | `SetMetadata(description, externalUrl, baseUri, caller)` | Metadata updated |
138
- | `SetProductName(upc, name, caller)` | Product name set |
139
- | `SetSvgContent(upc, svgContent, caller)` | SVG content stored on-chain |
140
- | `SetSvgHash(upc, svgHash, caller)` | SVG hash registered |
141
-
142
- ## Errors
143
-
144
- | Error | When |
145
- |-------|------|
146
- | `Banny721TokenUriResolver_ArrayLengthMismatch` | Batch setter called with mismatched array lengths |
147
- | `Banny721TokenUriResolver_BannyBodyNotBodyCategory` | Passing a non-body-category token as bannyBodyId to decorateBannyWith |
148
- | `Banny721TokenUriResolver_CantAccelerateTheLock` | Trying to lock a body that's already locked for longer |
149
- | `Banny721TokenUriResolver_ContentsAlreadyStored` | SVG content already exists for this UPC |
150
- | `Banny721TokenUriResolver_ContentsMismatch` | Uploaded content doesn't match registered hash |
151
- | `Banny721TokenUriResolver_HashAlreadyStored` | Hash already registered for this UPC |
152
- | `Banny721TokenUriResolver_HashNotFound` | No hash registered for UPC (must register before uploading content) |
153
- | `Banny721TokenUriResolver_HeadAlreadyAdded` | Trying to add eyes/glasses/mouth/headtop when head is already attached |
154
- | `Banny721TokenUriResolver_OutfitChangesLocked` | Body is locked (7-day lock active) |
155
- | `Banny721TokenUriResolver_SuitAlreadyAdded` | Trying to add suit top/bottom when full suit is already attached |
156
- | `Banny721TokenUriResolver_UnauthorizedBannyBody` | Caller doesn't own the body |
157
- | `Banny721TokenUriResolver_UnauthorizedOutfit` | Caller doesn't own the outfit or the body wearing it |
158
- | `Banny721TokenUriResolver_UnauthorizedBackground` | Caller doesn't own the background or the body using it |
159
- | `Banny721TokenUriResolver_UnorderedCategories` | Outfit IDs not in ascending category order |
160
- | `Banny721TokenUriResolver_UnrecognizedCategory` | Category ID not in valid range (0-17) |
161
- | `Banny721TokenUriResolver_UnrecognizedBackground` | Token is not a background category |
162
- | `Banny721TokenUriResolver_UnrecognizedProduct` | Token's UPC doesn't map to a known product |
163
- | `Banny721TokenUriResolver_UnauthorizedTransfer` | `onERC721Received` called by non-self operator |
164
-
165
- ## Constants
166
-
167
- | Constant | Value | Purpose |
168
- |----------|-------|---------|
169
- | `_LOCK_DURATION` | 7 days | Fixed outfit lock period |
170
- | `_ONE_BILLION` | 1,000,000,000 | Token ID encoding: `tokenId = upc * 1B + sequenceNumber` |
171
- | `_BODY_CATEGORY` | 0 | Category ID for base Banny body |
172
- | `_BACKGROUND_CATEGORY` | 1 | Category ID for backgrounds |
173
- | Categories 2-17 | 2-17 | Backside, necklace, head, eyes, glasses, mouth, legs, suit, suit bottom, suit top, headtop, hand, specials |
174
-
175
- ## Storage
176
-
177
- | Mapping | Type | Purpose |
178
- |---------|------|---------|
179
- | `_attachedOutfitIdsOf` | `hook => bannyBodyId => uint256[]` | Outfit token IDs attached to a body |
180
- | `_attachedBackgroundIdOf` | `hook => bannyBodyId => uint256` | Background token ID attached to a body |
181
- | `_svgContentOf` | `upc => string` | On-chain SVG content (immutable once set) |
182
- | `svgHashOf` | `upc => bytes32` | Expected SVG content hash (immutable once set) |
183
- | `_customProductNameOf` | `upc => string` | Human-readable product name |
184
- | `outfitLockedUntil` | `hook => bannyBodyId => uint256` | Timestamp until outfit changes locked (declared as `upc` but keyed by token ID) |
185
- | `_userOf` | `hook => backgroundId => uint256` | Which body uses a background |
186
- | `_wearerOf` | `hook => outfitId => uint256` | Which body wears an outfit |
187
- | `svgBaseUri` | `string` | IPFS/HTTP base URI for fallback SVG loading |
188
- | `svgDescription` | `string` | Token metadata description |
189
- | `svgExternalUrl` | `string` | Token metadata external URL |
190
-
191
- ## Gotchas
192
-
193
- 1. **7-day lock is fixed and non-cancellable.** `lockOutfitChangesFor` always sets `outfitLockedUntil = block.timestamp + 7 days`. Cannot be shortened, cancelled, or accelerated. Only extended.
194
- 2. **SVG content is immutable.** Once `setSvgContentsOf` stores content for a UPC, it cannot be changed. A mistake requires deploying a new resolver.
195
- 3. **SVG hashes are also immutable.** `setSvgHashesOf` reverts if a hash already exists for a UPC. Register carefully.
196
- 4. **Hash registration is owner-only, but content upload is permissionless.** Anyone can call `setSvgContentsOf` as long as the content matches the registered hash. This enables community-driven lazy uploads.
197
- 5. **Strict ascending category order.** `decorateBannyWith` requires outfits passed in ascending category order. Reverts with `UnorderedCategories` if violated.
198
- 6. **Slot conflicts block combinations.** Head (4) blocks eyes (5), glasses (6), mouth (7), and headtop (12). Full suit (9) blocks suit top (11) and suit bottom (10). These are enforced at decoration time.
199
- 7. **Default injection.** If no explicit necklace, eyes, or mouth outfit is attached, the resolver auto-injects defaults during SVG rendering. Alien bodies get `DEFAULT_ALIEN_EYES`; others get `DEFAULT_STANDARD_EYES`.
200
- 8. **Outfits are held in contract custody.** Attached outfits and backgrounds are transferred to `address(this)` via `safeTransferFrom`. They are returned to the caller when detached (by passing a new outfit set that excludes them). If the return transfer fails (e.g., owner is a non-receiver contract), the asset is **retained** in the attached list rather than stranded — the owner can retry once the transfer issue is resolved.
201
- 9. **Complex outfit ownership rules.** If an outfit is unworn: only its owner can attach it. If already worn by another body: the caller must own THAT body to reassign the outfit. This allows body owners to swap outfits between their own bodies.
202
- 10. **Token ID encoding.** `tokenId = upc * 1_000_000_000 + sequenceNumber`. The resolver extracts UPC via integer division and sequence via modulo to display inventory counts like "42/100".
203
- 11. **`onERC721Received` only accepts self-transfers.** Reverts unless `operator == address(this)`. The contract calls `safeTransferFrom` on itself during decoration, triggering this callback.
204
- 12. **Via-IR required.** `foundry.toml` must have `via_ir = true` due to stack-too-deep in the SVG composition logic.
205
- 13. **SVG fallback chain.** If on-chain content exists: use it. Else if category <= 17: fall back to `svgBaseUri + IPFS URI`. Else: use the hook's `baseURI() + IPFS URI`.
206
- 14. **Mannequin rendering.** Outfit and background tokens (not bodies) are rendered on a grayscale mannequin Banny for preview purposes. The mannequin has `fill:#808080` styling.
207
- 15. **ERC2771 meta-transaction support.** Constructor takes a `trustedForwarder` address. All `_msgSender()` calls use `ERC2771Context`, allowing relayers to submit decoration transactions on behalf of users.
208
- 16. **Empty metadata fields clear the value.** `setMetadata` always writes all three fields. Passing an empty string clears that field. To keep a field unchanged, pass its current value.
209
- 17. **Outfit locks persist through transfers.** The `outfitLockedUntil` mapping is keyed by `(hook, bannyBodyId)` and is never cleared on transfer. When a locked body NFT is transferred, the new owner inherits the lock and cannot change outfits until it expires. Equipped outfits also travel with the body -- the new owner can unequip them after the lock expires. Sellers should unequip valuable outfits before transferring a body.
210
-
211
- ## Example Integration
212
-
213
- ```solidity
214
- import {IBanny721TokenUriResolver} from "@bannynet/core-v6/src/interfaces/IBanny721TokenUriResolver.sol";
215
-
216
- // --- Get the composed SVG for a dressed Banny ---
217
-
218
- string memory svg = resolver.svgOf(
219
- hookAddress,
220
- bannyBodyTokenId,
221
- true, // dress the banny with attached outfits
222
- true // include the attached background
223
- );
224
-
225
- // --- Dress a Banny body with outfits ---
226
-
227
- uint256[] memory outfitIds = new uint256[](2);
228
- outfitIds[0] = hatTokenId; // must be category 4 (head)
229
- outfitIds[1] = shirtTokenId; // must be category 11 (suit top)
230
-
231
- // Caller must own the body, background, and all outfits on the same hook
232
- resolver.decorateBannyWith(
233
- hookAddress,
234
- bannyBodyTokenId,
235
- backgroundTokenId,
236
- outfitIds // MUST be in ascending category order
237
- );
26
+ ## Purpose
238
27
 
239
- // --- Lock outfit changes for 7 days ---
28
+ Application-layer token URI resolver for Juicebox 721 collections that lets Banny body NFTs equip outfit and background NFTs, custody them while equipped, and render fully on-chain layered SVG metadata.
240
29
 
241
- resolver.lockOutfitChangesFor(hookAddress, bannyBodyTokenId);
30
+ ## Reference Files
242
31
 
243
- // --- Register and upload SVG content ---
32
+ - Open [`references/runtime.md`](./references/runtime.md) when you need attachment and custody behavior, rendering order, or the main invariants that protect equipped assets.
33
+ - Open [`references/operations.md`](./references/operations.md) when you need upload and metadata-management behavior, deployment breadcrumbs, or the common stale-data traps around SVG content and scripts.
244
34
 
245
- // Step 1: Owner registers content hashes
246
- uint256[] memory upcs = new uint256[](1);
247
- upcs[0] = 42;
248
- bytes32[] memory hashes = new bytes32[](1);
249
- hashes[0] = keccak256(bytes(svgContent));
250
- resolver.setSvgHashesOf(upcs, hashes);
35
+ ## Working Rules
251
36
 
252
- // Step 2: Anyone uploads matching content
253
- string[] memory contents = new string[](1);
254
- contents[0] = svgContent;
255
- resolver.setSvgContentsOf(upcs, contents);
256
- ```
37
+ - Start in [`src/Banny721TokenUriResolver.sol`](./src/Banny721TokenUriResolver.sol) for both rendering and attachment behavior. This repo is mostly one contract with several tightly coupled responsibilities.
38
+ - Treat custody, stale attachment cleanup, and lock timing as high-risk. Rendering bugs are visible, but custody bugs are worse.
39
+ - When a task mentions minting, pricing, or terminal accounting, verify that the problem is not actually in the upstream 721 hook repo.
40
+ - If you touch SVG or metadata behavior, check whether the issue is in stored content, rendering composition, or the hook-to-resolver integration point before patching.