@bannynet/core-v6 0.0.8 → 0.0.10

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.
@@ -0,0 +1,327 @@
1
+ # banny-retail-v6 -- Audit Instructions
2
+
3
+ Target: `Banny721TokenUriResolver` -- a single-contract system that manages on-chain SVG-based NFT composition for the Juicebox V6 Banny ecosystem.
4
+
5
+ ## Contract Table
6
+
7
+ | Contract | Lines | Role |
8
+ |----------|-------|------|
9
+ | `Banny721TokenUriResolver` | ~1,404 | Token URI resolver, outfit custody, SVG storage, decoration logic, lock mechanism |
10
+ | `IBanny721TokenUriResolver` | ~175 | Interface: events, view functions, mutating functions |
11
+
12
+ Inheritance chain: `Ownable`, `ERC2771Context`, `ReentrancyGuard`, `IJB721TokenUriResolver`, `IBanny721TokenUriResolver`, `IERC721Receiver`.
13
+
14
+ Compiler: Solidity 0.8.26, Cancun EVM, via-IR optimizer (200 runs).
15
+
16
+ ## Architecture Overview
17
+
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.
19
+
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.
21
+
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
+ ```
34
+
35
+ ## Core Concepts
36
+
37
+ ### Token ID Structure
38
+
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.
40
+
41
+ ### Category System
42
+
43
+ There are 18 categories (0-17), each representing a layer or slot:
44
+
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 |
65
+
66
+ Categories 0 and 1 cannot be used as outfits (enforced at line 1275). Outfits must be categories 2-17.
67
+
68
+ ### Conflict Rules
69
+
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 1289-1294).
72
+ 2. **Suit blocks top/bottom**: If category 9 (Suit) is equipped, categories 10 (SuitBottom) and 11 (SuitTop) are rejected (line 1295-1299).
73
+
74
+ Outfits must be passed in **ascending category order** (line 1280-1281). No two outfits can share a category.
75
+
76
+ ## Outfit Decoration System
77
+
78
+ ### How Dressing Works
79
+
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`.
81
+
82
+ The function:
83
+ 1. Verifies the caller owns the banny body (line 987).
84
+ 2. Verifies the body token is actually category 0 (line 990).
85
+ 3. Verifies the body is not locked (line 995).
86
+ 4. Delegates background handling to `_decorateBannyWithBackground` (line 1004).
87
+ 5. Delegates outfit handling to `_decorateBannyWithOutfits` (line 1007).
88
+
89
+ ### Background Decoration (`_decorateBannyWithBackground`, line 1155)
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 1222)
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 1257-1268).
103
+ 2. Category validation: must be 2-17, ascending order, no conflicts (lines 1275-1300).
104
+ 3. The inner `while` loop transfers out old outfits up to the current category (lines 1308-1326).
105
+ 4. If the outfit is not already worn by this body, state is updated and the outfit is transferred in (lines 1329-1337).
106
+
107
+ After all new outfits are processed, a second `while` loop (line 1348) transfers out any remaining old outfits.
108
+
109
+ Finally, `_attachedOutfitIdsOf[hook][bannyBodyId]` is overwritten wholesale with the new array (line 1368).
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 1015).
136
+ - Lock can only be extended, never shortened (line 1024). 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 1130). Hashes are write-once per UPC.
148
+ 2. Anyone calls `setSvgContentsOf(upcs, svgContents)` to reveal content (line 1100). 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-942).
150
+
151
+ ### SVG Composition
152
+
153
+ `tokenUriOf` (line 200) 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 880-884).
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 1038) 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 1254-1268.
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 1262: `if (wearerId == 0) revert` -- ensures unworn outfits require direct ownership.
190
+ - Line 1266: `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 1250-1368. 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 1348) 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 1352 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 1368, but `_wearerOf` is only set for *new* outfits at line 1331. 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 1390-1393. 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 1192-1203) updates state before transfers. Verify:
231
+ - `_attachedBackgroundIdOf` and `_userOf` are updated at lines 1192-1193 before the try-transfer at line 1197 and the incoming transfer at line 1202.
232
+ - No reachable state where a reentrancy callback during the safeTransferFrom at line 1202 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 622) 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**: 11 test files, ~130+ 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
+
285
+ **Untested areas** (potential audit additions):
286
+ - Meta-transaction flows with a real forwarder (all tests use `address(0)`).
287
+ - SVG content containing special characters or potential injection payloads.
288
+ - Gas consumption for `tokenUriOf` with maximum outfit count.
289
+ - Ownership transfer of the resolver (`transferOwnership`) and continued admin access.
290
+ - Product name overwriting (no write-once protection on `_customProductNameOf`).
291
+ - The `transferFrom` (non-safe) path where NFTs bypass `onERC721Received`.
292
+
293
+ ## External Dependencies
294
+
295
+ | Dependency | Used For |
296
+ |------------|----------|
297
+ | OpenZeppelin `Ownable` | Admin access control |
298
+ | OpenZeppelin `ERC2771Context` | Meta-transaction sender extraction |
299
+ | OpenZeppelin `ReentrancyGuard` | Reentrancy protection on `decorateBannyWith` |
300
+ | OpenZeppelin `Strings` | `uint256.toString()` for metadata |
301
+ | `base64` (lib) | Base64 encoding for data URIs |
302
+ | `@bananapus/721-hook-v6` | `IJB721TiersHook`, `IJB721TiersHookStore`, `JBIpfsDecoder`, `JB721Tier`, `IERC721` |
303
+
304
+ 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).
305
+
306
+ ## Error Reference
307
+
308
+ | Error | Trigger |
309
+ |-------|---------|
310
+ | `ArrayLengthMismatch` | `upcs.length != svgHashes/svgContents/names.length` |
311
+ | `BannyBodyNotBodyCategory` | `bannyBodyId` is not category 0 |
312
+ | `CantAccelerateTheLock` | New lock expires sooner than current lock |
313
+ | `ContentsAlreadyStored` | SVG content already set for this UPC |
314
+ | `ContentsMismatch` | SVG content hash does not match stored hash |
315
+ | `HashAlreadyStored` | SVG hash already set for this UPC |
316
+ | `HashNotFound` | No hash set for this UPC (cannot upload content) |
317
+ | `HeadAlreadyAdded` | Conflict: Head + Eyes/Glasses/Mouth/HeadTop |
318
+ | `OutfitChangesLocked` | Body is locked, cannot change outfits |
319
+ | `SuitAlreadyAdded` | Conflict: Suit + SuitBottom/SuitTop |
320
+ | `UnauthorizedBackground` | Caller does not own the background |
321
+ | `UnauthorizedBannyBody` | Caller does not own the banny body |
322
+ | `UnauthorizedOutfit` | Caller does not own the outfit or its wearer's body |
323
+ | `UnauthorizedTransfer` | NFT sent to resolver not by resolver itself |
324
+ | `UnorderedCategories` | Outfits not in ascending category order |
325
+ | `UnrecognizedBackground` | Token is not category 1 |
326
+ | `UnrecognizedCategory` | Outfit category not in 2-17 range |
327
+ | `UnrecognizedProduct` | Body UPC not 1-4 (Alien/Pink/Orange/Original) |
package/CHANGE_LOG.md ADDED
@@ -0,0 +1,222 @@
1
+ # banny-retail-v6 Changelog (v5 → v6)
2
+
3
+ This document describes all changes between `banny-retail` (v5) and `banny-retail-v6` (v6).
4
+
5
+ ---
6
+
7
+ ## 1. Breaking Changes
8
+
9
+ ### Solidity Version Bump
10
+ - **v5:** `pragma solidity 0.8.23;`
11
+ - **v6:** `pragma solidity 0.8.26;`
12
+
13
+ ### Dependency Imports Updated
14
+ All `@bananapus/721-hook-v5` imports replaced with `@bananapus/721-hook-v6`:
15
+ - `IERC721`, `IJB721TiersHook`, `IJB721TiersHookStore`, `IJB721TokenUriResolver`, `JB721Tier`, `JBIpfsDecoder`
16
+
17
+ ### `setSvgBaseUri()` Removed and Replaced by `setMetadata()`
18
+ - **v5:** `setSvgBaseUri(string calldata baseUri)` -- sets only the SVG base URI. Emits `SetSvgBaseUri`.
19
+ - **v6:** `setMetadata(string calldata description, string calldata url, string calldata baseUri)` -- sets description, external URL, and base URI in a single call. Emits `SetMetadata`.
20
+ - Callers that previously used `setSvgBaseUri()` must migrate to `setMetadata()`.
21
+
22
+ ### `setSvgHashsOf()` Renamed to `setSvgHashesOf()`
23
+ - **v5:** `setSvgHashsOf(uint256[] memory upcs, bytes32[] memory svgHashs)`
24
+ - **v6:** `setSvgHashesOf(uint256[] memory upcs, bytes32[] memory svgHashes)`
25
+ - Function name and parameter name corrected for proper English pluralization.
26
+
27
+ ### `pricingContext()` Return Value Change
28
+ - **v5:** `(uint256 currency, uint256 decimals,) = IJB721TiersHook(hook).pricingContext();` -- three return values (third ignored).
29
+ - **v6:** `(uint256 currency, uint256 decimals) = IJB721TiersHook(hook).pricingContext();` -- two return values.
30
+ - Reflects an upstream change in `IJB721TiersHook` where `pricingContext()` now returns only two values.
31
+
32
+ ### Token Metadata `description` and `external_url` Are Now Dynamic
33
+ - **v5:** Hardcoded in `tokenUriOf()`: `"description":"A piece of Banny Retail."` and `"external_url":"https://retail.banny.eth.sucks"`.
34
+ - **v6:** Read from state variables `svgDescription` and `svgExternalUrl`, set via `setMetadata()`. These default to empty strings until the owner sets them.
35
+
36
+ ---
37
+
38
+ ## 2. New Features
39
+
40
+ ### New State Variables: `svgDescription` and `svgExternalUrl`
41
+ - `string public svgDescription` -- the description used in token metadata JSON.
42
+ - `string public svgExternalUrl` -- the external URL used in token metadata JSON.
43
+ - Both are settable via the new `setMetadata()` function.
44
+
45
+ ### Body Category Validation in `decorateBannyWith()`
46
+ - **v6 adds:** A check that the `bannyBodyId` actually belongs to a body-category tier (`_BODY_CATEGORY == 0`). If not, reverts with `Banny721TokenUriResolver_BannyBodyNotBodyCategory()`.
47
+ - **v5:** No such check existed; any token ID could be passed as a banny body.
48
+
49
+ ### `_tryTransferFrom()` -- Fault-Tolerant Transfers
50
+ - **v6 adds:** `_tryTransferFrom(address hook, address from, address to, uint256 assetId)` -- wraps `safeTransferFrom` in a try-catch, silently succeeding if the transfer fails (e.g., if the token was burned or its tier removed).
51
+ - Used in `_decorateBannyWithBackground()` and `_decorateBannyWithOutfits()` when returning previously equipped items.
52
+ - **v5:** Used `_transferFrom()` (which reverts on failure) for all transfers, meaning a single burned/removed outfit could block the entire decoration operation.
53
+
54
+ ### `_isInArray()` Helper
55
+ - **v6 adds:** `_isInArray(uint256 value, uint256[] memory array)` -- checks if a value is present in an array.
56
+ - Used during outfit cleanup to skip outfits being re-equipped rather than transferring them out and back in.
57
+
58
+ ### Array Length Validation on Batch Setters
59
+ - **v6 adds:** `Banny721TokenUriResolver_ArrayLengthMismatch()` error.
60
+ - `setProductNames()`, `setSvgContentsOf()`, and `setSvgHashesOf()` now validate that the `upcs` and values arrays have matching lengths. v5 had no such check, risking out-of-bounds reverts.
61
+
62
+ ### `assetIdsOf()` Array Resize via Assembly
63
+ - **v6 adds:** After filtering outfits, the returned `outfitIds` array is resized via inline assembly (`mstore(outfitIds, numberOfIncludedOutfits)`) to remove trailing zeros.
64
+ - **v5:** Returned the full-length array with trailing zero entries for unincluded outfits.
65
+
66
+ ### `_encodeTokenUri()` Extracted Helper
67
+ - **v6 adds:** `_encodeTokenUri(uint256 tokenId, JB721Tier memory product, string memory extraMetadata, string memory imageContents)` -- an internal view function that encodes the token URI JSON with base64.
68
+ - Uses nested `abi.encodePacked()` calls to avoid "stack too deep" errors.
69
+ - **v5:** Inlined the entire JSON encoding in `tokenUriOf()` as a single large `abi.encodePacked()` call.
70
+
71
+ ### Default Eyes Bug Fix (`_outfitContentsFor`)
72
+ - **v5 (bug):** `_outfitContentsFor()` used the current outfit's `upc` to decide alien vs. standard default eyes: `if (upc == ALIEN_UPC)`. This checked the UPC of the *outfit being iterated*, not the banny body.
73
+ - **v6 (fix):** `_outfitContentsFor()` now accepts an additional `bodyUpc` parameter and uses `if (bodyUpc == ALIEN_UPC)` to correctly select default eyes based on the banny body type.
74
+
75
+ ### Improved Background Authorization Logic
76
+ - **v6:** `_decorateBannyWithBackground()` now explicitly checks if an unused background (where `userId == 0`) can only be attached by its owner. In v5, the authorization check `_msgSender() != owner && _msgSender() != IERC721(hook).ownerOf(userOf(hook, backgroundId))` could behave unexpectedly when `userOf()` returned 0 (querying `ownerOf(0)` on the hook).
77
+
78
+ ### CEI Pattern in `_decorateBannyWithBackground()`
79
+ - **v6:** Updates all state (`_attachedBackgroundIdOf`, `_userOf`) before any external transfers. Previous background transfer-out happens after state updates.
80
+ - **v5:** Transferred the previous background out *before* updating state for the new background, creating a less safe interaction ordering.
81
+
82
+ ### Background Category Validation
83
+ - **v5:** Only checked `backgroundProduct.id == 0` to reject invalid backgrounds.
84
+ - **v6:** Also checks `backgroundProduct.category != _BACKGROUND_CATEGORY`, ensuring only actual background-category items can be used as backgrounds.
85
+
86
+ ### Outfit Re-equip Optimization
87
+ - **v6:** When cleaning up remaining previous outfits, checks `_isInArray(previousOutfitId, outfitIds)` to skip outfits being re-equipped, avoiding unnecessary transfer-out-and-back-in cycles.
88
+ - **v5:** Would transfer the outfit out and then transfer it back in during the same transaction.
89
+
90
+ ### Improved Loop Guard in `_decorateBannyWithOutfits()`
91
+ - **v5:** `while (previousOutfitProductCategory <= outfitProductCategory && previousOutfitProductCategory != 0)` -- stops on category 0 but could re-enter after exhaustion.
92
+ - **v6:** `while (previousOutfitId != 0 && previousOutfitProductCategory <= outfitProductCategory)` -- guards on `previousOutfitId != 0` as primary condition, correctly handling removed tiers (category 0) by always processing and advancing past them.
93
+
94
+ ### Re-check Ownership Before Transfer in `_decorateBannyWithOutfits()`
95
+ - **v5:** Cached `owner = IERC721(hook).ownerOf(outfitId)` at the top of the loop, then later checked `if (owner != address(this))`.
96
+ - **v6:** Re-checks `IERC721(hook).ownerOf(outfitId) != address(this)` at transfer time, avoiding stale ownership data after intermediate transfers.
97
+
98
+ ---
99
+
100
+ ## 3. Event Changes
101
+
102
+ ### Added
103
+ | Event | Signature |
104
+ |-------|-----------|
105
+ | `SetMetadata` | `SetMetadata(string description, string externalUrl, string baseUri, address caller)` |
106
+
107
+ ### Removed
108
+ | Event | Signature |
109
+ |-------|-----------|
110
+ | `SetSvgBaseUri` | `SetSvgBaseUri(string baseUri, address caller)` |
111
+
112
+ ### Unchanged
113
+ | Event | Notes |
114
+ |-------|-------|
115
+ | `DecorateBanny` | Same signature in both versions |
116
+ | `SetProductName` | Same signature in both versions |
117
+ | `SetSvgContent` | Same signature in both versions |
118
+ | `SetSvgHash` | Same signature in both versions |
119
+
120
+ ### `msg.sender` Replaced with `_msgSender()` in Event Emissions
121
+ - **v5:** `setProductNames()`, `setSvgBaseUri()`, `setSvgContentsOf()`, and `setSvgHashsOf()` used `msg.sender` in event emissions.
122
+ - **v6:** All event emissions consistently use `_msgSender()` (ERC-2771 compatible).
123
+
124
+ ---
125
+
126
+ ## 4. Error Changes
127
+
128
+ ### Added
129
+ | Error | Purpose |
130
+ |-------|---------|
131
+ | `Banny721TokenUriResolver_ArrayLengthMismatch()` | Reverts when batch setter arrays have mismatched lengths |
132
+ | `Banny721TokenUriResolver_BannyBodyNotBodyCategory()` | Reverts when `decorateBannyWith()` is called with a non-body-category token |
133
+
134
+ ### Unchanged
135
+ | Error |
136
+ |-------|
137
+ | `Banny721TokenUriResolver_CantAccelerateTheLock()` |
138
+ | `Banny721TokenUriResolver_ContentsAlreadyStored()` |
139
+ | `Banny721TokenUriResolver_ContentsMismatch()` |
140
+ | `Banny721TokenUriResolver_HashAlreadyStored()` |
141
+ | `Banny721TokenUriResolver_HashNotFound()` |
142
+ | `Banny721TokenUriResolver_HeadAlreadyAdded()` |
143
+ | `Banny721TokenUriResolver_OutfitChangesLocked()` |
144
+ | `Banny721TokenUriResolver_SuitAlreadyAdded()` |
145
+ | `Banny721TokenUriResolver_UnauthorizedBackground()` |
146
+ | `Banny721TokenUriResolver_UnauthorizedBannyBody()` |
147
+ | `Banny721TokenUriResolver_UnauthorizedOutfit()` |
148
+ | `Banny721TokenUriResolver_UnauthorizedTransfer()` |
149
+ | `Banny721TokenUriResolver_UnorderedCategories()` |
150
+ | `Banny721TokenUriResolver_UnrecognizedBackground()` |
151
+ | `Banny721TokenUriResolver_UnrecognizedCategory()` |
152
+ | `Banny721TokenUriResolver_UnrecognizedProduct()` |
153
+
154
+ ---
155
+
156
+ ## 5. Struct Changes
157
+
158
+ No struct changes. Both versions use `JB721Tier` from the respective `721-hook` dependency. Any changes to `JB721Tier` are upstream in `nana-721-hook-v6`.
159
+
160
+ ---
161
+
162
+ ## 6. Implementation Changes (Non-Interface)
163
+
164
+ ### Token URI JSON Encoding Refactored
165
+ - **v5:** Single large `abi.encodePacked()` call with all JSON fields inlined in `tokenUriOf()`.
166
+ - **v6:** Split into `pricingMetadata` string built separately, then delegated to `_encodeTokenUri()`. Uses nested `abi.encodePacked()` to avoid "stack too deep".
167
+
168
+ ### `_outfitContentsFor()` Signature Change
169
+ - **v5:** `_outfitContentsFor(address hook, uint256[] memory outfitIds)`
170
+ - **v6:** `_outfitContentsFor(address hook, uint256[] memory outfitIds, uint256 bodyUpc)` -- added `bodyUpc` parameter for correct default eyes selection.
171
+
172
+ ### `_bannyBodySvgOf()` Relocated
173
+ - **v5:** Located after `_msgSender()` / `_msgData()` overrides (line ~700).
174
+ - **v6:** Relocated to immediately after `_categoryNameOf()` (line ~532), grouped with other internal view functions.
175
+
176
+ ### `_contextSuffixLength()` Relocated
177
+ - **v5:** Located before `_bannyBodySvgOf()` (line ~562).
178
+ - **v6:** Relocated after `_categoryNameOf()` and `_bannyBodySvgOf()` (line ~616).
179
+
180
+ ### Named Parameters in `JBIpfsDecoder.decode()` Calls
181
+ - **v5:** Positional arguments: `JBIpfsDecoder.decode(baseUri, ...)`.
182
+ - **v6:** Named arguments: `JBIpfsDecoder.decode({baseUri: baseUri, hexString: ...})`.
183
+
184
+ ### NatDoc / Comment Improvements
185
+ - Typo fixes: "Nakes" to "Naked", "receieved" to "received", "prefered" to "preferred", "categorie's" to "category's", "transfered" to "transferred", "scg" to "svg", "lateset" to "latest".
186
+ - Added detailed NatDoc to all interface functions (v5 interface had no NatDoc).
187
+ - Added documentation for outfit travel behavior on banny body transfer.
188
+ - Added documentation for unbounded array gas considerations on `_attachedOutfitIdsOf`.
189
+ - Added detailed authorization rules in `decorateBannyWith()` NatDoc (6-point checklist).
190
+ - Added warning about outfit/background travel on banny body transfer.
191
+ - Added comment about `transferFrom` vs `safeTransferFrom` limitation in `onERC721Received`.
192
+
193
+ ### Lint Suppression Comments
194
+ - **v6 adds:** `// forge-lint: disable-next-line(mixed-case-variable)` above `DEFAULT_ALIEN_EYES`, `DEFAULT_MOUTH`, `DEFAULT_NECKLACE`, `DEFAULT_STANDARD_EYES`, and `BANNY_BODY`.
195
+
196
+ ### Import Order Change
197
+ - **v5:** OpenZeppelin imports mixed with `@bananapus` imports.
198
+ - **v6:** OpenZeppelin imports first, then `@bananapus` imports, separated by a blank line.
199
+
200
+ ### Slither Annotations
201
+ - **v6 adds:** `// slither-disable-next-line calls-loop` on several `IERC721(hook).ownerOf()` calls inside loops.
202
+ - **v6 adds:** `// slither-disable-next-line encode-packed-collision` on the `_encodeTokenUri()` return.
203
+
204
+ ---
205
+
206
+ ## 7. Migration Table
207
+
208
+ | v5 Function / Event | v6 Equivalent | Notes |
209
+ |---|---|---|
210
+ | `setSvgBaseUri(string)` | `setMetadata(string, string, string)` | Now sets description + external URL + base URI together |
211
+ | `setSvgHashsOf(uint256[], bytes32[])` | `setSvgHashesOf(uint256[], bytes32[])` | Renamed (typo fix) |
212
+ | `SetSvgBaseUri` event | `SetMetadata` event | Different parameters |
213
+ | N/A | `svgDescription` (state variable) | New |
214
+ | N/A | `svgExternalUrl` (state variable) | New |
215
+ | N/A | `Banny721TokenUriResolver_ArrayLengthMismatch` | New error |
216
+ | N/A | `Banny721TokenUriResolver_BannyBodyNotBodyCategory` | New error |
217
+ | `_transferFrom()` (for returning items) | `_tryTransferFrom()` | Fault-tolerant; `_transferFrom()` still exists for mandatory transfers |
218
+ | N/A | `_isInArray()` | New helper |
219
+ | N/A | `_encodeTokenUri()` | Extracted from `tokenUriOf()` |
220
+ | `_outfitContentsFor(hook, outfitIds)` | `_outfitContentsFor(hook, outfitIds, bodyUpc)` | Added `bodyUpc` param (bug fix) |
221
+ | `@bananapus/721-hook-v5` | `@bananapus/721-hook-v6` | Dependency upgrade |
222
+ | `pragma solidity 0.8.23` | `pragma solidity 0.8.26` | Compiler version bump |