@bannynet/core-v6 0.0.8 → 0.0.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AUDIT_INSTRUCTIONS.md +327 -0
- package/CHANGE_LOG.md +222 -0
- package/RISKS.md +30 -148
- package/USER_JOURNEYS.md +523 -0
- package/package.json +8 -8
- package/script/Add.Denver.s.sol +6 -4
- package/script/Deploy.s.sol +5 -8
- package/script/Drop1.s.sol +10 -2
- package/script/helpers/BannyverseDeploymentLib.sol +2 -2
- package/src/Banny721TokenUriResolver.sol +28 -10
- package/test/Banny721TokenUriResolver.t.sol +12 -10
- package/test/BannyAttacks.t.sol +2 -0
- package/test/DecorateFlow.t.sol +2 -0
- package/test/Fork.t.sol +12 -9
- package/test/OutfitTransferLifecycle.t.sol +391 -0
- package/test/TestAuditGaps.sol +720 -0
- package/test/TestQALastMile.t.sol +443 -0
- package/test/regression/BodyCategoryValidation.t.sol +1 -0
- package/test/regression/BurnedTokenCheck.t.sol +1 -0
- package/test/regression/CEIReorder.t.sol +1 -0
- package/test/regression/MsgSenderEvents.t.sol +1 -0
- package/test/regression/RemovedTierDesync.t.sol +1 -0
package/USER_JOURNEYS.md
ADDED
|
@@ -0,0 +1,523 @@
|
|
|
1
|
+
# banny-retail-v6 -- User Journeys
|
|
2
|
+
|
|
3
|
+
Every interaction path through `Banny721TokenUriResolver`, traced from entry point to final state. All function signatures and line references are verified against `src/Banny721TokenUriResolver.sol`.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Journey 1: Buy a Naked Banny
|
|
8
|
+
|
|
9
|
+
The resolver does not handle minting. Banny bodies are minted through the Juicebox 721 tier system. This journey is included for context because it produces the token that all other journeys depend on.
|
|
10
|
+
|
|
11
|
+
### Entry Point
|
|
12
|
+
|
|
13
|
+
Payment to a `JBMultiTerminal` for the project that owns the `JB721TiersHook`. The hook's `afterPayRecordedWith` callback mints a 721 token from the appropriate tier.
|
|
14
|
+
|
|
15
|
+
### What the Resolver Does
|
|
16
|
+
|
|
17
|
+
After minting, when any caller requests the token URI:
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
JB721TiersHook.tokenURI(tokenId)
|
|
21
|
+
-> Banny721TokenUriResolver.tokenUriOf(hook, tokenId)
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
**Function**: `tokenUriOf(address hook, uint256 tokenId) external view returns (string memory)` (line 200)
|
|
25
|
+
|
|
26
|
+
### Parameters
|
|
27
|
+
|
|
28
|
+
- `hook`: The `JB721TiersHook` contract address.
|
|
29
|
+
- `tokenId`: The minted token ID (e.g., `4_000_000_001` for Original Banny #1).
|
|
30
|
+
|
|
31
|
+
### State Read
|
|
32
|
+
|
|
33
|
+
1. `_productOfTokenId(hook, tokenId)` calls `hook.STORE().tierOfTokenId(hook, tokenId, false)`.
|
|
34
|
+
2. If `product.category == 0` (body), calls `svgOf(hook, tokenId, true, true)`.
|
|
35
|
+
3. `svgOf` calls `assetIdsOf(hook, tokenId)` which returns empty arrays (no outfits or background attached).
|
|
36
|
+
4. The body SVG is composed with default accessories (necklace, eyes, mouth) and no custom outfits.
|
|
37
|
+
|
|
38
|
+
### Result
|
|
39
|
+
|
|
40
|
+
A base64-encoded data URI containing JSON metadata with:
|
|
41
|
+
- Product name (Alien, Pink, Orange, or Original).
|
|
42
|
+
- Category name ("Banny body").
|
|
43
|
+
- An SVG image with the naked banny body and default accessories.
|
|
44
|
+
- Empty `outfitIds` array, `backgroundId: 0`.
|
|
45
|
+
|
|
46
|
+
### Edge Cases
|
|
47
|
+
|
|
48
|
+
- **Token ID does not exist**: `_productOfTokenId` returns a tier with `id == 0`. Function returns `""` (line 205).
|
|
49
|
+
- **SVG content not uploaded**: Falls back to IPFS URI via `JBIpfsDecoder` (line 303).
|
|
50
|
+
- **Token from category > 17**: Falls back to hook's `baseURI()` for IPFS resolution (line 300).
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## Journey 2: Dress a Banny with Outfits
|
|
55
|
+
|
|
56
|
+
### Entry Point
|
|
57
|
+
|
|
58
|
+
```solidity
|
|
59
|
+
function decorateBannyWith(
|
|
60
|
+
address hook,
|
|
61
|
+
uint256 bannyBodyId,
|
|
62
|
+
uint256 backgroundId,
|
|
63
|
+
uint256[] calldata outfitIds
|
|
64
|
+
) external nonReentrant
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Line 977. Protected by `ReentrancyGuard`.
|
|
68
|
+
|
|
69
|
+
### Prerequisites
|
|
70
|
+
|
|
71
|
+
- Caller owns the banny body NFT (`IERC721(hook).ownerOf(bannyBodyId) == _msgSender()`).
|
|
72
|
+
- Body is not locked (`outfitLockedUntil[hook][bannyBodyId] <= block.timestamp`).
|
|
73
|
+
- Caller owns each outfit NFT directly, or owns the banny body currently wearing that outfit.
|
|
74
|
+
- Caller owns the background NFT directly (or owns the banny body currently using it), if `backgroundId != 0`.
|
|
75
|
+
- Caller has approved the resolver for the hook contract (`hook.setApprovalForAll(resolver, true)`).
|
|
76
|
+
|
|
77
|
+
### Parameters
|
|
78
|
+
|
|
79
|
+
- `hook`: The `JB721TiersHook` contract address.
|
|
80
|
+
- `bannyBodyId`: Token ID of the banny body being dressed. Must be category 0.
|
|
81
|
+
- `backgroundId`: Token ID of the background to attach. Pass `0` for no background.
|
|
82
|
+
- `outfitIds`: Array of outfit token IDs. Must be in ascending category order (categories 2-17). Pass `[]` for no outfits.
|
|
83
|
+
|
|
84
|
+
### State Changes
|
|
85
|
+
|
|
86
|
+
**Checks (lines 987-997)**:
|
|
87
|
+
1. `_checkIfSenderIsOwner(hook, bannyBodyId)` -- reverts `UnauthorizedBannyBody` if caller is not the body owner.
|
|
88
|
+
2. `_productOfTokenId(hook, bannyBodyId).category` must equal 0 -- reverts `BannyBodyNotBodyCategory`.
|
|
89
|
+
3. `outfitLockedUntil[hook][bannyBodyId]` must be `<= block.timestamp` -- reverts `OutfitChangesLocked`.
|
|
90
|
+
|
|
91
|
+
**Event emission (line 999)**:
|
|
92
|
+
```
|
|
93
|
+
DecorateBanny(hook, bannyBodyId, backgroundId, outfitIds, _msgSender())
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
**Background processing (`_decorateBannyWithBackground`, line 1155)**:
|
|
97
|
+
|
|
98
|
+
If the background is changing:
|
|
99
|
+
- `_attachedBackgroundIdOf[hook][bannyBodyId]` is updated to `backgroundId` (or cleared to 0).
|
|
100
|
+
- `_userOf[hook][backgroundId]` is set to `bannyBodyId`.
|
|
101
|
+
- Old background NFT is returned to caller via `_tryTransferFrom` (silent failure OK).
|
|
102
|
+
- New background NFT is transferred into the resolver via `_transferFrom` (reverts on failure).
|
|
103
|
+
|
|
104
|
+
**Outfit processing (`_decorateBannyWithOutfits`, line 1222)**:
|
|
105
|
+
|
|
106
|
+
For each new outfit:
|
|
107
|
+
- Authorization verified: caller owns the outfit or the body wearing it.
|
|
108
|
+
- Category validated: in range 2-17, ascending order, no conflicts.
|
|
109
|
+
- Old outfits up to the current category are transferred out via `_tryTransferFrom`.
|
|
110
|
+
- New outfit transferred into the resolver via `_transferFrom` (if not already held).
|
|
111
|
+
- `_wearerOf[hook][outfitId]` set to `bannyBodyId`.
|
|
112
|
+
|
|
113
|
+
After all new outfits are processed:
|
|
114
|
+
- Remaining old outfits are transferred out.
|
|
115
|
+
- `_attachedOutfitIdsOf[hook][bannyBodyId]` is overwritten with the new `outfitIds` array (line 1368).
|
|
116
|
+
|
|
117
|
+
### Example: Dress with Hat and Glasses
|
|
118
|
+
|
|
119
|
+
```
|
|
120
|
+
Precondition: Alice owns body #4_000_000_001 (Original, category 0)
|
|
121
|
+
Alice owns hat #5_000_000_001 (category 4, Head)
|
|
122
|
+
Alice owns glasses #6_000_000_001 (category 6, Glasses)
|
|
123
|
+
Alice has called hook.setApprovalForAll(resolver, true)
|
|
124
|
+
|
|
125
|
+
Call: decorateBannyWith(hook, 4_000_000_001, 0, [5_000_000_001, 6_000_000_001])
|
|
126
|
+
|
|
127
|
+
REVERTS -- Head (category 4) blocks Glasses (category 6) per conflict rule.
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
```
|
|
131
|
+
Corrected: decorateBannyWith(hook, 4_000_000_001, 0, [5_000_000_001])
|
|
132
|
+
|
|
133
|
+
Result:
|
|
134
|
+
- Hat NFT transferred from Alice to resolver.
|
|
135
|
+
- _wearerOf[hook][5_000_000_001] = 4_000_000_001
|
|
136
|
+
- _attachedOutfitIdsOf[hook][4_000_000_001] = [5_000_000_001]
|
|
137
|
+
- Body's tokenURI now renders with hat layer.
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### Edge Cases
|
|
141
|
+
|
|
142
|
+
- **Empty outfitIds with backgroundId=0**: Strips all outfits and background. All previously equipped NFTs returned to caller.
|
|
143
|
+
- **Outfit already worn by this body**: Not re-transferred. `_wearerOf` retains existing value. The outfit stays in resolver custody.
|
|
144
|
+
- **Outfit worn by another body owned by caller**: Authorized via `ownerOf(wearerId) == _msgSender()`. The outfit is unlinked from the old body and transferred to the new body's custody.
|
|
145
|
+
- **Burned outfit in previous set**: `_tryTransferFrom` silently fails. The burned outfit is removed from the attachment array but no NFT is returned.
|
|
146
|
+
- **Removed tier in previous set**: `_productOfTokenId` returns category 0. The while loop processes it (category 0 <= any new category) and transfers it out.
|
|
147
|
+
- **Categories not ascending**: Reverts `UnorderedCategories`.
|
|
148
|
+
- **Duplicate categories**: Reverts `UnorderedCategories` (equality fails the `<` check).
|
|
149
|
+
- **Category 0 or 1 as outfit**: Reverts `UnrecognizedCategory`.
|
|
150
|
+
- **Reentrancy via safeTransferFrom callback**: Blocked by `nonReentrant` modifier.
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
## Journey 3: Undress a Banny
|
|
155
|
+
|
|
156
|
+
Undressing is not a separate function. It is performed by calling `decorateBannyWith` with empty arrays.
|
|
157
|
+
|
|
158
|
+
### Entry Point
|
|
159
|
+
|
|
160
|
+
```solidity
|
|
161
|
+
decorateBannyWith(hook, bannyBodyId, 0, [])
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### Parameters
|
|
165
|
+
|
|
166
|
+
- `hook`: The hook address.
|
|
167
|
+
- `bannyBodyId`: The banny body to undress.
|
|
168
|
+
- `backgroundId`: `0` (remove background).
|
|
169
|
+
- `outfitIds`: `[]` (remove all outfits).
|
|
170
|
+
|
|
171
|
+
### State Changes
|
|
172
|
+
|
|
173
|
+
1. All checks pass (ownership, not locked, body category).
|
|
174
|
+
2. **Background**: `_attachedBackgroundIdOf[hook][bannyBodyId]` set to 0. Old background NFT returned to caller.
|
|
175
|
+
3. **Outfits**: The new `outfitIds` array is empty, so the merge loop does not execute. The tail loop transfers out all previous outfits. `_attachedOutfitIdsOf[hook][bannyBodyId]` set to `[]`.
|
|
176
|
+
4. All outfit NFTs and the background NFT are returned to `_msgSender()`.
|
|
177
|
+
|
|
178
|
+
### Partial Undress
|
|
179
|
+
|
|
180
|
+
To remove some outfits but keep others, pass only the outfits you want to keep:
|
|
181
|
+
|
|
182
|
+
```
|
|
183
|
+
Before: outfitIds = [hat, glasses, necklace]
|
|
184
|
+
Call: decorateBannyWith(hook, bodyId, backgroundId, [necklace])
|
|
185
|
+
After: hat and glasses returned to caller, necklace stays equipped.
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
### Edge Cases
|
|
189
|
+
|
|
190
|
+
- **Body is locked**: Reverts `OutfitChangesLocked`. Cannot undress during lock period.
|
|
191
|
+
- **Some equipped outfits were burned**: `_tryTransferFrom` silently fails for burned tokens. No revert, no NFT returned for those tokens.
|
|
192
|
+
- **Caller is not the current owner**: Reverts `UnauthorizedBannyBody`. This can happen if the body was recently transferred.
|
|
193
|
+
|
|
194
|
+
---
|
|
195
|
+
|
|
196
|
+
## Journey 4: Lock a Banny
|
|
197
|
+
|
|
198
|
+
### Entry Point
|
|
199
|
+
|
|
200
|
+
```solidity
|
|
201
|
+
function lockOutfitChangesFor(
|
|
202
|
+
address hook,
|
|
203
|
+
uint256 bannyBodyId
|
|
204
|
+
) public
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
Line 1013.
|
|
208
|
+
|
|
209
|
+
### Prerequisites
|
|
210
|
+
|
|
211
|
+
- Caller owns the banny body NFT.
|
|
212
|
+
|
|
213
|
+
### Parameters
|
|
214
|
+
|
|
215
|
+
- `hook`: The hook address.
|
|
216
|
+
- `bannyBodyId`: The banny body to lock.
|
|
217
|
+
|
|
218
|
+
### State Changes
|
|
219
|
+
|
|
220
|
+
1. `_checkIfSenderIsOwner(hook, bannyBodyId)` -- reverts `UnauthorizedBannyBody` if not owner.
|
|
221
|
+
2. `newLockUntil = block.timestamp + 7 days` (line 1021).
|
|
222
|
+
3. If `currentLockedUntil > newLockUntil`, reverts `CantAccelerateTheLock` (line 1024).
|
|
223
|
+
4. `outfitLockedUntil[hook][bannyBodyId] = newLockUntil` (line 1027).
|
|
224
|
+
|
|
225
|
+
### Result
|
|
226
|
+
|
|
227
|
+
The body cannot have its outfits or background changed until `block.timestamp > outfitLockedUntil[hook][bannyBodyId]`.
|
|
228
|
+
|
|
229
|
+
### Use Case: Marketplace Sale
|
|
230
|
+
|
|
231
|
+
```
|
|
232
|
+
1. Alice owns body #1 dressed with rare hat and suit.
|
|
233
|
+
2. Alice calls lockOutfitChangesFor(hook, bodyId).
|
|
234
|
+
3. Alice lists body #1 on marketplace.
|
|
235
|
+
4. Bob buys body #1. Body transfers to Bob.
|
|
236
|
+
5. Lock persists -- Bob receives the body with guaranteed outfits.
|
|
237
|
+
6. After 7 days, Bob can undress and receive the hat and suit NFTs.
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
### Edge Cases
|
|
241
|
+
|
|
242
|
+
- **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.
|
|
243
|
+
- **Locking an undressed body**: Valid. The body is locked with no outfits. No outfits can be added during the lock period.
|
|
244
|
+
- **Lock after transfer**: The new owner can lock. The old owner cannot (they no longer pass `_checkIfSenderIsOwner`).
|
|
245
|
+
- **Lock does not prevent body transfer**: The lock only affects `decorateBannyWith`. The body NFT can still be transferred on the hook contract.
|
|
246
|
+
- **No admin override**: The contract owner cannot unlock a body. The lock must expire naturally.
|
|
247
|
+
|
|
248
|
+
---
|
|
249
|
+
|
|
250
|
+
## Journey 5: Transfer a Decorated Banny
|
|
251
|
+
|
|
252
|
+
The resolver does not intercept or control body transfers. Body transfers happen on the `JB721TiersHook` contract. The resolver observes the ownership change lazily.
|
|
253
|
+
|
|
254
|
+
### What Happens
|
|
255
|
+
|
|
256
|
+
1. Alice transfers body #1 to Bob via the hook contract (`hook.safeTransferFrom(alice, bob, bodyId)`).
|
|
257
|
+
2. No resolver functions are called during the transfer.
|
|
258
|
+
3. The resolver's state remains unchanged:
|
|
259
|
+
- `_attachedOutfitIdsOf[hook][bodyId]` still contains the outfit array.
|
|
260
|
+
- `_attachedBackgroundIdOf[hook][bodyId]` still contains the background ID.
|
|
261
|
+
- `_wearerOf[hook][outfitId]` still maps each outfit to `bodyId`.
|
|
262
|
+
- `_userOf[hook][backgroundId]` still maps to `bodyId`.
|
|
263
|
+
4. All equipped outfit and background NFTs remain held by the resolver.
|
|
264
|
+
|
|
265
|
+
### What Bob Can Do
|
|
266
|
+
|
|
267
|
+
Bob now owns the body. All resolver authorization checks (`_checkIfSenderIsOwner`) will pass for Bob.
|
|
268
|
+
|
|
269
|
+
- **Undress**: `decorateBannyWith(hook, bodyId, 0, [])` returns all equipped NFTs to Bob.
|
|
270
|
+
- **Redress**: `decorateBannyWith(hook, bodyId, newBg, [newOutfits])` replaces outfits. Old outfits returned to Bob (even though Alice originally equipped them).
|
|
271
|
+
- **Lock**: `lockOutfitChangesFor(hook, bodyId)` locks the body under Bob's control.
|
|
272
|
+
|
|
273
|
+
### What Alice Can No Longer Do
|
|
274
|
+
|
|
275
|
+
Alice cannot call `decorateBannyWith` or `lockOutfitChangesFor` for that body -- she no longer owns it.
|
|
276
|
+
|
|
277
|
+
### Edge Cases
|
|
278
|
+
|
|
279
|
+
- **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).
|
|
280
|
+
- **Body transferred while locked**: Lock persists. Bob cannot change outfits until the lock expires.
|
|
281
|
+
- **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).
|
|
282
|
+
- **Double transfer (Alice -> Bob -> Charlie)**: Only Charlie can interact with the body's outfits. Each transfer implicitly transfers outfit control.
|
|
283
|
+
|
|
284
|
+
---
|
|
285
|
+
|
|
286
|
+
## Journey 6: Move an Outfit Between Bodies
|
|
287
|
+
|
|
288
|
+
A user who owns multiple bodies can move an equipped outfit from one body to another in a single call.
|
|
289
|
+
|
|
290
|
+
### Entry Point
|
|
291
|
+
|
|
292
|
+
```solidity
|
|
293
|
+
decorateBannyWith(hook, newBodyId, 0, [outfitId])
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
Where `outfitId` is currently equipped on `oldBodyId`, and the caller owns both bodies.
|
|
297
|
+
|
|
298
|
+
### Prerequisites
|
|
299
|
+
|
|
300
|
+
- Caller owns `newBodyId`.
|
|
301
|
+
- Caller owns `oldBodyId` (which currently wears the outfit).
|
|
302
|
+
- `newBodyId` is not locked.
|
|
303
|
+
|
|
304
|
+
### State Changes
|
|
305
|
+
|
|
306
|
+
1. Authorization passes: caller does not own the outfit directly (resolver holds it), but caller owns `oldBodyId` which is the `wearerOf(hook, outfitId)`.
|
|
307
|
+
2. `_wearerOf[hook][outfitId]` is updated to `newBodyId` (line 1331).
|
|
308
|
+
3. The outfit is not transferred (resolver already holds it, line 1335 check).
|
|
309
|
+
4. `_attachedOutfitIdsOf[hook][newBodyId]` is set to the new array including this outfit.
|
|
310
|
+
5. `_attachedOutfitIdsOf[hook][oldBodyId]` is NOT explicitly updated. However, `wearerOf(hook, outfitId)` will now return `newBodyId`, so `assetIdsOf(hook, oldBodyId)` will exclude this outfit from its filtered result.
|
|
311
|
+
|
|
312
|
+
### Edge Cases
|
|
313
|
+
|
|
314
|
+
- **Old body is locked**: The lock is on `oldBodyId`, but the caller is calling `decorateBannyWith` on `newBodyId`. The lock only prevents changes to the locked body, not removal of its outfits via a different body's decoration call. **Wait -- verify this**: The outfit's `wearerOf` returns `oldBodyId`. The caller owns `oldBodyId`. The authorization check at line 1266 checks `ownerOf(wearerId)` which is the caller. So this succeeds. However, `oldBodyId`'s `_attachedOutfitIdsOf` still contains the outfit. The outfit has been moved at the `_wearerOf` level, but the old array is stale. This is handled by `assetIdsOf` which filters by checking `wearerOf` (line 383). **Auditors should verify the lock on `oldBodyId` does not prevent this path.** The lock check is only in `decorateBannyWith` at line 995, and it checks the body being decorated (`bannyBodyId`), not the body being undressed.
|
|
315
|
+
|
|
316
|
+
---
|
|
317
|
+
|
|
318
|
+
## Journey 7: Admin -- Store SVG Content
|
|
319
|
+
|
|
320
|
+
### Step 1: Commit Hashes
|
|
321
|
+
|
|
322
|
+
**Entry Point**:
|
|
323
|
+
```solidity
|
|
324
|
+
function setSvgHashesOf(
|
|
325
|
+
uint256[] memory upcs,
|
|
326
|
+
bytes32[] memory svgHashes
|
|
327
|
+
) external onlyOwner
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
Line 1130.
|
|
331
|
+
|
|
332
|
+
**Parameters**:
|
|
333
|
+
- `upcs`: Array of universal product codes to set hashes for.
|
|
334
|
+
- `svgHashes`: Array of `keccak256` hashes of the SVG content strings.
|
|
335
|
+
|
|
336
|
+
**State Changes**:
|
|
337
|
+
- `svgHashOf[upc] = svgHash` for each pair.
|
|
338
|
+
- Reverts `HashAlreadyStored` if any UPC already has a hash set.
|
|
339
|
+
- Emits `SetSvgHash(upc, svgHash, caller)`.
|
|
340
|
+
|
|
341
|
+
### Step 2: Upload Content
|
|
342
|
+
|
|
343
|
+
**Entry Point**:
|
|
344
|
+
```solidity
|
|
345
|
+
function setSvgContentsOf(
|
|
346
|
+
uint256[] memory upcs,
|
|
347
|
+
string[] calldata svgContents
|
|
348
|
+
) external
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
Line 1100. **Not restricted to owner** -- anyone can upload content as long as it matches the hash.
|
|
352
|
+
|
|
353
|
+
**Parameters**:
|
|
354
|
+
- `upcs`: Array of universal product codes to upload content for.
|
|
355
|
+
- `svgContents`: Array of SVG content strings (without wrapping `<svg>` tags).
|
|
356
|
+
|
|
357
|
+
**State Changes**:
|
|
358
|
+
- Reverts `ContentsAlreadyStored` if content already set for this UPC.
|
|
359
|
+
- Reverts `HashNotFound` if no hash set for this UPC.
|
|
360
|
+
- Reverts `ContentsMismatch` if `keccak256(abi.encodePacked(svgContent)) != svgHashOf[upc]`.
|
|
361
|
+
- `_svgContentOf[upc] = svgContent`.
|
|
362
|
+
- Emits `SetSvgContent(upc, svgContent, caller)`.
|
|
363
|
+
|
|
364
|
+
### Edge Cases
|
|
365
|
+
|
|
366
|
+
- **Hash set but content never uploaded**: `_svgOf` falls back to IPFS resolution. Tokens render with IPFS images instead of on-chain SVG.
|
|
367
|
+
- **Content with special characters**: Stored verbatim. No sanitization. A `<script>` tag in SVG content would be included in the data URI.
|
|
368
|
+
- **Multiple UPCs in one call**: Array lengths must match or `ArrayLengthMismatch` reverts. The entire call reverts if any single UPC fails.
|
|
369
|
+
|
|
370
|
+
---
|
|
371
|
+
|
|
372
|
+
## Journey 8: Admin -- Set Metadata and Product Names
|
|
373
|
+
|
|
374
|
+
### Set Metadata
|
|
375
|
+
|
|
376
|
+
**Entry Point**:
|
|
377
|
+
```solidity
|
|
378
|
+
function setMetadata(
|
|
379
|
+
string calldata description,
|
|
380
|
+
string calldata url,
|
|
381
|
+
string calldata baseUri
|
|
382
|
+
) external onlyOwner
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
Line 1065.
|
|
386
|
+
|
|
387
|
+
**State Changes**:
|
|
388
|
+
- `svgDescription = description`
|
|
389
|
+
- `svgExternalUrl = url`
|
|
390
|
+
- `svgBaseUri = baseUri`
|
|
391
|
+
- All three fields are always overwritten. Pass current values for fields you do not want to change. Pass `""` to clear.
|
|
392
|
+
|
|
393
|
+
### Set Product Names
|
|
394
|
+
|
|
395
|
+
**Entry Point**:
|
|
396
|
+
```solidity
|
|
397
|
+
function setProductNames(
|
|
398
|
+
uint256[] memory upcs,
|
|
399
|
+
string[] memory names
|
|
400
|
+
) external onlyOwner
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
Line 1084.
|
|
404
|
+
|
|
405
|
+
**State Changes**:
|
|
406
|
+
- `_customProductNameOf[upc] = name` for each pair.
|
|
407
|
+
- Unlike SVG hashes and content, names are **not write-once**. Names can be overwritten.
|
|
408
|
+
- Built-in names for UPCs 1-4 (Alien, Pink, Orange, Original) are hardcoded in `_productNameOf` (line 896-909) and cannot be overridden by this function. The `_productNameOf` function checks UPCs 1-4 first and returns the hardcoded name before checking `_customProductNameOf`.
|
|
409
|
+
|
|
410
|
+
### Edge Cases
|
|
411
|
+
|
|
412
|
+
- **Overwriting a product name**: No revert. The old name is replaced. This could change how existing NFTs display.
|
|
413
|
+
- **Setting name for UPCs 1-4**: The `_customProductNameOf` mapping is written, but `_productNameOf` returns the hardcoded name first. The custom name is never read for these UPCs.
|
|
414
|
+
- **Empty name string**: Valid. Sets the custom name to empty, causing `_productNameOf` to return `""` for that UPC.
|
|
415
|
+
|
|
416
|
+
---
|
|
417
|
+
|
|
418
|
+
## Journey 9: View Functions -- Query Banny State
|
|
419
|
+
|
|
420
|
+
### Get Attached Assets
|
|
421
|
+
|
|
422
|
+
```solidity
|
|
423
|
+
function assetIdsOf(
|
|
424
|
+
address hook,
|
|
425
|
+
uint256 bannyBodyId
|
|
426
|
+
) public view returns (uint256 backgroundId, uint256[] memory outfitIds)
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
Line 356.
|
|
430
|
+
|
|
431
|
+
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.
|
|
432
|
+
|
|
433
|
+
### Get Outfit Wearer
|
|
434
|
+
|
|
435
|
+
```solidity
|
|
436
|
+
function wearerOf(address hook, uint256 outfitId) public view returns (uint256)
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
Line 509. Returns the body ID wearing this outfit, or 0 if unworn. Verifies the outfit is still in the body's `_attachedOutfitIdsOf` array.
|
|
440
|
+
|
|
441
|
+
### Get Background User
|
|
442
|
+
|
|
443
|
+
```solidity
|
|
444
|
+
function userOf(address hook, uint256 backgroundId) public view returns (uint256)
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
Line 494. Returns the body ID using this background, or 0 if unused. Verifies the background is still the body's `_attachedBackgroundIdOf` entry.
|
|
448
|
+
|
|
449
|
+
### Get SVG
|
|
450
|
+
|
|
451
|
+
```solidity
|
|
452
|
+
function svgOf(
|
|
453
|
+
address hook,
|
|
454
|
+
uint256 tokenId,
|
|
455
|
+
bool shouldDressBannyBody,
|
|
456
|
+
bool shouldIncludeBackgroundOnBannyBody
|
|
457
|
+
) public view returns (string memory)
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
Line 434. Returns the composed SVG for any token. For bodies, can toggle dressing and background. For non-bodies, returns the outfit/background SVG alone.
|
|
461
|
+
|
|
462
|
+
### Get Names
|
|
463
|
+
|
|
464
|
+
```solidity
|
|
465
|
+
function namesOf(
|
|
466
|
+
address hook,
|
|
467
|
+
uint256 tokenId
|
|
468
|
+
) public view returns (string memory, string memory, string memory)
|
|
469
|
+
```
|
|
470
|
+
|
|
471
|
+
Line 406. Returns (fullName, categoryName, productName).
|
|
472
|
+
|
|
473
|
+
### Get Lock Status
|
|
474
|
+
|
|
475
|
+
```solidity
|
|
476
|
+
mapping(address hook => mapping(uint256 upc => uint256)) public outfitLockedUntil;
|
|
477
|
+
```
|
|
478
|
+
|
|
479
|
+
Line 96. Directly readable. Returns the timestamp until which the body is locked, or 0 if never locked.
|
|
480
|
+
|
|
481
|
+
---
|
|
482
|
+
|
|
483
|
+
## Summary: State Machine per Banny Body
|
|
484
|
+
|
|
485
|
+
```
|
|
486
|
+
+-----------+
|
|
487
|
+
| NAKED |
|
|
488
|
+
| (minted) |
|
|
489
|
+
+-----+-----+
|
|
490
|
+
|
|
|
491
|
+
decorateBannyWith(outfits)
|
|
492
|
+
|
|
|
493
|
+
+-----v-----+
|
|
494
|
+
| DRESSED |<----+
|
|
495
|
+
| | |
|
|
496
|
+
+-----+-----+ |
|
|
497
|
+
| |
|
|
498
|
+
+-------------+-------+ |
|
|
499
|
+
| | |
|
|
500
|
+
decorateBannyWith([]) decorateBannyWith(newOutfits)
|
|
501
|
+
| |
|
|
502
|
+
+-----v-----+ +---+
|
|
503
|
+
| NAKED |
|
|
504
|
+
+-----+-----+
|
|
505
|
+
|
|
|
506
|
+
lockOutfitChangesFor()
|
|
507
|
+
|
|
|
508
|
+
+-----v-----+
|
|
509
|
+
| LOCKED |
|
|
510
|
+
| (7 days) |
|
|
511
|
+
+-----+-----+
|
|
512
|
+
|
|
|
513
|
+
block.timestamp > lockUntil
|
|
514
|
+
|
|
|
515
|
+
+-----v-----+
|
|
516
|
+
| UNLOCKED |
|
|
517
|
+
| (NAKED) |
|
|
518
|
+
+-----------+
|
|
519
|
+
```
|
|
520
|
+
|
|
521
|
+
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.
|
|
522
|
+
|
|
523
|
+
Body transfers do not change the resolver state. The new owner inherits the current state (dressed/naked, locked/unlocked) and all custody rights.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bannynet/core-v6",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.10",
|
|
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.
|
|
24
|
-
"@bananapus/core-v6": "^0.0.
|
|
25
|
-
"@bananapus/permission-ids-v6": "^0.0.
|
|
26
|
-
"@bananapus/router-terminal-v6": "^0.0.
|
|
27
|
-
"@bananapus/suckers-v6": "^0.0.
|
|
28
|
-
"@croptop/core-v6": "^0.0.
|
|
23
|
+
"@bananapus/721-hook-v6": "^0.0.17",
|
|
24
|
+
"@bananapus/core-v6": "^0.0.17",
|
|
25
|
+
"@bananapus/permission-ids-v6": "^0.0.10",
|
|
26
|
+
"@bananapus/router-terminal-v6": "^0.0.13",
|
|
27
|
+
"@bananapus/suckers-v6": "^0.0.11",
|
|
28
|
+
"@croptop/core-v6": "^0.0.18",
|
|
29
29
|
"@openzeppelin/contracts": "^5.6.1",
|
|
30
|
-
"@rev-net/core-v6": "^0.0.
|
|
30
|
+
"@rev-net/core-v6": "^0.0.13",
|
|
31
31
|
"keccak": "^3.0.4"
|
|
32
32
|
},
|
|
33
33
|
"devDependencies": {
|
package/script/Add.Denver.s.sol
CHANGED
|
@@ -76,14 +76,16 @@ contract Drop1Script is Script, Sphinx {
|
|
|
76
76
|
splits: new JBSplit[](0)
|
|
77
77
|
});
|
|
78
78
|
|
|
79
|
-
// Get the next tier ID so we can set names and hashes for the new product.
|
|
80
|
-
uint256 nextTierId = hook.STORE().maxTierIdOf(address(hook)) + 1;
|
|
81
|
-
|
|
82
79
|
hook.adjustTiers({tiersToAdd: products, tierIdsToRemove: new uint256[](0)});
|
|
83
80
|
|
|
81
|
+
// Read maxTierIdOf after adjustTiers so the value reflects our newly added tiers,
|
|
82
|
+
// avoiding a race condition where another transaction could change maxTierIdOf between
|
|
83
|
+
// the read and the adjustTiers call.
|
|
84
|
+
uint256 maxTierId = hook.STORE().maxTierIdOf(address(hook));
|
|
85
|
+
|
|
84
86
|
// Build the product IDs array for the newly added tier(s).
|
|
85
87
|
uint256[] memory productIds = new uint256[](1);
|
|
86
|
-
productIds[0] =
|
|
88
|
+
productIds[0] = maxTierId;
|
|
87
89
|
|
|
88
90
|
bannyverse.resolver.setSvgHashesOf({upcs: productIds, svgHashes: svgHashes});
|
|
89
91
|
bannyverse.resolver.setProductNames({upcs: productIds, names: names});
|
package/script/Deploy.s.sol
CHANGED
|
@@ -82,6 +82,8 @@ contract DeployScript is Script, Sphinx {
|
|
|
82
82
|
uint104 constant BAN_BASE_AUTO_ISSUANCE = 10_097_684_379_816_492_953_872;
|
|
83
83
|
uint104 constant BAN_OP_AUTO_ISSUANCE = 328_366_065_858_064_488_000;
|
|
84
84
|
uint104 constant BAN_ARB_AUTO_ISSUANCE = 2_825_980_000_000_000_000_000;
|
|
85
|
+
// forge-lint: disable-next-line(unsafe-typecast)
|
|
86
|
+
uint104 constant BAN_PREMINT_COUNT = uint104(1_000_000 * DECIMAL_MULTIPLIER);
|
|
85
87
|
|
|
86
88
|
function configureSphinx() public override {
|
|
87
89
|
sphinxConfig.projectName = "banny-core-v6";
|
|
@@ -179,12 +181,8 @@ contract DeployScript is Script, Sphinx {
|
|
|
179
181
|
|
|
180
182
|
{
|
|
181
183
|
REVAutoIssuance[] memory autoIssuances = new REVAutoIssuance[](1);
|
|
182
|
-
autoIssuances[0] =
|
|
183
|
-
|
|
184
|
-
chainId: PREMINT_CHAIN_ID,
|
|
185
|
-
count: uint104(1_000_000 * DECIMAL_MULTIPLIER),
|
|
186
|
-
beneficiary: operator
|
|
187
|
-
});
|
|
184
|
+
autoIssuances[0] =
|
|
185
|
+
REVAutoIssuance({chainId: PREMINT_CHAIN_ID, count: BAN_PREMINT_COUNT, beneficiary: operator});
|
|
188
186
|
|
|
189
187
|
// decrease by a smaller percent more frequently. 30 days, 7%-ish.
|
|
190
188
|
stageConfigurations[1] = REVStageConfig({
|
|
@@ -300,8 +298,7 @@ contract DeployScript is Script, Sphinx {
|
|
|
300
298
|
tokenMappings[0] = JBTokenMapping({
|
|
301
299
|
localToken: JBConstants.NATIVE_TOKEN,
|
|
302
300
|
minGas: 200_000,
|
|
303
|
-
remoteToken: bytes32(uint256(uint160(JBConstants.NATIVE_TOKEN)))
|
|
304
|
-
minBridgeAmount: 0.01 ether
|
|
301
|
+
remoteToken: bytes32(uint256(uint160(JBConstants.NATIVE_TOKEN)))
|
|
305
302
|
});
|
|
306
303
|
|
|
307
304
|
JBSuckerDeployerConfig[] memory suckerDeployerConfigurations;
|
package/script/Drop1.s.sol
CHANGED
|
@@ -1043,12 +1043,20 @@ contract Drop1Script is Script, Sphinx {
|
|
|
1043
1043
|
splits: new JBSplit[](0)
|
|
1044
1044
|
});
|
|
1045
1045
|
|
|
1046
|
+
hook.adjustTiers({tiersToAdd: products, tierIdsToRemove: new uint256[](0)});
|
|
1047
|
+
|
|
1048
|
+
// Read maxTierIdOf after adjustTiers so the value reflects our newly added tiers,
|
|
1049
|
+
// avoiding a race condition where another transaction could change maxTierIdOf between
|
|
1050
|
+
// the read and the adjustTiers call.
|
|
1051
|
+
uint256 maxTierId = hook.STORE().maxTierIdOf(address(hook));
|
|
1052
|
+
|
|
1053
|
+
// Build the product IDs array for the newly added tiers.
|
|
1054
|
+
// The last 47 tier IDs correspond to the 47 tiers we just added.
|
|
1046
1055
|
uint256[] memory productIds = new uint256[](47);
|
|
1047
1056
|
for (uint256 i; i < 47; i++) {
|
|
1048
|
-
productIds[i] =
|
|
1057
|
+
productIds[i] = maxTierId - 46 + i;
|
|
1049
1058
|
}
|
|
1050
1059
|
|
|
1051
|
-
hook.adjustTiers({tiersToAdd: products, tierIdsToRemove: new uint256[](0)});
|
|
1052
1060
|
bannyverse.resolver.setSvgHashesOf({upcs: productIds, svgHashes: svgHashes});
|
|
1053
1061
|
bannyverse.resolver.setProductNames({upcs: productIds, names: names});
|
|
1054
1062
|
}
|
|
@@ -79,9 +79,9 @@ library BannyverseDeploymentLib {
|
|
|
79
79
|
view
|
|
80
80
|
returns (address)
|
|
81
81
|
{
|
|
82
|
-
// forge-lint: disable-next-line(unsafe-cheatcode)
|
|
83
82
|
string memory deploymentJson =
|
|
84
|
-
|
|
83
|
+
// forge-lint: disable-next-line(unsafe-cheatcode)
|
|
84
|
+
vm.readFile(string.concat(path, projectName, "/", networkName, "/", contractName, ".json"));
|
|
85
85
|
return stdJson.readAddress({json: deploymentJson, key: ".address"});
|
|
86
86
|
}
|
|
87
87
|
}
|