@bannynet/core-v6 0.0.11 → 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 +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 +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 +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/ADMINISTRATION.md
CHANGED
|
@@ -6,40 +6,40 @@ Admin privileges and their scope in banny-retail-v6. The contract (`Banny721Toke
|
|
|
6
6
|
|
|
7
7
|
| Role | How Assigned | Scope |
|
|
8
8
|
|------|-------------|-------|
|
|
9
|
-
| **Contract Owner** | Set via constructor `Ownable(owner)
|
|
10
|
-
| **NFT Body Owner** | Whoever holds the banny body token on the JB721TiersHook contract. Checked via `IERC721(hook).ownerOf(bannyBodyId)
|
|
9
|
+
| **Contract Owner** | Set via constructor `Ownable(owner)`. Transferable via OpenZeppelin `transferOwnership()`. | Global: SVG asset management, metadata, product naming |
|
|
10
|
+
| **NFT Body Owner** | Whoever holds the banny body token on the JB721TiersHook contract. Checked via `IERC721(hook).ownerOf(bannyBodyId)`. | Per-token: decoration, locking |
|
|
11
11
|
| **NFT Outfit/Background Owner** | Whoever holds the outfit or background token on the hook contract. | Per-token: authorize equipping to a body they own |
|
|
12
12
|
| **Anyone (Permissionless)** | No restriction. | Upload SVG content (if matching hash exists) |
|
|
13
|
-
| **Trusted Forwarder** | Set at construction via `ERC2771Context(trustedForwarder)
|
|
13
|
+
| **Trusted Forwarder** | Set at construction via `ERC2771Context(trustedForwarder)`. Immutable after deploy. | Meta-transaction relay: `_msgSender()` resolves to the relayed sender |
|
|
14
14
|
|
|
15
15
|
## Privileged Functions
|
|
16
16
|
|
|
17
17
|
### Banny721TokenUriResolver -- Owner-Only Functions
|
|
18
18
|
|
|
19
|
-
| Function |
|
|
20
|
-
|
|
21
|
-
| `setMetadata(description, url, baseUri)` |
|
|
22
|
-
| `setProductNames(upcs, names)` |
|
|
23
|
-
| `setSvgHashesOf(upcs, svgHashes)` |
|
|
19
|
+
| Function | Guard | What It Does |
|
|
20
|
+
|----------|-------|-------------|
|
|
21
|
+
| `setMetadata(description, url, baseUri)` | `onlyOwner` | Overwrites the token metadata description, external URL, and SVG base URI. All three fields are always written (pass current value to keep, empty string to clear). |
|
|
22
|
+
| `setProductNames(upcs, names)` | `onlyOwner` | Sets custom display names for products identified by UPC. Names are stored in `_customProductNameOf` mapping. Can overwrite previously set names. |
|
|
23
|
+
| `setSvgHashesOf(upcs, svgHashes)` | `onlyOwner` | Commits keccak256 hashes for SVG content keyed by UPC. Each hash can only be set once -- reverts with `HashAlreadyStored` if the UPC already has a hash. This is the gating step that controls which SVG content can later be uploaded permissionlessly. |
|
|
24
24
|
|
|
25
25
|
### Banny721TokenUriResolver -- Permissionless Functions
|
|
26
26
|
|
|
27
|
-
| Function |
|
|
28
|
-
|
|
29
|
-
| `setSvgContentsOf(upcs, svgContents)` |
|
|
27
|
+
| Function | Guard | What It Does |
|
|
28
|
+
|----------|-------|-------------|
|
|
29
|
+
| `setSvgContentsOf(upcs, svgContents)` | None (anyone) | Stores SVG content for a UPC, but only if: (1) a hash was previously committed by the owner via `setSvgHashesOf`, (2) `keccak256(content) == storedHash`, and (3) content has not already been stored (`ContentsAlreadyStored`). This is the permissionless "lazy upload" mechanism. |
|
|
30
30
|
|
|
31
31
|
### Banny721TokenUriResolver -- NFT-Owner Functions
|
|
32
32
|
|
|
33
|
-
| Function |
|
|
34
|
-
|
|
35
|
-
| `decorateBannyWith(hook, bannyBodyId, backgroundId, outfitIds)` |
|
|
36
|
-
| `lockOutfitChangesFor(hook, bannyBodyId)` |
|
|
33
|
+
| Function | Guard | What It Does |
|
|
34
|
+
|----------|-------|-------------|
|
|
35
|
+
| `decorateBannyWith(hook, bannyBodyId, backgroundId, outfitIds)` | `_checkIfSenderIsOwner` + `nonReentrant` | Equips/unequips outfits and background on a banny body. Caller must own the body token. For each outfit/background: caller must own the asset directly, OR own the banny body that currently wears/uses it. Transfers outfit NFTs into the resolver contract (custodial). |
|
|
36
|
+
| `lockOutfitChangesFor(hook, bannyBodyId)` | `_checkIfSenderIsOwner` | Locks a banny body's outfit for 7 days (`_LOCK_DURATION`). Lock can only be extended, never shortened (`CantAccelerateTheLock`). Prevents `decorateBannyWith` during the lock period. |
|
|
37
37
|
|
|
38
38
|
### Banny721TokenUriResolver -- Restricted Receiver
|
|
39
39
|
|
|
40
|
-
| Function |
|
|
41
|
-
|
|
42
|
-
| `onERC721Received(operator, from, tokenId, data)` |
|
|
40
|
+
| Function | Guard | What It Does |
|
|
41
|
+
|----------|-------|-------------|
|
|
42
|
+
| `onERC721Received(operator, from, tokenId, data)` | `operator == address(this)` | Only accepts incoming NFT transfers when the resolver itself initiated the transfer. Rejects all direct user transfers to the resolver contract with `UnauthorizedTransfer`. |
|
|
43
43
|
|
|
44
44
|
## Asset Management
|
|
45
45
|
|
|
@@ -49,37 +49,48 @@ Admin privileges and their scope in banny-retail-v6. The contract (`Banny721Toke
|
|
|
49
49
|
2. **Anyone** can then upload the actual SVG content via `setSvgContentsOf()`, provided the content's keccak256 hash matches the owner-committed hash. This is intentional: the owner sets the commitment, and anyone can fulfill it (useful for gas-efficient lazy uploading).
|
|
50
50
|
|
|
51
51
|
**Immutability of stored content:**
|
|
52
|
-
- Once a hash is set for a UPC, it cannot be changed (
|
|
53
|
-
- Once content is uploaded for a UPC, it cannot be replaced (
|
|
52
|
+
- Once a hash is set for a UPC, it cannot be changed (`HashAlreadyStored`).
|
|
53
|
+
- Once content is uploaded for a UPC, it cannot be replaced (`ContentsAlreadyStored`).
|
|
54
54
|
- There is no function to delete or modify stored SVG hashes or content.
|
|
55
55
|
|
|
56
56
|
**Product names:**
|
|
57
|
-
- Can be overwritten by the owner at any time via `setProductNames()`. There is no immutability guard on names -- the owner can rename products freely. The built-in names for UPCs 1-4 (Alien, Pink, Orange, Original) are hardcoded in `_productNameOf()`
|
|
57
|
+
- Can be overwritten by the owner at any time via `setProductNames()`. There is no immutability guard on names -- the owner can rename products freely. The built-in names for UPCs 1-4 (Alien, Pink, Orange, Original) are hardcoded in `_productNameOf()` and cannot be overridden.
|
|
58
58
|
|
|
59
59
|
**Metadata:**
|
|
60
60
|
- `svgDescription`, `svgExternalUrl`, and `svgBaseUri` can be changed by the owner at any time via `setMetadata()`. All three are always overwritten in a single call.
|
|
61
61
|
|
|
62
|
+
## Custodial Model
|
|
63
|
+
|
|
64
|
+
When a user equips an outfit or background on a banny body via `decorateBannyWith()`, the outfit/background NFT is transferred from the user's wallet into the `Banny721TokenUriResolver` contract. The resolver holds these NFTs in custody until the user unequips them (by calling `decorateBannyWith()` again with different outfits).
|
|
65
|
+
|
|
66
|
+
**Trust assumptions:**
|
|
67
|
+
- Users must trust that the resolver contract's `decorateBannyWith()` function correctly returns NFTs when outfits are changed. There is no separate `withdraw()` or `rescue()` function.
|
|
68
|
+
- The `onERC721Received` guard (`operator == address(this)`) prevents anyone from sending arbitrary NFTs to the resolver. Only self-initiated transfers during `decorateBannyWith()` are accepted.
|
|
69
|
+
- If the banny body NFT is transferred to a new owner while outfits are equipped, the new body owner can unequip and claim the outfit NFTs (the ownership check in `_checkIfSenderIsOwner` is against the current body owner).
|
|
70
|
+
- The `lockOutfitChangesFor()` function can lock outfits for up to 7 days per call (extendable). During a lock, neither the body owner nor anyone else can change the equipped outfits.
|
|
71
|
+
- There is no admin override to rescue stuck outfit NFTs. If a bug in `decorateBannyWith()` prevents unequipping, the outfits remain locked in the resolver permanently.
|
|
72
|
+
|
|
62
73
|
## Immutable Configuration
|
|
63
74
|
|
|
64
75
|
The following are set at construction and cannot be changed:
|
|
65
76
|
|
|
66
77
|
| Property | Set At | Value |
|
|
67
78
|
|----------|--------|-------|
|
|
68
|
-
| `BANNY_BODY` | Constructor
|
|
69
|
-
| `DEFAULT_NECKLACE` | Constructor
|
|
70
|
-
| `DEFAULT_MOUTH` | Constructor
|
|
71
|
-
| `DEFAULT_STANDARD_EYES` | Constructor
|
|
72
|
-
| `DEFAULT_ALIEN_EYES` | Constructor
|
|
73
|
-
| `trustedForwarder` | Constructor
|
|
74
|
-
| `_LOCK_DURATION` | Constant
|
|
75
|
-
| Body color fills | Hardcoded in `_fillsFor()`
|
|
76
|
-
| Category IDs | Constants
|
|
79
|
+
| `BANNY_BODY` | Constructor | Base SVG path for all banny body rendering |
|
|
80
|
+
| `DEFAULT_NECKLACE` | Constructor | Default necklace SVG injected when no custom necklace equipped |
|
|
81
|
+
| `DEFAULT_MOUTH` | Constructor | Default mouth SVG injected when no custom mouth equipped |
|
|
82
|
+
| `DEFAULT_STANDARD_EYES` | Constructor | Default eyes SVG for non-alien bodies |
|
|
83
|
+
| `DEFAULT_ALIEN_EYES` | Constructor | Default eyes SVG for alien bodies |
|
|
84
|
+
| `trustedForwarder` | Constructor | ERC-2771 forwarder address for meta-transactions |
|
|
85
|
+
| `_LOCK_DURATION` | Constant | 7 days -- hardcoded, not configurable |
|
|
86
|
+
| Body color fills | Hardcoded in `_fillsFor()` | Color palettes for Alien, Pink, Orange, Original body types |
|
|
87
|
+
| Category IDs | Constants | 18 category slots (0-17), hardcoded |
|
|
77
88
|
|
|
78
89
|
## Admin Boundaries
|
|
79
90
|
|
|
80
91
|
**What the owner CANNOT do:**
|
|
81
92
|
|
|
82
|
-
- **Cannot move or steal user NFTs.** The resolver only holds custody of outfit/background NFTs that users voluntarily equip. The `onERC721Received` guard
|
|
93
|
+
- **Cannot move or steal user NFTs.** The resolver only holds custody of outfit/background NFTs that users voluntarily equip. The `onERC721Received` guard ensures only self-initiated transfers are accepted.
|
|
83
94
|
- **Cannot modify stored SVG content.** Once hash + content are committed, they are permanent. No delete or update function exists.
|
|
84
95
|
- **Cannot modify stored SVG hashes.** Each UPC's hash is write-once.
|
|
85
96
|
- **Cannot change the lock duration.** The 7-day lock is a compile-time constant.
|
package/ARCHITECTURE.md
CHANGED
|
@@ -13,6 +13,16 @@ src/
|
|
|
13
13
|
└── IBanny721TokenUriResolver.sol — Interface for outfit and asset operations
|
|
14
14
|
```
|
|
15
15
|
|
|
16
|
+
## UPC System
|
|
17
|
+
|
|
18
|
+
A UPC (Universal Product Code) is the tier ID from the 721 hook's tier system, used as the primary identifier for every asset type (bodies, backgrounds, outfits). Each tier in the `IJB721TiersHook` has an `id` and a `category` -- the UPC is that `id`.
|
|
19
|
+
|
|
20
|
+
- **Bodies** (category 0) have hardcoded UPCs with built-in color palettes: Alien (1), Pink (2), Orange (3), Original (4).
|
|
21
|
+
- **Outfits and backgrounds** use arbitrary UPCs assigned when their tiers are created in the 721 hook. Their SVG content is stored on-chain via the hash-then-upload flow described below.
|
|
22
|
+
- **Token IDs** encode the UPC: `tokenId = UPC * 1_000_000_000 + mintNumber`. The resolver recovers the tier (and thus UPC and category) via `tierOfTokenId` from the hook's store.
|
|
23
|
+
|
|
24
|
+
All asset storage, outfit attachment, and SVG generation are keyed by UPC.
|
|
25
|
+
|
|
16
26
|
## Key Operations
|
|
17
27
|
|
|
18
28
|
### Asset Storage
|
|
@@ -22,7 +32,8 @@ Owner → setSvgHashesOf(upcs, hashes)
|
|
|
22
32
|
|
|
23
33
|
Anyone → setSvgContentsOf(upcs, contents)
|
|
24
34
|
→ Upload SVG content matching registered hashes
|
|
25
|
-
→ Content validated
|
|
35
|
+
→ Content validated: keccak256(content) must equal stored hash
|
|
36
|
+
→ Content is immutable once stored (cannot be overwritten)
|
|
26
37
|
|
|
27
38
|
Owner → setProductNames(upcs, names)
|
|
28
39
|
→ Register product names for UPCs
|
|
@@ -33,7 +44,7 @@ Owner → setProductNames(upcs, names)
|
|
|
33
44
|
Body Owner → decorateBannyWith(hook, bodyId, backgroundId, outfitIds)
|
|
34
45
|
→ Attach outfit and background NFTs to a body NFT
|
|
35
46
|
→ Outfit/background NFTs transferred to resolver contract
|
|
36
|
-
→ Previous outfits returned to owner
|
|
47
|
+
→ Previous outfits returned to owner (if transfer fails, retained in attached list)
|
|
37
48
|
→ Composite SVG generated from layered components
|
|
38
49
|
|
|
39
50
|
Body Owner → lockOutfitChangesFor(hook, bodyId)
|
|
@@ -45,11 +56,26 @@ Body Owner → lockOutfitChangesFor(hook, bodyId)
|
|
|
45
56
|
JB721TiersHook → tokenURI(tokenId)
|
|
46
57
|
→ Banny721TokenUriResolver.tokenURI(hook, tokenId)
|
|
47
58
|
→ Look up body tier and any attached outfits
|
|
48
|
-
→ Compose SVG layers
|
|
59
|
+
→ Compose SVG layers in fixed z-order (back to front):
|
|
60
|
+
1. Background (if attached)
|
|
61
|
+
2. Body (with color palette fills from UPC)
|
|
62
|
+
3. Outfits in category order:
|
|
63
|
+
Backside(2) → Necklace(3)* → Head(4) → Eyes(5)† →
|
|
64
|
+
Glasses(6) → Mouth(7)† → Legs(8) → Suit(9) →
|
|
65
|
+
Suit Bottom(10) → Suit Top(11) → Necklace‡ →
|
|
66
|
+
Headtop(12) → Hand(13) → Special categories(14-17)
|
|
67
|
+
(* = default necklace inserted if no custom equipped)
|
|
68
|
+
(† = default inserted if no custom equipped and no full Head)
|
|
69
|
+
(‡ = custom necklace rendered here, above suit layers)
|
|
70
|
+
→ Wrap in <svg> container (400x400 viewBox)
|
|
49
71
|
→ Encode as base64 data URI with JSON metadata
|
|
50
72
|
→ Return fully on-chain SVG
|
|
51
73
|
```
|
|
52
74
|
|
|
75
|
+
Non-body tokens are shown on a grey mannequin Banny (body fills set to `none`, outline to `#808080`).
|
|
76
|
+
|
|
77
|
+
For IPFS-backed assets without on-chain SVG content, the resolver falls back to an `<image href="...">` tag referencing the tier's encoded IPFS URI at 400x400 pixels.
|
|
78
|
+
|
|
53
79
|
## Dependencies
|
|
54
80
|
- `@bananapus/721-hook-v6` — NFT tier system (IJB721TiersHook, IJB721TokenUriResolver)
|
|
55
81
|
- `@bananapus/core-v6` — Core protocol interfaces
|
|
@@ -58,3 +84,15 @@ JB721TiersHook → tokenURI(tokenId)
|
|
|
58
84
|
- `@rev-net/core-v6` — Revnet integration
|
|
59
85
|
- `@openzeppelin/contracts` — Ownable, ERC2771, ReentrancyGuard, Strings
|
|
60
86
|
- `keccak` — Hashing utilities
|
|
87
|
+
|
|
88
|
+
## Design Decisions
|
|
89
|
+
|
|
90
|
+
**Hash-then-upload for SVG storage.** The owner registers a keccak256 hash first (`setSvgHashesOf`, owner-only), then anyone can upload the matching content (`setSvgContentsOf`, permissionless). This separates content approval from upload gas costs -- the owner commits to exactly which artwork is valid, and community members or bots can pay the gas to actually store it. Both hash and content are immutable once set, preventing artwork tampering after mint.
|
|
91
|
+
|
|
92
|
+
**7-day outfit lock.** `lockOutfitChangesFor` freezes a body's outfit for 7 days (`_LOCK_DURATION`). This exists so that a dressed Banny can be used as a stable visual identity (e.g. a PFP or on-chain avatar) without risk of the outfit changing underneath external references. The lock can be extended but never shortened -- calling it again resets the timer to 7 days from now, and reverts if the existing lock expires later than the new one would.
|
|
93
|
+
|
|
94
|
+
**UPC as tier ID.** Rather than introducing a separate asset registry, the resolver reuses the 721 hook's tier ID as the universal product code. This means asset identity, pricing, supply caps, and category are all managed by the existing tier system with no additional storage. The resolver is purely a read/compose layer on top of the hook's data.
|
|
95
|
+
|
|
96
|
+
**Fixed category ordering for outfit layering.** Outfits must be passed in ascending category order (2-17) and only one outfit per category is allowed. This constraint eliminates ambiguity in SVG z-ordering -- the category number directly determines the layer position. It also enables the resolver to insert default accessories (necklace, eyes, mouth) at the correct z-position when no custom one is equipped and no full-head item occludes them.
|
|
97
|
+
|
|
98
|
+
**Equipped assets travel with the body.** When a body NFT is transferred, all equipped outfits and backgrounds remain attached. The new owner inherits them and can unequip to receive the outfit NFTs. This was chosen over auto-unequip to preserve the dressed Banny as a complete visual unit, but it means sellers should unequip valuable outfits before listing.
|
package/AUDIT_INSTRUCTIONS.md
CHANGED
|
@@ -6,12 +6,12 @@ Target: `Banny721TokenUriResolver` -- a single-contract system that manages on-c
|
|
|
6
6
|
|
|
7
7
|
| Contract | Lines | Role |
|
|
8
8
|
|----------|-------|------|
|
|
9
|
-
| `Banny721TokenUriResolver` | ~1,
|
|
9
|
+
| `Banny721TokenUriResolver` | ~1,428 | Token URI resolver, outfit custody, SVG storage, decoration logic, lock mechanism |
|
|
10
10
|
| `IBanny721TokenUriResolver` | ~175 | Interface: events, view functions, mutating functions |
|
|
11
11
|
|
|
12
12
|
Inheritance chain: `Ownable`, `ERC2771Context`, `ReentrancyGuard`, `IJB721TokenUriResolver`, `IBanny721TokenUriResolver`, `IERC721Receiver`.
|
|
13
13
|
|
|
14
|
-
Compiler: Solidity 0.8.26, Cancun EVM, via-IR optimizer (200 runs).
|
|
14
|
+
Compiler: Solidity ^0.8.26, Cancun EVM, via-IR optimizer (200 runs).
|
|
15
15
|
|
|
16
16
|
## Architecture Overview
|
|
17
17
|
|
|
@@ -63,15 +63,15 @@ There are 18 categories (0-17), each representing a layer or slot:
|
|
|
63
63
|
| 16 | `_SPECIAL_HEAD_CATEGORY` | Special Head | Special outfit slot |
|
|
64
64
|
| 17 | `_SPECIAL_BODY_CATEGORY` | Special Body | Special outfit slot |
|
|
65
65
|
|
|
66
|
-
Categories 0 and 1 cannot be used as outfits (enforced at line
|
|
66
|
+
Categories 0 and 1 cannot be used as outfits (enforced at line 1299). Outfits must be categories 2-17.
|
|
67
67
|
|
|
68
68
|
### Conflict Rules
|
|
69
69
|
|
|
70
70
|
Two conflict rules prevent contradictory outfits:
|
|
71
|
-
1. **Head blocks face pieces**: If category 4 (Head) is equipped, categories 5 (Eyes), 6 (Glasses), 7 (Mouth), and 12 (HeadTop) are rejected (line
|
|
72
|
-
2. **Suit blocks top/bottom**: If category 9 (Suit) is equipped, categories 10 (SuitBottom) and 11 (SuitTop) are rejected (line
|
|
71
|
+
1. **Head blocks face pieces**: If category 4 (Head) is equipped, categories 5 (Eyes), 6 (Glasses), 7 (Mouth), and 12 (HeadTop) are rejected (line 1312-1318).
|
|
72
|
+
2. **Suit blocks top/bottom**: If category 9 (Suit) is equipped, categories 10 (SuitBottom) and 11 (SuitTop) are rejected (line 1319-1323).
|
|
73
73
|
|
|
74
|
-
Outfits must be passed in **ascending category order** (line
|
|
74
|
+
Outfits must be passed in **ascending category order** (line 1304-1305). No two outfits can share a category.
|
|
75
75
|
|
|
76
76
|
## Outfit Decoration System
|
|
77
77
|
|
|
@@ -80,13 +80,13 @@ Outfits must be passed in **ascending category order** (line 1280-1281). No two
|
|
|
80
80
|
`decorateBannyWith(hook, bannyBodyId, backgroundId, outfitIds)` is the single entry point for all decoration changes. There is no separate "undress" function -- undressing is accomplished by calling `decorateBannyWith` with an empty `outfitIds` array and `backgroundId = 0`.
|
|
81
81
|
|
|
82
82
|
The function:
|
|
83
|
-
1. Verifies the caller owns the banny body (line
|
|
84
|
-
2. Verifies the body token is actually category 0 (line
|
|
85
|
-
3. Verifies the body is not locked (line
|
|
86
|
-
4. Delegates background handling to `_decorateBannyWithBackground` (line
|
|
87
|
-
5. Delegates outfit handling to `_decorateBannyWithOutfits` (line
|
|
83
|
+
1. Verifies the caller owns the banny body (line 993).
|
|
84
|
+
2. Verifies the body token is actually category 0 (line 996).
|
|
85
|
+
3. Verifies the body is not locked (line 1001).
|
|
86
|
+
4. Delegates background handling to `_decorateBannyWithBackground` (line 1010).
|
|
87
|
+
5. Delegates outfit handling to `_decorateBannyWithOutfits` (line 1013).
|
|
88
88
|
|
|
89
|
-
### Background Decoration (`_decorateBannyWithBackground`, line
|
|
89
|
+
### Background Decoration (`_decorateBannyWithBackground`, line 1173)
|
|
90
90
|
|
|
91
91
|
- If `backgroundId != 0`, the caller must own the background NFT or own the banny body currently using it.
|
|
92
92
|
- The background must be category 1 (`_BACKGROUND_CATEGORY`).
|
|
@@ -94,19 +94,19 @@ The function:
|
|
|
94
94
|
- The old background is returned via `_tryTransferFrom` (silent failure on burned tokens).
|
|
95
95
|
- The new background is transferred into the resolver via `_transferFrom` (reverts on failure).
|
|
96
96
|
|
|
97
|
-
### Outfit Decoration (`_decorateBannyWithOutfits`, line
|
|
97
|
+
### Outfit Decoration (`_decorateBannyWithOutfits`, line 1243)
|
|
98
98
|
|
|
99
99
|
This is the most complex function. It performs a **merge-style iteration** over two arrays: the new `outfitIds` and the previously equipped `previousOutfitIds`.
|
|
100
100
|
|
|
101
101
|
For each new outfit:
|
|
102
|
-
1. Authorization: caller must own the outfit OR own the banny body currently wearing it (lines
|
|
103
|
-
2. Category validation: must be 2-17, ascending order, no conflicts (lines
|
|
104
|
-
3. The inner `while` loop transfers out old outfits up to the current category (lines
|
|
105
|
-
4. If the outfit is not already worn by this body, state is updated and the outfit is transferred in (lines
|
|
102
|
+
1. Authorization: caller must own the outfit OR own the banny body currently wearing it (lines 1278-1293).
|
|
103
|
+
2. Category validation: must be 2-17, ascending order, no conflicts (lines 1299-1324).
|
|
104
|
+
3. The inner `while` loop transfers out old outfits up to the current category (lines 1332-1350).
|
|
105
|
+
4. If the outfit is not already worn by this body, state is updated and the outfit is transferred in (lines 1353-1361).
|
|
106
106
|
|
|
107
|
-
After all new outfits are processed, a second `while` loop (line
|
|
107
|
+
After all new outfits are processed, a second `while` loop (line 1372) transfers out any remaining old outfits.
|
|
108
108
|
|
|
109
|
-
Finally, `_attachedOutfitIdsOf[hook][bannyBodyId]` is overwritten wholesale with the new array (line
|
|
109
|
+
Finally, `_attachedOutfitIdsOf[hook][bannyBodyId]` is overwritten wholesale with the new array (line 1392).
|
|
110
110
|
|
|
111
111
|
## Custody Model
|
|
112
112
|
|
|
@@ -132,8 +132,8 @@ Every outfit NFT held by the resolver must be recoverable by the current owner o
|
|
|
132
132
|
`lockOutfitChangesFor(hook, bannyBodyId)` locks a body for 7 days (`_LOCK_DURATION = 7 days = 604,800 seconds`). While locked, `decorateBannyWith` reverts with `OutfitChangesLocked`.
|
|
133
133
|
|
|
134
134
|
Rules:
|
|
135
|
-
- Only the body owner can lock (line
|
|
136
|
-
- Lock can only be extended, never shortened (line
|
|
135
|
+
- Only the body owner can lock (line 1021).
|
|
136
|
+
- Lock can only be extended, never shortened (line 1030). Attempting to set a shorter lock reverts with `CantAccelerateTheLock`.
|
|
137
137
|
- Equal-time relocks succeed (the `>` check allows `currentLockedUntil == newLockUntil`).
|
|
138
138
|
- The lock survives body transfers -- a buyer who receives a locked body cannot change outfits until the lock expires.
|
|
139
139
|
- The `outfitLockedUntil` mapping is public and readable by marketplaces.
|
|
@@ -144,19 +144,19 @@ Rules:
|
|
|
144
144
|
|
|
145
145
|
### Hash-Then-Reveal Pattern
|
|
146
146
|
|
|
147
|
-
1. Owner calls `setSvgHashesOf(upcs, svgHashes)` to commit `keccak256` hashes (line
|
|
148
|
-
2. Anyone calls `setSvgContentsOf(upcs, svgContents)` to reveal content (line
|
|
149
|
-
3. If content is not yet uploaded, `_svgOf` falls back to IPFS resolution via `JBIpfsDecoder` (line 936-
|
|
147
|
+
1. Owner calls `setSvgHashesOf(upcs, svgHashes)` to commit `keccak256` hashes (line 1138). Hashes are write-once per UPC.
|
|
148
|
+
2. Anyone calls `setSvgContentsOf(upcs, svgContents)` to reveal content (line 1108). Content must match the stored hash. Content is write-once per UPC.
|
|
149
|
+
3. If content is not yet uploaded, `_svgOf` falls back to IPFS resolution via `JBIpfsDecoder` (line 936-949).
|
|
150
150
|
|
|
151
151
|
### SVG Composition
|
|
152
152
|
|
|
153
|
-
`tokenUriOf` (line
|
|
153
|
+
`tokenUriOf` (line 203) builds a complete on-chain data URI:
|
|
154
154
|
1. For non-body tokens: renders the outfit SVG on a grey mannequin banny.
|
|
155
155
|
2. For body tokens: composes background + body + all outfit layers in category order.
|
|
156
156
|
|
|
157
157
|
The body SVG uses CSS classes (`.b1`-`.b4`, `.a1`-`.a3`) with color fills specific to each body type (Alien=green, Pink=pink, Orange=orange, Original=yellow).
|
|
158
158
|
|
|
159
|
-
Default accessories (necklace, eyes, mouth) are injected when no custom outfit occupies that slot. The necklace has special layering: it is stored during iteration but rendered after `_SUIT_TOP_CATEGORY` (line
|
|
159
|
+
Default accessories (necklace, eyes, mouth) are injected when no custom outfit occupies that slot. The necklace has special layering: it is stored during iteration but rendered after `_SUIT_TOP_CATEGORY` (line 885-890).
|
|
160
160
|
|
|
161
161
|
### SVG Sanitization
|
|
162
162
|
|
|
@@ -174,7 +174,7 @@ If a non-zero forwarder is set, that forwarder contract can append arbitrary sen
|
|
|
174
174
|
|
|
175
175
|
## `onERC721Received` Gate
|
|
176
176
|
|
|
177
|
-
The resolver implements `IERC721Receiver.onERC721Received` (line
|
|
177
|
+
The resolver implements `IERC721Receiver.onERC721Received` (line 1044) and rejects all incoming transfers unless `operator == address(this)`. This means:
|
|
178
178
|
- Only the resolver itself can send NFTs to itself (via its own `_transferFrom` calls).
|
|
179
179
|
- Users cannot accidentally send NFTs directly to the resolver.
|
|
180
180
|
- If a user sends an NFT via `transferFrom` (not `safeTransferFrom`), the callback is not triggered and the NFT is silently deposited. This is an inherent ERC-721 limitation.
|
|
@@ -183,24 +183,24 @@ The resolver implements `IERC721Receiver.onERC721Received` (line 1038) and rejec
|
|
|
183
183
|
|
|
184
184
|
### 1. Outfit Authorization Logic (CRITICAL)
|
|
185
185
|
|
|
186
|
-
File: `src/Banny721TokenUriResolver.sol`, lines
|
|
186
|
+
File: `src/Banny721TokenUriResolver.sol`, lines 1278-1293.
|
|
187
187
|
|
|
188
188
|
The authorization check for outfits allows the caller to use an outfit if they either own it directly or own the banny body currently wearing it. A historical bug (L18, now fixed) allowed `ownerOf(0)` bypass when `wearerOf` returned 0 for unworn outfits. Verify the current fix is sound:
|
|
189
|
-
- Line
|
|
190
|
-
- Line
|
|
189
|
+
- Line 1283: `if (wearerId == 0) revert` -- ensures unworn outfits require direct ownership.
|
|
190
|
+
- Line 1287: `ownerOf(wearerId)` -- verifies caller owns the body wearing the outfit.
|
|
191
191
|
|
|
192
192
|
Look for: any path where an attacker can pass authorization without actually owning the outfit or the body wearing it.
|
|
193
193
|
|
|
194
194
|
### 2. Merge Iteration in `_decorateBannyWithOutfits` (HIGH)
|
|
195
195
|
|
|
196
|
-
Lines
|
|
196
|
+
Lines 1271-1392. The merge between new `outfitIds` and `previousOutfitIds` is complex. The inner `while` loop advances through previous outfits, transferring them out. The second `while` loop (line 1372) handles remaining previous outfits.
|
|
197
197
|
|
|
198
198
|
Look for:
|
|
199
199
|
- Off-by-one errors in the `previousOutfitIndex` counter.
|
|
200
200
|
- Skipped outfits that should be returned.
|
|
201
201
|
- Double-transfer of outfits (both in the inner while and the tail while).
|
|
202
202
|
- Removed-tier outfits (category=0) causing infinite loops or skipped entries.
|
|
203
|
-
- The `_isInArray` check at line
|
|
203
|
+
- The `_isInArray` check at line 1376 preventing outfits in the new set from being transferred out.
|
|
204
204
|
|
|
205
205
|
### 3. Custody Accounting Consistency (HIGH)
|
|
206
206
|
|
|
@@ -211,11 +211,11 @@ These four mappings must remain consistent. After every `decorateBannyWith` call
|
|
|
211
211
|
- The background in `_attachedBackgroundIdOf[hook][bodyId]` should have `_userOf[hook][backgroundId] == bodyId`.
|
|
212
212
|
- Every outfit/background held by the resolver should be tracked in these mappings.
|
|
213
213
|
|
|
214
|
-
**Note**: `_attachedOutfitIdsOf` is overwritten wholesale at line
|
|
214
|
+
**Note**: `_attachedOutfitIdsOf` is overwritten wholesale at line 1392, but `_wearerOf` is only set for *new* outfits at line 1355. Outfits that were already worn by this body retain their `_wearerOf` entry from the previous call. Verify this does not cause stale state.
|
|
215
215
|
|
|
216
216
|
### 4. `_tryTransferFrom` Silent Failures (MEDIUM)
|
|
217
217
|
|
|
218
|
-
Line
|
|
218
|
+
Line 1424-1428. When returning old outfits, transfer failures are silently caught. This is intentional (handles burned tokens) but could mask real bugs.
|
|
219
219
|
|
|
220
220
|
Look for: scenarios where `_tryTransferFrom` silently fails but the state mappings (`_wearerOf`, `_attachedOutfitIdsOf`) have already been updated, causing an outfit to be "lost" -- not held by the resolver, not returned to the owner, but still tracked as worn.
|
|
221
221
|
|
|
@@ -227,13 +227,13 @@ Verify: a malicious hook cannot affect outfits custodied from a different (legit
|
|
|
227
227
|
|
|
228
228
|
### 6. CEI Ordering in Background Replacement (MEDIUM)
|
|
229
229
|
|
|
230
|
-
`_decorateBannyWithBackground` (lines
|
|
231
|
-
- `_attachedBackgroundIdOf` and `_userOf` are updated at lines
|
|
232
|
-
- No reachable state where a reentrancy callback during the safeTransferFrom at line
|
|
230
|
+
`_decorateBannyWithBackground` (lines 1212-1224) updates state before transfers. Verify:
|
|
231
|
+
- `_attachedBackgroundIdOf` and `_userOf` are updated at lines 1213-1214 before the try-transfer at line 1218 and the incoming transfer at line 1223.
|
|
232
|
+
- No reachable state where a reentrancy callback during the safeTransferFrom at line 1223 could observe inconsistent state.
|
|
233
233
|
|
|
234
234
|
### 7. SVG Content Safety (LOW)
|
|
235
235
|
|
|
236
|
-
SVG content is stored verbatim. While this is a view-function-only concern, verify that the encoding in `_encodeTokenUri` (line
|
|
236
|
+
SVG content is stored verbatim. While this is a view-function-only concern, verify that the encoding in `_encodeTokenUri` (line 628) cannot produce malformed JSON that breaks parsers. Specifically check for unescaped characters in `svgDescription`, `svgExternalUrl`, and custom product names injected into the JSON.
|
|
237
237
|
|
|
238
238
|
## Key Invariants to Test
|
|
239
239
|
|
|
@@ -266,7 +266,7 @@ RPC_ETHEREUM_MAINNET=<your-rpc-url> forge test --match-path test/Fork.t.sol -vvv
|
|
|
266
266
|
forge test --gas-report
|
|
267
267
|
```
|
|
268
268
|
|
|
269
|
-
**Test suite**:
|
|
269
|
+
**Test suite**: 14 test files, ~230+ tests.
|
|
270
270
|
|
|
271
271
|
| File | Purpose |
|
|
272
272
|
|------|---------|
|
|
@@ -281,10 +281,13 @@ forge test --gas-report
|
|
|
281
281
|
| `regression/MsgSenderEvents.t.sol` | ERC-2771 event correctness |
|
|
282
282
|
| `regression/BurnedTokenCheck.t.sol` | Burned token handling |
|
|
283
283
|
| `regression/ClearMetadata.t.sol` | Metadata clearing |
|
|
284
|
+
| `OutfitTransferLifecycle.t.sol` | Outfit transfer lifecycle flows |
|
|
285
|
+
| `TestAuditGaps.sol` | Audit gap coverage: ERC-2771 meta-transaction flows (forwarder relay, spoofing prevention, owner-only relay), SVG rendering edge cases (special characters, script tags, unicode, long content, JSON-breaking chars), SVG composition validation (default decorations, alien vs standard eyes, background inclusion/exclusion) |
|
|
286
|
+
| `TestQALastMile.t.sol` | QA last-mile edge cases |
|
|
284
287
|
|
|
285
288
|
**Untested areas** (potential audit additions):
|
|
286
|
-
- Meta-transaction flows with a real forwarder (all tests use `address(0)`).
|
|
287
|
-
- SVG content containing special characters or potential injection payloads.
|
|
289
|
+
- Meta-transaction flows with a real forwarder (basic coverage exists in TestAuditGaps.sol; advanced scenarios with a real forwarder remain untested -- all tests use `address(0)`).
|
|
290
|
+
- SVG content containing special characters or potential injection payloads (basic coverage exists in TestAuditGaps.sol for script tags, unicode, JSON-breaking chars, and long content; advanced payloads remain untested).
|
|
288
291
|
- Gas consumption for `tokenUriOf` with maximum outfit count.
|
|
289
292
|
- Ownership transfer of the resolver (`transferOwnership`) and continued admin access.
|
|
290
293
|
- Product name overwriting (no write-once protection on `_customProductNameOf`).
|
|
@@ -325,3 +328,27 @@ The resolver makes external calls to the `hook` and its `STORE()` but does not c
|
|
|
325
328
|
| `UnrecognizedBackground` | Token is not category 1 |
|
|
326
329
|
| `UnrecognizedCategory` | Outfit category not in 2-17 range |
|
|
327
330
|
| `UnrecognizedProduct` | Body UPC not 1-4 (Alien/Pink/Orange/Original) |
|
|
331
|
+
|
|
332
|
+
## How to Report Findings
|
|
333
|
+
|
|
334
|
+
For each finding:
|
|
335
|
+
|
|
336
|
+
1. **Title** -- one line, starts with severity (CRITICAL/HIGH/MEDIUM/LOW)
|
|
337
|
+
2. **Affected contract(s)** -- exact file path and line numbers
|
|
338
|
+
3. **Description** -- what is wrong, in plain language
|
|
339
|
+
4. **Trigger sequence** -- step-by-step, minimal steps to reproduce
|
|
340
|
+
5. **Impact** -- what an attacker gains, what a user loses
|
|
341
|
+
6. **Proof** -- code trace or Foundry test
|
|
342
|
+
7. **Fix** -- minimal code change
|
|
343
|
+
|
|
344
|
+
**Severity guide:**
|
|
345
|
+
- **CRITICAL**: Permanent NFT custody loss, unauthorized outfit theft.
|
|
346
|
+
- **HIGH**: Conditional NFT loss, authorization bypass, broken custody invariant.
|
|
347
|
+
- **MEDIUM**: State inconsistency without fund loss, griefing that locks outfits.
|
|
348
|
+
- **LOW**: Cosmetic SVG issues, informational, edge-case-only.
|
|
349
|
+
|
|
350
|
+
## Previous Audit Findings
|
|
351
|
+
|
|
352
|
+
| ID | Severity | Status | Description |
|
|
353
|
+
|----|----------|--------|-------------|
|
|
354
|
+
| L18 | HIGH | FIXED | `ownerOf(0)` bypass in outfit authorization. When `wearerOf` returned 0 for unworn outfits, `hook.ownerOf(0)` could succeed for some hooks, allowing unauthorized outfit use. Fixed by adding `if (wearerId == 0) revert` guard at line 1283. Regression test in `DecorateFlow.t.sol`. |
|
package/CHANGE_LOG.md
CHANGED
|
@@ -2,13 +2,21 @@
|
|
|
2
2
|
|
|
3
3
|
This document describes all changes between `banny-retail` (v5) and `banny-retail-v6` (v6).
|
|
4
4
|
|
|
5
|
+
## Summary
|
|
6
|
+
|
|
7
|
+
- **Key bug fixes**: Default eyes incorrectly selected based on outfit UPC instead of body UPC; decoration operations blocked when a previously-equipped item was burned/removed.
|
|
8
|
+
- **Fault-tolerant transfers**: New `_tryTransferFrom()` wraps returns of previously-equipped items in try-catch — a burned or removed outfit no longer blocks the entire `decorateBannyWith()` operation.
|
|
9
|
+
- **Richer metadata**: `setSvgBaseUri()` replaced by `setMetadata()` which sets description, external URL, and base URI together. Token JSON `description` and `external_url` are now dynamic.
|
|
10
|
+
- **Body category validation**: `decorateBannyWith()` now verifies the `bannyBodyId` actually belongs to a body-category tier before proceeding.
|
|
11
|
+
- **Batch setter safety**: Array length mismatch checks added to `setProductNames()`, `setSvgContentsOf()`, and `setSvgHashesOf()`.
|
|
12
|
+
|
|
5
13
|
---
|
|
6
14
|
|
|
7
15
|
## 1. Breaking Changes
|
|
8
16
|
|
|
9
17
|
### Solidity Version Bump
|
|
10
18
|
- **v5:** `pragma solidity 0.8.23;`
|
|
11
|
-
- **v6:** `pragma solidity 0.8.26;`
|
|
19
|
+
- **v6:** `pragma solidity ^0.8.26;`
|
|
12
20
|
|
|
13
21
|
### Dependency Imports Updated
|
|
14
22
|
All `@bananapus/721-hook-v5` imports replaced with `@bananapus/721-hook-v6`:
|
|
@@ -46,11 +54,20 @@ All `@bananapus/721-hook-v5` imports replaced with `@bananapus/721-hook-v6`:
|
|
|
46
54
|
- **v6 adds:** A check that the `bannyBodyId` actually belongs to a body-category tier (`_BODY_CATEGORY == 0`). If not, reverts with `Banny721TokenUriResolver_BannyBodyNotBodyCategory()`.
|
|
47
55
|
- **v5:** No such check existed; any token ID could be passed as a banny body.
|
|
48
56
|
|
|
49
|
-
### `_tryTransferFrom()` -- Fault-Tolerant Transfers
|
|
50
|
-
- **v6 adds:** `_tryTransferFrom(address hook, address from, address to, uint256 assetId)` -- wraps `safeTransferFrom` in a try-catch
|
|
57
|
+
### `_tryTransferFrom()` -- Fault-Tolerant, Return-Aware Transfers
|
|
58
|
+
- **v6 adds:** `_tryTransferFrom(address hook, address from, address to, uint256 assetId) returns (bool success)` -- wraps `safeTransferFrom` in a try-catch and returns whether the transfer succeeded.
|
|
51
59
|
- Used in `_decorateBannyWithBackground()` and `_decorateBannyWithOutfits()` when returning previously equipped items.
|
|
60
|
+
- When the return transfer fails, **state is preserved** instead of cleared — preventing NFT stranding:
|
|
61
|
+
- **Backgrounds**: Failed return aborts the entire background change (old background stays attached, new one is not equipped).
|
|
62
|
+
- **Outfits**: Failed-to-return outfits are retained in the attached list via `_storeOutfitsWithRetained()`.
|
|
52
63
|
- **v5:** Used `_transferFrom()` (which reverts on failure) for all transfers, meaning a single burned/removed outfit could block the entire decoration operation.
|
|
53
64
|
|
|
65
|
+
> **Why this mattered**: In v5, if a project owner removed a tier that contained an equipped outfit, the Banny body owner could never change decorations again — the `safeTransferFrom` for the removed item would revert, permanently blocking the `decorateBannyWith()` function. This was the most-reported user issue.
|
|
66
|
+
|
|
67
|
+
### `_storeOutfitsWithRetained()` -- Anti-Stranding Merge
|
|
68
|
+
- **v6 adds:** `_storeOutfitsWithRetained(address hook, uint256 bannyBodyId, uint256[] memory outfitIds, uint256[] memory previousOutfitIds)` -- stores the new outfit array, appending any previously equipped outfits whose return transfer failed (non-zero entries in `previousOutfitIds`).
|
|
69
|
+
- This ensures that NFTs held by the resolver but not successfully returned to the owner remain tracked and recoverable in subsequent `decorateBannyWith` calls.
|
|
70
|
+
|
|
54
71
|
### `_isInArray()` Helper
|
|
55
72
|
- **v6 adds:** `_isInArray(uint256 value, uint256[] memory array)` -- checks if a value is present in an array.
|
|
56
73
|
- Used during outfit cleanup to skip outfits being re-equipped rather than transferring them out and back in.
|
|
@@ -72,6 +89,8 @@ All `@bananapus/721-hook-v5` imports replaced with `@bananapus/721-hook-v6`:
|
|
|
72
89
|
- **v5 (bug):** `_outfitContentsFor()` used the current outfit's `upc` to decide alien vs. standard default eyes: `if (upc == ALIEN_UPC)`. This checked the UPC of the *outfit being iterated*, not the banny body.
|
|
73
90
|
- **v6 (fix):** `_outfitContentsFor()` now accepts an additional `bodyUpc` parameter and uses `if (bodyUpc == ALIEN_UPC)` to correctly select default eyes based on the banny body type.
|
|
74
91
|
|
|
92
|
+
> **Why this mattered**: The bug caused alien Bannys to get standard eyes and vice versa when any outfit was equipped, breaking the visual identity of the NFT. The fix ensures default eyes are always selected based on the body type, not whatever outfit happens to be iterated.
|
|
93
|
+
|
|
75
94
|
### Improved Background Authorization Logic
|
|
76
95
|
- **v6:** `_decorateBannyWithBackground()` now explicitly checks if an unused background (where `userId == 0`) can only be attached by its owner. In v5, the authorization check `_msgSender() != owner && _msgSender() != IERC721(hook).ownerOf(userOf(hook, backgroundId))` could behave unexpectedly when `userOf()` returned 0 (querying `ownerOf(0)` on the hook).
|
|
77
96
|
|
|
@@ -171,7 +190,7 @@ No struct changes. Both versions use `JB721Tier` from the respective `721-hook`
|
|
|
171
190
|
|
|
172
191
|
### `_bannyBodySvgOf()` Relocated
|
|
173
192
|
- **v5:** Located after `_msgSender()` / `_msgData()` overrides (line ~700).
|
|
174
|
-
- **v6:** Relocated to immediately
|
|
193
|
+
- **v6:** Relocated to immediately before `_categoryNameOf()` (line ~538), grouped with other internal view functions.
|
|
175
194
|
|
|
176
195
|
### `_contextSuffixLength()` Relocated
|
|
177
196
|
- **v5:** Located before `_bannyBodySvgOf()` (line ~562).
|
|
@@ -194,8 +213,8 @@ No struct changes. Both versions use `JB721Tier` from the respective `721-hook`
|
|
|
194
213
|
- **v6 adds:** `// forge-lint: disable-next-line(mixed-case-variable)` above `DEFAULT_ALIEN_EYES`, `DEFAULT_MOUTH`, `DEFAULT_NECKLACE`, `DEFAULT_STANDARD_EYES`, and `BANNY_BODY`.
|
|
195
214
|
|
|
196
215
|
### Import Order Change
|
|
197
|
-
- **v5:**
|
|
198
|
-
- **v6:** OpenZeppelin imports first, then `@bananapus` imports
|
|
216
|
+
- **v5:** `@bananapus` imports first, then OpenZeppelin imports.
|
|
217
|
+
- **v6:** OpenZeppelin imports first, then `@bananapus` imports.
|
|
199
218
|
|
|
200
219
|
### Slither Annotations
|
|
201
220
|
- **v6 adds:** `// slither-disable-next-line calls-loop` on several `IERC721(hook).ownerOf()` calls inside loops.
|
|
@@ -219,4 +238,6 @@ No struct changes. Both versions use `JB721Tier` from the respective `721-hook`
|
|
|
219
238
|
| N/A | `_encodeTokenUri()` | Extracted from `tokenUriOf()` |
|
|
220
239
|
| `_outfitContentsFor(hook, outfitIds)` | `_outfitContentsFor(hook, outfitIds, bodyUpc)` | Added `bodyUpc` param (bug fix) |
|
|
221
240
|
| `@bananapus/721-hook-v5` | `@bananapus/721-hook-v6` | Dependency upgrade |
|
|
222
|
-
| `pragma solidity 0.8.23` | `pragma solidity 0.8.26` | Compiler version bump |
|
|
241
|
+
| `pragma solidity 0.8.23` | `pragma solidity ^0.8.26` | Compiler version bump |
|
|
242
|
+
|
|
243
|
+
> **Cross-repo impact**: The `pricingContext()` return change (3 values → 2) is driven by the upstream `nana-721-hook-v6` `IJB721TiersHook` interface change. The `@bananapus/721-hook-v6` dependency brings in the new tier splits system, though Banny Retail does not use tier splits.
|
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
|
```
|
|
@@ -33,6 +35,41 @@ Banny is a composable NFT character system built on top of Juicebox 721 hooks. E
|
|
|
33
35
|
→ Falls back to IPFS base URI if on-chain content not yet stored
|
|
34
36
|
```
|
|
35
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
|
+
|
|
36
73
|
### Asset Categories
|
|
37
74
|
|
|
38
75
|
| Category ID | Name | Slot Rules |
|
|
@@ -139,9 +176,24 @@ script/
|
|
|
139
176
|
| `setProductNames` | Contract owner only |
|
|
140
177
|
| `setMetadata` | Contract owner only |
|
|
141
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
|
+
|
|
142
194
|
## Risks
|
|
143
195
|
|
|
144
|
-
- **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.
|
|
145
197
|
- **7-day lock is fixed.** Cannot be shortened or cancelled once set. The lock duration is hardcoded.
|
|
146
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.
|
|
147
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.
|