@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/ADMINISTRATION.md CHANGED
@@ -2,6 +2,34 @@
2
2
 
3
3
  Admin privileges and their scope in banny-retail-v6. The contract (`Banny721TokenUriResolver`) is a single-file system with one admin role (Ownable) and per-token owner privileges.
4
4
 
5
+ ## At A Glance
6
+
7
+ | Item | Details |
8
+ |------|---------|
9
+ | Scope | Resolver-level metadata, SVG asset commitments/uploads, and body-level outfit management for Banny NFTs. |
10
+ | Operators | The resolver owner for global metadata and asset commitments, body owners for decoration actions, and anyone for permissionless SVG uploads that match committed hashes. |
11
+ | Highest-risk actions | Committing the wrong SVG hash, changing global metadata unexpectedly, or locking outfit changes while assets are held custodially in the resolver. |
12
+ | Recovery posture | Write-once SVG commitments cannot be corrected in place. If resolver behavior is wrong, recovery usually means deploying a replacement resolver rather than editing stored content. |
13
+
14
+ ## Routine Operations
15
+
16
+ - Commit SVG hashes only after verifying the exact UPC-to-content pairing, since the commitment is permanent.
17
+ - Treat metadata and product-name changes as ecosystem-wide display changes that affect every token rendered through the resolver.
18
+ - Remind users that equipped assets are held by the resolver contract until they are unequipped through the supported flow.
19
+ - Use outfit locks deliberately, because they freeze body-level changes for the fixed lock window.
20
+
21
+ ## One-Way Or High-Risk Actions
22
+
23
+ - `setSvgHashesOf` is write-once per UPC.
24
+ - `setSvgContentsOf` is also write-once once valid content is uploaded.
25
+ - `lockOutfitChangesFor` can only extend the active lock, never shorten it.
26
+ - There is no admin rescue path for custodially held outfit NFTs.
27
+
28
+ ## Recovery Notes
29
+
30
+ - If committed artwork is wrong, the practical recovery path is a new resolver or a new UPC strategy, not overwriting the existing entry.
31
+ - If outfits become stuck because of a resolver bug, this contract exposes no owner rescue flow; recovery would require replacement infrastructure rather than an admin intervention.
32
+
5
33
  ## Roles
6
34
 
7
35
  | Role | How Assigned | Scope |
package/ARCHITECTURE.md CHANGED
@@ -1,100 +1,76 @@
1
- # banny-retail-v6 — Architecture
1
+ # Architecture
2
2
 
3
3
  ## Purpose
4
4
 
5
- Banny NFT asset manager for Juicebox V6. Stores on-chain SVG artwork for Banny characters and generates fully on-chain token URI metadata. Supports outfit composition (bodies, backgrounds, heads, suits) with lockable outfit changes.
5
+ `banny-retail-v6` provides the metadata layer for Banny collections. The repo does not mint or own the NFTs itself. Instead, a Juicebox 721 hook points at `Banny721TokenUriResolver`, and the resolver composes body, background, and outfit NFTs into a single on-chain representation.
6
6
 
7
- ## Contract Map
7
+ ## Boundaries
8
8
 
9
- ```
10
- src/
11
- ├── Banny721TokenUriResolver.sol Token URI resolver: SVG generation, outfit management, asset storage
12
- └── interfaces/
13
- └── IBanny721TokenUriResolver.sol — Interface for outfit and asset operations
14
- ```
15
-
16
- ## UPC System
9
+ - The repo owns Banny-specific composition logic, asset registration, and outfit locking.
10
+ - `nana-721-hook-v6` still owns token minting, transfers, and collection-level ERC-721 behavior.
11
+ - The resolver is intentionally application-specific. Generic 721 hook behavior should stay in `nana-721-hook-v6`.
17
12
 
18
- A UPC (Universal Product Code) is the tier ID from the 721 hook's tier system, used as the primary identifier for every asset type (bodies, backgrounds, outfits). Each tier in the `IJB721TiersHook` has an `id` and a `category` -- the UPC is that `id`.
13
+ ## Main Components
19
14
 
20
- - **Bodies** (category 0) have hardcoded UPCs with built-in color palettes: Alien (1), Pink (2), Orange (3), Original (4).
21
- - **Outfits and backgrounds** use arbitrary UPCs assigned when their tiers are created in the 721 hook. Their SVG content is stored on-chain via the hash-then-upload flow described below.
22
- - **Token IDs** encode the UPC: `tokenId = UPC * 1_000_000_000 + mintNumber`. The resolver recovers the tier (and thus UPC and category) via `tierOfTokenId` from the hook's store.
15
+ | Component | Responsibility |
16
+ | --- | --- |
17
+ | `Banny721TokenUriResolver` | Stores SVG content references, tracks equipped assets per body, enforces outfit-lock windows, and returns composed token metadata |
18
+ | `IBanny721TokenUriResolver` | Integration surface for the 721 hook or external tooling |
23
19
 
24
- All asset storage, outfit attachment, and SVG generation are keyed by UPC.
20
+ ## Runtime Model
25
21
 
26
- ## Key Operations
22
+ ### Decoration
27
23
 
28
- ### Asset Storage
24
+ ```text
25
+ body owner
26
+ -> calls decorateBannyWith(...)
27
+ -> resolver verifies ownership of the body token
28
+ -> resolver pulls the chosen background and outfit NFTs into escrow
29
+ -> resolver records which assets are attached to the body
30
+ -> resolver returns any replaced assets to the owner
29
31
  ```
