@bannynet/core-v6 0.0.23 → 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
@@ -11,21 +11,21 @@
11
11
 
12
12
  ## Purpose
13
13
 
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 from custody if resolver logic fails.
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.
15
15
 
16
16
  ## Control Model
17
17
 
18
18
  - `Banny721TokenUriResolver` is `Ownable`.
19
19
  - Global admin power is limited to metadata, product naming, and SVG hash commitments.
20
- - Actual SVG payload upload is permissionless once the hash is committed.
20
+ - Actual SVG upload is permissionless once the hash is committed.
21
21
  - Body owners control decoration and locking for their own bodies.
22
- - Equipped accessories are held custodially by the resolver while attached.
22
+ - Equipped accessories are held by the resolver while attached.
23
23
 
24
24
  ## Roles
25
25
 
26
26
  | Role | How Assigned | Scope | Notes |
27
27
  | --- | --- | --- | --- |
28
- | Resolver owner | `Ownable(owner)` at construction | Global | Can transfer ownership with OpenZeppelin `transferOwnership()` |
28
+ | Resolver owner | `Ownable(owner)` at construction | Global | Can transfer ownership with `transferOwnership()` |
29
29
  | Body owner | `IERC721(hook).ownerOf(bannyBodyId)` | Per body | Can decorate and lock that body |
30
30
  | Anyone | No assignment | Global | Can upload SVG bytes only if they match a committed hash |
31
31
 
@@ -44,35 +44,35 @@
44
44
 
45
45
  - SVG hash commitments are write-once.
46
46
  - SVG contents are write-once once uploaded.
47
- - `lockOutfitChangesFor(...)` only extends the active lock; it never shortens it.
48
- - The lock duration is fixed by the `_LOCK_DURATION` constant.
47
+ - `lockOutfitChangesFor(...)` only extends the active lock.
48
+ - The lock duration is fixed by `_LOCK_DURATION`.
49
49
  - Default art fragments, category semantics, and the trusted forwarder are constructor or code immutables.
50
50
 
51
51
  ## Operational Notes
52
52
 
53
- - Treat `setSvgHashesOf(...)` like a release gate. A wrong hash usually means new resolver or new UPC strategy, not an edit.
53
+ - Treat `setSvgHashesOf(...)` like a release gate. A wrong hash usually means a new resolver or new UPC strategy, not a small edit.
54
54
  - Treat `setMetadata(...)` and `setProductNames(...)` as collection-wide display changes.
55
55
  - Remind users that equipped assets are in resolver custody while attached.
56
- - Only lock outfits when temporary non-editability is the intended user experience.
57
- - Use safe ERC-721 transfer flows when assets are sent into the resolver path; plain `transferFrom` can bypass receiver checks and strand NFTs without a recovery path.
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.
58
58
 
59
59
  ## Machine Notes
60
60
 
61
- - Do not infer a rescue path for equipped assets; there is none in this contract.
61
+ - Do not assume there is a rescue path for equipped assets. There is none.
62
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.
63
+ - If a committed hash and intended asset bytes differ, stop. The contract does not support overwrite repair.
64
64
  - If an asset arrived through non-safe ERC-721 transfer semantics, do not assume the resolver can detect or recover it.
65
65
 
66
66
  ## Recovery
67
67
 
68
68
  - Bad metadata can be changed by the owner.
69
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 in this contract.
71
- - If NFTs are stranded through non-safe transfer semantics, this contract does not provide a recovery flow.
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.
72
72
 
73
73
  ## Admin Boundaries
74
74
 
75
- - The owner cannot withdraw equipped user NFTs arbitrarily.
75
+ - The owner cannot arbitrarily withdraw equipped user NFTs.
76
76
  - The owner cannot overwrite committed hashes or uploaded SVG contents.
77
77
  - The owner cannot bypass body-owner checks on decoration or locking.
78
78
  - Nobody can shorten an active outfit lock.
package/ARCHITECTURE.md CHANGED
@@ -2,33 +2,33 @@
2
2
 
3
3
  ## Purpose
