@bannynet/core-v6 0.0.22 → 0.0.24

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
@@ -1,136 +1,87 @@
1
1
  # Administration
2
2
 
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
-
5
3
  ## At A Glance
6
4
 
7
5
  | 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
6
+ | --- | --- |
7
+ | Scope | `Banny721TokenUriResolver` metadata, SVG commitments, and outfit-state control |
8
+ | Control posture | Global `Ownable` metadata control plus per-body owner control |
9
+ | Highest-risk actions | Wrong SVG hash commitments, incorrect metadata updates, and long outfit locks |
10
+ | Recovery posture | Metadata is editable, but committed hashes, uploaded SVGs, and active locks are not reversible |
15
11
 
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.
12
+ ## Purpose
20
13
 
21
- ## One-Way Or High-Risk Actions
14
+ `banny-retail-v6` has a small but real control plane. The resolver owner controls collection-wide metadata and SVG commitments. Body owners control decoration and outfit locks. No admin can rescue equipped NFTs if resolver logic fails.
22
15
 
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.
16
+ ## Control Model
27
17
 
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.
18
+ - `Banny721TokenUriResolver` is `Ownable`.
19
+ - Global admin power is limited to metadata, product naming, and SVG hash commitments.
20
+ - Actual SVG upload is permissionless once the hash is committed.
21
+ - Body owners control decoration and locking for their own bodies.
22
+ - Equipped accessories are held by the resolver while attached.
32
23
 
33
24
  ## Roles
34
25
 
35
- | Role | How Assigned | Scope |
36
- |------|-------------|-------|
37
- | **Contract Owner** | Set via constructor `Ownable(owner)`. Transferable via OpenZeppelin `transferOwnership()`. | Global: SVG asset management, metadata, product naming |
38
- | **NFT Body Owner** | Whoever holds the banny body token on the JB721TiersHook contract. Checked via `IERC721(hook).ownerOf(bannyBodyId)`. | Per-token: decoration, locking |
39
- | **NFT Outfit/Background Owner** | Whoever holds the outfit or background token on the hook contract. | Per-token: authorize equipping to a body they own |
40
- | **Anyone (Permissionless)** | No restriction. | Upload SVG content (if matching hash exists) |
41
- | **Trusted Forwarder** | Set at construction via `ERC2771Context(trustedForwarder)`. Immutable after deploy. | Meta-transaction relay: `_msgSender()` resolves to the relayed sender |
42
-
43
- ## Privileged Functions
44
-
45
- ### Banny721TokenUriResolver -- Owner-Only Functions
46
-
47
- | Function | Guard | What It Does |
48
- |----------|-------|-------------|
49
- | `setMetadata(description, url, baseUri)` | `onlyOwner` | Overwrites the token metadata description, external URL, and SVG base URI. All three fields are always written (pass current value to keep, empty string to clear). |
50
- | `setProductNames(upcs, names)` | `onlyOwner` | Sets custom display names for products identified by UPC. Names are stored in `_customProductNameOf` mapping. Can overwrite previously set names. |
51
- | `setSvgHashesOf(upcs, svgHashes)` | `onlyOwner` | Commits keccak256 hashes for SVG content keyed by UPC. Each hash can only be set once -- reverts with `HashAlreadyStored` if the UPC already has a hash. This is the gating step that controls which SVG content can later be uploaded permissionlessly. |
52
-
53
- ### Banny721TokenUriResolver -- Permissionless Functions
54
-
55
- | Function | Guard | What It Does |
56
- |----------|-------|-------------|
57
- | `setSvgContentsOf(upcs, svgContents)` | None (anyone) | Stores SVG content for a UPC, but only if: (1) a hash was previously committed by the owner via `setSvgHashesOf`, (2) `keccak256(content) == storedHash`, and (3) content has not already been stored (`ContentsAlreadyStored`). This is the permissionless "lazy upload" mechanism. |
26
+ | Role | How Assigned | Scope | Notes |
27
+ | --- | --- | --- | --- |
28
+ | Resolver owner | `Ownable(owner)` at construction | Global | Can transfer ownership with `transferOwnership()` |
29
+ | Body owner | `IERC721(hook).ownerOf(bannyBodyId)` | Per body | Can decorate and lock that body |
30
+ | Anyone | No assignment | Global | Can upload SVG bytes only if they match a committed hash |
58
31
 
59
- ### Banny721TokenUriResolver -- NFT-Owner Functions
32
+ ## Privileged Surfaces
60
33
 
61
- | Function | Guard | What It Does |
62
- |----------|-------|-------------|
63
- | `decorateBannyWith(hook, bannyBodyId, backgroundId, outfitIds)` | `_checkIfSenderIsOwner` + `nonReentrant` | Equips/unequips outfits and background on a banny body. Caller must own the body token. For each outfit/background: caller must own the asset directly, OR own the banny body that currently wears/uses it. Transfers outfit NFTs into the resolver contract (custodial). |
64
- | `lockOutfitChangesFor(hook, bannyBodyId)` | `_checkIfSenderIsOwner` | Locks a banny body's outfit for 7 days (`_LOCK_DURATION`). Lock can only be extended, never shortened (`CantAccelerateTheLock`). Prevents `decorateBannyWith` during the lock period. |
34
+ | Contract | Function | Who Can Call | Effect |
35
+ | --- | --- | --- | --- |
36
+ | `Banny721TokenUriResolver` | `setMetadata(...)` | Resolver owner | Changes global description, URL, and base URI |
37
+ | `Banny721TokenUriResolver` | `setProductNames(...)` | Resolver owner | Changes display names for products |
38
+ | `Banny721TokenUriResolver` | `setSvgHashesOf(...)` | Resolver owner | Commits write-once SVG hashes for UPCs |
39
+ | `Banny721TokenUriResolver` | `setSvgContentsOf(...)` | Anyone with matching bytes | Uploads write-once SVG payloads for committed hashes |
40
+ | `Banny721TokenUriResolver` | `decorateBannyWith(...)` | Current body owner | Equips or unequips accessories and updates custody |
41
+ | `Banny721TokenUriResolver` | `lockOutfitChangesFor(...)` | Current body owner | Extends the outfit lock window for that body |
65
42
 
66
- ### Banny721TokenUriResolver -- Restricted Receiver
43
+ ## Immutable And One-Way
67
44
 
68
- | Function | Guard | What It Does |
69
- |----------|-------|-------------|
70
- | `onERC721Received(operator, from, tokenId, data)` | `operator == address(this)` | Only accepts incoming NFT transfers when the resolver itself initiated the transfer. Rejects all direct user transfers to the resolver contract with `UnauthorizedTransfer`. |
45
+ - SVG hash commitments are write-once.
46
+ - SVG contents are write-once once uploaded.
47
+ - `lockOutfitChangesFor(...)` only extends the active lock.
48
+ - The lock duration is fixed by `_LOCK_DURATION`.
49
+ - Default art fragments, category semantics, and the trusted forwarder are constructor or code immutables.
71
50
 
72
- ## Asset Management
51
+ ## Operational Notes
73
52
 
74
- **Who can add SVG assets:**
53
+ - Treat `setSvgHashesOf(...)` like a release gate. A wrong hash usually means a new resolver or new UPC strategy, not a small edit.
54
+ - Treat `setMetadata(...)` and `setProductNames(...)` as collection-wide display changes.
55
+ - Remind users that equipped assets are in resolver custody while attached.
56
+ - Only lock outfits when temporary non-editability is the intended experience.
57
+ - Use safe ERC-721 transfer flows when assets enter the resolver path. Plain `transferFrom` can strand NFTs without a recovery path.
75
58
 
76
- 1. The contract **owner** commits SVG hashes via `setSvgHashesOf()`. This is the only gatekeeping step -- only the owner decides which UPCs get artwork.
77
- 2. **Anyone** can then upload the actual SVG content via `setSvgContentsOf()`, provided the content's keccak256 hash matches the owner-committed hash. This is intentional: the owner sets the commitment, and anyone can fulfill it (useful for gas-efficient lazy uploading).
59
+ ## Machine Notes
78
60
 
79
- **Immutability of stored content:**
80
- - Once a hash is set for a UPC, it cannot be changed (`HashAlreadyStored`).
81
- - Once content is uploaded for a UPC, it cannot be replaced (`ContentsAlreadyStored`).
82
- - There is no function to delete or modify stored SVG hashes or content.
61
+ - Do not assume there is a rescue path for equipped assets. There is none.
62
+ - Treat `src/Banny721TokenUriResolver.sol` as the source of truth for lock extension and write-once SVG behavior.
63
+ - If a committed hash and intended asset bytes differ, stop. The contract does not support overwrite repair.
64
+ - If an asset arrived through non-safe ERC-721 transfer semantics, do not assume the resolver can detect or recover it.
83
65
 