30
- Owner → setSvgHashesOf(upcs, hashes)
31
- → Register content hashes for UPCs (owner-only)
32
32
 
33
- Anyone → setSvgContentsOf(upcs, contents)
34
- → Upload SVG content matching registered hashes
35
- → Content validated: keccak256(content) must equal stored hash
36
- → Content is immutable once stored (cannot be overwritten)
33
+ ### Rendering
37
34
 
38
- Owner → setProductNames(upcs, names)
39
- → Register product names for UPCs
35
+ ```text
36
+ tokenURI(bodyId)
37
+ -> resolve body, background, and equipped outfit slots
38
+ -> fetch registered SVG fragments
39
+ -> compose layered SVG
40
+ -> return base64 JSON metadata
40
41
  ```
41
42
 
42
- ### Outfit Composition
43
- ```
44
- Body Owner → decorateBannyWith(hook, bodyId, backgroundId, outfitIds)
45
- → Attach outfit and background NFTs to a body NFT
46
- → Outfit/background NFTs transferred to resolver contract
47
- → Previous outfits returned to owner (if transfer fails, retained in attached list)
48
- → Composite SVG generated from layered components
49
-
50
- Body Owner → lockOutfitChangesFor(hook, bodyId)
51
- → Lock outfit changes for 7 days
52
- ```
43
+ ### Locking
53
44
 
54
- ### Token URI Generation
45
+ ```text
46
+ owner
47
+ -> lockOutfitChangesFor(...)
48
+ -> body enters a temporary no-change window
49
+ -> decoration and removal paths must respect the lock
55
50
  ```
56
- JB721TiersHook → tokenURI(tokenId)
57
- → Banny721TokenUriResolver.tokenURI(hook, tokenId)
58
- → Look up body tier and any attached outfits
59
- → Compose SVG layers in fixed z-order (back to front):
60
- 1. Background (if attached)
61
- 2. Body (with color palette fills from UPC)
62
- 3. Outfits in category order:
63
- Backside(2) → Necklace(3)* → Head(4) → Eyes(5)† →
64
- Glasses(6) → Mouth(7)† → Legs(8) → Suit(9) →
65
- Suit Bottom(10) → Suit Top(11) → Necklace‡ →
66
- Headtop(12) → Hand(13) → Special categories(14-17)
67
- (* = default necklace inserted if no custom equipped)
68
- († = default inserted if no custom equipped and no full Head)
69
- (‡ = custom necklace rendered here, above suit layers)
70
- → Wrap in <svg> container (400x400 viewBox)
71
- → Encode as base64 data URI with JSON metadata
72
- → Return fully on-chain SVG
73
- ```
74
-
75
- Non-body tokens are shown on a grey mannequin Banny (body fills set to `none`, outline to `#808080`).
76
51
 
77
- For IPFS-backed assets without on-chain SVG content, the resolver falls back to an `<image href="...">` tag referencing the tier's encoded IPFS URI at 400x400 pixels.
52
+ ## Critical Invariants
78
53
 
79
- ## Dependencies
80
- - `@bananapus/721-hook-v6` NFT tier system (IJB721TiersHook, IJB721TokenUriResolver)
81
- - `@bananapus/core-v6` Core protocol interfaces
82
- - `@bananapus/router-terminal-v6` Payment routing
83
- - `@bananapus/suckers-v6` — Cross-chain support
84
- - `@rev-net/core-v6` — Revnet integration
85
- - `@openzeppelin/contracts` — Ownable, ERC2771, ReentrancyGuard, Strings
86
- - `keccak` — Hashing utilities
54
+ - A body can only point at assets currently escrowed by the resolver.
55
+ - Slot replacement must be one-for-one. Replacing an equipped item returns the old item instead of orphaning it.
56
+ - Outfit locks must block both direct edits and indirect attempts to reclaim an equipped item through another decoration call.
57
+ - Asset registration is split between hash registration and content upload so content can be trustlessly verified before it is stored on-chain.
87
58
 
88
- ## Design Decisions
59
+ ## Where Complexity Lives
89
60
 
90
- **Hash-then-upload for SVG storage.** The owner registers a keccak256 hash first (`setSvgHashesOf`, owner-only), then anyone can upload the matching content (`setSvgContentsOf`, permissionless). This separates content approval from upload gas costs -- the owner commits to exactly which artwork is valid, and community members or bots can pay the gas to actually store it. Both hash and content are immutable once set, preventing artwork tampering after mint.
61
+ - Escrow bookkeeping and slot replacement must stay synchronized.
62
+ - Lock enforcement has to cover both explicit removal and implicit replacement paths.
63
+ - SVG composition order is application logic, not a cosmetic detail.
91
64
 
92
- **7-day outfit lock.** `lockOutfitChangesFor` freezes a body's outfit for 7 days (`_LOCK_DURATION`). This exists so that a dressed Banny can be used as a stable visual identity (e.g. a PFP or on-chain avatar) without risk of the outfit changing underneath external references. The lock can be extended but never shortened -- calling it again resets the timer to 7 days from now, and reverts if the existing lock expires later than the new one would.
93
-
94
- **UPC as tier ID.** Rather than introducing a separate asset registry, the resolver reuses the 721 hook's tier ID as the universal product code. This means asset identity, pricing, supply caps, and category are all managed by the existing tier system with no additional storage. The resolver is purely a read/compose layer on top of the hook's data.
65
+ ## Dependencies
95
66
 