4
4
 
5
- `banny-retail-v6` is the Banny-specific metadata and attachment layer for Juicebox 721 collections. It does not mint the NFTs or own treasury logic. It owns attachment custody, outfit-lock rules, and final token rendering.
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
7
  ## System Overview
8
8
 
9
- The repo is centered on `Banny721TokenUriResolver`. A 721 hook from `nana-721-hook-v6` points to this resolver for `tokenURI(...)`, while 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.
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.
10
10
 
11
11
  ## Core Invariants
12
12
 
13
13
  - A body can only reference accessories that are currently escrowed by the resolver.
14
14
  - Replacing an equipped item must atomically return the old item and escrow the new item.
15
15
  - Outfit locks must block both explicit removal and implicit replacement until the lock expires.
16
- - Equipped assets travel with the body NFT on transfer until the new owner unequips them.
17
- - Registered SVG payloads must match their pre-registered content hash before they become renderable.
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
18
  - Rendering must stay deterministic for the same stored body state.
19
19
 
20
20
  ## Modules
21
21
 
22
22
  | Module | Responsibility | Notes |
23
23
  | --- | --- | --- |
24
- | `Banny721TokenUriResolver` | Escrow, attachment state, lock windows, and metadata rendering | Main contract; application-specific |
24
+ | `Banny721TokenUriResolver` | Escrow, attachment state, lock windows, and metadata rendering | Main contract |
25
25
  | `IBanny721TokenUriResolver` | External integration surface | Used by hooks and offchain tooling |
26
26
 
27
27
  ## Trust Boundaries
28
28
 
29
29
  - Minting, ownership transfer, and collection-level ERC-721 semantics live in `nana-721-hook-v6`.
30
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 the uploaded bytes against the stored hash.
31
+ - Asset-content upload is controlled by the registered content owner, but the contract verifies uploaded bytes against the stored hash.
32
32
 
33
33
  ## Critical Flows
34
34
 
@@ -66,20 +66,20 @@ body owner
66
66
 
67
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.
68
68
 
69
- That custody model uses lazy reconciliation for some stale attachment records. Read paths filter against current ownership and attachment state instead of eagerly rewriting storage on every external transfer.
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.
70
70
 
71
71
  ## Security Model
72
72
 
73
73
  - The main failure mode is custody drift between slot state and actual escrowed NFTs.
74
- - Rendering order is part of application semantics, not cosmetic output.
75
- - Lazy reconciliation is intentional. Changes that assume attachment arrays are perfectly clean in storage can strand assets or mis-render bodies.
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
76
  - Any new asset category adds both a rendering concern and a custody concern.
77
77
 
78
78
  ## Safe Change Guide
79
79
 
80
80
  - Keep generic ERC-721 behavior in `nana-721-hook-v6`, not here.
81
81
  - Review escrow writes and transfer behavior together whenever changing attachment logic.
82
- - If transfer or cleanup behavior changes, re-check lazy reconciliation assumptions alongside body-transfer inheritance of equipped assets.
82
+ - If transfer or cleanup behavior changes, re-check lazy reconciliation alongside body-transfer inheritance.
83
83
  - If `tokenURI(...)` changes, test stable output for unchanged state and replacement behavior for changed state.
84
84
  - If adding slots or asset classes, update rendering order, slot replacement, and lock enforcement in one change.
85
85
 
@@ -1,10 +1,11 @@
1
1
  # Audit Instructions
2
2
 
3
- This repo is the Banny avatar composition layer. It does not mint the base NFTs, but it does custody equipped accessories and define the metadata users see.
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
5
  ## Audit Objective
6
6
 
7
7
  Find issues that:
8
+
8
9
  - strand outfits or backgrounds in resolver custody
9
10
  - let the wrong actor equip, unequip, overwrite, or recover accessories
10
11
  - break outfit-lock timing or freeze a body longer than intended
@@ -14,9 +15,10 @@ Find issues that:
14
15
  ## Scope
15
16
 
16
17
  In scope:
18
+
17
19
  - `src/Banny721TokenUriResolver.sol`
18
20
  - `src/interfaces/IBanny721TokenUriResolver.sol`
19
- - all deployment helpers in `script/`
21
+ - deployment helpers in `script/`
20
22
 
21
23
  ## Start Here
22
24
 
@@ -27,6 +29,7 @@ In scope:
27
29
  ## Security Model
28
30
 
29
31
  The resolver is an attachment and rendering layer around a `JB721TiersHook` collection.
32
+
30
33
  - the underlying 721 hook remains the token contract and source of body ownership
31
34
  - the resolver temporarily holds accessory NFTs while they are equipped
32
35
  - body ownership should be the only authority that changes equipped state
@@ -53,7 +56,7 @@ The resolver is an attachment and rendering layer around a `JB721TiersHook` coll
53
56
  2. Only the current body owner or an intended delegate can change that body's equipped state.
54
57
  3. Conflicting categories cannot be equipped together, including through replacement or invalidation edge paths.
55
58
  4. Outfit-lock state only affects the intended body for the intended duration.
56
- 5. Metadata and SVG generation reflect current state and do not expose impossible combinations.
59
+ 5. Metadata and SVG generation reflect current state and do not show impossible combinations.
57
60
 
58
61
  ## Attack Surfaces
59
62
 
@@ -61,7 +64,7 @@ The resolver is an attachment and rendering layer around a `JB721TiersHook` coll
61
64
  - ERC-721 receipt hooks and any path that accepts custody
62
65
  - release paths after redecorating, burning, or invalid token state
63
66
  - category validation and conflict checks
64
- - metadata assembly that assumes on-chain assets or tier data remain available
67
+ - metadata assembly that assumes onchain assets or tier data remain available
65
68
 
66
69
  ## Accepted Risks Or Behaviors
67
70
 
package/README.md CHANGED
@@ -1,6 +1,6 @@
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 resolve to a base64-encoded JSON token URI whose image field contains an on-chain SVG.
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
6
  Architecture: [ARCHITECTURE.md](./ARCHITECTURE.md)
@@ -12,34 +12,32 @@ Audit instructions: [AUDIT_INSTRUCTIONS.md](./AUDIT_INSTRUCTIONS.md)
12
12
 
13
13
  ## Overview
14
14
 
15
- 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.
16
16
 
17
17
  The main user flows are:
18
18
 
19
19
  - mint body, outfit, and background NFTs through a Juicebox 721 hook
20
20
  - attach accessories to a body with `decorateBannyWith`
21
- - optionally freeze the current look for seven days with `lockOutfitChangesFor`
22
- - 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
23
23
 
24
- 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.
25
-
26
- 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.
27
25
 
28
26
  ## Key Contract
29
27
 
30
28
  | Contract | Role |
31
29
  | --- | --- |