84
- **Product names:**
85
- - Can be overwritten by the owner at any time via `setProductNames()`. There is no immutability guard on names -- the owner can rename products freely. The built-in names for UPCs 1-4 (Alien, Pink, Orange, Original) are hardcoded in `_productNameOf()` and cannot be overridden.
66
+ ## Recovery
86
67
 
87
- **Metadata:**
88
- - `svgDescription`, `svgExternalUrl`, and `svgBaseUri` can be changed by the owner at any time via `setMetadata()`. All three are always overwritten in a single call.
89
-
90
- ## Custodial Model
91
-
92
- When a user equips an outfit or background on a banny body via `decorateBannyWith()`, the outfit/background NFT is transferred from the user's wallet into the `Banny721TokenUriResolver` contract. The resolver holds these NFTs in custody until the user unequips them (by calling `decorateBannyWith()` again with different outfits).
93
-
94
- **Trust assumptions:**
95
- - Users must trust that the resolver contract's `decorateBannyWith()` function correctly returns NFTs when outfits are changed. There is no separate `withdraw()` or `rescue()` function.
96
- - The `onERC721Received` guard (`operator == address(this)`) prevents anyone from sending arbitrary NFTs to the resolver. Only self-initiated transfers during `decorateBannyWith()` are accepted.
97
- - If the banny body NFT is transferred to a new owner while outfits are equipped, the new body owner can unequip and claim the outfit NFTs (the ownership check in `_checkIfSenderIsOwner` is against the current body owner).
98
- - The `lockOutfitChangesFor()` function can lock outfits for up to 7 days per call (extendable). During a lock, neither the body owner nor anyone else can change the equipped outfits.
99
- - There is no admin override to rescue stuck outfit NFTs. If a bug in `decorateBannyWith()` prevents unequipping, the outfits remain locked in the resolver permanently.
100
-
101
- ## Immutable Configuration
102
-
103
- The following are set at construction and cannot be changed:
104
-
105
- | Property | Set At | Value |
106
- |----------|--------|-------|
107
- | `BANNY_BODY` | Constructor | Base SVG path for all banny body rendering |
108
- | `DEFAULT_NECKLACE` | Constructor | Default necklace SVG injected when no custom necklace equipped |
109
- | `DEFAULT_MOUTH` | Constructor | Default mouth SVG injected when no custom mouth equipped |
110
- | `DEFAULT_STANDARD_EYES` | Constructor | Default eyes SVG for non-alien bodies |
111
- | `DEFAULT_ALIEN_EYES` | Constructor | Default eyes SVG for alien bodies |
112
- | `trustedForwarder` | Constructor | ERC-2771 forwarder address for meta-transactions |
113
- | `_LOCK_DURATION` | Constant | 7 days -- hardcoded, not configurable |
114
- | Body color fills | Hardcoded in `_fillsFor()` | Color palettes for Alien, Pink, Orange, Original body types |
115
- | Category IDs | Constants | 18 category slots (0-17), hardcoded |
68
+ - Bad metadata can be changed by the owner.
69
+ - Bad SVG commitments or uploaded content cannot be corrected in place.
70
+ - If equipped assets become stuck because of resolver logic, there is no owner rescue path.
71
+ - If NFTs are stranded through non-safe transfer semantics, this contract does not provide recovery.
116
72
 
117
73
  ## Admin Boundaries
118
74
 
119
- **What the owner CANNOT do:**
120
-
121
- - **Cannot move or steal user NFTs.** The resolver only holds custody of outfit/background NFTs that users voluntarily equip. The `onERC721Received` guard ensures only self-initiated transfers are accepted.
122
- - **Cannot modify stored SVG content.** Once hash + content are committed, they are permanent. No delete or update function exists.
123
- - **Cannot modify stored SVG hashes.** Each UPC's hash is write-once.
124
- - **Cannot change the lock duration.** The 7-day lock is a compile-time constant.
125
- - **Cannot force-equip or force-unequip outfits.** Only the body NFT's owner can call `decorateBannyWith` and `lockOutfitChangesFor`.
126
- - **Cannot override hardcoded body names.** UPCs 1-4 always resolve to Alien, Pink, Orange, Original regardless of `_customProductNameOf`.
127
- - **Cannot change the trusted forwarder.** It is immutable after construction.
128
- - **Cannot pause the contract.** There is no pause mechanism.
129
- - **Cannot upgrade the contract.** It is not upgradeable.
75
+ - The owner cannot arbitrarily withdraw equipped user NFTs.
76
+ - The owner cannot overwrite committed hashes or uploaded SVG contents.
77
+ - The owner cannot bypass body-owner checks on decoration or locking.
78
+ - Nobody can shorten an active outfit lock.
79
+ - There is no pause, upgrade, or rescue mechanism.
130
80
 
131
- **What the owner CAN do that affects users:**
81
+ ## Source Map
132
82
 
133
- - **Change metadata** (`svgDescription`, `svgExternalUrl`, `svgBaseUri`). This affects how all tokens render in wallets/marketplaces. Clearing `svgBaseUri` would break IPFS-based fallback rendering for products without on-chain SVG content.
134
- - **Rename products.** Custom product names (UPC > 4) can be changed at any time, altering how NFTs display.
135
- - **Commit new SVG hashes.** This controls which new artwork can be uploaded, but cannot affect already-stored content.
136
- - **Transfer ownership.** Via OpenZeppelin `transferOwnership()`, the owner can hand off all admin privileges to a new address (including a multisig, DAO, or malicious actor).
83
+ - `src/Banny721TokenUriResolver.sol`
84
+ - `src/interfaces/IBanny721TokenUriResolver.sol`
85
+ - `script/Deploy.s.sol`
86
+ - `test/TestAuditGaps.sol`
87
+ - `test/TestQALastMile.t.sol`
package/ARCHITECTURE.md CHANGED
@@ -2,75 +2,100 @@
2
2
 
3
3
  ## Purpose
4
4
 
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.
5
+ `banny-retail-v6` is the Banny-specific metadata and attachment layer for Juicebox 721 collections. It does not mint NFTs or own treasury logic. It owns attachment custody, outfit-lock rules, and final token rendering.
6
6
 
7
- ## Boundaries
7
+ ## System Overview
8
8
 
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`.
9
+ The repo centers on `Banny721TokenUriResolver`. A 721 hook from `nana-721-hook-v6` points to this resolver for `tokenURI(...)`. Bodies, outfits, and backgrounds remain separate NFTs at the collection layer. The resolver escrows equipped accessories, records which assets are attached to each body, and composes the final SVG and JSON metadata on demand.
12
10
 
13
- ## Main Components
11
+ ## Core Invariants
14
12
 
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 |
13
+ - A body can only reference accessories that are currently escrowed by the resolver.
14
+ - Replacing an equipped item must atomically return the old item and escrow the new item.
15
+ - Outfit locks must block both explicit removal and implicit replacement until the lock expires.
16
+ - Equipped assets move with the body NFT on transfer until the new owner unequips them.
17
+ - Registered SVG payloads must match their committed content hash before they can render.
18
+ - Rendering must stay deterministic for the same stored body state.
19
19
 
20
- ## Runtime Model
20
+ ## Modules
21
21
 
22
- ### Decoration
22
+ | Module | Responsibility | Notes |
23
+ | --- | --- | --- |
24
+ | `Banny721TokenUriResolver` | Escrow, attachment state, lock windows, and metadata rendering | Main contract |
25
+ | `IBanny721TokenUriResolver` | External integration surface | Used by hooks and offchain tooling |
26
+
27
+ ## Trust Boundaries
28
+
29
+ - Minting, ownership transfer, and collection-level ERC-721 semantics live in `nana-721-hook-v6`.
30
+ - This repo is trusted for rendering correctness and custody of equipped assets.
31
+ - Asset-content upload is controlled by the registered content owner, but the contract verifies uploaded bytes against the stored hash.
32
+
33
+ ## Critical Flows
34
+
35
+ ### Decorate
23
36
 
24
37
  ```text
25
38
  body owner
26
39
  -> 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
40
+ -> resolver verifies body ownership and lock status
41
+ -> resolver pulls new accessories into escrow
42
+ -> resolver updates equipped slots
43
+ -> resolver returns replaced accessories to the owner
31
44
  ```
32
45
 
33
- ### Rendering
46
+ ### Render
34
47
 
35
48
  ```text
36
49
  tokenURI(bodyId)
37
- -> resolve body, background, and equipped outfit slots
38
- -> fetch registered SVG fragments
39
- -> compose layered SVG
40
- -> return base64 JSON metadata
50
+ -> resolver loads body, background, and equipped slot state
51
+ -> fetches registered SVG fragments
52
+ -> composes layered SVG in Banny-specific order
53
+ -> returns base64 JSON metadata
41
54
  ```
42
55
 
43
- ### Locking
56
+ ### Lock Outfit
44
57
 
45
58
  ```text