96
- **Fixed category ordering for outfit layering.** Outfits must be passed in ascending category order (2-17) and only one outfit per category is allowed. This constraint eliminates ambiguity in SVG z-ordering -- the category number directly determines the layer position. It also enables the resolver to insert default accessories (necklace, eyes, mouth) at the correct z-position when no custom one is equipped and no full-head item occludes them.
67
+ - `nana-721-hook-v6` for collection ownership and transfer semantics
68
+ - Juicebox metadata resolver patterns for token URI integration
97
69
 
98
- **Equipped assets travel with the body.** When a body NFT is transferred, all equipped outfits and backgrounds remain attached. The new owner inherits them and can unequip to receive the outfit NFTs. This was chosen over auto-unequip to preserve the dressed Banny as a complete visual unit, but it means sellers should unequip valuable outfits before listing.
70
+ ## Safe Change Guide
99
71
 
100
- **Outfits burn with the body.** When a body NFT is burned, equipped outfits and backgrounds held by the resolver become permanently unrecoverable. There is no recovery function — outfits share the body's fate. Users must unequip outfits before burning the body if they want to keep them.
72
+ - Put new generic 721 behavior in `nana-721-hook-v6`, not here.
73
+ - Treat slot accounting and escrow transfers as coupled logic. Changing one without the other is how equipment duplication bugs appear.
74
+ - Changes to `tokenURI` should preserve deterministic output for the same body state.
75
+ - If adding new asset categories, verify render order and replacement semantics together.
76
+ - If a change touches both metadata composition and escrow state, test transfer lifecycle behavior, not just rendered output.
@@ -1,354 +1,87 @@
1
- # banny-retail-v6 -- Audit Instructions
1
+ # Audit Instructions
2
2
 
3
- Target: `Banny721TokenUriResolver` -- a single-contract system that manages on-chain SVG-based NFT composition for the Juicebox V6 Banny ecosystem.
3
+ This repo is the Banny avatar composition layer. Its runtime surface is small, but it directly custodizes NFTs and decides what a Banny body can wear.
4
4
 
5
- ## Contract Table
5
+ ## Objective
6
6
 
7
- | Contract | Lines | Role |
8
- |----------|-------|------|
9
- | `Banny721TokenUriResolver` | ~1,428 | Token URI resolver, outfit custody, SVG storage, decoration logic, lock mechanism |
10
- | `IBanny721TokenUriResolver` | ~175 | Interface: events, view functions, mutating functions |
7
+ Find issues that:
8
+ - strand body, outfit, or background NFTs in the resolver
9
+ - let the wrong actor equip, unequip, steal, or overwrite accessories
10
+ - bypass outfit-lock timing or freeze users longer than intended
11
+ - return incorrect metadata for bodies, outfits, or backgrounds
12
+ - break category exclusivity or layering assumptions
11
13
 
12
- Inheritance chain: `Ownable`, `ERC2771Context`, `ReentrancyGuard`, `IJB721TokenUriResolver`, `IBanny721TokenUriResolver`, `IERC721Receiver`.
14
+ ## Scope
13
15
 
14
- Compiler: Solidity 0.8.28, Cancun EVM, via-IR optimizer (200 runs).
16
+ In scope:
17
+ - `src/Banny721TokenUriResolver.sol`
18
+ - `src/interfaces/IBanny721TokenUriResolver.sol`
19
+ - deployment scripts in `script/`
15
20
 
16
- ## Architecture Overview
21
+ Primary integration assumptions to verify:
22
+ - the resolver is used as a token URI resolver for a `JB721TiersHook`
23
+ - the underlying 721 hook remains the token contract and tier source
24
+ - the resolver temporarily holds accessory NFTs while they are equipped
17
25
 
18
- The resolver serves as both a **token URI generator** and an **NFT custodian**. It is registered as the URI resolver for a `JB721TiersHook` (the Juicebox 721 tier system). When a marketplace or wallet requests `tokenURI()`, the hook delegates to this resolver, which composes layered SVG artwork on-chain.
26
+ ## System Model
19
27
 
20
- The resolver does not mint or burn tokens. It holds outfit and background NFTs in custody on behalf of banny body owners, and generates composed SVG output by layering those assets.
28
+ The resolver does not mint project NFTs. It:
29
+ - reads tier and token metadata from the attached 721 hook
30
+ - receives outfit and background NFTs through safe transfers
31
+ - records which body currently has which attachments
32
+ - enforces category and conflict rules
33
+ - renders composed metadata and SVG output
21
34
 
22
- ```
23
- JB721TiersHook (the NFT contract)
24
- |
25
- v
26
- Banny721TokenUriResolver (this contract)
27
- |-- holds outfit/background NFTs in custody
28
- |-- generates composed SVG token URIs
29
- |-- enforces decoration rules and lock mechanism
30
- |
31
- v
32
- JB721TiersHookStore (read-only: tier metadata lookups)
33
- ```
35
+ The critical custody model is:
36
+ - body owner controls decoration
37
+ - resolver holds equipped accessories
38
+ - on unequip or invalidation, assets must become recoverable by the rightful owner
34
39
 
35
- ## Core Concepts
40
+ ## Critical Invariants
36
41
 
37
- ### Token ID Structure
42
+ 1. No asset loss in custody
43
+ Every outfit or background transferred into the resolver must remain attributable to exactly one body or be withdrawable back to the rightful owner.
38
44
 
