@bannynet/core-v6 0.0.10 → 0.0.12
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 +53 -13
- package/AUDIT_INSTRUCTIONS.md +68 -41
- package/CHANGE_LOG.md +28 -7
- package/README.md +72 -7
- package/RISKS.md +34 -8
- package/SKILLS.md +44 -3
- package/STYLE_GUIDE.md +1 -1
- package/USER_JOURNEYS.md +327 -325
- 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 +148 -24
- package/test/Banny721TokenUriResolver.t.sol +1 -1
- package/test/BannyAttacks.t.sol +1 -1
- package/test/DecorateFlow.t.sol +32 -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/README.md
CHANGED
|
@@ -8,6 +8,8 @@ On-chain composable avatar system for Juicebox 721 collections -- manages Banny
|
|
|
8
8
|
|
|
9
9
|
Banny is a composable NFT character system built on top of Juicebox 721 hooks. Each Banny is a body NFT (Alien, Pink, Orange, or Original) that can wear outfit NFTs and sit on a background NFT. The resolver composes these layers into a single SVG image, fully on-chain.
|
|
10
10
|
|
|
11
|
+
Outfit changes can be locked for 7 days via `lockOutfitChangesFor`. This proves that a Banny's appearance is stable -- useful for PFPs, social displays, or any context where others rely on a Banny looking the way it does right now. While locked, neither the owner nor anyone else can change or remove the body's outfits or background.
|
|
12
|
+
|
|
11
13
|
### How It Works
|
|
12
14
|
|
|
13
15
|
```
|
|
@@ -23,7 +25,8 @@ Banny is a composable NFT character system built on top of Juicebox 721 hooks. E
|
|
|
23
25
|
→ Body's tokenURI now renders the full dressed composition
|
|
24
26
|
|
|
|
25
27
|
4. Outfit lock (optional): lockOutfitChangesFor(hook, bodyId)
|
|
26
|
-
→ Freezes outfit changes for 7 days
|
|
28
|
+
→ Freezes outfit and background changes for 7 days
|
|
29
|
+
→ Prevents moving currently equipped assets away through another body's decoration call
|
|
27
30
|
→ Proves the Banny's look is stable (useful for PFPs, displays)
|
|
28
31
|
|
|
|
29
32
|
5. SVG content is stored on-chain via a two-step process:
|
|
@@ -32,6 +35,41 @@ Banny is a composable NFT character system built on top of Juicebox 721 hooks. E
|
|
|
32
35
|
→ Falls back to IPFS base URI if on-chain content not yet stored
|
|
33
36
|
```
|
|
34
37
|
|
|
38
|
+
### Decoration Flow
|
|
39
|
+
|
|
40
|
+
```mermaid
|
|
41
|
+
sequenceDiagram
|
|
42
|
+
participant Owner as Body Owner
|
|
43
|
+
participant Resolver as Banny721TokenUriResolver
|
|
44
|
+
participant Hook as 721 Hook (NFT Contract)
|
|
45
|
+
|
|
46
|
+
Owner->>Resolver: decorateBannyWith(hook, bodyId, bgId, outfitIds)
|
|
47
|
+
Resolver->>Hook: ownerOf(bodyId) -- verify caller owns body
|
|
48
|
+
Hook-->>Resolver: owner address
|
|
49
|
+
|
|
50
|
+
Note over Resolver: Check body is not locked
|
|
51
|
+
|
|
52
|
+
alt Background is changing
|
|
53
|
+
Resolver->>Hook: transferFrom(owner, resolver, bgId)
|
|
54
|
+
Note over Resolver: Store bgId as body's background
|
|
55
|
+
opt Previous background exists
|
|
56
|
+
Resolver->>Hook: transferFrom(resolver, owner, prevBgId)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
loop Each outfit in outfitIds
|
|
61
|
+
Resolver->>Hook: transferFrom(owner, resolver, outfitId)
|
|
62
|
+
Note over Resolver: Track outfitId as worn by body
|
|
63
|
+
opt Previous outfit in same category
|
|
64
|
+
Resolver->>Hook: transferFrom(resolver, owner, prevOutfitId)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
Note over Resolver: tokenURI(bodyId) now renders composed SVG
|
|
69
|
+
Owner->>Resolver: tokenURI(bodyId)
|
|
70
|
+
Resolver-->>Owner: base64-encoded JSON with layered SVG
|
|
71
|
+
```
|
|
72
|
+
|
|
35
73
|
### Asset Categories
|
|
36
74
|
|
|
37
75
|
| Category ID | Name | Slot Rules |
|
|
@@ -92,20 +130,32 @@ Requires `via_ir = true` in foundry.toml due to stack depth in SVG composition.
|
|
|
92
130
|
| Command | Description |
|
|
93
131
|
|---------|-------------|
|
|
94
132
|
| `forge build` | Compile contracts (requires via-IR) |
|
|
95
|
-
| `forge test` | Run all tests (
|
|
133
|
+
| `forge test` | Run all tests (14 test files: functionality, attacks, decoration flows, fork integration, transfer lifecycle, audit gaps, QA, regressions) |
|
|
96
134
|
| `forge test -vvv` | Run tests with full trace |
|
|
97
135
|
|
|
98
136
|
## Repository Layout
|
|
99
137
|
|
|
100
138
|
```
|
|
101
139
|
src/
|
|
102
|
-
Banny721TokenUriResolver.sol # Sole contract (~1,
|
|
140
|
+
Banny721TokenUriResolver.sol # Sole contract (~1,428 lines)
|
|
103
141
|
interfaces/
|
|
104
142
|
IBanny721TokenUriResolver.sol # Public interface + events
|
|
105
143
|
test/
|
|
106
|
-
Banny721TokenUriResolver.t.sol # Unit tests
|
|
107
|
-
BannyAttacks.t.sol # Security/adversarial tests
|
|
108
|
-
DecorateFlow.t.sol # Decoration flow tests
|
|
144
|
+
Banny721TokenUriResolver.t.sol # Unit tests
|
|
145
|
+
BannyAttacks.t.sol # Security/adversarial tests
|
|
146
|
+
DecorateFlow.t.sol # Decoration flow tests
|
|
147
|
+
Fork.t.sol # Fork integration tests
|
|
148
|
+
OutfitTransferLifecycle.t.sol # Transfer lifecycle tests
|
|
149
|
+
TestAuditGaps.sol # Audit gap coverage (meta-tx, SVG edge cases)
|
|
150
|
+
TestQALastMile.t.sol # QA tests (gas, round-trip, fallback)
|
|
151
|
+
regression/
|
|
152
|
+
ArrayLengthValidation.t.sol # Input validation regression
|
|
153
|
+
BodyCategoryValidation.t.sol # Body category regression
|
|
154
|
+
BurnedTokenCheck.t.sol # Burned token regression
|
|
155
|
+
CEIReorder.t.sol # CEI ordering regression
|
|
156
|
+
ClearMetadata.t.sol # Metadata clearing regression
|
|
157
|
+
MsgSenderEvents.t.sol # Event emission regression
|
|
158
|
+
RemovedTierDesync.t.sol # Removed tier desync regression
|
|
109
159
|
script/
|
|
110
160
|
Deploy.s.sol # Sphinx multi-chain deployment
|
|
111
161
|
Drop1.s.sol # Outfit drop deployment
|
|
@@ -126,9 +176,24 @@ script/
|
|
|
126
176
|
| `setProductNames` | Contract owner only |
|
|
127
177
|
| `setMetadata` | Contract owner only |
|
|
128
178
|
|
|
179
|
+
## Supported Chains
|
|
180
|
+
|
|
181
|
+
Deployed via Sphinx deterministic deployment (`script/Deploy.s.sol`).
|
|
182
|
+
|
|
183
|
+
| Network | Chain ID |
|
|
184
|
+
|---------|----------|
|
|
185
|
+
| Ethereum | 1 |
|
|
186
|
+
| Optimism | 10 |
|
|
187
|
+
| Base | 8453 |
|
|
188
|
+
| Arbitrum | 42161 |
|
|
189
|
+
| Ethereum Sepolia | 11155111 |
|
|
190
|
+
| Optimism Sepolia | 11155420 |
|
|
191
|
+
| Base Sepolia | 84532 |
|
|
192
|
+
| Arbitrum Sepolia | 421614 |
|
|
193
|
+
|
|
129
194
|
## Risks
|
|
130
195
|
|
|
131
|
-
- **Outfit custody:** Attached outfits and backgrounds are held by the resolver contract. If
|
|
196
|
+
- **Outfit custody:** Attached outfits and backgrounds are held by the resolver contract. If a return transfer fails (e.g., owner is a non-receiver contract), the asset is retained in the attached list rather than stranded — the owner can retry once the issue is resolved. Permanently unrecoverable assets (burned NFTs) create phantom entries in SVG rendering.
|
|
132
197
|
- **7-day lock is fixed.** Cannot be shortened or cancelled once set. The lock duration is hardcoded.
|
|
133
198
|
- **SVG immutability.** Once SVG content is stored on-chain for a UPC, it cannot be changed. A mistake in the content requires deploying a new resolver.
|
|
134
199
|
- **Hash registration is owner-only, but content upload is not.** Anyone can call `setSvgContentsOf` as long as the content matches the registered hash. This is by design for lazy uploads.
|
package/RISKS.md
CHANGED
|
@@ -10,20 +10,19 @@
|
|
|
10
10
|
## 2. Economic / Manipulation Risks
|
|
11
11
|
|
|
12
12
|
- **Outfit theft via banny body transfer.** Equipped outfits and backgrounds travel with the banny body NFT on transfer. If a banny body is sold with valuable outfits equipped, the buyer gains control of all equipped items. Sellers must unequip before selling. Marketplaces may not surface this risk.
|
|
13
|
-
- **try-catch silent failures.** `_tryTransferFrom` silently catches all transfer failures
|
|
14
|
-
- **Lock griefing.** `lockOutfitChangesFor` extends the lock to `block.timestamp + 7 days`. Locking just before selling prevents the buyer from changing outfits for up to 7 days.
|
|
13
|
+
- **try-catch silent failures with retention.** `_tryTransferFrom` silently catches all transfer failures and returns `false`. When a transfer fails, the resolver preserves the attachment record instead of clearing state. For backgrounds, the entire background change is aborted. For outfits, failed-to-return items are retained in the attached list via `_storeOutfitsWithRetained`. This prevents NFT stranding — assets remain tracked and recoverable once the transfer issue is resolved (e.g., the owner contract becomes receivable). However, if an outfit NFT is burned or its tier removed, the retained record refers to a non-existent asset, creating a phantom entry in the SVG rendering.
|
|
14
|
+
- **Lock griefing.** `lockOutfitChangesFor` extends the lock to `block.timestamp + 7 days`. Locking just before selling prevents the buyer from changing outfits for up to 7 days. The lock now also freezes reassignment of currently equipped outfits/backgrounds away from that body during the lock window.
|
|
15
15
|
|
|
16
16
|
## 3. Access Control
|
|
17
17
|
|
|
18
|
-
- **No hook validation.** Any address can be passed as `hook`. A malicious hook can return `_msgSender()` from `ownerOf()` to pass authorization checks, execute arbitrary code during `safeTransferFrom`, or return manipulated tier data from `STORE().tierOfTokenId()`.
|
|
18
|
+
- **No hook validation (HIGH impact).** Any address can be passed as `hook`. A malicious hook can return `_msgSender()` from `ownerOf()` to pass authorization checks, execute arbitrary code during `safeTransferFrom`, or return manipulated tier data from `STORE().tierOfTokenId()`.
|
|
19
19
|
- **SVG content upload is permissionless (with hash).** `setSvgContentsOf` only requires the content to match a pre-committed hash. Safe if hashes are correctly committed.
|
|
20
20
|
- **onERC721Received restriction.** Only accepts NFTs when `operator == address(this)`. `transferFrom` (non-safe) bypasses this -- NFTs sent via `transferFrom` are permanently locked with no rescue function.
|
|
21
21
|
|
|
22
22
|
## 4. DoS Vectors
|
|
23
23
|
|
|
24
|
-
- **
|
|
25
|
-
- **
|
|
26
|
-
- **External hook calls in view functions.** `tokenUriOf` and `svgOf` call into the hook's store multiple times per outfit. A malicious hook that consumes excessive gas or reverts can make token metadata unretrievable.
|
|
24
|
+
- **External call iteration scales with outfit count.** `_attachedOutfitIdsOf[hook][bannyBodyId]` is replaced wholesale on each `decorateBannyWith` call (not appended to), so the array is bounded by the number of currently equipped outfits, not cumulative history. However, `decorateBannyWith` iterates over both the previous and new outfit arrays to diff them (transferring removed outfits back and new outfits in), so gas cost scales with the number of outfits being equipped/unequipped in a single call.
|
|
25
|
+
- **External hook calls in view functions.** `tokenUriOf` and `svgOf` call into the hook's store multiple times per outfit. A malicious hook that consumes excessive gas or reverts can make token metadata unretrievable. Measured: `tokenUriOf` with a well-behaved hook and 9 equipped outfits costs ~609k gas (see `test_tokenUri_gasSnapshot_9outfits`). The practical ceiling for a malicious hook is bounded only by the caller's gas limit — RPC nodes typically cap `eth_call` at 30M+ gas, so even expensive hooks won't fail for off-chain reads, but on-chain consumers (e.g., other contracts calling `tokenURI`) could revert.
|
|
27
26
|
|
|
28
27
|
## 5. Integration Risks
|
|
29
28
|
|
|
@@ -37,6 +36,33 @@
|
|
|
37
36
|
- Every outfit held by this contract has a corresponding `_wearerOf[hook][outfitId]` pointing to a valid banny body.
|
|
38
37
|
- Every background held by this contract has a corresponding `_userOf[hook][backgroundId]` pointing to a valid banny body.
|
|
39
38
|
- `outfitLockedUntil[hook][bannyBodyId]` is monotonically non-decreasing per banny body (lock can only be extended, never shortened).
|
|
40
|
-
- After `decorateBannyWith`, all previously equipped outfits not in the new set are transferred back to `_msgSender()`
|
|
41
|
-
- `_attachedOutfitIdsOf[hook][bannyBodyId]`
|
|
39
|
+
- After `decorateBannyWith`, all previously equipped outfits not in the new set are either transferred back to `_msgSender()` or retained in the attached list if the transfer failed.
|
|
40
|
+
- `_attachedOutfitIdsOf[hook][bannyBodyId]` contains the outfitIds passed to the most recent `decorateBannyWith` call, plus any retained outfits whose return transfer failed. Category exclusivity is enforced on the merged set (retained + new outfits), not just the new outfit set alone.
|
|
42
41
|
- SVG content integrity: `keccak256(_svgContentOf[upc]) == svgHashOf[upc]` for all populated entries.
|
|
42
|
+
- NFT custody balance: the number of outfit NFTs held by this contract (`IERC721(hook).balanceOf(address(this))`) equals the total number of outfits currently equipped across all banny bodies for that hook. Violations indicate phantom outfits (equipped in state but NFT lost via try-catch silent failure) or orphaned NFTs (held by contract but not tracked in `_wearerOf`).
|
|
43
|
+
|
|
44
|
+
## 7. Accepted Behaviors
|
|
45
|
+
|
|
46
|
+
### 7.1 Failed transfers retain attachment records (anti-stranding)
|
|
47
|
+
|
|
48
|
+
`_tryTransferFrom` catches all transfer failures and returns `false`. When returning a previously equipped item fails, the resolver preserves the attachment record rather than clearing state:
|
|
49
|
+
|
|
50
|
+
- **Backgrounds**: If returning the old background fails, the entire background change is aborted (`return` in `_decorateBannyWithBackground`). The old background stays attached and the new one is not equipped.
|
|
51
|
+
- **Background removal**: If returning the background fails during removal (backgroundId=0), `_attachedBackgroundIdOf` is not cleared. The background stays attached.
|
|
52
|
+
- **Outfits**: Failed-to-return outfits remain non-zero in the `previousOutfitIds` array. `_storeOutfitsWithRetained` appends them to the new outfit list, preserving their attachment record.
|
|
53
|
+
|
|
54
|
+
This prevents NFT stranding — assets held by the resolver stay tracked and recoverable. Once the transfer issue is resolved (e.g., the owner contract implements `IERC721Receiver`), a subsequent `decorateBannyWith` call will successfully return the retained items.
|
|
55
|
+
|
|
56
|
+
For permanently unrecoverable assets (burned NFTs, removed tiers), the retained record creates a phantom entry in the SVG rendering and attached list. This is cosmetically incorrect but not economically exploitable — phantom entries cannot be transferred or sold. The alternative — reverting on any failed transfer — would make `decorateBannyWith` fragile: a single burned outfit would prevent the banny owner from changing ANY outfits.
|
|
57
|
+
|
|
58
|
+
### 7.2 Lock griefing window is bounded at 7 days
|
|
59
|
+
|
|
60
|
+
`lockOutfitChangesFor` extends the lock to `block.timestamp + 7 days`. A seller who locks just before transferring the banny forces the buyer to wait up to 7 days. This is accepted because: (1) marketplaces can check `outfitLockedUntil` before displaying the item, (2) the lock duration is fixed (not owner-configurable), and (3) the lock prevents a more severe attack where a buyer immediately strips valuable outfits — the lock gives the previous owner time to arrange the sale intentionally.
|
|
61
|
+
|
|
62
|
+
### 7.3 On-chain SVG rendering gas is well within limits
|
|
63
|
+
|
|
64
|
+
`tokenUriOf` constructs full SVGs on-chain with string concatenation. Measured gas ceiling: ~609K gas for the worst case (9 non-conflicting outfits + background with on-chain SVG content), well within typical RPC node limits (30M+). Regression test: `test_tokenUri_gasSnapshot_9outfits` in `test/TestQALastMile.t.sol`.
|
|
65
|
+
|
|
66
|
+
### 7.4 Reentrancy in non-guarded functions is harmless
|
|
67
|
+
|
|
68
|
+
`lockOutfitChangesFor` and all view functions (`tokenUriOf`, `svgOf`) are not protected by `nonReentrant`. A malicious hook's `STORE().tierOfTokenId()` could re-enter `lockOutfitChangesFor` during a `tokenUriOf` call, but this is harmless -- `lockOutfitChangesFor` only extends the lock timestamp (monotonically non-decreasing) and has no state that could be corrupted by reentrancy. The view functions themselves are read-only at the contract level (no storage writes), so reentrancy through them cannot extract value.
|
package/SKILLS.md
CHANGED
|
@@ -23,7 +23,7 @@ On-chain composable NFT avatar system that renders Banny characters with layered
|
|
|
23
23
|
|
|
24
24
|
| Function | What it does |
|
|
25
25
|
|----------|-------------|
|
|
26
|
-
| `decorateBannyWith(hook, bannyBodyId, backgroundId, outfitIds)` | Attaches background and outfits to a body. Transfers assets to contract custody. Validates ownership, lock status, category ordering, slot conflicts.
|
|
26
|
+
| `decorateBannyWith(hook, bannyBodyId, backgroundId, outfitIds)` | Attaches background and outfits to a body. Transfers assets to contract custody. Validates ownership, lock status, category ordering, slot conflicts. Attempts to return previously attached items to caller — if a return fails, the item is retained (not stranded). Emits `DecorateBanny`. |
|
|
27
27
|
| `lockOutfitChangesFor(hook, bannyBodyId)` | Locks a body's outfit for 7 days. Cannot accelerate an existing lock. Caller must own the body NFT. |
|
|
28
28
|
|
|
29
29
|
### Views
|
|
@@ -58,6 +58,46 @@ On-chain composable NFT avatar system that renders Banny characters with layered
|
|
|
58
58
|
| `@openzeppelin/contracts` | `Ownable`, `ReentrancyGuard`, `ERC2771Context`, `IERC721Receiver`, `Strings` | Access control, reentrancy protection, meta-transactions, safe NFT receipt, string utilities |
|
|
59
59
|
| `lib/base64` | `Base64` | Base64 encoding for on-chain SVG and JSON metadata |
|
|
60
60
|
|
|
61
|
+
### Deployment / Setup Sequence
|
|
62
|
+
|
|
63
|
+
The resolver is connected to a 721 hook in one of two ways:
|
|
64
|
+
|
|
65
|
+
1. **At hook deployment**: Pass the resolver address as `tokenUriResolver` in `JBDeploy721TiersHookConfig` when calling `IJB721TiersHookDeployer.deployHookFor(...)`. The hook's `initialize()` stores it in the hook store via `recordSetTokenUriResolver`.
|
|
66
|
+
2. **After deployment**: Call `hook.setMetadata(...)` with the resolver address as the `tokenUriResolver` parameter. Requires `SET_721_METADATA` permission. Pass `IJB721TokenUriResolver(address(this))` as a sentinel value to leave it unchanged, since `address(0)` clears the resolver.
|
|
67
|
+
|
|
68
|
+
Once set, the hook's store maps `tokenUriResolverOf[hook]` to the resolver address. Any `tokenURI(tokenId)` call on the hook delegates to `resolver.tokenUriOf(hook, tokenId)`, which composes the on-chain SVG.
|
|
69
|
+
|
|
70
|
+
## Rendering Order
|
|
71
|
+
|
|
72
|
+
The composed SVG layers elements back-to-front. Later layers are drawn on top of earlier layers. The full rendering order for a dressed body is:
|
|
73
|
+
|
|
74
|
+
1. **Background** (category 1) -- if attached and `shouldIncludeBackgroundOnBannyBody` is true
|
|
75
|
+
2. **Body** (category 0) -- the base Banny character with color-fill styles (`b1`-`b4`, `a1`-`a3`)
|
|
76
|
+
3. **Backside** (category 2) -- rendered behind the body in the SVG source but layered by the SVG's internal z-ordering
|
|
77
|
+
4. **Necklace** (category 3) -- default injected if none attached; custom necklaces are deferred and rendered after suit top (category 11)
|
|
78
|
+
5. **Head** (category 4) -- if present, suppresses default injection of eyes, mouth
|
|
79
|
+
6. **Eyes** (category 5) -- default alien or standard eyes injected if no head and no explicit eyes
|
|
80
|
+
7. **Glasses** (category 6)
|
|
81
|
+
8. **Mouth** (category 7) -- default mouth injected if no head and no explicit mouth
|
|
82
|
+
9. **Legs** (category 8)
|
|
83
|
+
10. **Suit** (category 9)
|
|
84
|
+
11. **Suit Bottom** (category 10)
|
|
85
|
+
12. **Suit Top** (category 11)
|
|
86
|
+
13. **Custom Necklace** -- rendered here (after suit top) if a custom necklace was provided, so it layers on top of clothing
|
|
87
|
+
14. **Headtop** (category 12)
|
|
88
|
+
15. **Hand** (category 13)
|
|
89
|
+
16. **Special overlays** (categories 14-17) -- suit, legs, head, body specials
|
|
90
|
+
|
|
91
|
+
For non-body tokens (outfits, backgrounds), the item is rendered on a grayscale mannequin Banny (`fill:#808080`) for preview.
|
|
92
|
+
|
|
93
|
+
## SVG Format Requirements
|
|
94
|
+
|
|
95
|
+
- **Canvas**: All SVGs are rendered within a `400x400` viewport: `<svg width="400" height="400" viewBox="0 0 400 400">`.
|
|
96
|
+
- **Fragment, not document**: SVG content stored via `setSvgContentsOf` must NOT include the parent `<svg>` element. The resolver wraps all fragments in the outer SVG element with shared styles (`.o{fill:#050505;}.w{fill:#f9f9f9;}`).
|
|
97
|
+
- **Hash verification only**: The contract validates uploaded SVG content against a pre-registered `keccak256` hash but performs no structural validation (no dimension checks, no viewBox enforcement). Ensuring content renders correctly at 400x400 is the uploader's responsibility.
|
|
98
|
+
- **Body color classes**: Body SVGs use CSS classes `b1`-`b4` (body fills) and `a1`-`a3` (accent fills) which are injected as `<style>` elements based on the body's UPC. Outfit SVGs can reference `.o` (dark, #050505) and `.w` (light, #f9f9f9) classes provided by the wrapper.
|
|
99
|
+
- **Immutability**: Once stored, SVG content cannot be changed. A content or hash error requires deploying a new resolver instance.
|
|
100
|
+
|
|
61
101
|
## Key Types
|
|
62
102
|
|
|
63
103
|
### Asset Categories
|
|
@@ -141,7 +181,7 @@ On-chain composable NFT avatar system that renders Banny characters with layered
|
|
|
141
181
|
| `_svgContentOf` | `upc => string` | On-chain SVG content (immutable once set) |
|
|
142
182
|
| `svgHashOf` | `upc => bytes32` | Expected SVG content hash (immutable once set) |
|
|
143
183
|
| `_customProductNameOf` | `upc => string` | Human-readable product name |
|
|
144
|
-
| `outfitLockedUntil` | `hook =>
|
|
184
|
+
| `outfitLockedUntil` | `hook => bannyBodyId => uint256` | Timestamp until outfit changes locked (declared as `upc` but keyed by token ID) |
|
|
145
185
|
| `_userOf` | `hook => backgroundId => uint256` | Which body uses a background |
|
|
146
186
|
| `_wearerOf` | `hook => outfitId => uint256` | Which body wears an outfit |
|
|
147
187
|
| `svgBaseUri` | `string` | IPFS/HTTP base URI for fallback SVG loading |
|
|
@@ -157,7 +197,7 @@ On-chain composable NFT avatar system that renders Banny characters with layered
|
|
|
157
197
|
5. **Strict ascending category order.** `decorateBannyWith` requires outfits passed in ascending category order. Reverts with `UnorderedCategories` if violated.
|
|
158
198
|
6. **Slot conflicts block combinations.** Head (4) blocks eyes (5), glasses (6), mouth (7), and headtop (12). Full suit (9) blocks suit top (11) and suit bottom (10). These are enforced at decoration time.
|
|
159
199
|
7. **Default injection.** If no explicit necklace, eyes, or mouth outfit is attached, the resolver auto-injects defaults during SVG rendering. Alien bodies get `DEFAULT_ALIEN_EYES`; others get `DEFAULT_STANDARD_EYES`.
|
|
160
|
-
8. **Outfits are held in contract custody.** Attached outfits and backgrounds are transferred to `address(this)` via `safeTransferFrom`. They are returned to the caller when detached (by passing a new outfit set that excludes them).
|
|
200
|
+
8. **Outfits are held in contract custody.** Attached outfits and backgrounds are transferred to `address(this)` via `safeTransferFrom`. They are returned to the caller when detached (by passing a new outfit set that excludes them). If the return transfer fails (e.g., owner is a non-receiver contract), the asset is **retained** in the attached list rather than stranded — the owner can retry once the transfer issue is resolved.
|
|
161
201
|
9. **Complex outfit ownership rules.** If an outfit is unworn: only its owner can attach it. If already worn by another body: the caller must own THAT body to reassign the outfit. This allows body owners to swap outfits between their own bodies.
|
|
162
202
|
10. **Token ID encoding.** `tokenId = upc * 1_000_000_000 + sequenceNumber`. The resolver extracts UPC via integer division and sequence via modulo to display inventory counts like "42/100".
|
|
163
203
|
11. **`onERC721Received` only accepts self-transfers.** Reverts unless `operator == address(this)`. The contract calls `safeTransferFrom` on itself during decoration, triggering this callback.
|
|
@@ -166,6 +206,7 @@ On-chain composable NFT avatar system that renders Banny characters with layered
|
|
|
166
206
|
14. **Mannequin rendering.** Outfit and background tokens (not bodies) are rendered on a grayscale mannequin Banny for preview purposes. The mannequin has `fill:#808080` styling.
|
|
167
207
|
15. **ERC2771 meta-transaction support.** Constructor takes a `trustedForwarder` address. All `_msgSender()` calls use `ERC2771Context`, allowing relayers to submit decoration transactions on behalf of users.
|
|
168
208
|
16. **Empty metadata fields clear the value.** `setMetadata` always writes all three fields. Passing an empty string clears that field. To keep a field unchanged, pass its current value.
|
|
209
|
+
17. **Outfit locks persist through transfers.** The `outfitLockedUntil` mapping is keyed by `(hook, bannyBodyId)` and is never cleared on transfer. When a locked body NFT is transferred, the new owner inherits the lock and cannot change outfits until it expires. Equipped outfits also travel with the body -- the new owner can unequip them after the lock expires. Sellers should unequip valuable outfits before transferring a body.
|
|
169
210
|
|
|
170
211
|
## Example Integration
|
|
171
212
|
|
package/STYLE_GUIDE.md
CHANGED
|
@@ -21,7 +21,7 @@ One contract/interface/struct/enum per file. Name the file after the type it con
|
|
|
21
21
|
|
|
22
22
|
```solidity
|
|
23
23
|
// Contracts — pin to exact version
|
|
24
|
-
pragma solidity 0.8.26;
|
|
24
|
+
pragma solidity ^0.8.26;
|
|
25
25
|
|
|
26
26
|
// Interfaces, structs, enums — caret for forward compatibility
|
|
27
27
|
pragma solidity ^0.8.0;
|