@bannynet/core-v6 0.0.17 → 0.0.19
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 +28 -0
- package/ARCHITECTURE.md +51 -75
- package/AUDIT_INSTRUCTIONS.md +64 -331
- package/CHANGELOG.md +31 -0
- package/README.md +53 -167
- package/RISKS.md +18 -1
- package/SKILLS.md +27 -243
- package/STYLE_GUIDE.md +56 -17
- package/USER_JOURNEYS.md +51 -496
- package/package.json +8 -8
- package/references/operations.md +25 -0
- package/references/runtime.md +27 -0
- package/script/Add.Denver.s.sol +10 -7
- package/script/Deploy.s.sol +37 -28
- package/script/Drop1.s.sol +424 -329
- package/src/Banny721TokenUriResolver.sol +109 -59
- package/test/Banny721TokenUriResolver.t.sol +8 -5
- package/test/BannyAttacks.t.sol +8 -5
- package/test/DecorateFlow.t.sol +8 -5
- package/test/Fork.t.sol +25 -17
- package/test/OutfitTransferLifecycle.t.sol +8 -5
- package/test/TestAuditGaps.sol +8 -5
- package/test/TestQALastMile.t.sol +8 -5
- package/test/audit/AntiStrandingRetention.t.sol +33 -5
- package/test/audit/BurnedBodyStrandsAssets.t.sol +9 -5
- package/test/audit/MergedOutfitExclusivity.t.sol +8 -5
- package/test/audit/MigrationHelperVerificationBypass.t.sol +102 -0
- package/test/audit/TryTransferFromStrandsAssets.t.sol +8 -5
- package/test/regression/BodyCategoryValidation.t.sol +8 -5
- package/test/regression/BurnedTokenCheck.t.sol +8 -5
- package/test/regression/CEIReorder.t.sol +8 -5
- package/test/regression/RemovedTierDesync.t.sol +8 -5
- package/CHANGE_LOG.md +0 -243
- package/assets/findings/banny-retail-v6-pashov-ai-audit-report-20260330-102839.md +0 -34
package/README.md
CHANGED
|
@@ -1,115 +1,40 @@
|
|
|
1
1
|
# Banny Retail
|
|
2
2
|
|
|
3
|
-
|
|
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.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Docs: <https://docs.juicebox.money>
|
|
6
|
+
Architecture: [ARCHITECTURE.md](./ARCHITECTURE.md)
|
|
6
7
|
|
|
7
|
-
##
|
|
8
|
+
## Overview
|
|
8
9
|
|
|
9
|
-
|
|
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.
|
|
10
11
|
|
|
11
|
-
|
|
12
|
+
The main user flows are:
|
|
12
13
|
|
|
13
|
-
|
|
14
|
+
- mint body, outfit, and background NFTs through a Juicebox 721 hook
|
|
15
|
+
- 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
|
|
14
18
|
|
|
15
|
-
|
|
16
|
-
1. A Juicebox 721 hook registers Banny721TokenUriResolver as its token URI resolver
|
|
17
|
-
→ All tokenURI() calls are forwarded to the resolver
|
|
18
|
-
|
|
|
19
|
-
2. Users mint Banny body NFTs + outfit/background NFTs via the 721 hook
|
|
20
|
-
→ Bodies are the "base" layer; outfits and backgrounds are accessories
|
|
21
|
-
|
|
|
22
|
-
3. Body owner calls decorateBannyWith(hook, bodyId, backgroundId, outfitIds)
|
|
23
|
-
→ Outfit and background NFTs are transferred to the resolver contract
|
|
24
|
-
→ Resolver tracks which body wears which outfits
|
|
25
|
-
→ Body's tokenURI now renders the full dressed composition
|
|
26
|
-
|
|
|
27
|
-
4. Outfit lock (optional): lockOutfitChangesFor(hook, bodyId)
|
|
28
|
-
→ Freezes outfit and background changes for 7 days
|
|
29
|
-
→ Prevents moving currently equipped assets away through another body's decoration call
|
|
30
|
-
→ Proves the Banny's look is stable (useful for PFPs, displays)
|
|
31
|
-
|
|
|
32
|
-
5. SVG content is stored on-chain via a two-step process:
|
|
33
|
-
→ Owner registers content hashes: setSvgHashesOf(upcs, hashes)
|
|
34
|
-
→ Anyone uploads matching content: setSvgContentsOf(upcs, contents)
|
|
35
|
-
→ Falls back to IPFS base URI if on-chain content not yet stored
|
|
36
|
-
```
|
|
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.
|
|
37
20
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
Resolver->>Hook: ownerOf(bodyId) -- verify caller owns body
|
|
48
|
-
Hook-->>Resolver: owner address
|
|
49
|
-
|
|
50
|
-
Note over Resolver: Check body is not locked
|
|
51
|
-
|
|
52
|
-
alt Background is changing
|
|
53
|
-
Resolver->>Hook: transferFrom(owner, resolver, bgId)
|
|
54
|
-
Note over Resolver: Store bgId as body's background
|
|
55
|
-
opt Previous background exists
|
|
56
|
-
Resolver->>Hook: transferFrom(resolver, owner, prevBgId)
|
|
57
|
-
end
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
loop Each outfit in outfitIds
|
|
61
|
-
Resolver->>Hook: transferFrom(owner, resolver, outfitId)
|
|
62
|
-
Note over Resolver: Track outfitId as worn by body
|
|
63
|
-
opt Previous outfit in same category
|
|
64
|
-
Resolver->>Hook: transferFrom(resolver, owner, prevOutfitId)
|
|
65
|
-
end
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
Note over Resolver: tokenURI(bodyId) now renders composed SVG
|
|
69
|
-
Owner->>Resolver: tokenURI(bodyId)
|
|
70
|
-
Resolver-->>Owner: base64-encoded JSON with layered SVG
|
|
71
|
-
```
|
|
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.
|
|
22
|
+
|
|
23
|
+
## Key Contract
|
|
24
|
+
|
|
25
|
+
| Contract | Role |
|
|
26
|
+
| --- | --- |
|
|
27
|
+
| `Banny721TokenUriResolver` | Resolves token metadata, stores equipped accessories, enforces outfit locks, and renders layered SVG output for Banny collections. |
|
|
28
|
+
|
|
29
|
+
## Mental Model
|
|
72
30
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
| 3 | Necklace | Accessory (default provided if none attached). |
|
|
81
|
-
| 4 | Head | Full head accessory. Blocks eyes, glasses, mouth, and headtop. |
|
|
82
|
-
| 5 | Eyes | Eye style (defaults: alien or standard based on body type). |
|
|
83
|
-
| 6 | Glasses | Eyewear layer. Blocked by head. |
|
|
84
|
-
| 7 | Mouth | Mouth expression (default provided). Blocked by head. |
|
|
85
|
-
| 8 | Legs | Lower body clothing. |
|
|
86
|
-
| 9 | Suit | Full body one-piece. Blocks suit top and suit bottom. |
|
|
87
|
-
| 10 | Suit Bottom | Lower suit piece. Blocked by full suit. |
|
|
88
|
-
| 11 | Suit Top | Upper suit piece. Blocked by full suit. |
|
|
89
|
-
| 12 | Headtop | Top-of-head accessory. Blocked by head. |
|
|
90
|
-
| 13 | Hand | Held item layer. |
|
|
91
|
-
| 14-17 | Special | Special suit, legs, head, and body overlays. |
|
|
92
|
-
|
|
93
|
-
### Body Types
|
|
94
|
-
|
|
95
|
-
| UPC | Type | Color Palette |
|
|
96
|
-
|-----|------|--------------|
|
|
97
|
-
| 1 | Alien | Green tones (`67d757`, `30a220`, `217a15`, `09490f`) with purple accents |
|
|
98
|
-
| 2 | Pink | Pink tones (`ffd8c5`, `ff96a9`, `fe588b`, `c92f45`) |
|
|
99
|
-
| 3 | Orange | Orange tones (`f3a603`, `ff7c02`, `fd3600`, `c32e0d`) |
|
|
100
|
-
| 4 | Original | Yellow tones (`ffe900`, `ffc700`, `f3a603`, `965a1a`) |
|
|
101
|
-
|
|
102
|
-
## Architecture
|
|
103
|
-
|
|
104
|
-
| Contract | Description |
|
|
105
|
-
|----------|-------------|
|
|
106
|
-
| `Banny721TokenUriResolver` | The sole contract. Implements `IJB721TokenUriResolver` to serve fully on-chain SVG token URIs for any Juicebox 721 hook. Manages outfit attachment, background assignment, outfit locking, on-chain SVG storage, and layered SVG rendering. Inherits `Ownable`, `ReentrancyGuard`, `ERC2771Context`, `IERC721Receiver`. |
|
|
107
|
-
|
|
108
|
-
### Interface
|
|
109
|
-
|
|
110
|
-
| Interface | Description |
|
|
111
|
-
|-----------|-------------|
|
|
112
|
-
| `IBanny721TokenUriResolver` | Public API: `tokenUriOf`, `svgOf`, `decorateBannyWith`, `lockOutfitChangesFor`, `assetIdsOf`, `namesOf`, `userOf`, `wearerOf`, SVG management, metadata management, plus all events. |
|
|
31
|
+
This repo owns three things:
|
|
32
|
+
|
|
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
|
+
|
|
37
|
+
It does not own mint pricing, tier issuance, or project accounting.
|
|
113
38
|
|
|
114
39
|
## Install
|
|
115
40
|
|
|
@@ -117,84 +42,45 @@ sequenceDiagram
|
|
|
117
42
|
npm install @bannynet/core-v6
|
|
118
43
|
```
|
|
119
44
|
|
|
120
|
-
|
|
45
|
+
## Development
|
|
46
|
+
|
|
47
|
+
The contract stack relies on `via_ir = true` in `foundry.toml`.
|
|
121
48
|
|
|
122
49
|
```bash
|
|
123
|
-
|
|
50
|
+
npm install
|
|
51
|
+
forge build
|
|
52
|
+
forge test
|
|
124
53
|
```
|
|
125
54
|
|
|
126
|
-
|
|
55
|
+
Useful scripts:
|
|
127
56
|
|
|
128
|
-
|
|
57
|
+
- `npm run deploy:mainnets`
|
|
58
|
+
- `npm run deploy:testnets`
|
|
59
|
+
- `npm run deploy:mainnets:drop:1`
|
|
60
|
+
- `npm run deploy:testnets:drop:1`
|
|
129
61
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
| `forge test` | Run all tests (14 test files: functionality, attacks, decoration flows, fork integration, transfer lifecycle, audit gaps, QA, regressions) |
|
|
134
|
-
| `forge test -vvv` | Run tests with full trace |
|
|
62
|
+
## Deployment Notes
|
|
63
|
+
|
|
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.
|
|
135
65
|
|
|
136
66
|
## Repository Layout
|
|
137
67
|
|
|
138
|
-
```
|
|
68
|
+
```text
|
|
139
69
|
src/
|
|
140
|
-
Banny721TokenUriResolver.sol
|
|
70
|
+
Banny721TokenUriResolver.sol
|
|
141
71
|
interfaces/
|
|
142
|
-
IBanny721TokenUriResolver.sol # Public interface + events
|
|
143
72
|
test/
|
|
144
|
-
|
|
145
|
-
BannyAttacks.t.sol # Security/adversarial tests
|
|
146
|
-
DecorateFlow.t.sol # Decoration flow tests
|
|
147
|
-
Fork.t.sol # Fork integration tests
|
|
148
|
-
OutfitTransferLifecycle.t.sol # Transfer lifecycle tests
|
|
149
|
-
TestAuditGaps.sol # Audit gap coverage (meta-tx, SVG edge cases)
|
|
150
|
-
TestQALastMile.t.sol # QA tests (gas, round-trip, fallback)
|
|
151
|
-
regression/
|
|
152
|
-
ArrayLengthValidation.t.sol # Input validation regression
|
|
153
|
-
BodyCategoryValidation.t.sol # Body category regression
|
|
154
|
-
BurnedTokenCheck.t.sol # Burned token regression
|
|
155
|
-
CEIReorder.t.sol # CEI ordering regression
|
|
156
|
-
ClearMetadata.t.sol # Metadata clearing regression
|
|
157
|
-
MsgSenderEvents.t.sol # Event emission regression
|
|
158
|
-
RemovedTierDesync.t.sol # Removed tier desync regression
|
|
73
|
+
unit, attack, fork, audit, QA, and regression coverage
|
|
159
74
|
script/
|
|
160
|
-
Deploy.s.sol
|
|
161
|
-
Drop1.s.sol
|
|
162
|
-
Add.Denver.s.sol
|
|
75
|
+
Deploy.s.sol
|
|
76
|
+
Drop1.s.sol
|
|
77
|
+
Add.Denver.s.sol
|
|
163
78
|
helpers/
|
|
164
|
-
BannyverseDeploymentLib.sol # Deployment artifact reader
|
|
165
|
-
MigrationHelper.sol # Migration utilities
|
|
166
79
|
```
|
|
167
80
|
|
|
168
|
-
##
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
| `setSvgHashesOf` | Contract owner only |
|
|
175
|
-
| `setSvgContentsOf` | Anyone (content validated against registered hash) |
|
|
176
|
-
| `setProductNames` | Contract owner only |
|
|
177
|
-
| `setMetadata` | Contract owner only |
|
|
178
|
-
|
|
179
|
-
## Supported Chains
|
|
180
|
-
|
|
181
|
-
Deployed via Sphinx deterministic deployment (`script/Deploy.s.sol`).
|
|
182
|
-
|
|
183
|
-
| Network | Chain ID |
|
|
184
|
-
|---------|----------|
|
|
185
|
-
| Ethereum | 1 |
|
|
186
|
-
| Optimism | 10 |
|
|
187
|
-
| Base | 8453 |
|
|
188
|
-
| Arbitrum | 42161 |
|
|
189
|
-
| Ethereum Sepolia | 11155111 |
|
|
190
|
-
| Optimism Sepolia | 11155420 |
|
|
191
|
-
| Base Sepolia | 84532 |
|
|
192
|
-
| Arbitrum Sepolia | 421614 |
|
|
193
|
-
|
|
194
|
-
## Risks
|
|
195
|
-
|
|
196
|
-
- **Outfit custody:** Attached outfits and backgrounds are held by the resolver contract. If a return transfer fails (e.g., owner is a non-receiver contract), the asset is retained in the attached list rather than stranded — the owner can retry once the issue is resolved. Permanently unrecoverable assets (burned NFTs) create phantom entries in SVG rendering.
|
|
197
|
-
- **7-day lock is fixed.** Cannot be shortened or cancelled once set. The lock duration is hardcoded.
|
|
198
|
-
- **SVG immutability.** Once SVG content is stored on-chain for a UPC, it cannot be changed. A mistake in the content requires deploying a new resolver.
|
|
199
|
-
- **Hash registration is owner-only, but content upload is not.** Anyone can call `setSvgContentsOf` as long as the content matches the registered hash. This is by design for lazy uploads.
|
|
200
|
-
- **Single resolver per hook.** The resolver is set on the 721 hook and applies to all tiers. Different collections would need different resolver instances.
|
|
81
|
+
## Risks And Notes
|
|
82
|
+
|
|
83
|
+
- attached outfits and backgrounds are custodied by the resolver while equipped
|
|
84
|
+
- 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
|
package/RISKS.md
CHANGED
|
@@ -1,4 +1,21 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Banny Retail Risk Register
|
|
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.
|
|
4
|
+
|
|
5
|
+
## How to use this file
|
|
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.
|
|
9
|
+
- Treat `Invariants to Verify` as required test and audit targets.
|
|
10
|
+
|
|
11
|
+
## Priority risks
|
|
12
|
+
|
|
13
|
+
| Priority | Risk | Why it matters | Primary controls |
|
|
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
|
+
|
|
2
19
|
|
|
3
20
|
## 1. Trust Assumptions
|
|
4
21
|
|
package/SKILLS.md
CHANGED
|
@@ -1,256 +1,40 @@
|
|
|
1
1
|
# Banny Retail
|
|
2
2
|
|
|
3
|
-
##
|
|
4
|
-
|
|
5
|
-
On-chain composable NFT avatar system that renders Banny characters with layered SVG outfits and backgrounds for Juicebox 721 collections. Bodies can be dressed, locked, and rendered entirely on-chain.
|
|
6
|
-
|
|
7
|
-
## Contracts
|
|
8
|
-
|
|
9
|
-
| Contract | Role |
|
|
10
|
-
|----------|------|
|
|
11
|
-
| `Banny721TokenUriResolver` | Resolves token URIs for any Juicebox 721 hook by composing layered SVGs from registered asset content. Manages outfit attachment, background assignment, outfit locking, on-chain SVG storage, and metadata generation. Inherits `Ownable`, `ReentrancyGuard`, `ERC2771Context`, `IERC721Receiver`. (~1,331 lines) |
|
|
12
|
-
|
|
13
|
-
## Key Functions
|
|
14
|
-
|
|
15
|
-
### Token URI & Rendering
|
|
16
|
-
|
|
17
|
-
| Function | What it does |
|
|
18
|
-
|----------|-------------|
|
|
19
|
-
| `tokenUriOf(hook, tokenId)` | Returns base64-encoded JSON metadata URI with on-chain SVG image. For bodies: composes dressed Banny with attached outfits and background. For outfit/background tokens: renders the item on a grayscale mannequin Banny. |
|
|
20
|
-
| `svgOf(hook, tokenId, shouldDressBannyBody, shouldIncludeBackgroundOnBannyBody)` | Returns the composed SVG string. Layers (in order): background, backside, body, necklace, eyes, mouth, outfits by category. Falls back to IPFS base URI if on-chain content not stored. |
|
|
21
|
-
|
|
22
|
-
### Decoration
|
|
23
|
-
|
|
24
|
-
| Function | What it does |
|
|
25
|
-
|----------|-------------|
|
|
26
|
-
| `decorateBannyWith(hook, bannyBodyId, backgroundId, outfitIds)` | Attaches background and outfits to a body. Transfers assets to contract custody. Validates ownership, lock status, category ordering, slot conflicts. Attempts to return previously attached items to caller — if a return fails, the item is retained (not stranded). Emits `DecorateBanny`. |
|
|
27
|
-
| `lockOutfitChangesFor(hook, bannyBodyId)` | Locks a body's outfit for 7 days. Cannot accelerate an existing lock. Caller must own the body NFT. |
|
|
28
|
-
|
|
29
|
-
### Views
|
|
30
|
-
|
|
31
|
-
| Function | What it does |
|
|
32
|
-
|----------|-------------|
|
|
33
|
-
| `assetIdsOf(hook, bannyBodyId)` | Returns `(backgroundId, outfitIds[])` currently attached to body. Filters out stale entries where `_wearerOf` no longer matches. |
|
|
34
|
-
| `namesOf(hook, tokenId)` | Returns `(fullName, categoryName, productName)`. Full name includes inventory count (e.g., "42/100"). |
|
|
35
|
-
| `userOf(hook, backgroundId)` | Returns the body ID using a background, or 0 if unused. Validates consistency. |
|
|
36
|
-
| `wearerOf(hook, outfitId)` | Returns the body ID wearing an outfit, or 0 if unworn. Validates outfit is in body's attached array. |
|
|
37
|
-
|
|
38
|
-
### SVG Content Management
|
|
39
|
-
|
|
40
|
-
| Function | Who | What it does |
|
|
41
|
-
|----------|-----|-------------|
|
|
42
|
-
| `setSvgHashesOf(upcs, svgHashes)` | Owner only | Registers expected SVG content hashes for UPCs. Immutable once set. |
|
|
43
|
-
| `setSvgContentsOf(upcs, svgContents)` | **Anyone** | Stores on-chain SVG content. Must match previously registered hash. Cannot overwrite existing content. |
|
|
44
|
-
| `setProductNames(upcs, names)` | Owner only | Sets human-readable names for product UPCs. |
|
|
45
|
-
| `setMetadata(description, url, baseUri)` | Owner only | Updates token metadata description, external URL, and SVG base URI. Empty strings clear the field. |
|
|
46
|
-
|
|
47
|
-
### Token Receipt
|
|
48
|
-
|
|
49
|
-
| Function | What it does |
|
|
50
|
-
|----------|-------------|
|
|
51
|
-
| `onERC721Received(operator, from, tokenId, data)` | Validates token receipt. Reverts unless `operator == address(this)`. Called when contract takes custody of outfits/backgrounds during decoration. |
|
|
52
|
-
|
|
53
|
-
## Integration Points
|
|
54
|
-
|
|
55
|
-
| Dependency | Import | Used For |
|
|
56
|
-
|------------|--------|----------|
|
|
57
|
-
| `@bananapus/721-hook-v6` | `IJB721TiersHook`, `IJB721TiersHookStore`, `IJB721TokenUriResolver`, `JB721Tier`, `JBIpfsDecoder`, `IERC721` | Token ownership checks, tier/product data resolution, IPFS URI decoding, hook store queries, safe transfers |
|
|
58
|
-
| `@openzeppelin/contracts` | `Ownable`, `ReentrancyGuard`, `ERC2771Context`, `IERC721Receiver`, `Strings` | Access control, reentrancy protection, meta-transactions, safe NFT receipt, string utilities |
|
|
59
|
-
| `lib/base64` | `Base64` | Base64 encoding for on-chain SVG and JSON metadata |
|
|
60
|
-
|
|
61
|
-
### Deployment / Setup Sequence
|
|
62
|
-
|
|
63
|
-
The resolver is connected to a 721 hook in one of two ways:
|
|
64
|
-
|
|
65
|
-
1. **At hook deployment**: Pass the resolver address as `tokenUriResolver` in `JBDeploy721TiersHookConfig` when calling `IJB721TiersHookDeployer.deployHookFor(...)`. The hook's `initialize()` stores it in the hook store via `recordSetTokenUriResolver`.
|
|
66
|
-
2. **After deployment**: Call `hook.setMetadata(...)` with the resolver address as the `tokenUriResolver` parameter. Requires `SET_721_METADATA` permission. Pass `IJB721TokenUriResolver(address(this))` as a sentinel value to leave it unchanged, since `address(0)` clears the resolver.
|
|
67
|
-
|
|
68
|
-
Once set, the hook's store maps `tokenUriResolverOf[hook]` to the resolver address. Any `tokenURI(tokenId)` call on the hook delegates to `resolver.tokenUriOf(hook, tokenId)`, which composes the on-chain SVG.
|
|
69
|
-
|
|
70
|
-
## Rendering Order
|
|
3
|
+
## Use This File For
|
|
71
4
|
|
|
72
|
-
|
|
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.
|
|
73
7
|
|
|
74
|
-
|
|
75
|
-
2. **Body** (category 0) -- the base Banny character with color-fill styles (`b1`-`b4`, `a1`-`a3`)
|
|
76
|
-
3. **Backside** (category 2) -- rendered behind the body in the SVG source but layered by the SVG's internal z-ordering
|
|
77
|
-
4. **Necklace** (category 3) -- default injected if none attached; custom necklaces are deferred and rendered after suit top (category 11)
|
|
78
|
-
5. **Head** (category 4) -- if present, suppresses default injection of eyes, mouth
|
|
79
|
-
6. **Eyes** (category 5) -- default alien or standard eyes injected if no head and no explicit eyes
|
|
80
|
-
7. **Glasses** (category 6)
|
|
81
|
-
8. **Mouth** (category 7) -- default mouth injected if no head and no explicit mouth
|
|
82
|
-
9. **Legs** (category 8)
|
|
83
|
-
10. **Suit** (category 9)
|
|
84
|
-
11. **Suit Bottom** (category 10)
|
|
85
|
-
12. **Suit Top** (category 11)
|
|
86
|
-
13. **Custom Necklace** -- rendered here (after suit top) if a custom necklace was provided, so it layers on top of clothing
|
|
87
|
-
14. **Headtop** (category 12)
|
|
88
|
-
15. **Hand** (category 13)
|
|
89
|
-
16. **Special overlays** (categories 14-17) -- suit, legs, head, body specials
|
|
8
|
+
## Read This Next
|
|
90
9
|
|
|
91
|
-
|
|
10
|
+
| If you need... | Open this next |
|
|
11
|
+
|---|---|
|
|
12
|
+
| Repo overview and user-facing behavior | [`README.md`](./README.md), [`ARCHITECTURE.md`](./ARCHITECTURE.md) |
|
|
13
|
+
| Resolver implementation | [`src/Banny721TokenUriResolver.sol`](./src/Banny721TokenUriResolver.sol) |
|
|
14
|
+
| 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/) |
|
|
92
17
|
|
|
93
|
-
##
|
|
18
|
+
## Repo Map
|
|
94
19
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
20
|
+
| Area | Where to look |
|
|
21
|
+
|---|---|
|
|
22
|
+
| Main contract | [`src/Banny721TokenUriResolver.sol`](./src/Banny721TokenUriResolver.sol) |
|
|
23
|
+
| Scripts | [`script/`](./script/) |
|
|
24
|
+
| Tests | [`test/`](./test/) |
|
|
100
25
|
|
|
101
|
-
##
|
|
102
|
-
|
|
103
|
-
### Asset Categories
|
|
104
|
-
|
|
105
|
-
| ID | Name | Slot Rules |
|
|
106
|
-
|----|------|------------|
|
|
107
|
-
| 0 | Body | Base character. Owns outfits and backgrounds. |
|
|
108
|
-
| 1 | Background | One per body. |
|
|
109
|
-
| 2 | Backside | Behind body layer. |
|
|
110
|
-
| 3 | Necklace | Default provided if none attached. |
|
|
111
|
-
| 4 | Head | Blocks eyes, glasses, mouth, headtop. |
|
|
112
|
-
| 5 | Eyes | Defaults: alien eyes (UPC 1) or standard eyes (UPC 2-4). |
|
|
113
|
-
| 6 | Glasses | Blocked by head. |
|
|
114
|
-
| 7 | Mouth | Default provided. Blocked by head. |
|
|
115
|
-
| 8 | Legs | Lower body clothing. |
|
|
116
|
-
| 9 | Suit | Full one-piece. Blocks suit top and suit bottom. |
|
|
117
|
-
| 10 | Suit Bottom | Blocked by full suit. |
|
|
118
|
-
| 11 | Suit Top | Blocked by full suit. |
|
|
119
|
-
| 12 | Headtop | Blocked by head. |
|
|
120
|
-
| 13 | Hand | Held item. |
|
|
121
|
-
| 14-17 | Special | Special suit, legs, head, body overlays. |
|
|
122
|
-
|
|
123
|
-
### Body Types (by UPC)
|
|
124
|
-
|
|
125
|
-
| UPC | Type | Default Eyes |
|
|
126
|
-
|-----|------|-------------|
|
|
127
|
-
| 1 | Alien | `DEFAULT_ALIEN_EYES` (purple) |
|
|
128
|
-
| 2 | Pink | `DEFAULT_STANDARD_EYES` |
|
|
129
|
-
| 3 | Orange | `DEFAULT_STANDARD_EYES` |
|
|
130
|
-
| 4 | Original | `DEFAULT_STANDARD_EYES` |
|
|
131
|
-
|
|
132
|
-
## Events
|
|
133
|
-
|
|
134
|
-
| Event | When |
|
|
135
|
-
|-------|------|
|
|
136
|
-
| `DecorateBanny(hook, bannyBodyId, backgroundId, outfitIds, caller)` | Body decorated with new outfits/background |
|
|
137
|
-
| `SetMetadata(description, externalUrl, baseUri, caller)` | Metadata updated |
|
|
138
|
-
| `SetProductName(upc, name, caller)` | Product name set |
|
|
139
|
-
| `SetSvgContent(upc, svgContent, caller)` | SVG content stored on-chain |
|
|
140
|
-
| `SetSvgHash(upc, svgHash, caller)` | SVG hash registered |
|
|
141
|
-
|
|
142
|
-
## Errors
|
|
143
|
-
|
|
144
|
-
| Error | When |
|
|
145
|
-
|-------|------|
|
|
146
|
-
| `Banny721TokenUriResolver_ArrayLengthMismatch` | Batch setter called with mismatched array lengths |
|
|
147
|
-
| `Banny721TokenUriResolver_BannyBodyNotBodyCategory` | Passing a non-body-category token as bannyBodyId to decorateBannyWith |
|
|
148
|
-
| `Banny721TokenUriResolver_CantAccelerateTheLock` | Trying to lock a body that's already locked for longer |
|
|
149
|
-
| `Banny721TokenUriResolver_ContentsAlreadyStored` | SVG content already exists for this UPC |
|
|
150
|
-
| `Banny721TokenUriResolver_ContentsMismatch` | Uploaded content doesn't match registered hash |
|
|
151
|
-
| `Banny721TokenUriResolver_HashAlreadyStored` | Hash already registered for this UPC |
|
|
152
|
-
| `Banny721TokenUriResolver_HashNotFound` | No hash registered for UPC (must register before uploading content) |
|
|
153
|
-
| `Banny721TokenUriResolver_HeadAlreadyAdded` | Trying to add eyes/glasses/mouth/headtop when head is already attached |
|
|
154
|
-
| `Banny721TokenUriResolver_OutfitChangesLocked` | Body is locked (7-day lock active) |
|
|
155
|
-
| `Banny721TokenUriResolver_SuitAlreadyAdded` | Trying to add suit top/bottom when full suit is already attached |
|
|
156
|
-
| `Banny721TokenUriResolver_UnauthorizedBannyBody` | Caller doesn't own the body |
|
|
157
|
-
| `Banny721TokenUriResolver_UnauthorizedOutfit` | Caller doesn't own the outfit or the body wearing it |
|
|
158
|
-
| `Banny721TokenUriResolver_UnauthorizedBackground` | Caller doesn't own the background or the body using it |
|
|
159
|
-
| `Banny721TokenUriResolver_UnorderedCategories` | Outfit IDs not in ascending category order |
|
|
160
|
-
| `Banny721TokenUriResolver_UnrecognizedCategory` | Category ID not in valid range (0-17) |
|
|
161
|
-
| `Banny721TokenUriResolver_UnrecognizedBackground` | Token is not a background category |
|
|
162
|
-
| `Banny721TokenUriResolver_UnrecognizedProduct` | Token's UPC doesn't map to a known product |
|
|
163
|
-
| `Banny721TokenUriResolver_UnauthorizedTransfer` | `onERC721Received` called by non-self operator |
|
|
164
|
-
|
|
165
|
-
## Constants
|
|
166
|
-
|
|
167
|
-
| Constant | Value | Purpose |
|
|
168
|
-
|----------|-------|---------|
|
|
169
|
-
| `_LOCK_DURATION` | 7 days | Fixed outfit lock period |
|
|
170
|
-
| `_ONE_BILLION` | 1,000,000,000 | Token ID encoding: `tokenId = upc * 1B + sequenceNumber` |
|
|
171
|
-
| `_BODY_CATEGORY` | 0 | Category ID for base Banny body |
|
|
172
|
-
| `_BACKGROUND_CATEGORY` | 1 | Category ID for backgrounds |
|
|
173
|
-
| Categories 2-17 | 2-17 | Backside, necklace, head, eyes, glasses, mouth, legs, suit, suit bottom, suit top, headtop, hand, specials |
|
|
174
|
-
|
|
175
|
-
## Storage
|
|
176
|
-
|
|
177
|
-
| Mapping | Type | Purpose |
|
|
178
|
-
|---------|------|---------|
|
|
179
|
-
| `_attachedOutfitIdsOf` | `hook => bannyBodyId => uint256[]` | Outfit token IDs attached to a body |
|
|
180
|
-
| `_attachedBackgroundIdOf` | `hook => bannyBodyId => uint256` | Background token ID attached to a body |
|
|
181
|
-
| `_svgContentOf` | `upc => string` | On-chain SVG content (immutable once set) |
|
|
182
|
-
| `svgHashOf` | `upc => bytes32` | Expected SVG content hash (immutable once set) |
|
|
183
|
-
| `_customProductNameOf` | `upc => string` | Human-readable product name |
|
|
184
|
-
| `outfitLockedUntil` | `hook => bannyBodyId => uint256` | Timestamp until outfit changes locked (declared as `upc` but keyed by token ID) |
|
|
185
|
-
| `_userOf` | `hook => backgroundId => uint256` | Which body uses a background |
|
|
186
|
-
| `_wearerOf` | `hook => outfitId => uint256` | Which body wears an outfit |
|
|
187
|
-
| `svgBaseUri` | `string` | IPFS/HTTP base URI for fallback SVG loading |
|
|
188
|
-
| `svgDescription` | `string` | Token metadata description |
|
|
189
|
-
| `svgExternalUrl` | `string` | Token metadata external URL |
|
|
190
|
-
|
|
191
|
-
## Gotchas
|
|
192
|
-
|
|
193
|
-
1. **7-day lock is fixed and non-cancellable.** `lockOutfitChangesFor` always sets `outfitLockedUntil = block.timestamp + 7 days`. Cannot be shortened, cancelled, or accelerated. Only extended.
|
|
194
|
-
2. **SVG content is immutable.** Once `setSvgContentsOf` stores content for a UPC, it cannot be changed. A mistake requires deploying a new resolver.
|
|
195
|
-
3. **SVG hashes are also immutable.** `setSvgHashesOf` reverts if a hash already exists for a UPC. Register carefully.
|
|
196
|
-
4. **Hash registration is owner-only, but content upload is permissionless.** Anyone can call `setSvgContentsOf` as long as the content matches the registered hash. This enables community-driven lazy uploads.
|
|
197
|
-
5. **Strict ascending category order.** `decorateBannyWith` requires outfits passed in ascending category order. Reverts with `UnorderedCategories` if violated.
|
|
198
|
-
6. **Slot conflicts block combinations.** Head (4) blocks eyes (5), glasses (6), mouth (7), and headtop (12). Full suit (9) blocks suit top (11) and suit bottom (10). These are enforced at decoration time.
|
|
199
|
-
7. **Default injection.** If no explicit necklace, eyes, or mouth outfit is attached, the resolver auto-injects defaults during SVG rendering. Alien bodies get `DEFAULT_ALIEN_EYES`; others get `DEFAULT_STANDARD_EYES`.
|
|
200
|
-
8. **Outfits are held in contract custody.** Attached outfits and backgrounds are transferred to `address(this)` via `safeTransferFrom`. They are returned to the caller when detached (by passing a new outfit set that excludes them). If the return transfer fails (e.g., owner is a non-receiver contract), the asset is **retained** in the attached list rather than stranded — the owner can retry once the transfer issue is resolved.
|
|
201
|
-
9. **Complex outfit ownership rules.** If an outfit is unworn: only its owner can attach it. If already worn by another body: the caller must own THAT body to reassign the outfit. This allows body owners to swap outfits between their own bodies.
|
|
202
|
-
10. **Token ID encoding.** `tokenId = upc * 1_000_000_000 + sequenceNumber`. The resolver extracts UPC via integer division and sequence via modulo to display inventory counts like "42/100".
|
|
203
|
-
11. **`onERC721Received` only accepts self-transfers.** Reverts unless `operator == address(this)`. The contract calls `safeTransferFrom` on itself during decoration, triggering this callback.
|
|
204
|
-
12. **Via-IR required.** `foundry.toml` must have `via_ir = true` due to stack-too-deep in the SVG composition logic.
|
|
205
|
-
13. **SVG fallback chain.** If on-chain content exists: use it. Else if category <= 17: fall back to `svgBaseUri + IPFS URI`. Else: use the hook's `baseURI() + IPFS URI`.
|
|
206
|
-
14. **Mannequin rendering.** Outfit and background tokens (not bodies) are rendered on a grayscale mannequin Banny for preview purposes. The mannequin has `fill:#808080` styling.
|
|
207
|
-
15. **ERC2771 meta-transaction support.** Constructor takes a `trustedForwarder` address. All `_msgSender()` calls use `ERC2771Context`, allowing relayers to submit decoration transactions on behalf of users.
|
|
208
|
-
16. **Empty metadata fields clear the value.** `setMetadata` always writes all three fields. Passing an empty string clears that field. To keep a field unchanged, pass its current value.
|
|
209
|
-
17. **Outfit locks persist through transfers.** The `outfitLockedUntil` mapping is keyed by `(hook, bannyBodyId)` and is never cleared on transfer. When a locked body NFT is transferred, the new owner inherits the lock and cannot change outfits until it expires. Equipped outfits also travel with the body -- the new owner can unequip them after the lock expires. Sellers should unequip valuable outfits before transferring a body.
|
|
210
|
-
|
|
211
|
-
## Example Integration
|
|
212
|
-
|
|
213
|
-
```solidity
|
|
214
|
-
import {IBanny721TokenUriResolver} from "@bannynet/core-v6/src/interfaces/IBanny721TokenUriResolver.sol";
|
|
215
|
-
|
|
216
|
-
// --- Get the composed SVG for a dressed Banny ---
|
|
217
|
-
|
|
218
|
-
string memory svg = resolver.svgOf(
|
|
219
|
-
hookAddress,
|
|
220
|
-
bannyBodyTokenId,
|
|
221
|
-
true, // dress the banny with attached outfits
|
|
222
|
-
true // include the attached background
|
|
223
|
-
);
|
|
224
|
-
|
|
225
|
-
// --- Dress a Banny body with outfits ---
|
|
226
|
-
|
|
227
|
-
uint256[] memory outfitIds = new uint256[](2);
|
|
228
|
-
outfitIds[0] = hatTokenId; // must be category 4 (head)
|
|
229
|
-
outfitIds[1] = shirtTokenId; // must be category 11 (suit top)
|
|
230
|
-
|
|
231
|
-
// Caller must own the body, background, and all outfits on the same hook
|
|
232
|
-
resolver.decorateBannyWith(
|
|
233
|
-
hookAddress,
|
|
234
|
-
bannyBodyTokenId,
|
|
235
|
-
backgroundTokenId,
|
|
236
|
-
outfitIds // MUST be in ascending category order
|
|
237
|
-
);
|
|
26
|
+
## Purpose
|
|
238
27
|
|
|
239
|
-
|
|
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.
|
|
240
29
|
|
|
241
|
-
|
|
30
|
+
## Reference Files
|
|
242
31
|
|
|
243
|
-
|
|
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.
|
|
244
34
|
|
|
245
|
-
|
|
246
|
-
uint256[] memory upcs = new uint256[](1);
|
|
247
|
-
upcs[0] = 42;
|
|
248
|
-
bytes32[] memory hashes = new bytes32[](1);
|
|
249
|
-
hashes[0] = keccak256(bytes(svgContent));
|
|
250
|
-
resolver.setSvgHashesOf(upcs, hashes);
|
|
35
|
+
## Working Rules
|
|
251
36
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
resolver.
|
|
256
|
-
```
|
|
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
|
+
- Treat custody, stale attachment cleanup, and lock timing as high-risk. Rendering bugs are visible, but custody bugs are worse.
|
|
39
|
+
- When a task mentions minting, pricing, or terminal accounting, verify that the problem is not actually in the upstream 721 hook repo.
|
|
40
|
+
- 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.
|