39
- Token IDs encode product information: `tierId * 1_000_000_000 + sequenceNumber`. The `tierId` (called "UPC" or "product ID" in the codebase) maps to a tier in `JB721TiersHookStore`, which returns a `JB721Tier` struct containing `id`, `category`, `price`, `initialSupply`, `remainingSupply`, and other fields.
45
+ 2. Body ownership gates decoration
46
+ Only an authorized actor for the body may change its equipped state.
40
47
 
41
- ### Category System
48
+ 3. Category exclusivity
49
+ Conflicting categories must not be equipped together, and categories that are forbidden as accessories must never become equipped through edge paths.
42
50
 
43
- There are 18 categories (0-17), each representing a layer or slot:
51
+ 4. Lock correctness
52
+ `lockOutfitChangesFor` must only prevent changes for the intended body and duration. It must not be bypassable, extendable by unauthorized actors, or accidentally permanent.
44
53
 
45
- | ID | Constant | Name | Role |
46
- |----|----------|------|------|
47
- | 0 | `_BODY_CATEGORY` | Banny body | Base character (Alien, Pink, Orange, Original) |
48
- | 1 | `_BACKGROUND_CATEGORY` | Background | Scene behind the banny |
49
- | 2 | `_BACKSIDE_CATEGORY` | Backside | Behind-body accessories |
50
- | 3 | `_NECKLACE_CATEGORY` | Necklace | Has special layering (after suit top) |
51
- | 4 | `_HEAD_CATEGORY` | Head | Full head piece -- **blocks** Eyes, Glasses, Mouth, HeadTop |
52
- | 5 | `_EYES_CATEGORY` | Eyes | Blocked by Head |
53
- | 6 | `_GLASSES_CATEGORY` | Glasses | Blocked by Head |
54
- | 7 | `_MOUTH_CATEGORY` | Mouth | Blocked by Head |
55
- | 8 | `_LEGS_CATEGORY` | Legs | Leg wear |
56
- | 9 | `_SUIT_CATEGORY` | Suit | Full suit -- **blocks** SuitBottom, SuitTop |
57
- | 10 | `_SUIT_BOTTOM_CATEGORY` | Suit bottom | Blocked by Suit |
58
- | 11 | `_SUIT_TOP_CATEGORY` | Suit top | Blocked by Suit |
59
- | 12 | `_HEADTOP_CATEGORY` | Head top | Blocked by Head |
60
- | 13 | `_HAND_CATEGORY` | Fist | Hand accessory |
61
- | 14 | `_SPECIAL_SUIT_CATEGORY` | Special Suit | Special outfit slot |
62
- | 15 | `_SPECIAL_LEGS_CATEGORY` | Special Legs | Special outfit slot |
63
- | 16 | `_SPECIAL_HEAD_CATEGORY` | Special Head | Special outfit slot |
64
- | 17 | `_SPECIAL_BODY_CATEGORY` | Special Body | Special outfit slot |
54
+ 5. Metadata coherence
55
+ `tokenURI` and related rendering helpers must reflect actual equipped state and should not expose stale or impossible compositions.
65
56
 
66
- Categories 0 and 1 cannot be used as outfits (enforced at line 1299). Outfits must be categories 2-17.
57
+ ## Threat Model
67
58
 
68
- ### Conflict Rules
59
+ Prioritize adversaries that:
60
+ - transfer unexpected NFTs into the resolver
61
+ - try to decorate using burned, removed, or mismatched token IDs
62
+ - exploit reentrancy on NFT receipt or withdrawal
63
+ - use invalid category order or duplicate categories to desync state
64
+ - attempt to steal accessories by redecorating around lock windows
69
65
 
70
- Two conflict rules prevent contradictory outfits:
71
- 1. **Head blocks face pieces**: If category 4 (Head) is equipped, categories 5 (Eyes), 6 (Glasses), 7 (Mouth), and 12 (HeadTop) are rejected (line 1312-1318).
72
- 2. **Suit blocks top/bottom**: If category 9 (Suit) is equipped, categories 10 (SuitBottom) and 11 (SuitTop) are rejected (line 1319-1323).
66
+ ## Hotspots
73
67
 
74
- Outfits must be passed in **ascending category order** (line 1304-1305). No two outfits can share a category.
68
+ - `decorateBannyWith`: ownership checks, state replacement, and asset movement ordering
69
+ - any path that accepts NFT transfers into resolver custody
70
+ - outfit/background release paths after redecorating, burning, or invalid token states
71
+ - category validation and conflict checks
72
+ - lock timestamp handling
73
+ - token URI generation that assumes on-chain SVG data exists or remains consistent
75
74
 
76
- ## Outfit Decoration System
75
+ ## Build And Verification
77
76
 
78
- ### How Dressing Works
77
+ Standard workflow:
78
+ - `npm install`
79
+ - `forge build`
80
+ - `forge test`
79
81
 
80
- `decorateBannyWith(hook, bannyBodyId, backgroundId, outfitIds)` is the single entry point for all decoration changes. There is no separate "undress" function -- undressing is accomplished by calling `decorateBannyWith` with an empty `outfitIds` array and `backgroundId = 0`.
82
+ The current test tree emphasizes:
83
+ - attack and regression coverage around stranding and exclusivity
84
+ - decoration lifecycle flows
85
+ - fork and QA scenarios
81
86
 