46
- owner
47
- -> lockOutfitChangesFor(...)
48
- -> body enters a temporary no-change window
49
- -> decoration and removal paths must respect the lock
59
+ body owner
60
+ -> calls lockOutfitChangesFor(...)
61
+ -> resolver stores a no-change window
62
+ -> later decoration and removal paths must respect it
50
63
  ```
51
64
 
52
- ## Critical Invariants
65
+ ## Accounting Model
53
66
 
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.
67
+ This repo does not own treasury accounting. Its critical state is custody accounting: which NFTs are escrowed, which body they belong to, and when a body is locked against changes.
58
68
 
59
- ## Where Complexity Lives
69
+ That custody model uses lazy reconciliation for some stale attachment records. Read paths filter against current ownership and attachment state instead of rewriting storage on every outside transfer.
60
70
 
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.
71
+ ## Security Model
64
72
 
65
- ## Dependencies
66
-
67
- - `nana-721-hook-v6` for collection ownership and transfer semantics
68
- - Juicebox metadata resolver patterns for token URI integration
73
+ - The main failure mode is custody drift between slot state and actual escrowed NFTs.
74
+ - Rendering order is part of app semantics, not cosmetic output.
75
+ - Lazy reconciliation is intentional. Changes that assume storage is always perfectly clean can strand assets or mis-render bodies.
76
+ - Any new asset category adds both a rendering concern and a custody concern.
69
77
 
70
78
  ## Safe Change Guide
71
79
 
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.
80
+ - Keep generic ERC-721 behavior in `nana-721-hook-v6`, not here.
81
+ - Review escrow writes and transfer behavior together whenever changing attachment logic.
82
+ - If transfer or cleanup behavior changes, re-check lazy reconciliation alongside body-transfer inheritance.
83
+ - If `tokenURI(...)` changes, test stable output for unchanged state and replacement behavior for changed state.
84
+ - If adding slots or asset classes, update rendering order, slot replacement, and lock enforcement in one change.
85
+
86
+ ## Canonical Checks
87
+
88
+ - accessory escrow, replacement, and decoration flow:
89
+ `test/DecorateFlow.t.sol`
90
+ - burned-body custody edge cases:
91
+ `test/audit/BurnedBodyStrandsAssets.t.sol`
92
+ - transfer-path protection against stranded attachments:
93
+ `test/audit/TryTransferFromStrandsAssets.t.sol`
94
+
95
+ ## Source Map
96
+
97
+ - `src/Banny721TokenUriResolver.sol`
98
+ - `test/DecorateFlow.t.sol`
99
+ - `test/audit/BurnedBodyStrandsAssets.t.sol`
100
+ - `test/audit/TryTransferFromStrandsAssets.t.sol`
101
+ - `script/Deploy.s.sol`
@@ -1,87 +1,78 @@
1
1
  # Audit Instructions
2
2
 
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.
3
+ This repo is the Banny avatar composition layer. It does not mint the base NFTs, but it does hold equipped accessories and define the metadata users see.
4
4
 
5
- ## Objective
5
+ ## Audit Objective
6
6
 
7
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
8
+
9
+ - strand outfits or backgrounds in resolver custody
10
+ - let the wrong actor equip, unequip, overwrite, or recover accessories
11
+ - break outfit-lock timing or freeze a body longer than intended
12
+ - return metadata that does not match stored attachment state
13
+ - bypass category or layering constraints
13
14
 
14
15
  ## Scope
15
16
 
16
17
  In scope:
18
+
17
19
  - `src/Banny721TokenUriResolver.sol`
18
20
  - `src/interfaces/IBanny721TokenUriResolver.sol`
19
- - deployment scripts in `script/`
20
-
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
21
+ - deployment helpers in `script/`
25
22
 
26
- ## System Model
23
+ ## Start Here
27
24
 
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
25
+ 1. `src/Banny721TokenUriResolver.sol`
26
+ 2. accessory receipt and release paths
27
+ 3. deployment wiring in `script/`
34
28
 
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
29
+ ## Security Model
39
30
 
40
- ## Critical Invariants
31
+ The resolver is an attachment and rendering layer around a `JB721TiersHook` collection.
41
32
 
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.
33
+ - the underlying 721 hook remains the token contract and source of body ownership
34
+ - the resolver temporarily holds accessory NFTs while they are equipped
35
+ - body ownership should be the only authority that changes equipped state
36
+ - accessory contracts may be hostile or malformed, so receipt and release ordering matters
44
37
 
45
- 2. Body ownership gates decoration
46
- Only an authorized actor for the body may change its equipped state.
38
+ ## Roles And Privileges
47
39
 
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.
40
+ | Role | Powers | How constrained |
41
+ |------|--------|-----------------|
42
+ | Body owner | Equip, unequip, and lock accessories | Must be derived from the current hook-reported owner |
43
+ | Resolver owner | Update metadata and SVG-related admin state | Must not control equipped-state authorization |
44
+ | Accessory NFT contract | Execute callbacks during custody changes | Must not corrupt bookkeeping or steal custody |
50
45
 
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.
46
+ ## Integration Assumptions
53
47
 
54
- 5. Metadata coherence
55
- `tokenURI` and related rendering helpers must reflect actual equipped state and should not expose stale or impossible compositions.
48
+ | Dependency | Assumption | What breaks if wrong |
49
+ |------------|------------|----------------------|
50
+ | `JB721TiersHook` | Reports authentic body ownership and tier metadata | Unauthorized decoration or incorrect rendering |
51
+ | Accessory ERC-721s | Behave like standard transferable NFTs | Custody or release flows fail unexpectedly |
56
52
 
57
- ## Threat Model
53
+ ## Critical Invariants
58
54
 
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
55
+ 1. Every accessory transferred into the resolver remains attributable to one body or is recoverable by the rightful owner.
56
+ 2. Only the current body owner or an intended delegate can change that body's equipped state.
57
+ 3. Conflicting categories cannot be equipped together, including through replacement or invalidation edge paths.
58
+ 4. Outfit-lock state only affects the intended body for the intended duration.
59
+ 5. Metadata and SVG generation reflect current state and do not show impossible combinations.
65
60
 
66
- ## Hotspots
61
+ ## Attack Surfaces
67
62
 
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
63
+ - decoration entrypoints that replace one accessory with another
64
+ - ERC-721 receipt hooks and any path that accepts custody
65
+ - release paths after redecorating, burning, or invalid token state
71
66
  - category validation and conflict checks
72
- - lock timestamp handling
73
- - token URI generation that assumes on-chain SVG data exists or remains consistent
67
+ - metadata assembly that assumes onchain assets or tier data remain available
68
+
69
+ ## Accepted Risks Or Behaviors
70
+
71
+ - Equipped accessories intentionally follow the body unless they are unequipped first.
72
+ - Preserving attribution on failed transfer-out is safer than dropping custody state.
74
73
 
75
- ## Build And Verification
74
+ ## Verification
76
75
 
77
- Standard workflow:
78
76
  - `npm install`
79
77
  - `forge build`
80
78
  - `forge test`
81
-
82
- The current test tree emphasizes:
83
- - attack and regression coverage around stranding and exclusivity
84
- - decoration lifecycle flows
85
- - fork and QA scenarios
86
-
87
- Prefer proofs that show a body or accessory becoming inaccessible, transferable by the wrong party, or rendered inconsistently with stored state.
package/README.md CHANGED
@@ -1,40 +1,72 @@
1
1
  # Banny Retail
2
2
 
3
- Banny Retail is an on-chain avatar system for Juicebox 721 collections. A body NFT can wear outfit NFTs, sit on a background NFT, and render the full composition as an on-chain SVG and base64 metadata payload.
3
+ Banny Retail is an onchain avatar system for Juicebox 721 collections. A body NFT can wear outfit NFTs, use a background NFT, and resolve to a base64 JSON token URI whose image is an onchain SVG.
4
4
 
5
5
  Docs: <https://docs.juicebox.money>
6
- Architecture: [ARCHITECTURE.md](./ARCHITECTURE.md)
6
+ Architecture: [ARCHITECTURE.md](./ARCHITECTURE.md)
7
+ User journeys: [USER_JOURNEYS.md](./USER_JOURNEYS.md)
8
+ Skills: [SKILLS.md](./SKILLS.md)
9
+ Risks: [RISKS.md](./RISKS.md)
10
+ Administration: [ADMINISTRATION.md](./ADMINISTRATION.md)
11
+ Audit instructions: [AUDIT_INSTRUCTIONS.md](./AUDIT_INSTRUCTIONS.md)
7
12
 
8
13
  ## Overview
9
14
 
10
- This is a resolver-centric application built on top of [`@bananapus/721-hook-v6`](https://www.npmjs.com/package/@bananapus/721-hook-v6). The resolver owns attached outfit and background NFTs while a body is decorated, then composes the active layers into a single token URI response.
15
+ This is a resolver-centric app built on top of [`@bananapus/721-hook-v6`](https://www.npmjs.com/package/@bananapus/721-hook-v6). The resolver holds attached outfit and background NFTs while a body is decorated, then composes the active layers into a single token URI response.
11
16
 
12
17
  The main user flows are:
13
18
 
14
19
  - mint body, outfit, and background NFTs through a Juicebox 721 hook
15
20
  - attach accessories to a body with `decorateBannyWith`
16
- - optionally freeze the current look for seven days with `lockOutfitChangesFor`
17
- - upload SVG payloads lazily after an owner registers their content hashes
21
+ - optionally freeze the look for seven days with `lockOutfitChangesFor`
22
+ - upload SVG payloads after an owner registers the content hashes
18
23
 
19
- Use this repo when you need collection-specific, fully on-chain metadata composition on top of Juicebox NFTs. Do not use it as a generic 721 hook; it is an application-layer resolver, not a protocol NFT primitive.
20
-
21
- If a bug changes tier pricing, mint eligibility, or treasury flow, it is probably not here first. Start in the 721 hook repo and only come here once the issue is clearly in attachment, custody, or rendering behavior.
24
+ Use this repo when you need collection-specific, fully onchain metadata composition on top of Juicebox NFTs. Do not use it as a generic 721 hook. It is an app-layer resolver, not a protocol NFT primitive.
22
25
 
23
26
  ## Key Contract
24
27
 
25
28
  | Contract | Role |
26
29
  | --- | --- |
27
- | `Banny721TokenUriResolver` | Resolves token metadata, stores equipped accessories, enforces outfit locks, and renders layered SVG output for Banny collections. |
30
+ | `Banny721TokenUriResolver` | Resolves metadata, stores equipped accessories, enforces outfit locks, and renders layered SVG output for Banny collections. |
28
31
 
29
32
  ## Mental Model
30
33
 
31
34
  This repo owns three things:
32
35
 
33
- 1. custody of attached outfit and background NFTs while equipped
34
- 2. rules around what a body can wear and when that can change
35
- 3. rendering of the final token metadata payload
36
+ 1. custody of outfit and background NFTs while they are equipped
37
+ 2. rules for what a body can wear and when that can change
38
+ 3. rendering of the final metadata payload
39
+
40
+ It does not own mint pricing, tier issuance, or treasury accounting.
41
+
42
+ ## Read These Files First
43
+
44
+ 1. `src/Banny721TokenUriResolver.sol`
45
+ 2. `test/DecorateFlow.t.sol`
46
+ 3. `test/OutfitTransferLifecycle.t.sol`
47
+ 4. `nana-721-hook-v6/src/JB721TiersHook.sol` for upstream mint and tier behavior
48
+
49
+ ## High-Signal Tests
50
+
51
+ 1. `test/DecorateFlow.t.sol`
52
+ 2. `test/OutfitTransferLifecycle.t.sol`
53
+ 3. `test/audit/BurnedBodyStrandsAssets.t.sol`
54
+ 4. `test/audit/TryTransferFromStrandsAssets.t.sol`
55
+ 5. `test/TestQALastMile.t.sol`
36
56
 
37
- It does not own mint pricing, tier issuance, or project accounting.
57
+ ## Integration Traps
58
+
59
+ - the resolver holds equipped assets, so transfer edge cases matter as much as rendering output
60
+ - transferred bodies carry their equipped assets, so a new body holder can inherit control of them
61
+ - burned bodies and non-safe transfer patterns can strand expectations around resolver-held assets
62
+ - outfit locks survive body transfers until expiry
63
+ - metadata quality depends on lazily uploaded asset payloads, not only token state
64
+
65
+ ## Where State Lives
66
+
67
+ - equipped outfit and background state live in `Banny721TokenUriResolver`
68
+ - layer rendering and token URI generation live in the same resolver
69
+ - mint pricing, tier inventory, and treasury behavior live upstream in `nana-721-hook-v6`
38
70
 
39
71
  ## Install
40
72
 
@@ -61,7 +93,7 @@ Useful scripts:
61
93
 
62
94
  ## Deployment Notes
63
95
 
64
- Deployments are handled through Sphinx using the environments configured in `script/Deploy.s.sol`. The resolver is intended to be plugged into a Juicebox 721 hook as that hook's token URI resolver.
96
+ Deployments are handled through Sphinx using the environments configured in `script/Deploy.s.sol`. The resolver is meant to be plugged into a Juicebox 721 hook as that hook's token URI resolver.
65
97
 
66
98
  ## Repository Layout
67
99
 
@@ -80,7 +112,14 @@ script/
80
112
 
81
113
  ## Risks And Notes
82
114
 
83
- - attached outfits and backgrounds are custodied by the resolver while equipped
115
+ - attached outfits and backgrounds are held by the resolver while equipped
84
116
  - outfit locks are fixed-duration and cannot be shortened once set
85
- - on-chain SVG content is immutable once uploaded for a given registered hash
86
- - rendering quality and metadata correctness depend on the integrity of uploaded SVG assets
117
+ - onchain SVG content is immutable once uploaded for a committed hash
118
+ - plain `transferFrom` can still create asset-tracking surprises around resolver custody
119
+ - rendering quality depends on the integrity of uploaded SVG assets
120
+
121
+ ## For AI Agents
122
+
123
+ - Treat this repo as an app-layer resolver, not as the NFT issuance primitive.
124
+ - Start with `Banny721TokenUriResolver` and the lifecycle tests before summarizing attachment behavior.
125
+ - If the question is about mint economics or tier availability, inspect `nana-721-hook-v6` instead.
package/RISKS.md CHANGED
@@ -1,89 +1,80 @@
1
1
  # Banny Retail Risk Register
2
2
 
3
- This file focuses on failure modes that can break NFT custody, let untrusted hook integrations bypass assumptions, or leave a rendered Banny in a state that does not match the assets users think they own.
3
+ This file focuses on the failure modes that can break NFT custody, bypass hook assumptions, or leave a rendered Banny in a state that does not match the assets users think they own.
4
4
 
5
5
  ## How to use this file
6
6
 
7
- - Read `Priority risks` first; those are the highest-signal failure modes for operators, auditors, and integrators.
8
- - Use `Accepted Behaviors` to separate intentional tradeoffs from genuine bugs.
7
+ - Read `Priority risks` first.
8
+ - Use `Accepted Behaviors` to separate intentional tradeoffs from bugs.
9
9
  - Treat `Invariants to Verify` as required test and audit targets.
10
10
 
11
11
  ## Priority risks
12
12
 
13
13
  | Priority | Risk | Why it matters | Primary controls |
14
14
  |----------|------|----------------|------------------|
15
- | P0 | Untrusted `hook` or store integration | The caller chooses the hook, and the resolver trusts it for ownership checks, tier metadata, and transfers. A bad hook can fake authority or trap assets. | Operationally restrict supported hooks, scrutinize sections 1, 3, and 5, and test with hostile hook behavior. |
16
- | P1 | Silent transfer failure retention | Failed returns intentionally keep attachment records to avoid stranding NFTs, but this can leave phantom render state if the underlying asset is gone forever. | Explicit accepted-behavior rules, retained-item handling, and invariants around custody/state correspondence. |
17
- | P1 | Sale-time outfit lock griefing | A seller can transfer a locked body and force the buyer to wait up to 7 days before changing outfits. | Fixed-duration lock, marketplace disclosure, and user education before secondary sales. |
18
-
15
+ | P0 | Untrusted `hook` or store integration | The resolver trusts the supplied hook for ownership checks, tier metadata, and transfers. A bad hook can fake authority or trap assets. | Restrict supported hooks, scrutinize the hook boundary, and test hostile hook behavior. |
16
+ | P1 | Silent transfer-failure retention | Failed returns intentionally keep attachment records to avoid stranding NFTs, but this can leave phantom render state if the asset is gone forever. | Explicit accepted-behavior rules, retained-item handling, and custody/state invariants. |
17
+ | P1 | Sale-time outfit-lock griefing | A seller can transfer a locked body and force the buyer to wait up to 7 days before changing outfits. | Fixed-duration lock, marketplace disclosure, and user education. |
19
18
 
20
19
  ## 1. Trust Assumptions
21
20
 
22
- - **Trusted forwarder.** ERC-2771 `_msgSender()` is trusted for all ownership checks in `decorateBannyWith`, `lockOutfitChangesFor`, and admin functions. A compromised forwarder can dress/undress any banny and steal equipped outfits.
23
- - **Hook contract.** The `hook` parameter is caller-supplied and not validated against any registry. A malicious hook contract could return arbitrary tier data, manipulate ownership checks, or trap NFTs.
24
- - **Owner (Ownable).** The contract owner controls SVG hashes, product names, and metadata URIs. A compromised owner can set malicious SVG content hashes, enabling XSS via on-chain SVG injection after the matching content is uploaded.
25
- - **721 hook store.** `_storeOf(hook)` calls `IJB721TiersHook(hook).STORE()` -- trusts the hook to return a legitimate store. A malicious hook can return a fake store with manipulated tier data.
21
+ - **Trusted forwarder.** ERC-2771 `_msgSender()` is trusted for ownership checks and admin functions.
22
+ - **Hook contract.** The `hook` parameter is caller-supplied and not validated against a registry.
23
+ - **Owner.** The contract owner controls SVG hashes, product names, and metadata URIs.
24
+ - **721 hook store.** `_storeOf(hook)` trusts the hook to return a legitimate store.
26
25
 
27
- ## 2. Economic / Manipulation Risks
26
+ ## 2. Economic And Manipulation Risks
28
27
 
29
- - **Outfit theft via banny body transfer.** Equipped outfits and backgrounds travel with the banny body NFT on transfer. If a banny body is sold with valuable outfits equipped, the buyer gains control of all equipped items. Sellers must unequip before selling. Marketplaces may not surface this risk.
30
- - **try-catch silent failures with retention.** `_tryTransferFrom` silently catches all transfer failures and returns `false`. When a transfer fails, the resolver preserves the attachment record instead of clearing state. For backgrounds, the entire background change is aborted. For outfits, failed-to-return items are retained in the attached list via `_storeOutfitsWithRetained`. This prevents NFT stranding assets remain tracked and recoverable once the transfer issue is resolved (e.g., the owner contract becomes receivable). However, if an outfit NFT is burned or its tier removed, the retained record refers to a non-existent asset, creating a phantom entry in the SVG rendering.
31
- - **Lock griefing.** `lockOutfitChangesFor` extends the lock to `block.timestamp + 7 days`. Locking just before selling prevents the buyer from changing outfits for up to 7 days. The lock now also freezes reassignment of currently equipped outfits/backgrounds away from that body during the lock window.
28
+ - **Outfit theft via body transfer.** Equipped outfits and backgrounds move with the body NFT. If a body is sold while wearing valuable items, the buyer gains control of them.
29
+ - **Try-catch silent failures with retention.** Failed transfer-outs preserve attachment records instead of clearing state. This avoids stranding but can create phantom render entries for burned or removed assets.
30
+ - **Lock griefing.** `lockOutfitChangesFor` extends the lock to `block.timestamp + 7 days`. Locking just before a sale can block the buyer from changing the look for up to 7 days.
32
31
 
33
32
  ## 3. Access Control
34
33
 
35
- - **No hook validation (HIGH impact).** Any address can be passed as `hook`. A malicious hook can return `_msgSender()` from `ownerOf()` to pass authorization checks, execute arbitrary code during `safeTransferFrom`, or return manipulated tier data from `STORE().tierOfTokenId()`.
36
- - **SVG content upload is permissionless (with hash).** `setSvgContentsOf` only requires the content to match a pre-committed hash. Safe if hashes are correctly committed.
37
- - **onERC721Received restriction.** Only accepts NFTs when `operator == address(this)`. `transferFrom` (non-safe) bypasses this -- NFTs sent via `transferFrom` are permanently locked with no rescue function.
34
+ - **No hook validation.** Any address can be passed as `hook`. A malicious hook can fake `ownerOf`, execute arbitrary code during transfers, or return manipulated tier data.
35
+ - **SVG content upload is permissionless once the hash is committed.** This is safe only if the committed hash is correct.
36
+ - **`onERC721Received` restriction.** The contract only accepts NFTs when `operator == address(this)`. Plain `transferFrom` bypasses that and can permanently lock NFTs.
38
37
 
39
38
  ## 4. DoS Vectors
40
39
 
41
- - **External call iteration scales with outfit count.** `_attachedOutfitIdsOf[hook][bannyBodyId]` is replaced wholesale on each `decorateBannyWith` call (not appended to), so the array is bounded by the number of currently equipped outfits, not cumulative history. However, `decorateBannyWith` iterates over both the previous and new outfit arrays to diff them (transferring removed outfits back and new outfits in), so gas cost scales with the number of outfits being equipped/unequipped in a single call.
42
- - **External hook calls in view functions.** `tokenUriOf` and `svgOf` call into the hook's store multiple times per outfit. A malicious hook that consumes excessive gas or reverts can make token metadata unretrievable. Measured: `tokenUriOf` with a well-behaved hook and 9 equipped outfits costs ~609k gas (see `test_tokenUri_gasSnapshot_9outfits`). The practical ceiling for a malicious hook is bounded only by the caller's gas limit — RPC nodes typically cap `eth_call` at 30M+ gas, so even expensive hooks won't fail for off-chain reads, but on-chain consumers (e.g., other contracts calling `tokenURI`) could revert.
40
+ - **External call iteration scales with outfit count.** `decorateBannyWith` iterates both old and new outfit arrays, so gas cost scales with the number of outfits changed in one call.
41
+ - **External hook calls in view functions.** `tokenUriOf` and `svgOf` call into the hook's store multiple times. A malicious or gas-heavy hook can make metadata unreadable.
43
42
 
44
43
  ## 5. Integration Risks
45
44
 
46
- - **Cross-contract NFT custody.** Outfits are held by `Banny721TokenUriResolver` via `safeTransferFrom`. If approval is revoked on the hook contract, equipping fails.
47
- - **Tier removal desync.** If a tier is removed from the 721 hook while an outfit from that tier is equipped, `_productOfTokenId` returns a product with `id == 0`. The outfit remains equipped but renders as empty. `_tryTransferFrom` may fail silently when trying to return it.
48
- - **Non-safe transfer loss.** Outfits sent directly to this contract via `transferFrom` (not `safeTransferFrom`) are permanently stuck since there is no rescue function.
49
- - **ReentrancyGuard.** `decorateBannyWith` uses `nonReentrant`, but `lockOutfitChangesFor` and view functions do not. Reentrancy through hook callbacks is possible but state updates follow CEI pattern.
45
+ - **Cross-contract NFT custody.** Outfits are held by the resolver via `safeTransferFrom`. If approval is revoked on the hook contract, equipping fails.
46
+ - **Tier removal desync.** If a tier is removed while an outfit from that tier is equipped, the outfit may remain attached but render empty.
47
+ - **Non-safe transfer loss.** Outfits sent directly via `transferFrom` can be permanently stuck because there is no rescue function.
48
+ - **Reentrancy assumptions.** `decorateBannyWith` uses `nonReentrant`, but other functions rely on ordering and limited state impact instead.
50
49
 
51
50
  ## 6. Invariants to Verify
52
51
 
53
- - Every outfit held by this contract has a corresponding `_wearerOf[hook][outfitId]` pointing to a valid banny body.
54
- - Every background held by this contract has a corresponding `_userOf[hook][backgroundId]` pointing to a valid banny body.
55
- - `outfitLockedUntil[hook][bannyBodyId]` is monotonically non-decreasing per banny body (lock can only be extended, never shortened).
56
- - After `decorateBannyWith`, all previously equipped outfits not in the new set are either transferred back to `_msgSender()` or retained in the attached list if the transfer failed.
57
- - `_attachedOutfitIdsOf[hook][bannyBodyId]` contains the outfitIds passed to the most recent `decorateBannyWith` call, plus any retained outfits whose return transfer failed. Category exclusivity is enforced on the merged set (retained + new outfits), not just the new outfit set alone. Additionally, duplicate categories in the merged set are rejected with `Banny721TokenUriResolver_DuplicateCategory()` to prevent retained outfits from silently duplicating a category supplied in the new set.
58
- - SVG content integrity: `keccak256(_svgContentOf[upc]) == svgHashOf[upc]` for all populated entries.
59
- - NFT custody balance: the number of outfit NFTs held by this contract (`IERC721(hook).balanceOf(address(this))`) equals the total number of outfits currently equipped across all banny bodies for that hook. Violations indicate phantom outfits (equipped in state but NFT lost via try-catch silent failure) or orphaned NFTs (held by contract but not tracked in `_wearerOf`).
52
+ - Every outfit held by the contract has a corresponding wearer mapping to a valid body.
53
+ - Every background held by the contract has a corresponding user mapping to a valid body.
54
+ - `outfitLockedUntil` is monotonically non-decreasing per body.
55
+ - After `decorateBannyWith`, old outfits not in the new set are either returned or explicitly retained because transfer-out failed.
56
+ - Category exclusivity holds on the merged set of retained and newly supplied outfits.
57
+ - SVG content integrity holds for all populated entries.
58
+ - The number of held outfit NFTs should match the number of outfits still tracked as equipped for that hook.
60
59
 
61
60
  ## 7. Accepted Behaviors
62
61
 
63
- ### 7.1 Failed transfers retain attachment records (anti-stranding)
64
-
65
- `_tryTransferFrom` catches all transfer failures and returns `false`. When returning a previously equipped item fails, the resolver preserves the attachment record rather than clearing state:
66
-
67
- - **Backgrounds**: If returning the old background fails, the entire background change is aborted (`return` in `_decorateBannyWithBackground`). The old background stays attached and the new one is not equipped.
68
- - **Background removal**: If returning the background fails during removal (backgroundId=0), `_attachedBackgroundIdOf` is not cleared. The background stays attached.
69
- - **Outfits**: Failed-to-return outfits remain non-zero in the `previousOutfitIds` array. `_storeOutfitsWithRetained` appends them to the new outfit list, preserving their attachment record. After merging, the resolver verifies no two outfits share the same category (reverts with `DuplicateCategory` if a retained outfit conflicts with a newly supplied one).
70
-
71
- This prevents NFT stranding — assets held by the resolver stay tracked and recoverable. Once the transfer issue is resolved (e.g., the owner contract implements `IERC721Receiver`), a subsequent `decorateBannyWith` call will successfully return the retained items.
62
+ ### 7.1 Failed transfers retain attachment records
72
63
 
73
- For permanently unrecoverable assets (burned NFTs, removed tiers), the retained record creates a phantom entry in the SVG rendering and attached list. This is cosmetically incorrect but not economically exploitable phantom entries cannot be transferred or sold. The alternative — reverting on any failed transfer — would make `decorateBannyWith` fragile: a single burned outfit would prevent the banny owner from changing ANY outfits.
64
+ If returning a previously equipped item fails, the resolver keeps the attachment record instead of dropping it. This avoids stranding NFTs held by the resolver, but it can leave cosmetic phantom state for permanently unrecoverable assets.
74
65
 
75
- ### 7.2 Lock griefing window is bounded at 7 days
66
+ ### 7.2 Lock griefing is bounded at 7 days
76
67
 
77
- `lockOutfitChangesFor` extends the lock to `block.timestamp + 7 days`. A seller who locks just before transferring the banny forces the buyer to wait up to 7 days. This is accepted because: (1) marketplaces can check `outfitLockedUntil` before displaying the item, (2) the lock duration is fixed (not owner-configurable), and (3) the lock prevents a more severe attack where a buyer immediately strips valuable outfits — the lock gives the previous owner time to arrange the sale intentionally.
68
+ `lockOutfitChangesFor` can force a buyer to wait, but the window is fixed and cannot be extended arbitrarily beyond the current maximum.
78
69
 
79
- ### 7.3 On-chain SVG rendering gas is well within limits
70
+ ### 7.3 Onchain SVG rendering gas is acceptable
80
71
 
81
- `tokenUriOf` constructs full SVGs on-chain with string concatenation. Measured gas ceiling: ~609K gas for the worst case (9 non-conflicting outfits + background with on-chain SVG content), well within typical RPC node limits (30M+). Regression test: `test_tokenUri_gasSnapshot_9outfits` in `test/TestQALastMile.t.sol`.
72
+ Full `tokenUriOf` rendering is expensive but still within practical RPC limits for the supported outfit counts.
82
73
 
83
74
  ### 7.4 Outfits burn alongside the body
84
75
 
85
- When a banny body NFT is burned (e.g. via cash-out), any equipped outfits and backgrounds held by the resolver are permanently unrecoverable. The resolver has no recovery function and this is intentional — outfits are part of the body's identity and share its fate. Users who want to preserve outfits must unequip them before burning the body.
76
+ If a body NFT is burned while outfits are equipped, those outfits are intentionally unrecoverable. Users who want to keep them must unequip first.
86
77
 
87
- ### 7.5 Reentrancy in non-guarded functions is harmless
78
+ ### 7.5 Reentrancy in non-guarded functions is treated as harmless under the current model
88
79
 
89
- `lockOutfitChangesFor` and all view functions (`tokenUriOf`, `svgOf`) are not protected by `nonReentrant`. A malicious hook's `STORE().tierOfTokenId()` could re-enter `lockOutfitChangesFor` during a `tokenUriOf` call, but this is harmless -- `lockOutfitChangesFor` only extends the lock timestamp (monotonically non-decreasing) and has no state that could be corrupted by reentrancy. The view functions themselves are read-only at the contract level (no storage writes), so reentrancy through them cannot extract value.
80
+ `lockOutfitChangesFor` only extends a timestamp, and view functions do not write storage. That keeps the remaining reentrancy surface narrow.
package/SKILLS.md CHANGED
@@ -3,7 +3,7 @@
3
3
  ## Use This File For
4
4
 
5
5
  - Use this file when the task involves Banny outfit attachment, layered SVG rendering, token URI composition, or asset custody and lock behavior.
6
- - Start here, then open the resolver, scripts, or tests that match the exact rendering or attachment path in question.
6
+ - Start here, then decide whether the issue is custody state, lock timing, stored SVG content, or final token-URI composition.
7
7
 
8
8
  ## Read This Next
9
9
 
@@ -11,9 +11,10 @@
11
11
  |---|---|
12
12
  | Repo overview and user-facing behavior | [`README.md`](./README.md), [`ARCHITECTURE.md`](./ARCHITECTURE.md) |
13
13
  | Resolver implementation | [`src/Banny721TokenUriResolver.sol`](./src/Banny721TokenUriResolver.sol) |
14
+ | Runtime and content-management invariants | [`references/runtime.md`](./references/runtime.md), [`references/operations.md`](./references/operations.md) |
14
15
  | Deployment or scripted drops | [`script/Deploy.s.sol`](./script/Deploy.s.sol), [`script/Drop1.s.sol`](./script/Drop1.s.sol), [`script/Add.Denver.s.sol`](./script/Add.Denver.s.sol) |
15
- | Decoration lifecycle and regressions | [`test/DecorateFlow.t.sol`](./test/DecorateFlow.t.sol), [`test/OutfitTransferLifecycle.t.sol`](./test/OutfitTransferLifecycle.t.sol), [`test/regression/`](./test/regression/) |
16
- | Adversarial or QA coverage | [`test/BannyAttacks.t.sol`](./test/BannyAttacks.t.sol), [`test/TestQALastMile.t.sol`](./test/TestQALastMile.t.sol), [`test/audit/`](./test/audit/) |
16
+ | Decoration lifecycle and custody invariants | [`test/DecorateFlow.t.sol`](./test/DecorateFlow.t.sol), [`test/OutfitTransferLifecycle.t.sol`](./test/OutfitTransferLifecycle.t.sol) |
17
+ | Adversarial, fork, or final QA coverage | [`test/BannyAttacks.t.sol`](./test/BannyAttacks.t.sol), [`test/Fork.t.sol`](./test/Fork.t.sol), [`test/TestAuditGaps.sol`](./test/TestAuditGaps.sol), [`test/TestQALastMile.t.sol`](./test/TestQALastMile.t.sol) |
17
18
 
18
19
  ## Repo Map
19
20
 
@@ -25,16 +26,17 @@
25
26
 
26
27
  ## Purpose
27
28
 
28
- Application-layer token URI resolver for Juicebox 721 collections that lets Banny body NFTs equip outfit and background NFTs, custody them while equipped, and render fully on-chain layered SVG metadata.
29
+ App-layer token URI resolver for Juicebox 721 collections. It lets Banny body NFTs equip outfit and background NFTs, holds them while equipped, and renders fully onchain layered SVG metadata.
29
30
 
30
31
  ## Reference Files
31
32
 
32
- - Open [`references/runtime.md`](./references/runtime.md) when you need attachment and custody behavior, rendering order, or the main invariants that protect equipped assets.
33
- - Open [`references/operations.md`](./references/operations.md) when you need upload and metadata-management behavior, deployment breadcrumbs, or the common stale-data traps around SVG content and scripts.
33
+ - Open [`references/runtime.md`](./references/runtime.md) for attachment and custody behavior, rendering order, and the main invariants that protect equipped assets.
34
+ - Open [`references/operations.md`](./references/operations.md) for upload and metadata-management behavior, deployment breadcrumbs, and common stale-data traps around SVG content.
34
35
 
35
36
  ## Working Rules
36
37
 
37
- - Start in [`src/Banny721TokenUriResolver.sol`](./src/Banny721TokenUriResolver.sol) for both rendering and attachment behavior. This repo is mostly one contract with several tightly coupled responsibilities.
38
+ - Start in [`src/Banny721TokenUriResolver.sol`](./src/Banny721TokenUriResolver.sol) for both rendering and attachment behavior.
38
39
  - Treat custody, stale attachment cleanup, and lock timing as high-risk. Rendering bugs are visible, but custody bugs are worse.
40
+ - Equipped outfits and backgrounds travel with the body NFT. Treat that inheritance as intentional before calling it a bug.
39
41
  - When a task mentions minting, pricing, or terminal accounting, verify that the problem is not actually in the upstream 721 hook repo.
40
42
  - If you touch SVG or metadata behavior, check whether the issue is in stored content, rendering composition, or the hook-to-resolver integration point before patching.
package/USER_JOURNEYS.md CHANGED
@@ -1,80 +1,190 @@
1
1
  # User Journeys
2
2
 
3
- ## Who This Repo Serves
3
+ ## Repo Purpose
4
4
 
5
- - Banny collection operators publishing body, outfit, and background tiers
6
- - collectors minting avatars and equipping accessories
7
- - teams managing on-chain art payloads and metadata composition
5
+ This repo is the Banny-specific composition and metadata layer on top of a Juicebox 721 collection. It owns attachment custody, compatibility rules, outfit locks, and rendered token metadata. It does not own tier pricing, treasury accounting, or mint eligibility outside resolver-specific checks.
6
+
7
+ ## Primary Actors
8
+
9
+ - collection operators publishing bodies, outfits, backgrounds, and metadata
10
+ - collectors equipping and unequipping avatar pieces
11
+ - auditors reviewing custody, lock, and rendering behavior
12
+
13
+ ## Key Surfaces
14
+
15
+ - `Banny721TokenUriResolver`: custody, compatibility, locks, and rendered SVG metadata
16
+ - `decorateBannyWith(...)`: equips outfits and a background to a body and returns old items when possible
17
+ - `lockOutfitChangesFor(...)`: freezes appearance changes for the fixed lock window
18
+ - `setSvgHashesOf(...)` / `setSvgContentsOf(...)`: publish or repair art payloads
19
+ - `setMetadata(...)` / `setProductNames(...)`: update collection metadata and UPC naming
8
20
 
9
21
  ## Journey 1: Mint A Body, Outfit, And Background Set
10
22
 
11
- **Starting state:** the Banny collection is live through the 721 hook and the relevant tiers exist.
23
+ **Actor:** collector.
24
+
25
+ **Intent:** acquire the pieces needed to build a composed Banny.
26
+
27
+ **Preconditions**
12
28
 
13
- **Success:** the collector owns the pieces needed to build a composed avatar.
29
+ - the Banny collection is live through the 721 hook
30
+ - the required body, outfit, and background tiers exist
14
31
 
15
- **Flow**
16
- 1. Mint the body, outfit, and background NFTs through the underlying Juicebox 721 project.
17
- 2. Keep pricing, issuance, and treasury assumptions anchored in the 721 hook rather than this resolver.
18
- 3. Treat this repo as the composition layer that activates once the user owns the right pieces.
32
+ **Main Flow**
33
+
34
+ 1. Mint the body, outfit, and background NFTs through the underlying 721 project.
35
+ 2. Keep mint pricing and issuance assumptions anchored in the 721 hook, not this repo.
36
+ 3. Move to this resolver only once the user owns compatible pieces.
37
+
38
+ **Failure Modes**
39
+
40
+ - the wrong tiers are minted or the pieces are incompatible
41
+ - teams misread this repo as the minting or accounting surface
42
+
43
+ **Postconditions**
44
+
45
+ - the user holds the components needed for later composition
19
46
 
20
47
  ## Journey 2: Dress A Banny And Put Accessories Into Resolver Custody
21
48
 
22
- **Starting state:** the collector owns a body plus compatible accessories.
49
+ **Actor:** body owner.
50
+
51
+ **Intent:** equip a body with a background and outfits so the resolver serves the composed avatar.
23
52
 
24
- **Success:** the chosen outfit and background are attached to the body and the resolver renders the combined look.
53
+ **Preconditions**
54
+
55
+ - the caller controls the body and the accessories being equipped
56
+ - no active outfit lock blocks the change
57
+ - the selected pieces are compatible by category and collection rules
58
+
59
+ **Main Flow**
25
60
 
26
- **Flow**
27
61
  1. Call `decorateBannyWith(...)` for the target body.
28
- 2. `Banny721TokenUriResolver` checks compatibility rules and takes custody of the attached accessory NFTs while they are equipped.
29
- 3. The body's token URI now resolves to a layered SVG and metadata payload reflecting the active composition.
62
+ 2. The resolver checks compatibility and diffs old versus new attachments.
63
+ 3. Equipped accessories move into resolver custody while attached.
64
+ 4. The token URI for the body now reflects the combined SVG and metadata.
65
+
66
+ **Failure Modes**
67
+
68
+ - duplicate outfit categories or incompatible combinations are provided
69
+ - transfer-back of previously attached items fails, leaving retained custody state for later recovery
70
+ - reviewers forget that the resolver, not the wallet, holds equipped accessories while active
71
+
72
+ **Postconditions**
73
+
74
+ - the body renders with the new composition
75
+ - attached accessories stay in resolver custody until replaced or cleared
30
76
 
31
77
  ## Journey 3: Lock A Banny's Appearance For A Period
32
78
 
33
- **Starting state:** the collector likes the current look and does not want it changed immediately.
79
+ **Actor:** body owner.
80
+
81
+ **Intent:** freeze the current appearance for the fixed lock window.
82
+
83
+ **Preconditions**
84
+
85
+ - the body already has a state worth freezing
86
+ - the caller understands the lock is fixed-duration
87
+
88
+ **Main Flow**
89
+
90
+ 1. Call `lockOutfitChangesFor(...)`.
91
+ 2. The resolver extends the lock for that body.
92
+ 3. Future decoration or removal attempts must wait until the lock expires.
93
+
94
+ **Failure Modes**
34
95
 
35
- **Success:** the avatar's appearance is frozen for the lock window and later equipment changes must wait.
96
+ - a seller locks just before transfer and the buyer cannot restyle immediately
97
+ - integrations fail to show lock state before listing or sale
36
98
 
37
- **Flow**
38
- 1. Call `lockOutfitChangesFor(...)` on the resolver.
39
- 2. The resolver records the lock period for that body.
40
- 3. Future decorate or removal actions respect the lock until it expires.
99
+ **Postconditions**
41
100
 
42
- ## Journey 4: Publish Or Repair On-Chain Art Assets
101
+ - appearance changes are blocked until the lock expires
43
102
 
44
- **Starting state:** the collection's visual payloads are referenced by content hashes but the actual SVG payloads still need to be made available.
103
+ ## Journey 4: Publish Or Repair Onchain Art Assets
45
104
 
46
- **Success:** token URIs render complete art instead of placeholders or missing layers.
105
+ **Actor:** collection operator or art publisher.
47
106
 
48
- **Flow**
49
- 1. Register the content hashes for bodies, outfits, or backgrounds with `setSvgHashesOf(...)`.
50
- 2. Upload or repair the corresponding SVG payloads with `setSvgContentsOf(...)`.
51
- 3. Re-resolve token URIs to confirm the on-chain composition now renders correctly.
107
+ **Intent:** make token URIs render complete onchain art.
52
108
 
53
- **Failure cases that matter:** publishing content that does not match the registered hash, forgetting to set product names for new pieces, and assuming the 721 hook owns the art payload when this repo owns the rendered output.
109
+ **Preconditions**
110
+
111
+ - the relevant UPCs and content hashes are known
112
+ - the operator understands that the hash is the commitment and SVG content must match it exactly
113
+
114
+ **Main Flow**
115
+
116
+ 1. Register hashes with `setSvgHashesOf(...)`.
117
+ 2. Upload matching payloads with `setSvgContentsOf(...)`.
118
+ 3. Re-check token URI output after publication or repair.
119
+
120
+ **Failure Modes**
121
+
122
+ - uploaded SVG does not match the committed hash
123
+ - product names are missing or stale
124
+ - teams assume the 721 hook owns rendered output when this repo does
125
+
126
+ **Postconditions**
127
+
128
+ - token URIs can render the intended onchain art payloads
54
129
 
55
130
  ## Journey 5: Update Collection Metadata And Product Catalog Entries
56
131
 
57
- **Starting state:** the collection exists, but its descriptive metadata or UPC-to-name catalog needs to change.
132
+ **Actor:** collection operator.
133
+
134
+ **Intent:** change collection-level metadata and human-readable product labels.
135
+
136
+ **Preconditions**
58
137
 
59
- **Success:** token URIs and collection-level presentation reflect the intended description, external URL, base URI, and product naming.
138
+ - the operator has authority over the resolver metadata surface
139
+
140
+ **Main Flow**
60
141
 
61
- **Flow**
62
142
  1. Update collection metadata with `setMetadata(...)`.
63
- 2. Set or repair product names for the UPCs the renderer should expose with `setProductNames(...)`.
64
- 3. Re-check token URI output so the rendered Banny and its catalog labels agree.
143
+ 2. Set or repair UPC names with `setProductNames(...)`.
144
+ 3. Re-check a representative token URI so labels and art agree.
145
+
146
+ **Failure Modes**
147
+
148
+ - metadata and SVG state drift apart
149
+ - operators update catalog labels without checking already-minted assets
150
+
151
+ **Postconditions**
152
+
153
+ - collection-level metadata and UPC names line up with the published art set
65
154
 
66
155
  ## Journey 6: Unequip And Recover Custodied Accessories
67
156
 
68
- **Starting state:** a body has attached pieces held by the resolver and the owner wants to rearrange or transfer them.
157
+ **Actor:** body owner.
158
+
159
+ **Intent:** recover attached accessories from resolver custody.
160
+
161
+ **Preconditions**
162
+
163
+ - the current lock window, if any, has expired
164
+ - the owner understands that old pieces may only be returned as part of a later decoration update
165
+
166
+ **Main Flow**
167
+
168
+ 1. Replace or clear the equipped items through `decorateBannyWith(...)`.
169
+ 2. The resolver attempts to return no-longer-equipped accessories.
170
+ 3. Once returned, those NFTs can be transferred or reused independently.
171
+
172
+ **Failure Modes**
173
+
174
+ - previously equipped pieces remain retained because transfer-back failed
175
+ - burned or otherwise unrecoverable pieces leave cosmetic phantom state until corrected
176
+
177
+ **Postconditions**
178
+
179
+ - no-longer-equipped accessories are either returned or remain explicitly retained pending recovery
69
180
 
70
- **Success:** the accessories leave resolver custody and can be reused or transferred independently.
181
+ ## Trust Boundaries
71
182
 
72
- **Flow**
73
- 1. Remove or replace the equipped items once no lock blocks the change.
74
- 2. The resolver releases custody of the old accessory NFTs.
75
- 3. The owner can now transfer, burn, or re-equip those pieces elsewhere.
183
+ - this repo is trusted for custody of equipped accessories while attached
184
+ - the underlying 721 hook remains the source of mint pricing, tier issuance, and treasury behavior
185
+ - metadata correctness depends on operators publishing the intended SVG hashes and contents
76
186
 
77
187
  ## Hand-Offs
78
188
 
79
189
  - Use [nana-721-hook-v6](../nana-721-hook-v6/USER_JOURNEYS.md) for mint pricing, tier issuance, reserves, and treasury behavior.
80
- - Use this repo only once the question is about custody, compatibility, outfit locks, or SVG composition.
190
+ - Use this repo once the question is about custody, compatibility, outfit locks, or SVG composition.
package/foundry.toml CHANGED
@@ -14,6 +14,8 @@ runs = 1024
14
14
  depth = 100
15
15
  fail_on_revert = false
16
16
 
17
+ [lint]
18
+ exclude_lints = ["pascal-case-struct", "mixed-case-variable"]
17
19
  [fmt]
18
20
  number_underscore = "thousands"
19
21
  multiline_func_header = "all"
package/package.json CHANGED
@@ -1,37 +1,37 @@
1
1
  {
2
- "name": "@bannynet/core-v6",
3
- "version": "0.0.22",
4
- "license": "MIT",
5
- "repository": {
6
- "type": "git",
7
- "url": "git+https://github.com/mejango/banny-retail-v6"
8
- },
9
- "engines": {
10
- "node": ">=20.0.0"
11
- },
12
- "scripts": {
13
- "test": "forge test",
14
- "coverage": "forge coverage --match-path \"./src/*.sol\" --report lcov --report summary",
15
- "generate:migration": "node ./script/outfit_drop/generate-migration.js",
16
- "deploy:mainnets": "source ./.env && export START_TIME=$(date +%s) && npx sphinx propose ./script/Deploy.s.sol --networks mainnets",
17
- "deploy:testnets": "source ./.env && export START_TIME=$(date +%s) && npx sphinx propose ./script/Deploy.s.sol --networks testnets",
18
- "deploy:testnets:drop:1": "source ./.env && npx sphinx propose ./script/Drop1.s.sol --networks testnets",
19
- "deploy:mainnets:drop:1": "source ./.env && npx sphinx propose ./script/Drop1.s.sol --networks mainnets",
20
- "artifacts": "source ./.env && npx sphinx artifacts --org-id 'ea165b21-7cdc-4d7b-be59-ecdd4c26bee4' --project-name 'banny-core-v6'"
21
- },
22
- "dependencies": {
23
- "@bananapus/721-hook-v6": "^0.0.35",
24
- "@bananapus/core-v6": "^0.0.34",
25
- "@bananapus/permission-ids-v6": "^0.0.17",
26
- "@bananapus/router-terminal-v6": "^0.0.26",
27
- "@bananapus/suckers-v6": "^0.0.25",
28
- "@croptop/core-v6": "^0.0.33",
29
- "@openzeppelin/contracts": "^5.6.1",
30
- "@rev-net/core-v6": "^0.0.32",
31
- "keccak": "^3.0.4"
32
- },
33
- "devDependencies": {
34
- "@bananapus/address-registry-v6": "^0.0.17",
35
- "@sphinx-labs/plugins": "^0.33.3"
36
- }
2
+ "name": "@bannynet/core-v6",
3
+ "version": "0.0.24",
4
+ "license": "MIT",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/mejango/banny-retail-v6"
8
+ },
9
+ "engines": {
10
+ "node": ">=20.0.0"
11
+ },
12
+ "scripts": {
13
+ "test": "forge test",
14
+ "coverage": "forge coverage --match-path \"./src/*.sol\" --report lcov --report summary",
15
+ "generate:migration": "node ./script/outfit_drop/generate-migration.js",
16
+ "deploy:mainnets": "source ./.env && export START_TIME=$(date +%s) && npx sphinx propose ./script/Deploy.s.sol --networks mainnets",
17
+ "deploy:testnets": "source ./.env && export START_TIME=$(date +%s) && npx sphinx propose ./script/Deploy.s.sol --networks testnets",
18
+ "deploy:testnets:drop:1": "source ./.env && npx sphinx propose ./script/Drop1.s.sol --networks testnets",
19
+ "deploy:mainnets:drop:1": "source ./.env && npx sphinx propose ./script/Drop1.s.sol --networks mainnets",
20
+ "artifacts": "source ./.env && npx sphinx artifacts --org-id 'ea165b21-7cdc-4d7b-be59-ecdd4c26bee4' --project-name 'banny-core-v6'"
21
+ },
22
+ "dependencies": {
23
+ "@bananapus/721-hook-v6": "^0.0.38",
24
+ "@bananapus/core-v6": "^0.0.36",
25
+ "@bananapus/permission-ids-v6": "^0.0.19",
26
+ "@bananapus/router-terminal-v6": "^0.0.30",
27
+ "@bananapus/suckers-v6": "^0.0.28",
28
+ "@croptop/core-v6": "^0.0.36",
29
+ "@openzeppelin/contracts": "^5.6.1",
30
+ "@rev-net/core-v6": "^0.0.35",
31
+ "keccak": "^3.0.4"
32
+ },
33
+ "devDependencies": {
34
+ "@bananapus/address-registry-v6": "^0.0.17",
35
+ "@sphinx-labs/plugins": "^0.33.3"
36
+ }
37
37
  }
@@ -21,5 +21,5 @@
21
21
 
22
22
  ## Useful Proof Points
23
23
 
24
- - [`test/audit/`](../test/audit/) for security-sensitive assumptions.
25
- - [`script/helpers/`](../script/helpers/) when a deployment issue is really a script/config problem.
24
+ - [`test/BannyAttacks.t.sol`](../test/BannyAttacks.t.sol) and [`test/TestAuditGaps.sol`](../test/TestAuditGaps.sol) for security-sensitive assumptions.
25
+ - [`script/Drop1.s.sol`](../script/Drop1.s.sol) and [`script/Add.Denver.s.sol`](../script/Add.Denver.s.sol) when a deployment issue is really a script/config problem.
@@ -24,4 +24,4 @@
24
24
  - [`test/DecorateFlow.t.sol`](../test/DecorateFlow.t.sol) for the main equip/unequip lifecycle.
25
25
  - [`test/OutfitTransferLifecycle.t.sol`](../test/OutfitTransferLifecycle.t.sol) for custody and return behavior.
26
26
  - [`test/BannyAttacks.t.sol`](../test/BannyAttacks.t.sol) for adversarial flows.
27
- - [`test/TestQALastMile.t.sol`](../test/TestQALastMile.t.sol) and [`test/regression/`](../test/regression/) for pinned edge cases.
27
+ - [`test/Fork.t.sol`](../test/Fork.t.sol), [`test/TestAuditGaps.sol`](../test/TestAuditGaps.sol), and [`test/TestQALastMile.t.sol`](../test/TestQALastMile.t.sol) for integration and pinned edge cases.
@@ -109,15 +109,10 @@ contract Banny721TokenUriResolver is
109
109
  /// @custom:param upc The universal product code that the SVG hash represent.
110
110
  mapping(uint256 upc => bytes32) public override svgHashOf;
111
111
 
112
- // forge-lint: disable-next-line(mixed-case-variable)
113
112
  string public override DEFAULT_ALIEN_EYES;
114
- // forge-lint: disable-next-line(mixed-case-variable)
115
113
  string public override DEFAULT_MOUTH;
116
- // forge-lint: disable-next-line(mixed-case-variable)
117
114
  string public override DEFAULT_NECKLACE;
118
- // forge-lint: disable-next-line(mixed-case-variable)
119
115
  string public override DEFAULT_STANDARD_EYES;
120
- // forge-lint: disable-next-line(mixed-case-variable)
121
116
  string public override BANNY_BODY;
122
117
 
123
118
  //*********************************************************************//