@bannynet/core-v6 0.0.11 → 0.0.13
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 +42 -31
- package/ARCHITECTURE.md +41 -3
- package/AUDIT_INSTRUCTIONS.md +68 -41
- package/CHANGE_LOG.md +28 -7
- package/README.md +53 -1
- package/RISKS.md +33 -7
- package/SKILLS.md +44 -3
- package/STYLE_GUIDE.md +2 -2
- package/USER_JOURNEYS.md +327 -325
- package/foundry.toml +1 -1
- package/package.json +8 -8
- package/script/Add.Denver.s.sol +1 -1
- package/script/Deploy.s.sol +1 -1
- package/script/Drop1.s.sol +1 -1
- package/script/helpers/BannyverseDeploymentLib.sol +1 -1
- package/script/helpers/MigrationHelper.sol +1 -1
- package/src/Banny721TokenUriResolver.sol +132 -24
- package/test/Banny721TokenUriResolver.t.sol +1 -1
- package/test/BannyAttacks.t.sol +1 -1
- package/test/DecorateFlow.t.sol +1 -1
- package/test/Fork.t.sol +1 -1
- package/test/OutfitTransferLifecycle.t.sol +1 -1
- package/test/TestAuditGaps.sol +1 -1
- package/test/TestQALastMile.t.sol +1 -1
- package/test/audit/AntiStrandingRetention.t.sol +392 -0
- package/test/audit/MergedOutfitExclusivity.t.sol +223 -0
- package/test/audit/TryTransferFromStrandsAssets.t.sol +192 -0
- package/test/regression/ArrayLengthValidation.t.sol +1 -1
- package/test/regression/BodyCategoryValidation.t.sol +1 -1
- package/test/regression/BurnedTokenCheck.t.sol +1 -1
- package/test/regression/CEIReorder.t.sol +1 -1
- package/test/regression/ClearMetadata.t.sol +1 -1
- package/test/regression/MsgSenderEvents.t.sol +1 -1
- package/test/regression/RemovedTierDesync.t.sol +1 -1
- package/deployments/banny-core-v5/arbitrum/Banny721TokenUriResolver.json +0 -1809
- package/deployments/banny-core-v5/arbitrum_sepolia/Banny721TokenUriResolver.json +0 -1795
- package/deployments/banny-core-v5/base/Banny721TokenUriResolver.json +0 -1810
- package/deployments/banny-core-v5/base_sepolia/Banny721TokenUriResolver.json +0 -1796
- package/deployments/banny-core-v5/ethereum/Banny721TokenUriResolver.json +0 -1795
- package/deployments/banny-core-v5/optimism/Banny721TokenUriResolver.json +0 -1810
- package/deployments/banny-core-v5/optimism_sepolia/Banny721TokenUriResolver.json +0 -1796
- package/deployments/banny-core-v5/sepolia/Banny721TokenUriResolver.json +0 -1795
package/USER_JOURNEYS.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# banny-retail-v6 -- User Journeys
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
All interaction paths through `Banny721TokenUriResolver`, traced from entry point to final state. All function signatures, events, and revert names are verified against `src/Banny721TokenUriResolver.sol` and `src/interfaces/IBanny721TokenUriResolver.sol`.
|
|
4
4
|
|
|
5
5
|
---
|
|
6
6
|
|
|
@@ -8,11 +8,9 @@ Every interaction path through `Banny721TokenUriResolver`, traced from entry poi
|
|
|
8
8
|
|
|
9
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
10
|
|
|
11
|
-
|
|
11
|
+
**Entry point**: Payment to a `JBMultiTerminal` for the project that owns the `JB721TiersHook`. The hook's `afterPayRecordedWith` callback mints a 721 token from the appropriate tier.
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
### What the Resolver Does
|
|
13
|
+
**Who can call**: Anyone (via `JBMultiTerminal.pay`).
|
|
16
14
|
|
|
17
15
|
After minting, when any caller requests the token URI:
|
|
18
16
|
|
|
@@ -21,98 +19,87 @@ JB721TiersHook.tokenURI(tokenId)
|
|
|
21
19
|
-> Banny721TokenUriResolver.tokenUriOf(hook, tokenId)
|
|
22
20
|
```
|
|
23
21
|
|
|
24
|
-
**Function**: `tokenUriOf(address hook, uint256 tokenId) external view returns (string memory)`
|
|
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).
|
|
22
|
+
**Function**: `tokenUriOf(address hook, uint256 tokenId) external view returns (string memory)`
|
|
30
23
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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.
|
|
24
|
+
**Parameters**:
|
|
25
|
+
- `hook` -- The `JB721TiersHook` contract address
|
|
26
|
+
- `tokenId` -- The minted token ID (e.g., `4_000_000_001` for Original Banny #1)
|
|
37
27
|
|
|
38
|
-
|
|
28
|
+
**State read**:
|
|
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
|
|
39
33
|
|
|
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
|
|
34
|
+
**Result**: A base64-encoded data URI containing JSON metadata with:
|
|
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`
|
|
45
39
|
|
|
46
|
-
|
|
40
|
+
**Events**: None (view function).
|
|
47
41
|
|
|
48
|
-
|
|
49
|
-
- **
|
|
50
|
-
- **
|
|
42
|
+
**Edge cases**:
|
|
43
|
+
- **Token ID does not exist**: `_productOfTokenId` returns a tier with `id == 0`. Function returns `""`
|
|
44
|
+
- **SVG content not uploaded**: Falls back to IPFS URI via `JBIpfsDecoder`
|
|
45
|
+
- **Token from category > 17**: Falls back to hook's `baseURI()` for IPFS resolution
|
|
46
|
+
- **Unrecognized product**: Reverts `Banny721TokenUriResolver_UnrecognizedProduct`
|
|
51
47
|
|
|
52
48
|
---
|
|
53
49
|
|
|
54
50
|
## Journey 2: Dress a Banny with Outfits
|
|
55
51
|
|
|
56
|
-
|
|
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)`).
|
|
52
|
+
**Entry point**: `Banny721TokenUriResolver.decorateBannyWith(address hook, uint256 bannyBodyId, uint256 backgroundId, uint256[] calldata outfitIds)`
|
|
76
53
|
|
|
77
|
-
|
|
54
|
+
**Who can call**: The owner of the banny body NFT (`IERC721(hook).ownerOf(bannyBodyId) == _msgSender()`). Protected by `ReentrancyGuard`. ERC-2771 meta-transactions supported.
|
|
78
55
|
|
|
79
|
-
|
|
80
|
-
-
|
|
81
|
-
-
|
|
82
|
-
-
|
|
56
|
+
**Prerequisites**:
|
|
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)`)
|
|
83
62
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
If the background
|
|
99
|
-
- `
|
|
100
|
-
|
|
101
|
-
-
|
|
102
|
-
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
-
|
|
110
|
-
-
|
|
111
|
-
- `
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
-
|
|
115
|
-
- `
|
|
63
|
+
**Parameters**:
|
|
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
|
|
68
|
+
|
|
69
|
+
**State changes**:
|
|
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]`
|
|
84
|
+
|
|
85
|
+
**Events**: `DecorateBanny(address indexed hook, uint256 indexed bannyBodyId, uint256 indexed backgroundId, uint256[] outfitIds, address caller)` -- emitted before state changes, where `caller = _msgSender()`
|
|
86
|
+
|
|
87
|
+
**Edge cases**:
|
|
88
|
+
- **Empty outfitIds with backgroundId=0**: Strips all outfits and background. All previously equipped NFTs returned to caller
|
|
89
|
+
- **Outfit already worn by this body**: Not re-transferred. `_wearerOf` retains existing value. The outfit stays in resolver custody
|
|
90
|
+
- **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
|
|
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
|
|
116
103
|
|
|
117
104
|
### Example: Dress with Hat and Glasses
|
|
118
105
|
|
|
@@ -137,47 +124,32 @@ Result:
|
|
|
137
124
|
- Body's tokenURI now renders with hat layer.
|
|
138
125
|
```
|
|
139
126
|
|
|
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
127
|
---
|
|
153
128
|
|
|
154
129
|
## Journey 3: Undress a Banny
|
|
155
130
|
|
|
156
131
|
Undressing is not a separate function. It is performed by calling `decorateBannyWith` with empty arrays.
|
|
157
132
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
```solidity
|
|
161
|
-
decorateBannyWith(hook, bannyBodyId, 0, [])
|
|
162
|
-
```
|
|
163
|
-
|
|
164
|
-
### Parameters
|
|
133
|
+
**Entry point**: `Banny721TokenUriResolver.decorateBannyWith(address hook, uint256 bannyBodyId, 0, [])`
|
|
165
134
|
|
|
166
|
-
|
|
167
|
-
- `bannyBodyId`: The banny body to undress.
|
|
168
|
-
- `backgroundId`: `0` (remove background).
|
|
169
|
-
- `outfitIds`: `[]` (remove all outfits).
|
|
135
|
+
**Who can call**: The owner of the banny body NFT.
|
|
170
136
|
|
|
171
|
-
|
|
137
|
+
**Parameters**:
|
|
138
|
+
- `hook` -- The hook address
|
|
139
|
+
- `bannyBodyId` -- The banny body to undress
|
|
140
|
+
- `backgroundId` -- `0` (remove background)
|
|
141
|
+
- `outfitIds` -- `[]` (remove all outfits)
|
|
172
142
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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
|
|
177
149
|
|
|
178
|
-
|
|
150
|
+
**Events**: `DecorateBanny(hook, bannyBodyId, 0, [], caller)`
|
|
179
151
|
|
|
180
|
-
To remove some outfits but keep others, pass only the outfits you want to keep:
|
|
152
|
+
**Partial undress**: To remove some outfits but keep others, pass only the outfits you want to keep:
|
|
181
153
|
|
|
182
154
|
```
|
|
183
155
|
Before: outfitIds = [hat, glasses, necklace]
|
|
@@ -185,46 +157,38 @@ Call: decorateBannyWith(hook, bodyId, backgroundId, [necklace])
|
|
|
185
157
|
After: hat and glasses returned to caller, necklace stays equipped.
|
|
186
158
|
```
|
|
187
159
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
- **
|
|
191
|
-
- **
|
|
192
|
-
- **Caller is not the current owner**: Reverts `UnauthorizedBannyBody`. This can happen if the body was recently transferred.
|
|
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
|
|
193
164
|
|
|
194
165
|
---
|
|
195
166
|
|
|
196
167
|
## Journey 4: Lock a Banny
|
|
197
168
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
```solidity
|
|
201
|
-
function lockOutfitChangesFor(
|
|
202
|
-
address hook,
|
|
203
|
-
uint256 bannyBodyId
|
|
204
|
-
) public
|
|
205
|
-
```
|
|
206
|
-
|
|
207
|
-
Line 1013.
|
|
169
|
+
**Entry point**: `Banny721TokenUriResolver.lockOutfitChangesFor(address hook, uint256 bannyBodyId)`
|
|
208
170
|
|
|
209
|
-
|
|
171
|
+
**Who can call**: The owner of the banny body NFT (`IERC721(hook).ownerOf(bannyBodyId) == _msgSender()`).
|
|
210
172
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
- `hook`: The hook address.
|
|
216
|
-
- `bannyBodyId`: The banny body to lock.
|
|
217
|
-
|
|
218
|
-
### State Changes
|
|
173
|
+
**Parameters**:
|
|
174
|
+
- `hook` -- The hook address
|
|
175
|
+
- `bannyBodyId` -- The banny body to lock
|
|
219
176
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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`
|
|
224
182
|
|
|
225
|
-
|
|
183
|
+
**Events**: None. The `lockOutfitChangesFor` function does not emit an event.
|
|
226
184
|
|
|
227
|
-
|
|
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
|
|
228
192
|
|
|
229
193
|
### Use Case: Marketplace Sale
|
|
230
194
|
|
|
@@ -237,49 +201,39 @@ The body cannot have its outfits or background changed until `block.timestamp >
|
|
|
237
201
|
6. After 7 days, Bob can undress and receive the hat and suit NFTs.
|
|
238
202
|
```
|
|
239
203
|
|
|
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
204
|
---
|
|
249
205
|
|
|
250
206
|
## Journey 5: Transfer a Decorated Banny
|
|
251
207
|
|
|
252
208
|
The resolver does not intercept or control body transfers. Body transfers happen on the `JB721TiersHook` contract. The resolver observes the ownership change lazily.
|
|
253
209
|
|
|
254
|
-
|
|
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
|
|
210
|
+
**Entry point**: `JB721TiersHook.safeTransferFrom(address from, address to, uint256 tokenId)` (standard ERC-721 transfer on the hook contract, not the resolver)
|
|
266
211
|
|
|
267
|
-
|
|
212
|
+
**Who can call**: The body owner, or an approved operator on the hook contract.
|
|
268
213
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
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
|
|
272
222
|
|
|
273
|
-
|
|
223
|
+
**Events**: None from the resolver. The hook emits the standard ERC-721 `Transfer(from, to, tokenId)`.
|
|
274
224
|
|
|
275
|
-
|
|
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
|
|
276
229
|
|
|
277
|
-
|
|
230
|
+
**What Alice can no longer do**: Alice cannot call `decorateBannyWith` or `lockOutfitChangesFor` for that body -- she no longer owns it.
|
|
278
231
|
|
|
279
|
-
|
|
280
|
-
- **
|
|
281
|
-
- **Body transferred
|
|
282
|
-
- **
|
|
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
|
|
283
237
|
|
|
284
238
|
---
|
|
285
239
|
|
|
@@ -287,31 +241,29 @@ Alice cannot call `decorateBannyWith` or `lockOutfitChangesFor` for that body --
|
|
|
287
241
|
|
|
288
242
|
A user who owns multiple bodies can move an equipped outfit from one body to another in a single call.
|
|
289
243
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
```solidity
|
|
293
|
-
decorateBannyWith(hook, newBodyId, 0, [outfitId])
|
|
294
|
-
```
|
|
244
|
+
**Entry point**: `Banny721TokenUriResolver.decorateBannyWith(address hook, uint256 newBodyId, 0, [outfitId])`
|
|
295
245
|
|
|
296
246
|
Where `outfitId` is currently equipped on `oldBodyId`, and the caller owns both bodies.
|
|
297
247
|
|
|
298
|
-
|
|
248
|
+
**Who can call**: An address that owns both `newBodyId` and the body currently wearing the outfit (`oldBodyId`).
|
|
299
249
|
|
|
300
|
-
|
|
301
|
-
- Caller owns `
|
|
302
|
-
- `
|
|
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`)
|
|
303
255
|
|
|
304
|
-
|
|
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
|
|
305
262
|
|
|
306
|
-
|
|
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.
|
|
263
|
+
**Events**: `DecorateBanny(hook, newBodyId, 0, [outfitId], caller)`
|
|
311
264
|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
- **Old body is locked**: Reverts `OutfitChangesLocked`. A locked source body keeps its currently equipped outfits and background until the lock expires, even if the caller owns both bodies.
|
|
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
|
|
315
267
|
|
|
316
268
|
---
|
|
317
269
|
|
|
@@ -319,53 +271,46 @@ Where `outfitId` is currently equipped on `oldBodyId`, and the caller owns both
|
|
|
319
271
|
|
|
320
272
|
### Step 1: Commit Hashes
|
|
321
273
|
|
|
322
|
-
**Entry
|
|
323
|
-
```solidity
|
|
324
|
-
function setSvgHashesOf(
|
|
325
|
-
uint256[] memory upcs,
|
|
326
|
-
bytes32[] memory svgHashes
|
|
327
|
-
) external onlyOwner
|
|
328
|
-
```
|
|
274
|
+
**Entry point**: `Banny721TokenUriResolver.setSvgHashesOf(uint256[] memory upcs, bytes32[] memory svgHashes)`
|
|
329
275
|
|
|
330
|
-
|
|
276
|
+
**Who can call**: Contract owner only (`onlyOwner` modifier, from OpenZeppelin `Ownable`).
|
|
331
277
|
|
|
332
278
|
**Parameters**:
|
|
333
|
-
- `upcs
|
|
334
|
-
- `svgHashes
|
|
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`
|
|
335
284
|
|
|
336
|
-
**
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
-
|
|
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
|
|
340
291
|
|
|
341
292
|
### Step 2: Upload Content
|
|
342
293
|
|
|
343
|
-
**Entry
|
|
344
|
-
```solidity
|
|
345
|
-
function setSvgContentsOf(
|
|
346
|
-
uint256[] memory upcs,
|
|
347
|
-
string[] calldata svgContents
|
|
348
|
-
) external
|
|
349
|
-
```
|
|
294
|
+
**Entry point**: `Banny721TokenUriResolver.setSvgContentsOf(uint256[] memory upcs, string[] calldata svgContents)`
|
|
350
295
|
|
|
351
|
-
|
|
296
|
+
**Who can call**: Anyone. Not restricted to owner -- anyone can upload content as long as it matches the committed hash.
|
|
352
297
|
|
|
353
298
|
**Parameters**:
|
|
354
|
-
- `upcs
|
|
355
|
-
- `svgContents
|
|
299
|
+
- `upcs` -- Array of universal product codes to upload content for
|
|
300
|
+
- `svgContents` -- Array of SVG content strings (without wrapping `<svg>` tags)
|
|
356
301
|
|
|
357
|
-
**State
|
|
358
|
-
|
|
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)`.
|
|
302
|
+
**State changes**:
|
|
303
|
+
1. For each pair: `_svgContentOf[upc] = svgContent`
|
|
363
304
|
|
|
364
|
-
|
|
305
|
+
**Events**: `SetSvgContent(uint256 indexed upc, string svgContent, address caller)` -- emitted once per UPC in the array
|
|
365
306
|
|
|
366
|
-
|
|
367
|
-
- **Content
|
|
368
|
-
- **
|
|
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
|
|
369
314
|
|
|
370
315
|
---
|
|
371
316
|
|
|
@@ -373,151 +318,208 @@ Line 1100. **Not restricted to owner** -- anyone can upload content as long as i
|
|
|
373
318
|
|
|
374
319
|
### Set Metadata
|
|
375
320
|
|
|
376
|
-
**Entry
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
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
|
|
384
335
|
|
|
385
|
-
|
|
336
|
+
**Events**: `SetMetadata(string description, string externalUrl, string baseUri, address caller)`
|
|
386
337
|
|
|
387
|
-
**
|
|
388
|
-
-
|
|
389
|
-
-
|
|
390
|
-
- `svgBaseUri = baseUri`
|
|
391
|
-
- All three fields are always overwritten. Pass current values for fields you do not want to change. Pass `""` to clear.
|
|
338
|
+
**Edge cases**:
|
|
339
|
+
- **Empty strings**: Valid. Clears the respective metadata field
|
|
340
|
+
- **Non-owner caller**: Reverts with OpenZeppelin `OwnableUnauthorizedAccount`
|
|
392
341
|
|
|
393
342
|
### Set Product Names
|
|
394
343
|
|
|
395
|
-
**Entry
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
uint256[] memory upcs,
|
|
399
|
-
string[] memory names
|
|
400
|
-
) external onlyOwner
|
|
401
|
-
```
|
|
344
|
+
**Entry point**: `Banny721TokenUriResolver.setProductNames(uint256[] memory upcs, string[] memory names)`
|
|
345
|
+
|
|
346
|
+
**Who can call**: Contract owner only (`onlyOwner` modifier).
|
|
402
347
|
|
|
403
|
-
|
|
348
|
+
**Parameters**:
|
|
349
|
+
- `upcs` -- Array of universal product codes to name
|
|
350
|
+
- `names` -- Array of display names for each product
|
|
404
351
|
|
|
405
|
-
**State
|
|
406
|
-
|
|
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`.
|
|
352
|
+
**State changes**:
|
|
353
|
+
1. For each pair: `_customProductNameOf[upc] = name`
|
|
409
354
|
|
|
410
|
-
|
|
355
|
+
**Events**: `SetProductName(uint256 indexed upc, string name, address caller)` -- emitted once per UPC in the array
|
|
411
356
|
|
|
412
|
-
|
|
413
|
-
- **
|
|
414
|
-
- **
|
|
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`
|
|
415
362
|
|
|
416
363
|
---
|
|
417
364
|
|
|
418
365
|
## Journey 9: View Functions -- Query Banny State
|
|
419
366
|
|
|
367
|
+
All view functions. No access restrictions, no state changes, no events.
|
|
368
|
+
|
|
420
369
|
### Get Attached Assets
|
|
421
370
|
|
|
422
|
-
|
|
423
|
-
function assetIdsOf(
|
|
424
|
-
address hook,
|
|
425
|
-
uint256 bannyBodyId
|
|
426
|
-
) public view returns (uint256 backgroundId, uint256[] memory outfitIds)
|
|
427
|
-
```
|
|
371
|
+
**Entry point**: `Banny721TokenUriResolver.assetIdsOf(address hook, uint256 bannyBodyId) public view returns (uint256 backgroundId, uint256[] memory outfitIds)`
|
|
428
372
|
|
|
429
|
-
|
|
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
|
|
430
378
|
|
|
431
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.
|
|
432
380
|
|
|
433
381
|
### Get Outfit Wearer
|
|
434
382
|
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
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
|
|
438
390
|
|
|
439
|
-
|
|
391
|
+
Returns the body ID wearing this outfit, or 0 if unworn. Verifies the outfit is still in the body's `_attachedOutfitIdsOf` array.
|
|
440
392
|
|
|
441
393
|
### Get Background User
|
|
442
394
|
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
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
|
|
446
402
|
|
|
447
|
-
|
|
403
|
+
Returns the body ID using this background, or 0 if unused. Verifies the background is still the body's `_attachedBackgroundIdOf` entry.
|
|
448
404
|
|
|
449
405
|
### Get SVG
|
|
450
406
|
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
uint256 tokenId,
|
|
455
|
-
bool shouldDressBannyBody,
|
|
456
|
-
bool shouldIncludeBackgroundOnBannyBody
|
|
457
|
-
) public view returns (string memory)
|
|
458
|
-
```
|
|
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).
|
|
459
410
|
|
|
460
|
-
|
|
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.
|
|
461
418
|
|
|
462
419
|
### Get Names
|
|
463
420
|
|
|
464
|
-
|
|
465
|
-
function namesOf(
|
|
466
|
-
address hook,
|
|
467
|
-
uint256 tokenId
|
|
468
|
-
) public view returns (string memory, string memory, string memory)
|
|
469
|
-
```
|
|
421
|
+
**Entry point**: `Banny721TokenUriResolver.namesOf(address hook, uint256 tokenId) public view returns (string memory, string memory, string memory)`
|
|
470
422
|
|
|
471
|
-
|
|
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)`.
|
|
472
430
|
|
|
473
431
|
### Get Lock Status
|
|
474
432
|
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
433
|
+
**Entry point**: `Banny721TokenUriResolver.outfitLockedUntil(address hook, uint256 upc) public view returns (uint256)`
|
|
434
|
+
|
|
435
|
+
**Who can call**: Anyone (public mapping).
|
|
478
436
|
|
|
479
|
-
|
|
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.
|
|
480
442
|
|
|
481
443
|
---
|
|
482
444
|
|
|
483
445
|
## Summary: State Machine per Banny Body
|
|
484
446
|
|
|
485
447
|
```
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
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
|
+
+-----------------+
|
|
519
482
|
```
|
|
520
483
|
|
|
521
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.
|
|
522
485
|
|
|
523
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 |
|