82
- The function:
83
- 1. Verifies the caller owns the banny body (line 993).
84
- 2. Verifies the body token is actually category 0 (line 996).
85
- 3. Verifies the body is not locked (line 1001).
86
- 4. Delegates background handling to `_decorateBannyWithBackground` (line 1010).
87
- 5. Delegates outfit handling to `_decorateBannyWithOutfits` (line 1013).
88
-
89
- ### Background Decoration (`_decorateBannyWithBackground`, line 1173)
90
-
91
- - If `backgroundId != 0`, the caller must own the background NFT or own the banny body currently using it.
92
- - The background must be category 1 (`_BACKGROUND_CATEGORY`).
93
- - State updates (`_attachedBackgroundIdOf`, `_userOf`) happen before external transfers (CEI pattern).
94
- - The old background is returned via `_tryTransferFrom` (silent failure on burned tokens).
95
- - The new background is transferred into the resolver via `_transferFrom` (reverts on failure).
96
-
97
- ### Outfit Decoration (`_decorateBannyWithOutfits`, line 1243)
98
-
99
- This is the most complex function. It performs a **merge-style iteration** over two arrays: the new `outfitIds` and the previously equipped `previousOutfitIds`.
100
-
101
- For each new outfit:
102
- 1. Authorization: caller must own the outfit OR own the banny body currently wearing it (lines 1278-1293).
103
- 2. Category validation: must be 2-17, ascending order, no conflicts (lines 1299-1324).
104
- 3. The inner `while` loop transfers out old outfits up to the current category (lines 1332-1350).
105
- 4. If the outfit is not already worn by this body, state is updated and the outfit is transferred in (lines 1353-1361).
106
-
107
- After all new outfits are processed, a second `while` loop (line 1372) transfers out any remaining old outfits.
108
-
109
- Finally, `_attachedOutfitIdsOf[hook][bannyBodyId]` is overwritten wholesale with the new array (line 1392).
110
-
111
- ## Custody Model
112
-
113
- This is the highest-stakes part of the system.
114
-
115
- **Who holds the NFT**: When an outfit or background is equipped, the NFT is transferred from the caller to the resolver contract via `safeTransferFrom`. The resolver holds custody. The NFT is returned to the body owner when:
116
- - The body is redressed and the outfit is no longer in the new set.
117
- - The body is dressed with an empty outfit array (full undress).
118
- - The outfit is moved to a different body owned by the same person.
119
-
120
- **Transfer implications**: When a banny body NFT is transferred on the hook contract, all equipped outfits and the background remain associated with that body. The new body owner can call `decorateBannyWith` with empty arrays to receive all equipped NFTs. This is by design but creates a significant gotcha for sellers who forget to undress before selling.
121
-
122
- **No admin rescue**: The owner role has no function to force-return custody NFTs. If a bug prevents undressing, equipped NFTs are permanently locked.
123
-
124
- **`_tryTransferFrom` vs `_transferFrom`**: Returning old outfits uses `_tryTransferFrom` (try-catch, silent failure) because the token may have been burned or its tier removed. Equipping new outfits uses `_transferFrom` (reverts on failure) because the caller is asserting ownership of a token that must exist.
125
-
126
- ### Key Invariant
127
-
128
- Every outfit NFT held by the resolver must be recoverable by the current owner of the banny body it is associated with.
129
-
130
- ## Lock Mechanism
131
-
132
- `lockOutfitChangesFor(hook, bannyBodyId)` locks a body for 7 days (`_LOCK_DURATION = 7 days = 604,800 seconds`). While locked, `decorateBannyWith` reverts with `OutfitChangesLocked`.
133
-
134
- Rules:
135
- - Only the body owner can lock (line 1021).
136
- - Lock can only be extended, never shortened (line 1030). Attempting to set a shorter lock reverts with `CantAccelerateTheLock`.
137
- - Equal-time relocks succeed (the `>` check allows `currentLockedUntil == newLockUntil`).
138
- - The lock survives body transfers -- a buyer who receives a locked body cannot change outfits until the lock expires.
139
- - The `outfitLockedUntil` mapping is public and readable by marketplaces.
140
-
141
- **Purpose**: Enables trustless NFT marketplace sales of dressed bannys. A seller locks the body, lists it, and the buyer is guaranteed to receive the advertised outfit set.
142
-
143
- ## SVG Storage and Rendering
144
-
145
- ### Hash-Then-Reveal Pattern
146
-
147
- 1. Owner calls `setSvgHashesOf(upcs, svgHashes)` to commit `keccak256` hashes (line 1138). Hashes are write-once per UPC.
148
- 2. Anyone calls `setSvgContentsOf(upcs, svgContents)` to reveal content (line 1108). Content must match the stored hash. Content is write-once per UPC.
149
- 3. If content is not yet uploaded, `_svgOf` falls back to IPFS resolution via `JBIpfsDecoder` (line 936-949).
150
-
151
- ### SVG Composition
152
-
153
- `tokenUriOf` (line 203) builds a complete on-chain data URI:
154
- 1. For non-body tokens: renders the outfit SVG on a grey mannequin banny.
155
- 2. For body tokens: composes background + body + all outfit layers in category order.
156
-
157
- The body SVG uses CSS classes (`.b1`-`.b4`, `.a1`-`.a3`) with color fills specific to each body type (Alien=green, Pink=pink, Orange=orange, Original=yellow).
158
-
159
- Default accessories (necklace, eyes, mouth) are injected when no custom outfit occupies that slot. The necklace has special layering: it is stored during iteration but rendered after `_SUIT_TOP_CATEGORY` (line 885-890).
160
-
161
- ### SVG Sanitization
162
-
163
- **There is none.** SVG content is stored and rendered verbatim. The hash-commit pattern ensures only owner-approved content is stored, but the content itself is not sanitized. A malicious or compromised owner could commit hashes for SVGs containing `<script>` tags, external resource references (`<image href="https://...">`), or CSS injection.
164
-
165
- ## Meta-Transaction Support (ERC-2771)
166
-
167
- The contract inherits `ERC2771Context` with an immutable `trustedForwarder` set at construction. All authorization checks use `_msgSender()` instead of `msg.sender`.
168
-
169
- If `trustedForwarder == address(0)` (the default in all test setups), meta-transactions are effectively disabled -- `_msgSender()` returns `msg.sender`.
170
-
171
- If a non-zero forwarder is set, that forwarder contract can append arbitrary sender addresses to calldata, allowing gasless transactions. The forwarder is fully trusted and can impersonate any address for all operations.
172
-
173
- **Risk**: If the forwarder is compromised, all authorization checks (body ownership, outfit authorization, lock, admin functions) can be bypassed.
174
-
175
- ## `onERC721Received` Gate
176
-
177
- The resolver implements `IERC721Receiver.onERC721Received` (line 1044) and rejects all incoming transfers unless `operator == address(this)`. This means:
178
- - Only the resolver itself can send NFTs to itself (via its own `_transferFrom` calls).
179
- - Users cannot accidentally send NFTs directly to the resolver.
180
- - If a user sends an NFT via `transferFrom` (not `safeTransferFrom`), the callback is not triggered and the NFT is silently deposited. This is an inherent ERC-721 limitation.
181
-
182
- ## Priority Audit Areas
183
-
184
- ### 1. Outfit Authorization Logic (CRITICAL)
185
-
186
- File: `src/Banny721TokenUriResolver.sol`, lines 1278-1293.
187
-
188
- The authorization check for outfits allows the caller to use an outfit if they either own it directly or own the banny body currently wearing it. A historical bug (L18, now fixed) allowed `ownerOf(0)` bypass when `wearerOf` returned 0 for unworn outfits. Verify the current fix is sound:
189
- - Line 1283: `if (wearerId == 0) revert` -- ensures unworn outfits require direct ownership.
190
- - Line 1287: `ownerOf(wearerId)` -- verifies caller owns the body wearing the outfit.
191
-
192
- Look for: any path where an attacker can pass authorization without actually owning the outfit or the body wearing it.
193
-
194
- ### 2. Merge Iteration in `_decorateBannyWithOutfits` (HIGH)
195
-
196
- Lines 1271-1392. The merge between new `outfitIds` and `previousOutfitIds` is complex. The inner `while` loop advances through previous outfits, transferring them out. The second `while` loop (line 1372) handles remaining previous outfits.
197
-
198
- Look for:
199
- - Off-by-one errors in the `previousOutfitIndex` counter.
200
- - Skipped outfits that should be returned.
201
- - Double-transfer of outfits (both in the inner while and the tail while).
202
- - Removed-tier outfits (category=0) causing infinite loops or skipped entries.
203
- - The `_isInArray` check at line 1376 preventing outfits in the new set from being transferred out.
204
-
205
- ### 3. Custody Accounting Consistency (HIGH)
206
-
207
- State variables: `_attachedOutfitIdsOf`, `_attachedBackgroundIdOf`, `_wearerOf`, `_userOf`.
208
-
209
- These four mappings must remain consistent. After every `decorateBannyWith` call:
210
- - Every outfit in `_attachedOutfitIdsOf[hook][bodyId]` should have `_wearerOf[hook][outfitId] == bodyId`.
211
- - The background in `_attachedBackgroundIdOf[hook][bodyId]` should have `_userOf[hook][backgroundId] == bodyId`.
212
- - Every outfit/background held by the resolver should be tracked in these mappings.
213
-
214
- **Note**: `_attachedOutfitIdsOf` is overwritten wholesale at line 1392, but `_wearerOf` is only set for *new* outfits at line 1355. Outfits that were already worn by this body retain their `_wearerOf` entry from the previous call. Verify this does not cause stale state.
215
-
216
- ### 4. `_tryTransferFrom` Silent Failures (MEDIUM)
217
-
218
- Line 1424-1428. When returning old outfits, transfer failures are silently caught. This is intentional (handles burned tokens) but could mask real bugs.
219
-
220
- Look for: scenarios where `_tryTransferFrom` silently fails but the state mappings (`_wearerOf`, `_attachedOutfitIdsOf`) have already been updated, causing an outfit to be "lost" -- not held by the resolver, not returned to the owner, but still tracked as worn.
221
-
222
- ### 5. Cross-Hook Isolation (MEDIUM)
223
-
224
- All state mappings are keyed by `address hook`. The `hook` parameter is caller-supplied and never validated. A malicious hook contract could return arbitrary data from `ownerOf()`, `STORE()`, `tierOfTokenId()`.
225
-
226
- Verify: a malicious hook cannot affect outfits custodied from a different (legitimate) hook. The per-hook mapping keys should provide full isolation.
227
-
228
- ### 6. CEI Ordering in Background Replacement (MEDIUM)
229
-
230
- `_decorateBannyWithBackground` (lines 1212-1224) updates state before transfers. Verify:
231
- - `_attachedBackgroundIdOf` and `_userOf` are updated at lines 1213-1214 before the try-transfer at line 1218 and the incoming transfer at line 1223.
232
- - No reachable state where a reentrancy callback during the safeTransferFrom at line 1223 could observe inconsistent state.
233
-
234
- ### 7. SVG Content Safety (LOW)
235
-
236
- SVG content is stored verbatim. While this is a view-function-only concern, verify that the encoding in `_encodeTokenUri` (line 628) cannot produce malformed JSON that breaks parsers. Specifically check for unescaped characters in `svgDescription`, `svgExternalUrl`, and custom product names injected into the JSON.
237
-
238
- ## Key Invariants to Test
239
-
240
- 1. **Outfit recoverability**: Every outfit NFT held by the resolver can be recovered by the current body owner via `decorateBannyWith(hook, bodyId, 0, [])`.
241
- 2. **No orphaned custody**: After `decorateBannyWith`, the resolver does not hold any outfit NFTs that are not tracked in `_attachedOutfitIdsOf` or `_attachedBackgroundIdOf`.
242
- 3. **Category ascending order**: `_attachedOutfitIdsOf[hook][bodyId]` always contains outfits in ascending category order.
243
- 4. **Lock monotonicity**: `outfitLockedUntil[hook][bodyId]` can only increase or remain the same.
244
- 5. **Cross-hook isolation**: Operations on hook A never transfer, modify, or read custody state from hook B.
245
- 6. **SVG hash/content immutability**: Once `svgHashOf[upc]` is set, it cannot be changed. Once `_svgContentOf[upc]` is set, it cannot be changed.
246
- 7. **ReentrancyGuard blocks re-entry**: No call to `decorateBannyWith` can re-enter itself.
247
-
248
- ## Testing Setup
249
-
250
- **Framework**: Foundry (forge). Config in `foundry.toml`.
251
-
252
- ```bash
253
- # Run all tests
254
- forge test
255
-
256
- # Run with verbosity
257
- forge test -vvv
258
-
259
- # Run specific test file
260
- forge test --match-path test/Fork.t.sol -vvv
261
-
262
- # Run fork tests (requires RPC_ETHEREUM_MAINNET env var)
263
- RPC_ETHEREUM_MAINNET=<your-rpc-url> forge test --match-path test/Fork.t.sol -vvv
264
-
265
- # Gas report
266
- forge test --gas-report
267
- ```
268
-
269
- **Test suite**: 14 test files, ~230+ tests.
270
-
271
- | File | Purpose |
272
- |------|---------|
273
- | `Banny721TokenUriResolver.t.sol` | Unit tests with mock hook/store |
274
- | `DecorateFlow.t.sol` | Authorization flows, L18 fix proof, multi-party |
275
- | `BannyAttacks.t.sol` | Adversarial: outfit theft, lock bypass, category abuse |
276
- | `Fork.t.sol` | End-to-end with real JB infrastructure, reentrancy |
277
- | `regression/CEIReorder.t.sol` | CEI ordering verification |
278
- | `regression/RemovedTierDesync.t.sol` | Removed tier handling |
279
- | `regression/ArrayLengthValidation.t.sol` | Array mismatch reverts |
280
- | `regression/BodyCategoryValidation.t.sol` | Non-body token rejection |
281
- | `regression/MsgSenderEvents.t.sol` | ERC-2771 event correctness |
282
- | `regression/BurnedTokenCheck.t.sol` | Burned token handling |
283
- | `regression/ClearMetadata.t.sol` | Metadata clearing |
284
- | `OutfitTransferLifecycle.t.sol` | Outfit transfer lifecycle flows |
285
- | `TestAuditGaps.sol` | Audit gap coverage: ERC-2771 meta-transaction flows (forwarder relay, spoofing prevention, owner-only relay), SVG rendering edge cases (special characters, script tags, unicode, long content, JSON-breaking chars), SVG composition validation (default decorations, alien vs standard eyes, background inclusion/exclusion) |
286
- | `TestQALastMile.t.sol` | QA last-mile edge cases |
287
-
288
- **Untested areas** (potential audit additions):
289
- - Meta-transaction flows with a real forwarder (basic coverage exists in TestAuditGaps.sol; advanced scenarios with a real forwarder remain untested -- all tests use `address(0)`).
290
- - SVG content containing special characters or potential injection payloads (basic coverage exists in TestAuditGaps.sol for script tags, unicode, JSON-breaking chars, and long content; advanced payloads remain untested).
291
- - Gas consumption for `tokenUriOf` with maximum outfit count.
292
- - Ownership transfer of the resolver (`transferOwnership`) and continued admin access.
293
- - Product name overwriting (no write-once protection on `_customProductNameOf`).
294
- - The `transferFrom` (non-safe) path where NFTs bypass `onERC721Received`.
295
-
296
- ## External Dependencies
297
-
298
- | Dependency | Used For |
299
- |------------|----------|
300
- | OpenZeppelin `Ownable` | Admin access control |
301
- | OpenZeppelin `ERC2771Context` | Meta-transaction sender extraction |
302
- | OpenZeppelin `ReentrancyGuard` | Reentrancy protection on `decorateBannyWith` |
303
- | OpenZeppelin `Strings` | `uint256.toString()` for metadata |
304
- | `base64` (lib) | Base64 encoding for data URIs |
305
- | `@bananapus/721-hook-v6` | `IJB721TiersHook`, `IJB721TiersHookStore`, `JBIpfsDecoder`, `JB721Tier`, `IERC721` |
306
-
307
- The resolver makes external calls to the `hook` and its `STORE()` but does not call any core Juicebox protocol contracts (no terminal, controller, or directory interactions).
308
-
309
- ## Error Reference
310
-
311
- | Error | Trigger |
312
- |-------|---------|
313
- | `ArrayLengthMismatch` | `upcs.length != svgHashes/svgContents/names.length` |
314
- | `BannyBodyNotBodyCategory` | `bannyBodyId` is not category 0 |
315
- | `CantAccelerateTheLock` | New lock expires sooner than current lock |
316
- | `ContentsAlreadyStored` | SVG content already set for this UPC |
317
- | `ContentsMismatch` | SVG content hash does not match stored hash |
318
- | `HashAlreadyStored` | SVG hash already set for this UPC |
319
- | `HashNotFound` | No hash set for this UPC (cannot upload content) |
320
- | `HeadAlreadyAdded` | Conflict: Head + Eyes/Glasses/Mouth/HeadTop |
321
- | `OutfitChangesLocked` | Body is locked, cannot change outfits |
322
- | `SuitAlreadyAdded` | Conflict: Suit + SuitBottom/SuitTop |
323
- | `UnauthorizedBackground` | Caller does not own the background |
324
- | `UnauthorizedBannyBody` | Caller does not own the banny body |
325
- | `UnauthorizedOutfit` | Caller does not own the outfit or its wearer's body |
326
- | `UnauthorizedTransfer` | NFT sent to resolver not by resolver itself |
327
- | `UnorderedCategories` | Outfits not in ascending category order |
328
- | `UnrecognizedBackground` | Token is not category 1 |
329
- | `UnrecognizedCategory` | Outfit category not in 2-17 range |
330
- | `UnrecognizedProduct` | Body UPC not 1-4 (Alien/Pink/Orange/Original) |
331
-
332
- ## How to Report Findings
333
-
334
- For each finding:
335
-
336
- 1. **Title** -- one line, starts with severity (CRITICAL/HIGH/MEDIUM/LOW)
337
- 2. **Affected contract(s)** -- exact file path and line numbers
338
- 3. **Description** -- what is wrong, in plain language
339
- 4. **Trigger sequence** -- step-by-step, minimal steps to reproduce
340
- 5. **Impact** -- what an attacker gains, what a user loses
341
- 6. **Proof** -- code trace or Foundry test
342
- 7. **Fix** -- minimal code change
343
-
344
- **Severity guide:**
345
- - **CRITICAL**: Permanent NFT custody loss, unauthorized outfit theft.
346
- - **HIGH**: Conditional NFT loss, authorization bypass, broken custody invariant.
347
- - **MEDIUM**: State inconsistency without fund loss, griefing that locks outfits.
348
- - **LOW**: Cosmetic SVG issues, informational, edge-case-only.
349
-
350
- ## Previous Audit Findings
351
-
352
- | ID | Severity | Status | Description |
353
- |----|----------|--------|-------------|
354
- | L18 | HIGH | FIXED | `ownerOf(0)` bypass in outfit authorization. When `wearerOf` returned 0 for unworn outfits, `hook.ownerOf(0)` could succeed for some hooks, allowing unauthorized outfit use. Fixed by adding `if (wearerId == 0) revert` guard at line 1283. Regression test in `DecorateFlow.t.sol`. |
87
+ Prefer proofs that show a body or accessory becoming inaccessible, transferable by the wrong party, or rendered inconsistently with stored state.
package/CHANGELOG.md ADDED
@@ -0,0 +1,31 @@
1
+ # Changelog
2
+
3
+ ## Scope
4
+
5
+ This file describes the verified change from `banny-retail-v5` to the current `banny-retail-v6` repo.
6
+
7
+ ## Current v6 surface
8
+
9
+ - `Banny721TokenUriResolver`
10
+ - `IBanny721TokenUriResolver`
11
+
12
+ ## Summary
13
+
14
+ - Decoration flows now handle previously equipped assets more defensively. The v6 test suite adds explicit regression coverage for removed tiers, failed return transfers, and stranded-asset scenarios.
15
+ - Metadata management is broader than in v5. The resolver now manages description, external URL, and base URI together instead of only a base URI path.
16
+ - Validation is stricter. v6 adds array-length checks on batch setters and extra checks around valid body and background categories before decoration logic proceeds.
17
+ - The repo was upgraded to the v6 dependency set and Solidity `0.8.28`.
18
+
19
+ ## Verified deltas
20
+
21
+ - `setSvgBaseUri(...)` was replaced by `setMetadata(description, url, baseUri)`.
22
+ - Metadata JSON is no longer hardcoded around a fixed description and external URL. Those values now come from contract state.
23
+ - `pricingContext()` consumption changed with the v6 721 hook and now uses the two-value return shape.
24
+ - The resolver adds explicit `Banny721TokenUriResolver_ArrayLengthMismatch()` and `Banny721TokenUriResolver_BannyBodyNotBodyCategory()` errors.
25
+ - Outfit and background handling now includes logic intended to preserve attachment state when a previously equipped asset cannot be returned cleanly.
26
+
27
+ ## Migration notes
28
+
29
+ - Treat `setMetadata` as the metadata-management entry point. v5 assumptions around a base-URI-only setter no longer fit this repo.
30
+ - Decoration flows should be reviewed for failure handling, especially if an integration assumed every previously equipped NFT could always be transferred back out.
31
+ - Event and error expectations should be regenerated from the v6 ABI rather than copied from v5.