@bannynet/core-v6 0.0.16 → 0.0.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ADMINISTRATION.md +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 +3 -3
- 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 +7 -7
- 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 +8 -5
- package/test/audit/BurnedBodyStrandsAssets.t.sol +8 -5
- package/test/audit/MergedOutfitExclusivity.t.sol +8 -5
- 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/USER_JOURNEYS.md
CHANGED
|
@@ -1,525 +1,80 @@
|
|
|
1
|
-
#
|
|
1
|
+
# User Journeys
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
## Who This Repo Serves
|
|
4
4
|
|
|
5
|
-
|
|
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
|
|
6
8
|
|
|
7
|
-
## Journey 1:
|
|
9
|
+
## Journey 1: Mint A Body, Outfit, And Background Set
|
|
8
10
|
|
|
9
|
-
|
|
11
|
+
**Starting state:** the Banny collection is live through the 721 hook and the relevant tiers exist.
|
|
10
12
|
|
|
11
|
-
**
|
|
13
|
+
**Success:** the collector owns the pieces needed to build a composed avatar.
|
|
12
14
|
|
|
13
|
-
**
|
|
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.
|
|
14
19
|
|
|
15
|
-
|
|
20
|
+
## Journey 2: Dress A Banny And Put Accessories Into Resolver Custody
|
|
16
21
|
|
|
17
|
-
|
|
18
|
-
JB721TiersHook.tokenURI(tokenId)
|
|
19
|
-
-> Banny721TokenUriResolver.tokenUriOf(hook, tokenId)
|
|
20
|
-
```
|
|
22
|
+
**Starting state:** the collector owns a body plus compatible accessories.
|
|
21
23
|
|
|
22
|
-
**
|
|
24
|
+
**Success:** the chosen outfit and background are attached to the body and the resolver renders the combined look.
|
|
23
25
|
|
|
24
|
-
**
|
|
25
|
-
|
|
26
|
-
|
|
26
|
+
**Flow**
|
|
27
|
+
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.
|
|
27
30
|
|
|
28
|
-
|
|
29
|
-
1. `_productOfTokenId(hook, tokenId)` calls `hook.STORE().tierOfTokenId(hook, tokenId, false)`
|
|
30
|
-
2. If `product.category == 0` (body), calls `svgOf(hook, tokenId, true, true)`
|
|
31
|
-
3. `svgOf` calls `assetIdsOf(hook, tokenId)` which returns empty arrays (no outfits or background attached)
|
|
32
|
-
4. The body SVG is composed with default accessories (necklace, eyes, mouth) and no custom outfits
|
|
31
|
+
## Journey 3: Lock A Banny's Appearance For A Period
|
|
33
32
|
|
|
34
|
-
**
|
|
35
|
-
- Product name (Alien, Pink, Orange, or Original)
|
|
36
|
-
- Category name ("Banny body")
|
|
37
|
-
- An SVG image with the naked banny body and default accessories
|
|
38
|
-
- Empty `outfitIds` array, `backgroundId: 0`
|
|
33
|
+
**Starting state:** the collector likes the current look and does not want it changed immediately.
|
|
39
34
|
|
|
40
|
-
**
|
|
35
|
+
**Success:** the avatar's appearance is frozen for the lock window and later equipment changes must wait.
|
|
41
36
|
|
|
42
|
-
**
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
- **Unrecognized product**: Reverts `Banny721TokenUriResolver_UnrecognizedProduct`
|
|
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.
|
|
47
41
|
|
|
48
|
-
|
|
42
|
+
## Journey 4: Publish Or Repair On-Chain Art Assets
|
|
49
43
|
|
|
50
|
-
|
|
44
|
+
**Starting state:** the collection's visual payloads are referenced by content hashes but the actual SVG payloads still need to be made available.
|
|
51
45
|
|
|
52
|
-
**
|
|
46
|
+
**Success:** token URIs render complete art instead of placeholders or missing layers.
|
|
53
47
|
|
|
54
|
-
**
|
|
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.
|
|
55
52
|
|
|
56
|
-
**
|
|
57
|
-
- Caller owns the banny body NFT
|
|
58
|
-
- Body is not locked (`outfitLockedUntil[hook][bannyBodyId] <= block.timestamp`)
|
|
59
|
-
- Caller owns each outfit NFT directly, or owns the banny body currently wearing that outfit
|
|
60
|
-
- Caller owns the background NFT directly (or owns the banny body currently using it), if `backgroundId != 0`
|
|
61
|
-
- Caller has approved the resolver for the hook contract (`hook.setApprovalForAll(resolver, true)`)
|
|
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.
|
|
62
54
|
|
|
63
|
-
|
|
64
|
-
- `hook` -- The `JB721TiersHook` contract address
|
|
65
|
-
- `bannyBodyId` -- Token ID of the banny body being dressed. Must be category 0
|
|
66
|
-
- `backgroundId` -- Token ID of the background to attach. Pass `0` for no background
|
|
67
|
-
- `outfitIds` -- Array of outfit token IDs. Must be in ascending category order (categories 2--17). Pass `[]` for no outfits
|
|
55
|
+
## Journey 5: Update Collection Metadata And Product Catalog Entries
|
|
68
56
|
|
|
69
|
-
**
|
|
70
|
-
1. `_checkIfSenderIsOwner(hook, bannyBodyId)` -- verifies caller owns the body
|
|
71
|
-
2. `_productOfTokenId(hook, bannyBodyId).category` must equal `_BODY_CATEGORY` (0)
|
|
72
|
-
3. `outfitLockedUntil[hook][bannyBodyId]` must be `<= block.timestamp`
|
|
73
|
-
4. Background processing (`_decorateBannyWithBackground`) -- only executes if the background is changing (new `backgroundId` differs from the current one, or the current background is no longer assigned to this body):
|
|
74
|
-
- If a previous background was assigned to this body: old background NFT return attempted via `_tryTransferFrom`. If the return fails, the background change is **aborted** — old background stays attached, new background is not equipped
|
|
75
|
-
- On successful return: `_attachedBackgroundIdOf[hook][bannyBodyId]` updated to `backgroundId` (or cleared to 0)
|
|
76
|
-
- If `backgroundId != 0`: `_userOf[hook][backgroundId]` set to `bannyBodyId`
|
|
77
|
-
- If `backgroundId != 0` and the resolver does not already hold the new background: new background NFT transferred into the resolver via `_transferFrom` (reverts on failure)
|
|
78
|
-
- If `backgroundId == 0` and the return transfer fails: `_attachedBackgroundIdOf` is **not cleared** (background stays attached for recovery)
|
|
79
|
-
5. Outfit processing (`_decorateBannyWithOutfits`):
|
|
80
|
-
- For each new outfit not already worn by this body: `_wearerOf[hook][outfitId]` set to `bannyBodyId`
|
|
81
|
-
- If there are previous outfits in categories up to the current one: old outfits transfer attempted via `_tryTransferFrom`. If a transfer fails, the outfit is **retained** (not zeroed in the previous outfit tracking array)
|
|
82
|
-
- For each new outfit not already held by the resolver: transferred into the resolver via `_transferFrom`
|
|
83
|
-
- `_storeOutfitsWithRetained` merges the new `outfitIds` with any retained outfits whose transfers failed, then stores the combined array in `_attachedOutfitIdsOf[hook][bannyBodyId]`
|
|
57
|
+
**Starting state:** the collection exists, but its descriptive metadata or UPC-to-name catalog needs to change.
|
|
84
58
|
|
|
85
|
-
**
|
|
59
|
+
**Success:** token URIs and collection-level presentation reflect the intended description, external URL, base URI, and product naming.
|
|
86
60
|
|
|
87
|
-
**
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
-
|
|
91
|
-
- **Burned outfit in previous set**: `_tryTransferFrom` returns `false`. The burned outfit is **retained** in the attachment array (phantom entry) rather than removed. This prevents stranding of other assets and keeps the record for potential future recovery
|
|
92
|
-
- **Removed tier in previous set**: `_productOfTokenId` returns category 0. The while loop processes it (category 0 <= any new category) and transfers it out
|
|
93
|
-
- **Categories not ascending**: Reverts `Banny721TokenUriResolver_UnorderedCategories`
|
|
94
|
-
- **Duplicate categories**: Reverts `Banny721TokenUriResolver_UnorderedCategories` (equality fails the `<` check)
|
|
95
|
-
- **Category outside 2--17 as outfit**: Reverts `Banny721TokenUriResolver_UnrecognizedCategory`
|
|
96
|
-
- **Head category conflict**: Reverts `Banny721TokenUriResolver_HeadAlreadyAdded`
|
|
97
|
-
- **Suit category conflict**: Reverts `Banny721TokenUriResolver_SuitAlreadyAdded`
|
|
98
|
-
- **Unauthorized background**: Reverts `Banny721TokenUriResolver_UnauthorizedBackground`
|
|
99
|
-
- **Unauthorized outfit**: Reverts `Banny721TokenUriResolver_UnauthorizedOutfit`
|
|
100
|
-
- **Unrecognized background category**: Reverts `Banny721TokenUriResolver_UnrecognizedBackground`
|
|
101
|
-
- **Source body is locked**: Reverts `Banny721TokenUriResolver_OutfitChangesLocked` (via `_revertIfBodyLocked`)
|
|
102
|
-
- **Reentrancy via safeTransferFrom callback**: Blocked by `nonReentrant` modifier
|
|
61
|
+
**Flow**
|
|
62
|
+
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.
|
|
103
65
|
|
|
104
|
-
|
|
66
|
+
## Journey 6: Unequip And Recover Custodied Accessories
|
|
105
67
|
|
|
106
|
-
|
|
107
|
-
Precondition: Alice owns body #4_000_000_001 (Original, category 0)
|
|
108
|
-
Alice owns hat #5_000_000_001 (category 4, Head)
|
|
109
|
-
Alice owns glasses #6_000_000_001 (category 6, Glasses)
|
|
110
|
-
Alice has called hook.setApprovalForAll(resolver, true)
|
|
68
|
+
**Starting state:** a body has attached pieces held by the resolver and the owner wants to rearrange or transfer them.
|
|
111
69
|
|
|
112
|
-
|
|
70
|
+
**Success:** the accessories leave resolver custody and can be reused or transferred independently.
|
|
113
71
|
|
|
114
|
-
|
|
115
|
-
|
|
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.
|
|
116
76
|
|
|
117
|
-
|
|
118
|
-
Corrected: decorateBannyWith(hook, 4_000_000_001, 0, [5_000_000_001])
|
|
77
|
+
## Hand-Offs
|
|
119
78
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
- _wearerOf[hook][5_000_000_001] = 4_000_000_001
|
|
123
|
-
- _attachedOutfitIdsOf[hook][4_000_000_001] = [5_000_000_001]
|
|
124
|
-
- Body's tokenURI now renders with hat layer.
|
|
125
|
-
```
|
|
126
|
-
|
|
127
|
-
---
|
|
128
|
-
|
|
129
|
-
## Journey 3: Undress a Banny
|
|
130
|
-
|
|
131
|
-
Undressing is not a separate function. It is performed by calling `decorateBannyWith` with empty arrays.
|
|
132
|
-
|
|
133
|
-
**Entry point**: `Banny721TokenUriResolver.decorateBannyWith(address hook, uint256 bannyBodyId, 0, [])`
|
|
134
|
-
|
|
135
|
-
**Who can call**: The owner of the banny body NFT.
|
|
136
|
-
|
|
137
|
-
**Parameters**:
|
|
138
|
-
- `hook` -- The hook address
|
|
139
|
-
- `bannyBodyId` -- The banny body to undress
|
|
140
|
-
- `backgroundId` -- `0` (remove background)
|
|
141
|
-
- `outfitIds` -- `[]` (remove all outfits)
|
|
142
|
-
|
|
143
|
-
**State changes**:
|
|
144
|
-
1. All checks pass (ownership, not locked, body category)
|
|
145
|
-
2. If a background was attached: old background NFT return attempted via `_tryTransferFrom`. If successful, `_attachedBackgroundIdOf[hook][bannyBodyId]` set to 0. If the transfer fails (e.g., owner is a contract that rejects ERC-721), the background **stays attached** for recovery
|
|
146
|
-
3. The new `outfitIds` array is empty, so the merge loop does not execute. If there are previous outfits: the tail loop attempts to transfer out all previous outfits via `_tryTransferFrom`. Failed transfers are **retained** in the attached list
|
|
147
|
-
4. `_storeOutfitsWithRetained` stores `[]` plus any retained outfits in `_attachedOutfitIdsOf[hook][bannyBodyId]`
|
|
148
|
-
5. Successfully transferred outfits and backgrounds are returned to `_msgSender()`. Failed transfers remain in resolver custody but stay tracked
|
|
149
|
-
|
|
150
|
-
**Events**: `DecorateBanny(hook, bannyBodyId, 0, [], caller)`
|
|
151
|
-
|
|
152
|
-
**Partial undress**: To remove some outfits but keep others, pass only the outfits you want to keep:
|
|
153
|
-
|
|
154
|
-
```
|
|
155
|
-
Before: outfitIds = [hat, glasses, necklace]
|
|
156
|
-
Call: decorateBannyWith(hook, bodyId, backgroundId, [necklace])
|
|
157
|
-
After: hat and glasses returned to caller, necklace stays equipped.
|
|
158
|
-
```
|
|
159
|
-
|
|
160
|
-
**Edge cases**:
|
|
161
|
-
- **Body is locked**: Reverts `Banny721TokenUriResolver_OutfitChangesLocked`. Cannot undress during lock period
|
|
162
|
-
- **Some equipped outfits were burned**: `_tryTransferFrom` returns `false` for burned tokens. No revert — the burned outfits are **retained** in the attached list as phantom entries. No NFT is returned for those tokens
|
|
163
|
-
- **Caller is not the current owner**: Reverts `Banny721TokenUriResolver_UnauthorizedBannyBody`. This can happen if the body was recently transferred
|
|
164
|
-
|
|
165
|
-
---
|
|
166
|
-
|
|
167
|
-
## Journey 4: Lock a Banny
|
|
168
|
-
|
|
169
|
-
**Entry point**: `Banny721TokenUriResolver.lockOutfitChangesFor(address hook, uint256 bannyBodyId)`
|
|
170
|
-
|
|
171
|
-
**Who can call**: The owner of the banny body NFT (`IERC721(hook).ownerOf(bannyBodyId) == _msgSender()`).
|
|
172
|
-
|
|
173
|
-
**Parameters**:
|
|
174
|
-
- `hook` -- The hook address
|
|
175
|
-
- `bannyBodyId` -- The banny body to lock
|
|
176
|
-
|
|
177
|
-
**State changes**:
|
|
178
|
-
1. `_checkIfSenderIsOwner(hook, bannyBodyId)` -- verifies caller owns the body
|
|
179
|
-
2. `newLockUntil = block.timestamp + 7 days` (constant `_LOCK_DURATION`)
|
|
180
|
-
3. If `currentLockedUntil > newLockUntil`, reverts
|
|
181
|
-
4. `outfitLockedUntil[hook][bannyBodyId] = newLockUntil`
|
|
182
|
-
|
|
183
|
-
**Events**: None. The `lockOutfitChangesFor` function does not emit an event.
|
|
184
|
-
|
|
185
|
-
**Edge cases**:
|
|
186
|
-
- **Relocking while already locked**: Succeeds if the new expiry is >= the current expiry. Since `newLockUntil = block.timestamp + 7 days`, this effectively extends the lock by whatever time remains
|
|
187
|
-
- **Attempting to shorten the lock**: Reverts `Banny721TokenUriResolver_CantAccelerateTheLock`
|
|
188
|
-
- **Locking an undressed body**: Valid. The body is locked with no outfits. No outfits can be added during the lock period
|
|
189
|
-
- **Lock after transfer**: The new owner can lock. The old owner cannot (they no longer pass `_checkIfSenderIsOwner`)
|
|
190
|
-
- **Lock does not prevent body transfer**: The lock only affects `decorateBannyWith`. The body NFT can still be transferred on the hook contract
|
|
191
|
-
- **No admin override**: The contract owner cannot unlock a body. The lock must expire naturally
|
|
192
|
-
|
|
193
|
-
### Use Case: Marketplace Sale
|
|
194
|
-
|
|
195
|
-
```
|
|
196
|
-
1. Alice owns body #1 dressed with rare hat and suit.
|
|
197
|
-
2. Alice calls lockOutfitChangesFor(hook, bodyId).
|
|
198
|
-
3. Alice lists body #1 on marketplace.
|
|
199
|
-
4. Bob buys body #1. Body transfers to Bob.
|
|
200
|
-
5. Lock persists -- Bob receives the body with guaranteed outfits.
|
|
201
|
-
6. After 7 days, Bob can undress and receive the hat and suit NFTs.
|
|
202
|
-
```
|
|
203
|
-
|
|
204
|
-
---
|
|
205
|
-
|
|
206
|
-
## Journey 5: Transfer a Decorated Banny
|
|
207
|
-
|
|
208
|
-
The resolver does not intercept or control body transfers. Body transfers happen on the `JB721TiersHook` contract. The resolver observes the ownership change lazily.
|
|
209
|
-
|
|
210
|
-
**Entry point**: `JB721TiersHook.safeTransferFrom(address from, address to, uint256 tokenId)` (standard ERC-721 transfer on the hook contract, not the resolver)
|
|
211
|
-
|
|
212
|
-
**Who can call**: The body owner, or an approved operator on the hook contract.
|
|
213
|
-
|
|
214
|
-
**State changes**:
|
|
215
|
-
1. No resolver functions are called during the transfer
|
|
216
|
-
2. Resolver state remains unchanged:
|
|
217
|
-
- `_attachedOutfitIdsOf[hook][bodyId]` still contains the outfit array
|
|
218
|
-
- `_attachedBackgroundIdOf[hook][bodyId]` still contains the background ID
|
|
219
|
-
- `_wearerOf[hook][outfitId]` still maps each outfit to `bodyId`
|
|
220
|
-
- `_userOf[hook][backgroundId]` still maps to `bodyId`
|
|
221
|
-
3. All equipped outfit and background NFTs remain held by the resolver
|
|
222
|
-
|
|
223
|
-
**Events**: None from the resolver. The hook emits the standard ERC-721 `Transfer(from, to, tokenId)`.
|
|
224
|
-
|
|
225
|
-
**What the new owner (Bob) can do**:
|
|
226
|
-
- **Undress**: `decorateBannyWith(hook, bodyId, 0, [])` returns all equipped NFTs to Bob
|
|
227
|
-
- **Redress**: `decorateBannyWith(hook, bodyId, newBg, [newOutfits])` replaces outfits. Old outfits returned to Bob (even though Alice originally equipped them)
|
|
228
|
-
- **Lock**: `lockOutfitChangesFor(hook, bodyId)` locks the body under Bob's control
|
|
229
|
-
|
|
230
|
-
**What Alice can no longer do**: Alice cannot call `decorateBannyWith` or `lockOutfitChangesFor` for that body -- she no longer owns it.
|
|
231
|
-
|
|
232
|
-
**Edge cases**:
|
|
233
|
-
- **Alice equipped valuable outfits and forgot to undress before selling**: Bob receives full control of all equipped NFTs. This is the intended behavior but creates a seller gotcha. The lock mechanism exists to make this explicit (sellers lock, then sell at a higher price including outfits)
|
|
234
|
-
- **Body transferred while locked**: Lock persists. Bob cannot change outfits until the lock expires
|
|
235
|
-
- **Body transferred to a contract**: The new contract owner must be able to call `decorateBannyWith`. If the contract does not implement this call, equipped outfits are effectively locked forever (until the contract is upgraded or has a function to make this call)
|
|
236
|
-
- **Double transfer (Alice -> Bob -> Charlie)**: Only Charlie can interact with the body's outfits. Each transfer implicitly transfers outfit control
|
|
237
|
-
|
|
238
|
-
---
|
|
239
|
-
|
|
240
|
-
## Journey 6: Move an Outfit Between Bodies
|
|
241
|
-
|
|
242
|
-
A user who owns multiple bodies can move an equipped outfit from one body to another in a single call.
|
|
243
|
-
|
|
244
|
-
**Entry point**: `Banny721TokenUriResolver.decorateBannyWith(address hook, uint256 newBodyId, 0, [outfitId])`
|
|
245
|
-
|
|
246
|
-
Where `outfitId` is currently equipped on `oldBodyId`, and the caller owns both bodies.
|
|
247
|
-
|
|
248
|
-
**Who can call**: An address that owns both `newBodyId` and the body currently wearing the outfit (`oldBodyId`).
|
|
249
|
-
|
|
250
|
-
**Prerequisites**:
|
|
251
|
-
- Caller owns `newBodyId`
|
|
252
|
-
- Caller owns `oldBodyId` (which currently wears the outfit)
|
|
253
|
-
- `newBodyId` is not locked
|
|
254
|
-
- `oldBodyId` is not locked (enforced by `_revertIfBodyLocked`)
|
|
255
|
-
|
|
256
|
-
**State changes**:
|
|
257
|
-
1. Authorization passes: caller does not own the outfit directly (resolver holds it), but caller owns `oldBodyId` which is the `wearerOf(hook, outfitId)`
|
|
258
|
-
2. `_wearerOf[hook][outfitId]` updated to `newBodyId`
|
|
259
|
-
3. The outfit is not transferred (resolver already holds it)
|
|
260
|
-
4. `_attachedOutfitIdsOf[hook][newBodyId]` set to the new array including this outfit
|
|
261
|
-
5. `_attachedOutfitIdsOf[hook][oldBodyId]` is NOT explicitly updated. However, `wearerOf(hook, outfitId)` now returns `newBodyId`, so `assetIdsOf(hook, oldBodyId)` will exclude this outfit from its filtered result
|
|
262
|
-
|
|
263
|
-
**Events**: `DecorateBanny(hook, newBodyId, 0, [outfitId], caller)`
|
|
264
|
-
|
|
265
|
-
**Edge cases**:
|
|
266
|
-
- **Old body is locked**: Reverts `Banny721TokenUriResolver_OutfitChangesLocked` via `_revertIfBodyLocked`. A locked source body keeps its outfits and background until the lock expires, even if the caller owns both bodies
|
|
267
|
-
|
|
268
|
-
---
|
|
269
|
-
|
|
270
|
-
## Journey 7: Admin -- Store SVG Content
|
|
271
|
-
|
|
272
|
-
### Step 1: Commit Hashes
|
|
273
|
-
|
|
274
|
-
**Entry point**: `Banny721TokenUriResolver.setSvgHashesOf(uint256[] memory upcs, bytes32[] memory svgHashes)`
|
|
275
|
-
|
|
276
|
-
**Who can call**: Contract owner only (`onlyOwner` modifier, from OpenZeppelin `Ownable`).
|
|
277
|
-
|
|
278
|
-
**Parameters**:
|
|
279
|
-
- `upcs` -- Array of universal product codes to set hashes for
|
|
280
|
-
- `svgHashes` -- Array of `keccak256` hashes of the SVG content strings
|
|
281
|
-
|
|
282
|
-
**State changes**:
|
|
283
|
-
1. For each pair: `svgHashOf[upc] = svgHash`
|
|
284
|
-
|
|
285
|
-
**Events**: `SetSvgHash(uint256 indexed upc, bytes32 indexed svgHash, address caller)` -- emitted once per UPC in the array
|
|
286
|
-
|
|
287
|
-
**Edge cases**:
|
|
288
|
-
- **UPC already has a hash**: Reverts `Banny721TokenUriResolver_HashAlreadyStored` (write-once)
|
|
289
|
-
- **Array length mismatch**: Reverts `Banny721TokenUriResolver_ArrayLengthMismatch`
|
|
290
|
-
- **Partial failure**: The entire call reverts if any single UPC fails
|
|
291
|
-
|
|
292
|
-
### Step 2: Upload Content
|
|
293
|
-
|
|
294
|
-
**Entry point**: `Banny721TokenUriResolver.setSvgContentsOf(uint256[] memory upcs, string[] calldata svgContents)`
|
|
295
|
-
|
|
296
|
-
**Who can call**: Anyone. Not restricted to owner -- anyone can upload content as long as it matches the committed hash.
|
|
297
|
-
|
|
298
|
-
**Parameters**:
|
|
299
|
-
- `upcs` -- Array of universal product codes to upload content for
|
|
300
|
-
- `svgContents` -- Array of SVG content strings (without wrapping `<svg>` tags)
|
|
301
|
-
|
|
302
|
-
**State changes**:
|
|
303
|
-
1. For each pair: `_svgContentOf[upc] = svgContent`
|
|
304
|
-
|
|
305
|
-
**Events**: `SetSvgContent(uint256 indexed upc, string svgContent, address caller)` -- emitted once per UPC in the array
|
|
306
|
-
|
|
307
|
-
**Edge cases**:
|
|
308
|
-
- **Content already stored**: Reverts `Banny721TokenUriResolver_ContentsAlreadyStored` (write-once)
|
|
309
|
-
- **No hash set for UPC**: Reverts `Banny721TokenUriResolver_HashNotFound`
|
|
310
|
-
- **Content does not match hash**: Reverts `Banny721TokenUriResolver_ContentsMismatch` (checks `keccak256(abi.encodePacked(svgContent)) != svgHashOf[upc]`)
|
|
311
|
-
- **Array length mismatch**: Reverts `Banny721TokenUriResolver_ArrayLengthMismatch`
|
|
312
|
-
- **Hash set but content never uploaded**: `_svgOf` falls back to IPFS resolution. Tokens render with IPFS images instead of on-chain SVG
|
|
313
|
-
- **Content with special characters**: Stored verbatim. No sanitization. A `<script>` tag in SVG content would be included in the data URI
|
|
314
|
-
|
|
315
|
-
---
|
|
316
|
-
|
|
317
|
-
## Journey 8: Admin -- Set Metadata and Product Names
|
|
318
|
-
|
|
319
|
-
### Set Metadata
|
|
320
|
-
|
|
321
|
-
**Entry point**: `Banny721TokenUriResolver.setMetadata(string calldata description, string calldata url, string calldata baseUri)`
|
|
322
|
-
|
|
323
|
-
**Who can call**: Contract owner only (`onlyOwner` modifier).
|
|
324
|
-
|
|
325
|
-
**Parameters**:
|
|
326
|
-
- `description` -- The description to use in token metadata
|
|
327
|
-
- `url` -- The external URL to use in token metadata
|
|
328
|
-
- `baseUri` -- The base URI of the SVG files (used for IPFS fallback)
|
|
329
|
-
|
|
330
|
-
**State changes**:
|
|
331
|
-
1. `svgDescription = description`
|
|
332
|
-
2. `svgExternalUrl = url`
|
|
333
|
-
3. `svgBaseUri = baseUri`
|
|
334
|
-
4. All three fields are always overwritten. Pass current values for fields you do not want to change. Pass `""` to clear
|
|
335
|
-
|
|
336
|
-
**Events**: `SetMetadata(string description, string externalUrl, string baseUri, address caller)`
|
|
337
|
-
|
|
338
|
-
**Edge cases**:
|
|
339
|
-
- **Empty strings**: Valid. Clears the respective metadata field
|
|
340
|
-
- **Non-owner caller**: Reverts with OpenZeppelin `OwnableUnauthorizedAccount`
|
|
341
|
-
|
|
342
|
-
### Set Product Names
|
|
343
|
-
|
|
344
|
-
**Entry point**: `Banny721TokenUriResolver.setProductNames(uint256[] memory upcs, string[] memory names)`
|
|
345
|
-
|
|
346
|
-
**Who can call**: Contract owner only (`onlyOwner` modifier).
|
|
347
|
-
|
|
348
|
-
**Parameters**:
|
|
349
|
-
- `upcs` -- Array of universal product codes to name
|
|
350
|
-
- `names` -- Array of display names for each product
|
|
351
|
-
|
|
352
|
-
**State changes**:
|
|
353
|
-
1. For each pair: `_customProductNameOf[upc] = name`
|
|
354
|
-
|
|
355
|
-
**Events**: `SetProductName(uint256 indexed upc, string name, address caller)` -- emitted once per UPC in the array
|
|
356
|
-
|
|
357
|
-
**Edge cases**:
|
|
358
|
-
- **Overwriting a product name**: No revert. The old name is replaced. This could change how existing NFTs display (names are mutable, unlike SVG hashes/content)
|
|
359
|
-
- **Setting name for UPCs 1--4**: The `_customProductNameOf` mapping is written, but `_productNameOf` returns the hardcoded name first (Alien, Pink, Orange, Original). The custom name is never read for these UPCs
|
|
360
|
-
- **Empty name string**: Valid. Sets the custom name to empty, causing `_productNameOf` to return `""` for that UPC
|
|
361
|
-
- **Array length mismatch**: Reverts `Banny721TokenUriResolver_ArrayLengthMismatch`
|
|
362
|
-
|
|
363
|
-
---
|
|
364
|
-
|
|
365
|
-
## Journey 9: View Functions -- Query Banny State
|
|
366
|
-
|
|
367
|
-
All view functions. No access restrictions, no state changes, no events.
|
|
368
|
-
|
|
369
|
-
### Get Attached Assets
|
|
370
|
-
|
|
371
|
-
**Entry point**: `Banny721TokenUriResolver.assetIdsOf(address hook, uint256 bannyBodyId) public view returns (uint256 backgroundId, uint256[] memory outfitIds)`
|
|
372
|
-
|
|
373
|
-
**Who can call**: Anyone (view function).
|
|
374
|
-
|
|
375
|
-
**Parameters**:
|
|
376
|
-
- `hook` -- The hook address of the collection
|
|
377
|
-
- `bannyBodyId` -- The banny body to query
|
|
378
|
-
|
|
379
|
-
Returns the currently attached background and outfits. Filters by checking `wearerOf` and `userOf` for each stored ID, excluding outfits that have been moved to other bodies.
|
|
380
|
-
|
|
381
|
-
### Get Outfit Wearer
|
|
382
|
-
|
|
383
|
-
**Entry point**: `Banny721TokenUriResolver.wearerOf(address hook, uint256 outfitId) public view returns (uint256)`
|
|
384
|
-
|
|
385
|
-
**Who can call**: Anyone (view function).
|
|
386
|
-
|
|
387
|
-
**Parameters**:
|
|
388
|
-
- `hook` -- The hook address of the collection
|
|
389
|
-
- `outfitId` -- The outfit token ID to query
|
|
390
|
-
|
|
391
|
-
Returns the body ID wearing this outfit, or 0 if unworn. Verifies the outfit is still in the body's `_attachedOutfitIdsOf` array.
|
|
392
|
-
|
|
393
|
-
### Get Background User
|
|
394
|
-
|
|
395
|
-
**Entry point**: `Banny721TokenUriResolver.userOf(address hook, uint256 backgroundId) public view returns (uint256)`
|
|
396
|
-
|
|
397
|
-
**Who can call**: Anyone (view function).
|
|
398
|
-
|
|
399
|
-
**Parameters**:
|
|
400
|
-
- `hook` -- The hook address of the collection
|
|
401
|
-
- `backgroundId` -- The background token ID to query
|
|
402
|
-
|
|
403
|
-
Returns the body ID using this background, or 0 if unused. Verifies the background is still the body's `_attachedBackgroundIdOf` entry.
|
|
404
|
-
|
|
405
|
-
### Get SVG
|
|
406
|
-
|
|
407
|
-
**Entry point**: `Banny721TokenUriResolver.svgOf(address hook, uint256 tokenId, bool shouldDressBannyBody, bool shouldIncludeBackgroundOnBannyBody) public view returns (string memory)`
|
|
408
|
-
|
|
409
|
-
**Who can call**: Anyone (view function).
|
|
410
|
-
|
|
411
|
-
**Parameters**:
|
|
412
|
-
- `hook` -- The hook address of the collection
|
|
413
|
-
- `tokenId` -- The token ID to render
|
|
414
|
-
- `shouldDressBannyBody` -- Whether to include the banny body's attached outfits
|
|
415
|
-
- `shouldIncludeBackgroundOnBannyBody` -- Whether to include the banny body's attached background
|
|
416
|
-
|
|
417
|
-
Returns the composed SVG for any token. For bodies, can toggle dressing and background. For non-bodies, returns the outfit/background SVG alone.
|
|
418
|
-
|
|
419
|
-
### Get Names
|
|
420
|
-
|
|
421
|
-
**Entry point**: `Banny721TokenUriResolver.namesOf(address hook, uint256 tokenId) public view returns (string memory, string memory, string memory)`
|
|
422
|
-
|
|
423
|
-
**Who can call**: Anyone (view function).
|
|
424
|
-
|
|
425
|
-
**Parameters**:
|
|
426
|
-
- `hook` -- The hook address of the collection
|
|
427
|
-
- `tokenId` -- The token ID to look up
|
|
428
|
-
|
|
429
|
-
Returns `(fullName, categoryName, productName)`.
|
|
430
|
-
|
|
431
|
-
### Get Lock Status
|
|
432
|
-
|
|
433
|
-
**Entry point**: `Banny721TokenUriResolver.outfitLockedUntil(address hook, uint256 upc) public view returns (uint256)`
|
|
434
|
-
|
|
435
|
-
**Who can call**: Anyone (public mapping).
|
|
436
|
-
|
|
437
|
-
**Parameters**:
|
|
438
|
-
- `hook` -- The hook address of the collection
|
|
439
|
-
- `upc` -- The banny body token ID
|
|
440
|
-
|
|
441
|
-
Returns the timestamp until which the body is locked, or 0 if never locked.
|
|
442
|
-
|
|
443
|
-
---
|
|
444
|
-
|
|
445
|
-
## Summary: State Machine per Banny Body
|
|
446
|
-
|
|
447
|
-
```
|
|
448
|
-
+-----------+
|
|
449
|
-
| NAKED |
|
|
450
|
-
| (minted) |
|
|
451
|
-
+-----+-----+
|
|
452
|
-
|
|
|
453
|
-
decorateBannyWith(outfits)
|
|
454
|
-
|
|
|
455
|
-
+-----v-----+
|
|
456
|
-
| DRESSED |<----+
|
|
457
|
-
| | |
|
|
458
|
-
+--+--+--+--+ |
|
|
459
|
-
| | | |
|
|
460
|
-
+------------+ | +--------+
|
|
461
|
-
| | decorateBannyWith(newOutfits)
|
|
462
|
-
decorateBannyWith([]) |
|
|
463
|
-
| | lockOutfitChangesFor()
|
|
464
|
-
+-----v-----+ |
|
|
465
|
-
| NAKED | |
|
|
466
|
-
+-----+-----+ |
|
|
467
|
-
| |
|
|
468
|
-
lockOutfitChangesFor() |
|
|
469
|
-
| |
|
|
470
|
-
+-----v---------------v--+
|
|
471
|
-
| LOCKED |
|
|
472
|
-
| (7 days) |
|
|
473
|
-
+-----+-------------------+
|
|
474
|
-
|
|
|
475
|
-
block.timestamp > lockUntil
|
|
476
|
-
|
|
|
477
|
-
+-----v-----------+
|
|
478
|
-
| UNLOCKED |
|
|
479
|
-
| (NAKED or |
|
|
480
|
-
| DRESSED) |
|
|
481
|
-
+-----------------+
|
|
482
|
-
```
|
|
483
|
-
|
|
484
|
-
The lock state applies equally to dressed and naked bodies. A dressed body can be locked, preventing outfit changes. A naked body can be locked, preventing outfit additions.
|
|
485
|
-
|
|
486
|
-
Body transfers do not change the resolver state. The new owner inherits the current state (dressed/naked, locked/unlocked) and all custody rights.
|
|
487
|
-
|
|
488
|
-
---
|
|
489
|
-
|
|
490
|
-
## Events Reference
|
|
491
|
-
|
|
492
|
-
All events defined in `IBanny721TokenUriResolver`:
|
|
493
|
-
|
|
494
|
-
| Event | Emitted by | Signature |
|
|
495
|
-
|-------|-----------|-----------|
|
|
496
|
-
| `DecorateBanny` | `decorateBannyWith` | `DecorateBanny(address indexed hook, uint256 indexed bannyBodyId, uint256 indexed backgroundId, uint256[] outfitIds, address caller)` |
|
|
497
|
-
| `SetMetadata` | `setMetadata` | `SetMetadata(string description, string externalUrl, string baseUri, address caller)` |
|
|
498
|
-
| `SetProductName` | `setProductNames` | `SetProductName(uint256 indexed upc, string name, address caller)` |
|
|
499
|
-
| `SetSvgContent` | `setSvgContentsOf` | `SetSvgContent(uint256 indexed upc, string svgContent, address caller)` |
|
|
500
|
-
| `SetSvgHash` | `setSvgHashesOf` | `SetSvgHash(uint256 indexed upc, bytes32 indexed svgHash, address caller)` |
|
|
501
|
-
|
|
502
|
-
## Custom Errors Reference
|
|
503
|
-
|
|
504
|
-
All custom errors defined in `Banny721TokenUriResolver`:
|
|
505
|
-
|
|
506
|
-
| Error | Trigger |
|
|
507
|
-
|-------|---------|
|
|
508
|
-
| `Banny721TokenUriResolver_ArrayLengthMismatch` | `upcs` and values arrays have different lengths |
|
|
509
|
-
| `Banny721TokenUriResolver_BannyBodyNotBodyCategory` | `bannyBodyId` is not a category 0 (body) token |
|
|
510
|
-
| `Banny721TokenUriResolver_CantAccelerateTheLock` | New lock expiry would be earlier than current lock |
|
|
511
|
-
| `Banny721TokenUriResolver_ContentsAlreadyStored` | SVG content already uploaded for this UPC |
|
|
512
|
-
| `Banny721TokenUriResolver_ContentsMismatch` | SVG content hash does not match committed hash |
|
|
513
|
-
| `Banny721TokenUriResolver_HashAlreadyStored` | SVG hash already committed for this UPC |
|
|
514
|
-
| `Banny721TokenUriResolver_HashNotFound` | No hash committed for this UPC |
|
|
515
|
-
| `Banny721TokenUriResolver_HeadAlreadyAdded` | Outfit conflicts with an already-equipped head item |
|
|
516
|
-
| `Banny721TokenUriResolver_OutfitChangesLocked` | Body is locked and cannot change outfits |
|
|
517
|
-
| `Banny721TokenUriResolver_SuitAlreadyAdded` | Outfit conflicts with an already-equipped suit item |
|
|
518
|
-
| `Banny721TokenUriResolver_UnauthorizedBackground` | Caller does not own the background or the body using it |
|
|
519
|
-
| `Banny721TokenUriResolver_UnauthorizedBannyBody` | Caller does not own the banny body |
|
|
520
|
-
| `Banny721TokenUriResolver_UnauthorizedOutfit` | Caller does not own the outfit or the body wearing it |
|
|
521
|
-
| `Banny721TokenUriResolver_UnauthorizedTransfer` | NFT received from an external sender (not this contract) |
|
|
522
|
-
| `Banny721TokenUriResolver_UnorderedCategories` | Outfit categories are not in ascending order |
|
|
523
|
-
| `Banny721TokenUriResolver_UnrecognizedBackground` | Token is not a valid background category |
|
|
524
|
-
| `Banny721TokenUriResolver_UnrecognizedCategory` | Outfit category is not in the valid range (2--17) |
|
|
525
|
-
| `Banny721TokenUriResolver_UnrecognizedProduct` | Token does not belong to a recognized product tier |
|
|
79
|
+
- 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.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bannynet/core-v6",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.18",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -20,12 +20,12 @@
|
|
|
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.
|
|
23
|
+
"@bananapus/721-hook-v6": "^0.0.30",
|
|
24
24
|
"@bananapus/core-v6": "^0.0.28",
|
|
25
25
|
"@bananapus/permission-ids-v6": "^0.0.14",
|
|
26
26
|
"@bananapus/router-terminal-v6": "^0.0.21",
|
|
27
27
|
"@bananapus/suckers-v6": "^0.0.18",
|
|
28
|
-
"@croptop/core-v6": "^0.0.
|
|
28
|
+
"@croptop/core-v6": "^0.0.28",
|
|
29
29
|
"@openzeppelin/contracts": "^5.6.1",
|
|
30
30
|
"@rev-net/core-v6": "^0.0.24",
|
|
31
31
|
"keccak": "^3.0.4"
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Banny Operations
|
|
2
|
+
|
|
3
|
+
## Content and Deployment Surface
|
|
4
|
+
|
|
5
|
+
- [`script/Deploy.s.sol`](../script/Deploy.s.sol) is the main deployment entry point.
|
|
6
|
+
- [`script/Drop1.s.sol`](../script/Drop1.s.sol) and [`script/Add.Denver.s.sol`](../script/Add.Denver.s.sol) are the first places to check when a problem is drop-specific rather than core resolver logic.
|
|
7
|
+
- The resolver's metadata and SVG-content management functions live in [`src/Banny721TokenUriResolver.sol`](../src/Banny721TokenUriResolver.sol), not in separate admin helpers.
|
|
8
|
+
|
|
9
|
+
## Change Checklist
|
|
10
|
+
|
|
11
|
+
- If you edit decoration behavior, verify both attachment and return-to-owner paths.
|
|
12
|
+
- If you edit SVG rendering, re-check default layer injection and non-body preview rendering.
|
|
13
|
+
- If you edit content upload assumptions, verify hash registration and one-time content storage still match.
|
|
14
|
+
- If you edit metadata fields, check whether the issue belongs in resolver config or in the upstream hook that points to the resolver.
|
|
15
|
+
|
|
16
|
+
## Common Failure Modes
|
|
17
|
+
|
|
18
|
+
- Visible rendering issue is really stale or missing SVG content rather than code logic.
|
|
19
|
+
- Resolver is blamed for minting or tier problems that actually live upstream in the 721 hook repo.
|
|
20
|
+
- Attachment state looks inconsistent because a prior transfer or return failed and the resolver intentionally preserved safety over convenience.
|
|
21
|
+
|
|
22
|
+
## Useful Proof Points
|
|
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.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Banny Runtime
|
|
2
|
+
|
|
3
|
+
## Contract Role
|
|
4
|
+
|
|
5
|
+
- [`src/Banny721TokenUriResolver.sol`](../src/Banny721TokenUriResolver.sol) resolves token metadata, stores equipped outfits and backgrounds, enforces outfit locks, and composes layered SVG output for Banny collections.
|
|
6
|
+
|
|
7
|
+
## Runtime Path
|
|
8
|
+
|
|
9
|
+
1. The hook calls the resolver for `tokenURI`-style metadata.
|
|
10
|
+
2. The resolver reads tier and ownership context from the upstream 721 hook.
|
|
11
|
+
3. If the token is a body, it composes background, body, and equipped items into a single SVG.
|
|
12
|
+
4. If the token is an outfit or background, it renders a preview-style representation instead.
|
|
13
|
+
5. During decoration flows, the resolver takes custody of attached items and updates wearer/background mappings.
|
|
14
|
+
|
|
15
|
+
## High-Risk Areas
|
|
16
|
+
|
|
17
|
+
- Attachment custody: equipped items are held by the resolver, so transfer and return behavior matters.
|
|
18
|
+
- Outfit lock windows: lock duration is part of user-facing state and should not drift unexpectedly.
|
|
19
|
+
- Rendering composition: layer ordering and default-item behavior affect visible output and must stay deterministic.
|
|
20
|
+
- Stale attachment cleanup: views intentionally guard against inconsistent attachment state.
|
|
21
|
+
|
|
22
|
+
## Tests To Trust First
|
|
23
|
+
|
|
24
|
+
- [`test/DecorateFlow.t.sol`](../test/DecorateFlow.t.sol) for the main equip/unequip lifecycle.
|
|
25
|
+
- [`test/OutfitTransferLifecycle.t.sol`](../test/OutfitTransferLifecycle.t.sol) for custody and return behavior.
|
|
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.
|