32
- | `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. |
33
31
 
34
32
  ## Mental Model
35
33
 
36
34
  This repo owns three things:
37
35
 
38
- 1. custody of attached outfit and background NFTs while equipped
39
- 2. rules around what a body can wear and when that can change
40
- 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
41
39
 
42
- It does not own mint pricing, tier issuance, or project accounting.
40
+ It does not own mint pricing, tier issuance, or treasury accounting.
43
41
 
44
42
  ## Read These Files First
45
43
 
@@ -58,12 +56,11 @@ It does not own mint pricing, tier issuance, or project accounting.
58
56
 
59
57
  ## Integration Traps
60
58
 
61
- - the resolver custodies equipped assets, so transfer edge cases matter as much as rendering output
62
- - transferred bodies carry their equipped assets, so a new body holder can inherit control of those items
63
- - burned bodies and non-safe transfer patterns can strand expectations around resolver-held assets unless integrations model the attachment lifecycle correctly
64
- - outfit locks persist across body transfers until expiry, so a new holder can inherit a still-locked body
65
- - metadata quality depends on lazily uploaded asset payloads, not only on the token state
66
- - collection logic here assumes a Juicebox 721 hook upstream and should not be read as a generic NFT renderer
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
67
64
 
68
65
  ## Where State Lives
69
66
 
@@ -96,7 +93,7 @@ Useful scripts:
96
93
 
97
94
  ## Deployment Notes
98
95
 
99
- 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.
100
97
 
101
98
  ## Repository Layout
102
99
 
@@ -115,14 +112,14 @@ script/
115
112
 
116
113
  ## Risks And Notes
117
114
 
118
- - attached outfits and backgrounds are custodied by the resolver while equipped
115
+ - attached outfits and backgrounds are held by the resolver while equipped
119
116
  - outfit locks are fixed-duration and cannot be shortened once set
120
- - on-chain SVG content is immutable once uploaded for a given registered hash
121
- - ERC-721 `transferFrom` paths that bypass safe-receive checks can still create asset-tracking surprises around resolver custody
122
- - 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
123
120
 
124
121
  ## For AI Agents
125
122
 
126
- - Treat this repo as an application-layer resolver, not as the NFT issuance primitive.
123
+ - Treat this repo as an app-layer resolver, not as the NFT issuance primitive.
127
124
  - Start with `Banny721TokenUriResolver` and the lifecycle tests before summarizing attachment behavior.
128
- - If the question is about mint economics or tier availability, inspect `nana-721-hook-v6` instead of inferring from this repo.
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 decide whether the issue is custody state, lock semantics, stored SVG content, or final token-URI composition. Those problems often look similar from the outside.
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
 
@@ -26,17 +26,17 @@
26
26
 
27
27
  ## Purpose
28
28
 
29
- 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.
30
30
 
31
31
  ## Reference Files
32
32
 
33
- - Open [`references/runtime.md`](./references/runtime.md) when you need attachment and custody behavior, rendering order, or the main invariants that protect equipped assets.
34
- - 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.
35
35
 
36
36
  ## Working Rules
37
37
 
38
- - 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.
39
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 behavior as intentional before calling it a custody bug.
40
+ - Equipped outfits and backgrounds travel with the body NFT. Treat that inheritance as intentional before calling it a bug.
41
41
  - When a task mentions minting, pricing, or terminal accounting, verify that the problem is not actually in the upstream 721 hook repo.
42
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
@@ -2,9 +2,7 @@
2
2
 
3
3
  ## Repo Purpose
4
4
 
5
- This repo is the Banny-specific composition and metadata layer on top of a Juicebox 721 collection.
6
- It owns attachment custody, compatibility rules, outfit locks, and rendered token metadata. It does not own tier
7
- pricing, treasury accounting, or mint eligibility outside the resolver-specific checks.
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.
8
6
 
9
7
  ## Primary Actors
10
8
 
@@ -15,7 +13,7 @@ pricing, treasury accounting, or mint eligibility outside the resolver-specific
15
13
  ## Key Surfaces
16
14
 
17
15
  - `Banny721TokenUriResolver`: custody, compatibility, locks, and rendered SVG metadata
18
- - `decorateBannyWith(...)`: equips outfits and a background to a body and returns no-longer-equipped items when possible
16
+ - `decorateBannyWith(...)`: equips outfits and a background to a body and returns old items when possible
19
17
  - `lockOutfitChangesFor(...)`: freezes appearance changes for the fixed lock window
20
18
  - `setSvgHashesOf(...)` / `setSvgContentsOf(...)`: publish or repair art payloads
21
19
  - `setMetadata(...)` / `setProductNames(...)`: update collection metadata and UPC naming
@@ -27,21 +25,24 @@ pricing, treasury accounting, or mint eligibility outside the resolver-specific
27
25
  **Intent:** acquire the pieces needed to build a composed Banny.
28
26
 
29
27
  **Preconditions**
28
+
30
29
  - the Banny collection is live through the 721 hook
31
30
  - the required body, outfit, and background tiers exist
32
31
 
33
32
  **Main Flow**
33
+
34
34
  1. Mint the body, outfit, and background NFTs through the underlying 721 project.
35
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 actually owns compatible pieces.
36
+ 3. Move to this resolver only once the user owns compatible pieces.
37
37
 
38
38
  **Failure Modes**
39
- - the wrong tiers are minted or the pieces are not compatible
39
+
40
+ - the wrong tiers are minted or the pieces are incompatible
40
41
  - teams misread this repo as the minting or accounting surface
41
42
 
42
43
  **Postconditions**
44
+
43
45
  - the user holds the components needed for later composition
44
- - mint pricing, reserves, and treasury effects still belong to the underlying 721 project rather than this resolver
45
46
 
46
47
  ## Journey 2: Dress A Banny And Put Accessories Into Resolver Custody
47
48
 
@@ -50,24 +51,28 @@ pricing, treasury accounting, or mint eligibility outside the resolver-specific
50
51
  **Intent:** equip a body with a background and outfits so the resolver serves the composed avatar.
51
52
 
52
53
  **Preconditions**
54
+
53
55
  - the caller controls the body and the accessories being equipped
54
56
  - no active outfit lock blocks the change
55
57
  - the selected pieces are compatible by category and collection rules
56
58
 
57
59
  **Main Flow**
60
+
58
61
  1. Call `decorateBannyWith(...)` for the target body.
59
62
  2. The resolver checks compatibility and diffs old versus new attachments.
60
63
  3. Equipped accessories move into resolver custody while attached.
61
64
  4. The token URI for the body now reflects the combined SVG and metadata.
62
65
 
63
66
  **Failure Modes**
67
+
64
68
  - duplicate outfit categories or incompatible combinations are provided
65
- - a transfer-back of previously attached items fails, leaving retained custody state that must be recovered later
66
- - reviewers forget that the resolver, not the user wallet, holds equipped accessories while active
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
67
71
 
68
72
  **Postconditions**
69
- - the body renders with the newly attached composition
70
- - attached accessories remain in resolver custody until replaced or cleared
73
+
74
+ - the body renders with the new composition
75
+ - attached accessories stay in resolver custody until replaced or cleared
71
76
 
72
77
  ## Journey 3: Lock A Banny's Appearance For A Period
73
78
 
@@ -76,43 +81,51 @@ pricing, treasury accounting, or mint eligibility outside the resolver-specific
76
81
  **Intent:** freeze the current appearance for the fixed lock window.
77
82
 
78
83
  **Preconditions**
84
+
79
85
  - the body already has a state worth freezing
80
- - the caller understands the lock is intentionally fixed-duration
86
+ - the caller understands the lock is fixed-duration
81
87
 
82
88
  **Main Flow**
89
+
83
90
  1. Call `lockOutfitChangesFor(...)`.
84
91
  2. The resolver extends the lock for that body.
85
92
  3. Future decoration or removal attempts must wait until the lock expires.
86
93
 
87
94
  **Failure Modes**
88
- - a seller locks just before transfer and the buyer cannot re-style immediately
89
- - integrations fail to surface lock state before listing or sale
95
+
96
+ - a seller locks just before transfer and the buyer cannot restyle immediately
97
+ - integrations fail to show lock state before listing or sale
90
98
 
91
99
  **Postconditions**
100
+
92
101
  - appearance changes are blocked until the lock expires
93
102
 
94
- ## Journey 4: Publish Or Repair On-Chain Art Assets
103
+ ## Journey 4: Publish Or Repair Onchain Art Assets
95
104
 
96
105
  **Actor:** collection operator or art publisher.
97
106
 
98
107
  **Intent:** make token URIs render complete onchain art.
99
108
 
100
109
  **Preconditions**
110
+
101
111
  - the relevant UPCs and content hashes are known
102
- - the operator understands hashes are the commitment and SVG content must match them exactly
112
+ - the operator understands that the hash is the commitment and SVG content must match it exactly
103
113
 
104
114
  **Main Flow**
115
+
105
116
  1. Register hashes with `setSvgHashesOf(...)`.
106
117
  2. Upload matching payloads with `setSvgContentsOf(...)`.
107
118
  3. Re-check token URI output after publication or repair.
108
119
 
109
120
  **Failure Modes**
110
- - the uploaded SVG does not match the committed hash
121
+
122
+ - uploaded SVG does not match the committed hash
111
123
  - product names are missing or stale
112
124
  - teams assume the 721 hook owns rendered output when this repo does
113
125
 
114
126
  **Postconditions**
115
- - token URIs can render the intended onchain art payloads for published UPCs
127
+
128
+ - token URIs can render the intended onchain art payloads
116
129
 
117
130
  ## Journey 5: Update Collection Metadata And Product Catalog Entries
118
131
 
@@ -121,19 +134,23 @@ pricing, treasury accounting, or mint eligibility outside the resolver-specific
121
134
  **Intent:** change collection-level metadata and human-readable product labels.
122
135
 
123
136
  **Preconditions**
137
+
124
138
  - the operator has authority over the resolver metadata surface
125
139
 
126
140
  **Main Flow**
141
+
127
142
  1. Update collection metadata with `setMetadata(...)`.
128
143
  2. Set or repair UPC names with `setProductNames(...)`.
129
144
  3. Re-check a representative token URI so labels and art agree.
130
145
 
131
146
  **Failure Modes**
147
+
132
148
  - metadata and SVG state drift apart
133
149
  - operators update catalog labels without checking already-minted assets
134
150
 
135
151
  **Postconditions**
136
- - collection-level metadata and UPC names line up with the currently published art set
152
+
153
+ - collection-level metadata and UPC names line up with the published art set
137
154
 
138
155
  ## Journey 6: Unequip And Recover Custodied Accessories
139
156
 
@@ -142,20 +159,24 @@ pricing, treasury accounting, or mint eligibility outside the resolver-specific
142
159
  **Intent:** recover attached accessories from resolver custody.
143
160
 
144
161
  **Preconditions**
162
+
145
163
  - the current lock window, if any, has expired
146
- - the owner understands old pieces may only be returned as part of a later decoration update
164
+ - the owner understands that old pieces may only be returned as part of a later decoration update
147
165
 
148
166
  **Main Flow**
167
+
149
168
  1. Replace or clear the equipped items through `decorateBannyWith(...)`.
150
169
  2. The resolver attempts to return no-longer-equipped accessories.
151
- 3. Once returned, those NFTs can be transferred or re-used independently.
170
+ 3. Once returned, those NFTs can be transferred or reused independently.
152
171
 
153
172
  **Failure Modes**
173
+
154
174
  - previously equipped pieces remain retained because transfer-back failed
155
175
  - burned or otherwise unrecoverable pieces leave cosmetic phantom state until corrected
156
176
 
157
177
  **Postconditions**
158
- - no-longer-equipped accessories are either returned to the owner or remain explicitly retained pending recovery
178
+
179
+ - no-longer-equipped accessories are either returned or remain explicitly retained pending recovery
159
180
 
160
181
  ## Trust Boundaries
161
182
 
@@ -166,4 +187,4 @@ pricing, treasury accounting, or mint eligibility outside the resolver-specific
166
187
  ## Hand-Offs
167
188
 
168
189
  - Use [nana-721-hook-v6](../nana-721-hook-v6/USER_JOURNEYS.md) for mint pricing, tier issuance, reserves, and treasury behavior.
169
- - 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bannynet/core-v6",
3
- "version": "0.0.23",
3
+ "version": "0.0.24",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -20,14 +20,14 @@
20
20
  "artifacts": "source ./.env && npx sphinx artifacts --org-id 'ea165b21-7cdc-4d7b-be59-ecdd4c26bee4' --project-name 'banny-core-v6'"
21
21
  },
22
22
  "dependencies": {
23
- "@bananapus/721-hook-v6": "^0.0.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",
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
29
  "@openzeppelin/contracts": "^5.6.1",
30
- "@rev-net/core-v6": "^0.0.32",
30
+ "@rev-net/core-v6": "^0.0.35",
31
31
  "keccak": "^3.0.4"
32
32
  },
33
33
  "devDependencies": {