@bannynet/core-v6 0.0.2 → 0.0.4

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,53 +1,135 @@
1
- # banny-retail-v5
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.
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.
4
4
 
5
- ## Architecture
5
+ [Docs](https://docs.juicebox.money) | [Discord](https://discord.gg/juicebox)
6
6
 
7
- | Contract | Description |
8
- |----------|-------------|
9
- | `Banny721TokenUriResolver` | The sole contract. Implements `IJB721TokenUriResolver` to serve fully on-chain SVG token URIs for any Juicebox 721 hook. Manages a composable asset system where Banny body NFTs can be dressed with outfit NFTs and placed on background NFTs, all rendered as layered SVGs with base64-encoded JSON metadata. Owner can register SVG content and hashes for product IDs (UPCs). |
7
+ ## Conceptual Overview
8
+
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
+
11
+ ### How It Works
12
+
13
+ ```
14
+ 1. A Juicebox 721 hook registers Banny721TokenUriResolver as its token URI resolver
15
+ → All tokenURI() calls are forwarded to the resolver
16
+ |
17
+ 2. Users mint Banny body NFTs + outfit/background NFTs via the 721 hook
18
+ → Bodies are the "base" layer; outfits and backgrounds are accessories
19
+ |
20
+ 3. Body owner calls decorateBannyWith(hook, bodyId, backgroundId, outfitIds)
21
+ → Outfit and background NFTs are transferred to the resolver contract
22
+ → Resolver tracks which body wears which outfits
23
+ → Body's tokenURI now renders the full dressed composition
24
+ |
25
+ 4. Outfit lock (optional): lockOutfitChangesFor(hook, bodyId)
26
+ → Freezes outfit changes for 7 days
27
+ → Proves the Banny's look is stable (useful for PFPs, displays)
28
+ |
29
+ 5. SVG content is stored on-chain via a two-step process:
30
+ → Owner registers content hashes: setSvgHashesOf(upcs, hashes)
31
+ → Anyone uploads matching content: setSvgContentsOf(upcs, contents)
32
+ → Falls back to IPFS base URI if on-chain content not yet stored
33
+ ```
10
34
 
11
35
  ### Asset Categories
12
36
 
13
- | Category ID | Name | Role |
14
- |-------------|------|------|
15
- | 0 | Body | The base Banny character. Owns outfits and backgrounds. |
37
+ | Category ID | Name | Slot Rules |
38
+ |-------------|------|------------|
39
+ | 0 | Body | Base character. One of four types: Alien, Pink, Orange, Original. |
16
40
  | 1 | Background | Scene behind the Banny. One per body. |
17
- | 2 | Backside | Layer behind the body. |
18
- | 3 | Necklace | Accessory layer (default provided). |
19
- | 4 | Head | Head accessory. One per body. |
20
- | 5 | Eyes | Eye style (defaults: standard, alien). |
21
- | 6 | Glasses | Eyewear layer. |
22
- | 7 | Mouth | Mouth expression (default provided). |
41
+ | 2 | Backside | Layer rendered behind the body. |
42
+ | 3 | Necklace | Accessory (default provided if none attached). |
43
+ | 4 | Head | Full head accessory. Blocks eyes, glasses, mouth, and headtop. |
44
+ | 5 | Eyes | Eye style (defaults: alien or standard based on body type). |
45
+ | 6 | Glasses | Eyewear layer. Blocked by head. |
46
+ | 7 | Mouth | Mouth expression (default provided). Blocked by head. |
23
47
  | 8 | Legs | Lower body clothing. |
24
- | 9 | Suit | Full body suit (one-piece). |
25
- | 10 | Suit Bottom | Lower suit piece. |
26
- | 11 | Suit Top | Upper suit piece. |
27
- | 12 | Headtop | Top-of-head accessory. |
48
+ | 9 | Suit | Full body one-piece. Blocks suit top and suit bottom. |
49
+ | 10 | Suit Bottom | Lower suit piece. Blocked by full suit. |
50
+ | 11 | Suit Top | Upper suit piece. Blocked by full suit. |
51
+ | 12 | Headtop | Top-of-head accessory. Blocked by head. |
28
52
  | 13 | Hand | Held item layer. |
29
- | 14-17 | Special variants | Special suit, legs, head, and body overlays. |
53
+ | 14-17 | Special | Special suit, legs, head, and body overlays. |
54
+
55
+ ### Body Types
56
+
57
+ | UPC | Type | Color Palette |
58
+ |-----|------|--------------|
59
+ | 1 | Alien | Green tones (`67d757`, `30a220`, `217a15`, `09490f`) with purple accents |
60
+ | 2 | Pink | Pink tones (`ffd8c5`, `ff96a9`, `fe588b`, `c92f45`) |
61
+ | 3 | Orange | Orange tones (`f3a603`, `ff7c02`, `fd3600`, `c32e0d`) |
62
+ | 4 | Original | Yellow tones (`ffe900`, `ffc700`, `f3a603`, `965a1a`) |
63
+
64
+ ## Architecture
65
+
66
+ | Contract | Description |
67
+ |----------|-------------|
68
+ | `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`. |
69
+
70
+ ### Interface
71
+
72
+ | Interface | Description |
73
+ |-----------|-------------|
74
+ | `IBanny721TokenUriResolver` | Public API: `tokenUriOf`, `svgOf`, `decorateBannyWith`, `lockOutfitChangesFor`, `assetIdsOf`, `namesOf`, `userOf`, `wearerOf`, SVG management, metadata management, plus all events. |
30
75
 
31
76
  ## Install
32
77
 
33
78
  ```bash
34
- npm install @bannynet/core-v5
79
+ npm install @bannynet/core-v6
35
80
  ```
36
81
 
37
- ## Develop
38
-
39
- `banny-retail-v5` uses [npm](https://www.npmjs.com/) for package management and [Foundry](https://github.com/foundry-rs/foundry) for builds, tests, and deployments. Requires `via-ir = true` in foundry.toml.
82
+ If using Forge directly:
40
83
 
41
84
  ```bash
42
- curl -L https://foundry.paradigm.xyz | sh
43
- npm install && forge install
85
+ forge install
44
86
  ```
45
87
 
88
+ ## Develop
89
+
90
+ Requires `via_ir = true` in foundry.toml due to stack depth in SVG composition.
91
+
46
92
  | Command | Description |
47
93
  |---------|-------------|
48
- | `forge build` | Compile contracts and write artifacts to `out`. |
49
- | `forge test` | Run the test suite. |
50
- | `forge fmt` | Lint Solidity files. |
51
- | `forge build --sizes` | Get contract sizes. |
52
- | `forge coverage` | Generate a test coverage report. |
53
- | `forge clean` | Remove build artifacts and cache. |
94
+ | `forge build` | Compile contracts (requires via-IR) |
95
+ | `forge test` | Run all tests (3 test files: functionality, attacks, decoration flows) |
96
+ | `forge test -vvv` | Run tests with full trace |
97
+
98
+ ## Repository Layout
99
+
100
+ ```
101
+ src/
102
+ Banny721TokenUriResolver.sol # Sole contract (~1,331 lines)
103
+ interfaces/
104
+ IBanny721TokenUriResolver.sol # Public interface + events
105
+ test/
106
+ Banny721TokenUriResolver.t.sol # Unit tests (~690 lines)
107
+ BannyAttacks.t.sol # Security/adversarial tests (~323 lines)
108
+ DecorateFlow.t.sol # Decoration flow tests (~1,057 lines)
109
+ script/
110
+ Deploy.s.sol # Sphinx multi-chain deployment
111
+ Drop1.s.sol # Outfit drop deployment
112
+ Add.Denver.s.sol # Denver-specific deployment
113
+ helpers/
114
+ BannyverseDeploymentLib.sol # Deployment artifact reader
115
+ MigrationHelper.sol # Migration utilities
116
+ ```
117
+
118
+ ## Permissions
119
+
120
+ | Action | Who Can Do It |
121
+ |--------|--------------|
122
+ | `decorateBannyWith` | Body owner (must also own or have worn-by-body access to outfits/backgrounds) |
123
+ | `lockOutfitChangesFor` | Body owner |
124
+ | `setSvgHashesOf` | Contract owner only |
125
+ | `setSvgContentsOf` | Anyone (content validated against registered hash) |
126
+ | `setProductNames` | Contract owner only |
127
+ | `setMetadata` | Contract owner only |
128
+
129
+ ## Risks
130
+
131
+ - **Outfit custody:** Attached outfits and backgrounds are held by the resolver contract. If the contract has a bug in the return logic, assets could be stuck.
132
+ - **7-day lock is fixed.** Cannot be shortened or cancelled once set. The lock duration is hardcoded.
133
+ - **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.
134
+ - **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.
135
+ - **Single resolver per hook.** The resolver is set on the 721 hook and applies to all tiers. Different collections would need different resolver instances.
package/SKILLS.md CHANGED
@@ -1,77 +1,179 @@
1
- # banny-retail-v5
1
+ # Banny Retail
2
2
 
3
3
  ## Purpose
4
4
 
5
- On-chain composable NFT avatar system that renders Banny characters with layered SVG outfits and backgrounds for Juicebox 721 collections.
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
6
 
7
7
  ## Contracts
8
8
 
9
9
  | Contract | Role |
10
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, and on-chain SVG storage. |
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
12
 
13
13
  ## Key Functions
14
14
 
15
- | Function | Contract | What it does |
16
- |----------|----------|--------------|
17
- | `tokenUriOf` | `Banny721TokenUriResolver` | Returns a base64-encoded JSON metadata URI with an on-chain SVG image. For body tokens, composes the full dressed Banny with attached outfits and background. For outfit/background tokens, renders the item on a mannequin Banny. |
18
- | `decorateBannyWith` | `Banny721TokenUriResolver` | Attaches a background and outfit NFTs to a Banny body. Validates ownership (caller must own the body, background, and all outfits via the same 721 hook). Enforces category ordering and uniqueness (one head, one suit, etc.). Detaches previously worn items. |
19
- | `lockOutfitChangesFor` | `Banny721TokenUriResolver` | Locks a Banny body's outfit for 7 days. Cannot accelerate an existing lock. Caller must own the body NFT. |
20
- | `svgOf` | `Banny721TokenUriResolver` | Returns the composed SVG for a token. For bodies, layers: background + backside + body + necklace + eyes + mouth + outfits (in category order). Falls back to SVG base URI + hash if on-chain content is not stored. |
21
- | `assetIdsOf` | `Banny721TokenUriResolver` | Returns the background ID and outfit IDs currently attached to a Banny body. |
22
- | `namesOf` | `Banny721TokenUriResolver` | Returns the product name, category name, and Banny type (Alien/Pink/Orange/Original) for a token. |
23
- | `setSvgContentsOf` | `Banny721TokenUriResolver` | Owner-only. Stores SVG content strings on-chain for given UPCs. Content must match the previously registered hash. Cannot overwrite existing content. |
24
- | `setSvgHashsOf` | `Banny721TokenUriResolver` | Owner-only. Registers SVG content hashes for UPCs. Hash cannot be changed once set. |
25
- | `setProductNames` | `Banny721TokenUriResolver` | Owner-only. Sets human-readable names for product UPCs. |
26
- | `setSvgBaseUri` | `Banny721TokenUriResolver` | Owner-only. Sets the base URI for lazily-loaded SVG files (used when on-chain content is not yet stored). |
27
- | `userOf` | `Banny721TokenUriResolver` | Returns the Banny body ID that a background is attached to. |
28
- | `wearerOf` | `Banny721TokenUriResolver` | Returns the Banny body ID that an outfit is worn by. |
29
- | `onERC721Received` | `Banny721TokenUriResolver` | Handles receiving 721 tokens. Validates the transfer is authorized (outfit must be worn by sender's Banny, or sender is hook). Used for outfit management. |
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. Returns previously attached items to caller. 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. |
30
52
 
31
53
  ## Integration Points
32
54
 
33
55
  | Dependency | Import | Used For |
34
56
  |------------|--------|----------|
35
- | `@bananapus/721-hook-v6` | `IJB721TiersHook`, `IJB721TiersHookStore`, `IJB721TokenUriResolver`, `JB721Tier`, `JBIpfsDecoder`, `IERC721` (custom ERC721) | Token ownership checks, tier data resolution, IPFS URI decoding, hook store queries. |
36
- | `@openzeppelin/contracts` | `Ownable`, `ReentrancyGuard`, `ERC2771Context`, `IERC721Receiver`, `Strings` | Access control, reentrancy protection, meta-transactions, safe NFT receipt, string utilities. |
37
- | `lib/base64` | `Base64` | Base64 encoding for on-chain SVG and JSON metadata. |
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 |
38
60
 
39
61
  ## Key Types
40
62
 
41
- | Struct/Enum | Key Fields | Used In |
42
- |-------------|------------|---------|
43
- | `JB721Tier` | `id`, `category`, `encodedIPFSUri`, `price` | `tokenUriOf` (product resolution via tier store) |
44
-
45
- ### Internal Mappings (not structs, but critical state)
46
-
47
- | Mapping | Key | Value | Purpose |
48
- |---------|-----|-------|---------|
49
- | `_attachedOutfitIdsOf` | `hook => bannyBodyId` | `uint256[]` | Outfit token IDs attached to a body. |
50
- | `_attachedBackgroundIdOf` | `hook => bannyBodyId` | `uint256` | Background token ID attached to a body. |
51
- | `_svgContentOf` | `upc` | `string` | On-chain SVG content for a product. |
52
- | `svgHashOf` | `upc` | `bytes32` | Keccak256 hash of expected SVG content. |
53
- | `_customProductNameOf` | `upc` | `string` | Human-readable product name. |
54
- | `outfitLockedUntil` | `hook => upc` | `uint256` | Timestamp until outfit changes are locked. |
55
- | `_userOf` | `hook => backgroundId` | `uint256` | Which body is using a background. |
56
- | `_wearerOf` | `hook => outfitId` | `uint256` | Which body is wearing an outfit. |
63
+ ### Asset Categories
64
+
65
+ | ID | Name | Slot Rules |
66
+ |----|------|------------|
67
+ | 0 | Body | Base character. Owns outfits and backgrounds. |
68
+ | 1 | Background | One per body. |
69
+ | 2 | Backside | Behind body layer. |
70
+ | 3 | Necklace | Default provided if none attached. |
71
+ | 4 | Head | Blocks eyes, glasses, mouth, headtop. |
72
+ | 5 | Eyes | Defaults: alien eyes (UPC 1) or standard eyes (UPC 2-4). |
73
+ | 6 | Glasses | Blocked by head. |
74
+ | 7 | Mouth | Default provided. Blocked by head. |
75
+ | 8 | Legs | Lower body clothing. |
76
+ | 9 | Suit | Full one-piece. Blocks suit top and suit bottom. |
77
+ | 10 | Suit Bottom | Blocked by full suit. |
78
+ | 11 | Suit Top | Blocked by full suit. |
79
+ | 12 | Headtop | Blocked by head. |
80
+ | 13 | Hand | Held item. |
81
+ | 14-17 | Special | Special suit, legs, head, body overlays. |
82
+
83
+ ### Body Types (by UPC)
84
+
85
+ | UPC | Type | Default Eyes |
86
+ |-----|------|-------------|
87
+ | 1 | Alien | `DEFAULT_ALIEN_EYES` (purple) |
88
+ | 2 | Pink | `DEFAULT_STANDARD_EYES` |
89
+ | 3 | Orange | `DEFAULT_STANDARD_EYES` |
90
+ | 4 | Original | `DEFAULT_STANDARD_EYES` |
91
+
92
+ ## Events
93
+
94
+ | Event | When |
95
+ |-------|------|
96
+ | `DecorateBanny(hook, bannyBodyId, backgroundId, outfitIds, caller)` | Body decorated with new outfits/background |
97
+ | `SetMetadata(description, externalUrl, baseUri, caller)` | Metadata updated |
98
+ | `SetProductName(upc, name, caller)` | Product name set |
99
+ | `SetSvgContent(upc, svgContent, caller)` | SVG content stored on-chain |
100
+ | `SetSvgHash(upc, svgHash, caller)` | SVG hash registered |
101
+
102
+ ## Errors
103
+
104
+ | Error | When |
105
+ |-------|------|
106
+ | `Banny721TokenUriResolver_ArrayLengthMismatch` | Batch setter called with mismatched array lengths |
107
+ | `Banny721TokenUriResolver_BannyBodyNotBodyCategory` | Passing a non-body-category token as bannyBodyId to decorateBannyWith |
108
+ | `Banny721TokenUriResolver_CantAccelerateTheLock` | Trying to lock a body that's already locked for longer |
109
+ | `Banny721TokenUriResolver_ContentsAlreadyStored` | SVG content already exists for this UPC |
110
+ | `Banny721TokenUriResolver_ContentsMismatch` | Uploaded content doesn't match registered hash |
111
+ | `Banny721TokenUriResolver_HashAlreadyStored` | Hash already registered for this UPC |
112
+ | `Banny721TokenUriResolver_HashNotFound` | No hash registered for UPC (must register before uploading content) |
113
+ | `Banny721TokenUriResolver_HeadAlreadyAdded` | Trying to add eyes/glasses/mouth/headtop when head is already attached |
114
+ | `Banny721TokenUriResolver_OutfitChangesLocked` | Body is locked (7-day lock active) |
115
+ | `Banny721TokenUriResolver_SuitAlreadyAdded` | Trying to add suit top/bottom when full suit is already attached |
116
+ | `Banny721TokenUriResolver_UnauthorizedBannyBody` | Caller doesn't own the body |
117
+ | `Banny721TokenUriResolver_UnauthorizedOutfit` | Caller doesn't own the outfit or the body wearing it |
118
+ | `Banny721TokenUriResolver_UnauthorizedBackground` | Caller doesn't own the background or the body using it |
119
+ | `Banny721TokenUriResolver_UnorderedCategories` | Outfit IDs not in ascending category order |
120
+ | `Banny721TokenUriResolver_UnrecognizedCategory` | Category ID not in valid range (0-17) |
121
+ | `Banny721TokenUriResolver_UnrecognizedBackground` | Token is not a background category |
122
+ | `Banny721TokenUriResolver_UnrecognizedProduct` | Token's UPC doesn't map to a known product |
123
+ | `Banny721TokenUriResolver_UnauthorizedTransfer` | `onERC721Received` called by non-self operator |
124
+
125
+ ## Constants
126
+
127
+ | Constant | Value | Purpose |
128
+ |----------|-------|---------|
129
+ | `_LOCK_DURATION` | 7 days | Fixed outfit lock period |
130
+ | `_ONE_BILLION` | 1,000,000,000 | Token ID encoding: `tokenId = upc * 1B + sequenceNumber` |
131
+ | `_BODY_CATEGORY` | 0 | Category ID for base Banny body |
132
+ | `_BACKGROUND_CATEGORY` | 1 | Category ID for backgrounds |
133
+ | Categories 2-17 | 2-17 | Backside, necklace, head, eyes, glasses, mouth, legs, suit, suit bottom, suit top, headtop, hand, specials |
134
+
135
+ ## Storage
136
+
137
+ | Mapping | Type | Purpose |
138
+ |---------|------|---------|
139
+ | `_attachedOutfitIdsOf` | `hook => bannyBodyId => uint256[]` | Outfit token IDs attached to a body |
140
+ | `_attachedBackgroundIdOf` | `hook => bannyBodyId => uint256` | Background token ID attached to a body |
141
+ | `_svgContentOf` | `upc => string` | On-chain SVG content (immutable once set) |
142
+ | `svgHashOf` | `upc => bytes32` | Expected SVG content hash (immutable once set) |
143
+ | `_customProductNameOf` | `upc => string` | Human-readable product name |
144
+ | `outfitLockedUntil` | `hook => upc => uint256` | Timestamp until outfit changes locked |
145
+ | `_userOf` | `hook => backgroundId => uint256` | Which body uses a background |
146
+ | `_wearerOf` | `hook => outfitId => uint256` | Which body wears an outfit |
147
+ | `svgBaseUri` | `string` | IPFS/HTTP base URI for fallback SVG loading |
148
+ | `svgDescription` | `string` | Token metadata description |
149
+ | `svgExternalUrl` | `string` | Token metadata external URL |
57
150
 
58
151
  ## Gotchas
59
152
 
60
- - The `_LOCK_DURATION` is hardcoded to 7 days. `lockOutfitChangesFor` prevents reducing an existing lock (reverts with `CantAccelerateTheLock`).
61
- - SVG content is immutable once stored. `setSvgContentsOf` reverts if content already exists. The content must hash-match the previously registered `svgHashOf[upc]`.
62
- - SVG hashes are also immutable. `setSvgHashsOf` reverts if a hash is already registered for a UPC.
63
- - `decorateBannyWith` enforces strict ascending category order for outfits. Passing outfits in wrong order reverts with `UnorderedCategories`.
64
- - Only one item per "slot" category is allowed on a body: one head, one suit (full suit XOR suit top + suit bottom), one background.
65
- - Body type detection uses the UPC modulo: UPC 1 = Alien, 2 = Pink, 3 = Orange, 4 = Original. Alien bodies get `DEFAULT_ALIEN_EYES`, others get `DEFAULT_STANDARD_EYES`.
66
- - Outfit/background ownership is validated against the 721 hook, not `msg.sender` directly. The caller must own the body AND all attached items through the same hook contract.
67
- - `via-ir = true` is required in `foundry.toml` due to stack-too-deep in the complex SVG composition logic.
153
+ 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.
154
+ 2. **SVG content is immutable.** Once `setSvgContentsOf` stores content for a UPC, it cannot be changed. A mistake requires deploying a new resolver.
155
+ 3. **SVG hashes are also immutable.** `setSvgHashesOf` reverts if a hash already exists for a UPC. Register carefully.
156
+ 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.
157
+ 5. **Strict ascending category order.** `decorateBannyWith` requires outfits passed in ascending category order. Reverts with `UnorderedCategories` if violated.
158
+ 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.
159
+ 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`.
160
+ 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).
161
+ 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.
162
+ 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".
163
+ 11. **`onERC721Received` only accepts self-transfers.** Reverts unless `operator == address(this)`. The contract calls `safeTransferFrom` on itself during decoration, triggering this callback.
164
+ 12. **Via-IR required.** `foundry.toml` must have `via_ir = true` due to stack-too-deep in the SVG composition logic.
165
+ 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`.
166
+ 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.
167
+ 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.
168
+ 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.
68
169
 
69
170
  ## Example Integration
70
171
 
71
172
  ```solidity
72
173
  import {IBanny721TokenUriResolver} from "@bannynet/core-v6/src/interfaces/IBanny721TokenUriResolver.sol";
73
174
 
74
- // Get the composed SVG for a dressed Banny
175
+ // --- Get the composed SVG for a dressed Banny ---
176
+
75
177
  string memory svg = resolver.svgOf(
76
178
  hookAddress,
77
179
  bannyBodyTokenId,
@@ -79,7 +181,8 @@ string memory svg = resolver.svgOf(
79
181
  true // include the attached background
80
182
  );
81
183
 
82
- // Dress a Banny body with outfits
184
+ // --- Dress a Banny body with outfits ---
185
+
83
186
  uint256[] memory outfitIds = new uint256[](2);
84
187
  outfitIds[0] = hatTokenId; // must be category 4 (head)
85
188
  outfitIds[1] = shirtTokenId; // must be category 11 (suit top)
@@ -89,6 +192,24 @@ resolver.decorateBannyWith(
89
192
  hookAddress,
90
193
  bannyBodyTokenId,
91
194
  backgroundTokenId,
92
- outfitIds // must be in ascending category order
195
+ outfitIds // MUST be in ascending category order
93
196
  );
197
+
198
+ // --- Lock outfit changes for 7 days ---
199
+
200
+ resolver.lockOutfitChangesFor(hookAddress, bannyBodyTokenId);
201
+
202
+ // --- Register and upload SVG content ---
203
+
204
+ // Step 1: Owner registers content hashes
205
+ uint256[] memory upcs = new uint256[](1);
206
+ upcs[0] = 42;
207
+ bytes32[] memory hashes = new bytes32[](1);
208
+ hashes[0] = keccak256(bytes(svgContent));
209
+ resolver.setSvgHashesOf(upcs, hashes);
210
+
211
+ // Step 2: Anyone uploads matching content
212
+ string[] memory contents = new string[](1);
213
+ contents[0] = svgContent;
214
+ resolver.setSvgContentsOf(upcs, contents);
94
215
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bannynet/core-v6",
3
- "version": "0.0.2",
3
+ "version": "0.0.4",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -20,15 +20,15 @@
20
20
  "artifacts": "source ./.env && npx sphinx artifacts --org-id 'ea165b21-7cdc-4d7b-be59-ecdd4c26bee4' --project-name 'banny-core-v6'"
21
21
  },
22
22
  "dependencies": {
23
- "@bananapus/721-hook-v6": "^0.0.3",
24
- "@bananapus/core-v6": "^0.0.4",
25
- "@bananapus/suckers-v6": "^0.0.2",
26
- "@bananapus/router-terminal-v6": "^0.0.2",
27
- "@openzeppelin/contracts": "^5.2.0",
28
- "@rev-net/core-v6": "^0.0.2",
23
+ "@bananapus/721-hook-v6": "^0.0.9",
24
+ "@bananapus/core-v6": "^0.0.9",
25
+ "@bananapus/router-terminal-v6": "^0.0.6",
26
+ "@bananapus/suckers-v6": "^0.0.6",
27
+ "@openzeppelin/contracts": "5.2.0",
28
+ "@rev-net/core-v6": "^0.0.6",
29
29
  "keccak": "^3.0.4"
30
30
  },
31
31
  "devDependencies": {
32
- "@sphinx-labs/plugins": "0.33.2"
32
+ "@sphinx-labs/plugins": "0.33.3"
33
33
  }
34
34
  }
@@ -23,7 +23,7 @@ contract Drop1Script is Script, Sphinx {
23
23
 
24
24
  function configureSphinx() public override {
25
25
  // TODO: Update to contain revnet devs.
26
- sphinxConfig.projectName = "banny-core";
26
+ sphinxConfig.projectName = "banny-core-v6";
27
27
  sphinxConfig.mainnets = ["ethereum", "optimism", "base", "arbitrum"];
28
28
  sphinxConfig.testnets = ["ethereum_sepolia", "optimism_sepolia", "base_sepolia", "arbitrum_sepolia"];
29
29
  }
@@ -37,8 +37,10 @@ contract Drop1Script is Script, Sphinx {
37
37
  );
38
38
 
39
39
  // Get the deployment addresses for the 721 hook contracts for this chain.
40
- bannyverse =
41
- BannyverseDeploymentLib.getDeployment(vm.envOr("BANNYVERSE_CORE_DEPLOYMENT_PATH", string("deployments/")));
40
+ bannyverse = BannyverseDeploymentLib.getDeployment(
41
+ vm.envOr("BANNYVERSE_CORE_DEPLOYMENT_PATH", string("deployments/")),
42
+ vm.envOr("BANNYVERSE_REVNET_ID", uint256(4))
43
+ );
42
44
 
43
45
  // Get the hook address by using the deployer.
44
46
  hook = JB721TiersHook(address(revnet.basic_deployer.tiered721HookOf(bannyverse.revnetId)));
@@ -73,6 +75,16 @@ contract Drop1Script is Script, Sphinx {
73
75
  splits: new JBSplit[](0)
74
76
  });
75
77
 
78
+ // Get the next tier ID so we can set names and hashes for the new product.
79
+ uint256 nextTierId = hook.STORE().maxTierIdOf(address(hook)) + 1;
80
+
76
81
  hook.adjustTiers(products, new uint256[](0));
82
+
83
+ // Build the product IDs array for the newly added tier(s).
84
+ uint256[] memory productIds = new uint256[](1);
85
+ productIds[0] = nextTierId;
86
+
87
+ bannyverse.resolver.setSvgHashesOf(productIds, svgHashes);
88
+ bannyverse.resolver.setProductNames(productIds, names);
77
89
  }
78
90
  }
@@ -37,8 +37,10 @@ contract Drop1Script is Script, Sphinx {
37
37
  );
38
38
 
39
39
  // Get the deployment addresses for the 721 hook contracts for this chain.
40
- bannyverse =
41
- BannyverseDeploymentLib.getDeployment(vm.envOr("BANNYVERSE_CORE_DEPLOYMENT_PATH", string("deployments/")));
40
+ bannyverse = BannyverseDeploymentLib.getDeployment(
41
+ vm.envOr("BANNYVERSE_CORE_DEPLOYMENT_PATH", string("deployments/")),
42
+ vm.envOr("BANNYVERSE_REVNET_ID", uint256(4))
43
+ );
42
44
 
43
45
  // Get the hook address by using the deployer.
44
46
  hook = JB721TiersHook(address(revnet.basic_deployer.tiered721HookOf(bannyverse.revnetId)));
@@ -1045,29 +1047,8 @@ contract Drop1Script is Script, Sphinx {
1045
1047
  productIds[i] = i + 5;
1046
1048
  }
1047
1049
 
1048
- if (false) {
1049
- bytes memory adjustTiersData = abi.encodeCall(JB721TiersHook.adjustTiers, (products, new uint256[](0)));
1050
- vm.writeFile(
1051
- string.concat("./", vm.toString(block.chainid), "-adjustTiers.hex.txt"), vm.toString(adjustTiersData)
1052
- );
1053
-
1054
- bytes memory setSvgHashData =
1055
- abi.encodeCall(Banny721TokenUriResolver.setSvgHashesOf, (productIds, svgHashes));
1056
-
1057
- vm.writeFile(
1058
- string.concat("./", vm.toString(block.chainid), "-setSvgHashOf.hex.txt"), vm.toString(setSvgHashData)
1059
- );
1060
-
1061
- bytes memory setProductNamesData =
1062
- abi.encodeCall(Banny721TokenUriResolver.setProductNames, (productIds, names));
1063
- vm.writeFile(
1064
- string.concat("./", vm.toString(block.chainid), "-setProductNames.hex.txt"),
1065
- vm.toString(setProductNamesData)
1066
- );
1067
- } else {
1068
- hook.adjustTiers(products, new uint256[](0));
1069
- bannyverse.resolver.setSvgHashesOf(productIds, svgHashes);
1070
- bannyverse.resolver.setProductNames(productIds, names);
1071
- }
1050
+ hook.adjustTiers(products, new uint256[](0));
1051
+ bannyverse.resolver.setSvgHashesOf(productIds, svgHashes);
1052
+ bannyverse.resolver.setProductNames(productIds, names);
1072
1053
  }
1073
1054
  }
@@ -18,7 +18,13 @@ library BannyverseDeploymentLib {
18
18
  address internal constant VM_ADDRESS = address(uint160(uint256(keccak256("hevm cheat code"))));
19
19
  Vm internal constant vm = Vm(VM_ADDRESS);
20
20
 
21
- function getDeployment(string memory path) internal returns (BannyverseDeployment memory deployment) {
21
+ function getDeployment(
22
+ string memory path,
23
+ uint256 revnetId
24
+ )
25
+ internal
26
+ returns (BannyverseDeployment memory deployment)
27
+ {
22
28
  // get chainId for which we need to get the deployment.
23
29
  uint256 chainId = block.chainid;
24
30
 
@@ -29,7 +35,7 @@ library BannyverseDeploymentLib {
29
35
 
30
36
  for (uint256 _i; _i < networks.length; _i++) {
31
37
  if (networks[_i].chainId == chainId) {
32
- return getDeployment(path, networks[_i].name);
38
+ return getDeployment(path, networks[_i].name, revnetId);
33
39
  }
34
40
  }
35
41
 
@@ -38,7 +44,8 @@ library BannyverseDeploymentLib {
38
44
 
39
45
  function getDeployment(
40
46
  string memory path,
41
- string memory network_name
47
+ string memory network_name,
48
+ uint256 revnetId
42
49
  )
43
50
  internal
44
51
  view
@@ -48,7 +55,7 @@ library BannyverseDeploymentLib {
48
55
  _getDeploymentAddress(path, "banny-core-v6", network_name, "Banny721TokenUriResolver")
49
56
  );
50
57
 
51
- deployment.revnetId = 4;
58
+ deployment.revnetId = revnetId;
52
59
  }
53
60
 
54
61
  /// @notice Get the address of a contract that was deployed by the Deploy script.