@bannynet/core-v6 0.0.2 → 0.0.4
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/README.md +114 -32
- package/SKILLS.md +169 -48
- package/package.json +8 -8
- package/script/Add.Denver.s.sol +15 -3
- package/script/Drop1.s.sol +7 -26
- package/script/helpers/BannyverseDeploymentLib.sol +11 -4
- package/src/Banny721TokenUriResolver.sol +63 -25
- package/test/Banny721TokenUriResolver.t.sol +9 -8
- package/test/regression/I25_CEIReorder.t.sol +204 -0
- package/test/regression/L56_MsgSenderEvents.t.sol +152 -0
- package/test/regression/L57_BodyCategoryValidation.t.sol +142 -0
- package/test/regression/L58_ArrayLengthValidation.t.sol +58 -0
- package/test/regression/L59_ClearMetadata.t.sol +52 -0
- package/test/regression/L62_BurnedTokenCheck.t.sol +181 -0
package/README.md
CHANGED
|
@@ -1,53 +1,135 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Banny Retail
|
|
2
2
|
|
|
3
|
-
On-chain composable avatar system for Juicebox 721 collections -- manages Banny character bodies, backgrounds, and outfit NFTs with layered SVG rendering.
|
|
3
|
+
On-chain composable avatar system for Juicebox 721 collections -- manages Banny character bodies, backgrounds, and outfit NFTs with layered SVG rendering. Bodies can be dressed with outfits and placed on backgrounds, all composed into fully on-chain SVG images with base64-encoded JSON metadata.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
[Docs](https://docs.juicebox.money) | [Discord](https://discord.gg/juicebox)
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
## Conceptual Overview
|
|
8
|
+
|
|
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
|
+
|
|
11
|
+
### How It Works
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
1. A Juicebox 721 hook registers Banny721TokenUriResolver as its token URI resolver
|
|
15
|
+
→ All tokenURI() calls are forwarded to the resolver
|
|
16
|
+
|
|
|
17
|
+
2. Users mint Banny body NFTs + outfit/background NFTs via the 721 hook
|
|
18
|
+
→ Bodies are the "base" layer; outfits and backgrounds are accessories
|
|
19
|
+
|
|
|
20
|
+
3. Body owner calls decorateBannyWith(hook, bodyId, backgroundId, outfitIds)
|
|
21
|
+
→ Outfit and background NFTs are transferred to the resolver contract
|
|
22
|
+
→ Resolver tracks which body wears which outfits
|
|
23
|
+
→ Body's tokenURI now renders the full dressed composition
|
|
24
|
+
|
|
|
25
|
+
4. Outfit lock (optional): lockOutfitChangesFor(hook, bodyId)
|
|
26
|
+
→ Freezes outfit changes for 7 days
|
|
27
|
+
→ Proves the Banny's look is stable (useful for PFPs, displays)
|
|
28
|
+
|
|
|
29
|
+
5. SVG content is stored on-chain via a two-step process:
|
|
30
|
+
→ Owner registers content hashes: setSvgHashesOf(upcs, hashes)
|
|
31
|
+
→ Anyone uploads matching content: setSvgContentsOf(upcs, contents)
|
|
32
|
+
→ Falls back to IPFS base URI if on-chain content not yet stored
|
|
33
|
+
```
|
|
10
34
|
|
|
11
35
|
### Asset Categories
|
|
12
36
|
|
|
13
|
-
| Category ID | Name |
|
|
14
|
-
|
|
15
|
-
| 0 | Body |
|
|
37
|
+
| Category ID | Name | Slot Rules |
|
|
38
|
+
|-------------|------|------------|
|
|
39
|
+
| 0 | Body | Base character. One of four types: Alien, Pink, Orange, Original. |
|
|
16
40
|
| 1 | Background | Scene behind the Banny. One per body. |
|
|
17
|
-
| 2 | Backside | Layer behind the body. |
|
|
18
|
-
| 3 | Necklace | Accessory
|
|
19
|
-
| 4 | Head |
|
|
20
|
-
| 5 | Eyes | Eye style (defaults: standard
|
|
21
|
-
| 6 | Glasses | Eyewear layer. |
|
|
22
|
-
| 7 | Mouth | Mouth expression (default provided). |
|
|
41
|
+
| 2 | Backside | Layer rendered behind the body. |
|
|
42
|
+
| 3 | Necklace | Accessory (default provided if none attached). |
|
|
43
|
+
| 4 | Head | Full head accessory. Blocks eyes, glasses, mouth, and headtop. |
|
|
44
|
+
| 5 | Eyes | Eye style (defaults: alien or standard based on body type). |
|
|
45
|
+
| 6 | Glasses | Eyewear layer. Blocked by head. |
|
|
46
|
+
| 7 | Mouth | Mouth expression (default provided). Blocked by head. |
|
|
23
47
|
| 8 | Legs | Lower body clothing. |
|
|
24
|
-
| 9 | Suit | Full body
|
|
25
|
-
| 10 | Suit Bottom | Lower suit piece. |
|
|
26
|
-
| 11 | Suit Top | Upper suit piece. |
|
|
27
|
-
| 12 | Headtop | Top-of-head accessory. |
|
|
48
|
+
| 9 | Suit | Full body one-piece. Blocks suit top and suit bottom. |
|
|
49
|
+
| 10 | Suit Bottom | Lower suit piece. Blocked by full suit. |
|
|
50
|
+
| 11 | Suit Top | Upper suit piece. Blocked by full suit. |
|
|
51
|
+
| 12 | Headtop | Top-of-head accessory. Blocked by head. |
|
|
28
52
|
| 13 | Hand | Held item layer. |
|
|
29
|
-
| 14-17 | Special
|
|
53
|
+
| 14-17 | Special | Special suit, legs, head, and body overlays. |
|
|
54
|
+
|
|
55
|
+
### Body Types
|
|
56
|
+
|
|
57
|
+
| UPC | Type | Color Palette |
|
|
58
|
+
|-----|------|--------------|
|
|
59
|
+
| 1 | Alien | Green tones (`67d757`, `30a220`, `217a15`, `09490f`) with purple accents |
|
|
60
|
+
| 2 | Pink | Pink tones (`ffd8c5`, `ff96a9`, `fe588b`, `c92f45`) |
|
|
61
|
+
| 3 | Orange | Orange tones (`f3a603`, `ff7c02`, `fd3600`, `c32e0d`) |
|
|
62
|
+
| 4 | Original | Yellow tones (`ffe900`, `ffc700`, `f3a603`, `965a1a`) |
|
|
63
|
+
|
|
64
|
+
## Architecture
|
|
65
|
+
|
|
66
|
+
| Contract | Description |
|
|
67
|
+
|----------|-------------|
|
|
68
|
+
| `Banny721TokenUriResolver` | The sole contract. Implements `IJB721TokenUriResolver` to serve fully on-chain SVG token URIs for any Juicebox 721 hook. Manages outfit attachment, background assignment, outfit locking, on-chain SVG storage, and layered SVG rendering. Inherits `Ownable`, `ReentrancyGuard`, `ERC2771Context`, `IERC721Receiver`. |
|
|
69
|
+
|
|
70
|
+
### Interface
|
|
71
|
+
|
|
72
|
+
| Interface | Description |
|
|
73
|
+
|-----------|-------------|
|
|
74
|
+
| `IBanny721TokenUriResolver` | Public API: `tokenUriOf`, `svgOf`, `decorateBannyWith`, `lockOutfitChangesFor`, `assetIdsOf`, `namesOf`, `userOf`, `wearerOf`, SVG management, metadata management, plus all events. |
|
|
30
75
|
|
|
31
76
|
## Install
|
|
32
77
|
|
|
33
78
|
```bash
|
|
34
|
-
npm install @bannynet/core-
|
|
79
|
+
npm install @bannynet/core-v6
|
|
35
80
|
```
|
|
36
81
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
`banny-retail-v5` uses [npm](https://www.npmjs.com/) for package management and [Foundry](https://github.com/foundry-rs/foundry) for builds, tests, and deployments. Requires `via-ir = true` in foundry.toml.
|
|
82
|
+
If using Forge directly:
|
|
40
83
|
|
|
41
84
|
```bash
|
|
42
|
-
|
|
43
|
-
npm install && forge install
|
|
85
|
+
forge install
|
|
44
86
|
```
|
|
45
87
|
|
|
88
|
+
## Develop
|
|
89
|
+
|
|
90
|
+
Requires `via_ir = true` in foundry.toml due to stack depth in SVG composition.
|
|
91
|
+
|
|
46
92
|
| Command | Description |
|
|
47
93
|
|---------|-------------|
|
|
48
|
-
| `forge build` | Compile contracts
|
|
49
|
-
| `forge test` | Run
|
|
50
|
-
| `forge
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
94
|
+
| `forge build` | Compile contracts (requires via-IR) |
|
|
95
|
+
| `forge test` | Run all tests (3 test files: functionality, attacks, decoration flows) |
|
|
96
|
+
| `forge test -vvv` | Run tests with full trace |
|
|
97
|
+
|
|
98
|
+
## Repository Layout
|
|
99
|
+
|
|
100
|
+
```
|
|
101
|
+
src/
|
|
102
|
+
Banny721TokenUriResolver.sol # Sole contract (~1,331 lines)
|
|
103
|
+
interfaces/
|
|
104
|
+
IBanny721TokenUriResolver.sol # Public interface + events
|
|
105
|
+
test/
|
|
106
|
+
Banny721TokenUriResolver.t.sol # Unit tests (~690 lines)
|
|
107
|
+
BannyAttacks.t.sol # Security/adversarial tests (~323 lines)
|
|
108
|
+
DecorateFlow.t.sol # Decoration flow tests (~1,057 lines)
|
|
109
|
+
script/
|
|
110
|
+
Deploy.s.sol # Sphinx multi-chain deployment
|
|
111
|
+
Drop1.s.sol # Outfit drop deployment
|
|
112
|
+
Add.Denver.s.sol # Denver-specific deployment
|
|
113
|
+
helpers/
|
|
114
|
+
BannyverseDeploymentLib.sol # Deployment artifact reader
|
|
115
|
+
MigrationHelper.sol # Migration utilities
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## Permissions
|
|
119
|
+
|
|
120
|
+
| Action | Who Can Do It |
|
|
121
|
+
|--------|--------------|
|
|
122
|
+
| `decorateBannyWith` | Body owner (must also own or have worn-by-body access to outfits/backgrounds) |
|
|
123
|
+
| `lockOutfitChangesFor` | Body owner |
|
|
124
|
+
| `setSvgHashesOf` | Contract owner only |
|
|
125
|
+
| `setSvgContentsOf` | Anyone (content validated against registered hash) |
|
|
126
|
+
| `setProductNames` | Contract owner only |
|
|
127
|
+
| `setMetadata` | Contract owner only |
|
|
128
|
+
|
|
129
|
+
## Risks
|
|
130
|
+
|
|
131
|
+
- **Outfit custody:** Attached outfits and backgrounds are held by the resolver contract. If the contract has a bug in the return logic, assets could be stuck.
|
|
132
|
+
- **7-day lock is fixed.** Cannot be shortened or cancelled once set. The lock duration is hardcoded.
|
|
133
|
+
- **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
|
+
- **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.
|
|
135
|
+
- **Single resolver per hook.** The resolver is set on the 721 hook and applies to all tiers. Different collections would need different resolver instances.
|
package/SKILLS.md
CHANGED
|
@@ -1,77 +1,179 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Banny Retail
|
|
2
2
|
|
|
3
3
|
## Purpose
|
|
4
4
|
|
|
5
|
-
On-chain composable NFT avatar system that renders Banny characters with layered SVG outfits and backgrounds for Juicebox 721 collections.
|
|
5
|
+
On-chain composable NFT avatar system that renders Banny characters with layered SVG outfits and backgrounds for Juicebox 721 collections. Bodies can be dressed, locked, and rendered entirely on-chain.
|
|
6
6
|
|
|
7
7
|
## Contracts
|
|
8
8
|
|
|
9
9
|
| Contract | Role |
|
|
10
10
|
|----------|------|
|
|
11
|
-
| `Banny721TokenUriResolver` | Resolves token URIs for any Juicebox 721 hook by composing layered SVGs from registered asset content. Manages outfit attachment, background assignment, outfit locking,
|
|
11
|
+
| `Banny721TokenUriResolver` | Resolves token URIs for any Juicebox 721 hook by composing layered SVGs from registered asset content. Manages outfit attachment, background assignment, outfit locking, on-chain SVG storage, and metadata generation. Inherits `Ownable`, `ReentrancyGuard`, `ERC2771Context`, `IERC721Receiver`. (~1,331 lines) |
|
|
12
12
|
|
|
13
13
|
## Key Functions
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
|
18
|
-
|
|
19
|
-
| `
|
|
20
|
-
| `svgOf
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
|
25
|
-
|
|
26
|
-
| `
|
|
27
|
-
| `
|
|
28
|
-
|
|
29
|
-
|
|
15
|
+
### Token URI & Rendering
|
|
16
|
+
|
|
17
|
+
| Function | What it does |
|
|
18
|
+
|----------|-------------|
|
|
19
|
+
| `tokenUriOf(hook, tokenId)` | Returns base64-encoded JSON metadata URI with on-chain SVG image. For bodies: composes dressed Banny with attached outfits and background. For outfit/background tokens: renders the item on a grayscale mannequin Banny. |
|
|
20
|
+
| `svgOf(hook, tokenId, shouldDressBannyBody, shouldIncludeBackgroundOnBannyBody)` | Returns the composed SVG string. Layers (in order): background, backside, body, necklace, eyes, mouth, outfits by category. Falls back to IPFS base URI if on-chain content not stored. |
|
|
21
|
+
|
|
22
|
+
### Decoration
|
|
23
|
+
|
|
24
|
+
| Function | What it does |
|
|
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. Returns previously attached items to caller. Emits `DecorateBanny`. |
|
|
27
|
+
| `lockOutfitChangesFor(hook, bannyBodyId)` | Locks a body's outfit for 7 days. Cannot accelerate an existing lock. Caller must own the body NFT. |
|
|
28
|
+
|
|
29
|
+
### Views
|
|
30
|
+
|
|
31
|
+
| Function | What it does |
|
|
32
|
+
|----------|-------------|
|
|
33
|
+
| `assetIdsOf(hook, bannyBodyId)` | Returns `(backgroundId, outfitIds[])` currently attached to body. Filters out stale entries where `_wearerOf` no longer matches. |
|
|
34
|
+
| `namesOf(hook, tokenId)` | Returns `(fullName, categoryName, productName)`. Full name includes inventory count (e.g., "42/100"). |
|
|
35
|
+
| `userOf(hook, backgroundId)` | Returns the body ID using a background, or 0 if unused. Validates consistency. |
|
|
36
|
+
| `wearerOf(hook, outfitId)` | Returns the body ID wearing an outfit, or 0 if unworn. Validates outfit is in body's attached array. |
|
|
37
|
+
|
|
38
|
+
### SVG Content Management
|
|
39
|
+
|
|
40
|
+
| Function | Who | What it does |
|
|
41
|
+
|----------|-----|-------------|
|
|
42
|
+
| `setSvgHashesOf(upcs, svgHashes)` | Owner only | Registers expected SVG content hashes for UPCs. Immutable once set. |
|
|
43
|
+
| `setSvgContentsOf(upcs, svgContents)` | **Anyone** | Stores on-chain SVG content. Must match previously registered hash. Cannot overwrite existing content. |
|
|
44
|
+
| `setProductNames(upcs, names)` | Owner only | Sets human-readable names for product UPCs. |
|
|
45
|
+
| `setMetadata(description, url, baseUri)` | Owner only | Updates token metadata description, external URL, and SVG base URI. Empty strings clear the field. |
|
|
46
|
+
|
|
47
|
+
### Token Receipt
|
|
48
|
+
|
|
49
|
+
| Function | What it does |
|
|
50
|
+
|----------|-------------|
|
|
51
|
+
| `onERC721Received(operator, from, tokenId, data)` | Validates token receipt. Reverts unless `operator == address(this)`. Called when contract takes custody of outfits/backgrounds during decoration. |
|
|
30
52
|
|
|
31
53
|
## Integration Points
|
|
32
54
|
|
|
33
55
|
| Dependency | Import | Used For |
|
|
34
56
|
|------------|--------|----------|
|
|
35
|
-
| `@bananapus/721-hook-v6` | `IJB721TiersHook`, `IJB721TiersHookStore`, `IJB721TokenUriResolver`, `JB721Tier`, `JBIpfsDecoder`, `IERC721`
|
|
36
|
-
| `@openzeppelin/contracts` | `Ownable`, `ReentrancyGuard`, `ERC2771Context`, `IERC721Receiver`, `Strings` | Access control, reentrancy protection, meta-transactions, safe NFT receipt, string utilities
|
|
37
|
-
| `lib/base64` | `Base64` | Base64 encoding for on-chain SVG and JSON metadata
|
|
57
|
+
| `@bananapus/721-hook-v6` | `IJB721TiersHook`, `IJB721TiersHookStore`, `IJB721TokenUriResolver`, `JB721Tier`, `JBIpfsDecoder`, `IERC721` | Token ownership checks, tier/product data resolution, IPFS URI decoding, hook store queries, safe transfers |
|
|
58
|
+
| `@openzeppelin/contracts` | `Ownable`, `ReentrancyGuard`, `ERC2771Context`, `IERC721Receiver`, `Strings` | Access control, reentrancy protection, meta-transactions, safe NFT receipt, string utilities |
|
|
59
|
+
| `lib/base64` | `Base64` | Base64 encoding for on-chain SVG and JSON metadata |
|
|
38
60
|
|
|
39
61
|
## Key Types
|
|
40
62
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
|
48
|
-
|
|
49
|
-
|
|
|
50
|
-
|
|
|
51
|
-
|
|
|
52
|
-
|
|
|
53
|
-
|
|
|
54
|
-
|
|
|
55
|
-
|
|
|
56
|
-
|
|
|
63
|
+
### Asset Categories
|
|
64
|
+
|
|
65
|
+
| ID | Name | Slot Rules |
|
|
66
|
+
|----|------|------------|
|
|
67
|
+
| 0 | Body | Base character. Owns outfits and backgrounds. |
|
|
68
|
+
| 1 | Background | One per body. |
|
|
69
|
+
| 2 | Backside | Behind body layer. |
|
|
70
|
+
| 3 | Necklace | Default provided if none attached. |
|
|
71
|
+
| 4 | Head | Blocks eyes, glasses, mouth, headtop. |
|
|
72
|
+
| 5 | Eyes | Defaults: alien eyes (UPC 1) or standard eyes (UPC 2-4). |
|
|
73
|
+
| 6 | Glasses | Blocked by head. |
|
|
74
|
+
| 7 | Mouth | Default provided. Blocked by head. |
|
|
75
|
+
| 8 | Legs | Lower body clothing. |
|
|
76
|
+
| 9 | Suit | Full one-piece. Blocks suit top and suit bottom. |
|
|
77
|
+
| 10 | Suit Bottom | Blocked by full suit. |
|
|
78
|
+
| 11 | Suit Top | Blocked by full suit. |
|
|
79
|
+
| 12 | Headtop | Blocked by head. |
|
|
80
|
+
| 13 | Hand | Held item. |
|
|
81
|
+
| 14-17 | Special | Special suit, legs, head, body overlays. |
|
|
82
|
+
|
|
83
|
+
### Body Types (by UPC)
|
|
84
|
+
|
|
85
|
+
| UPC | Type | Default Eyes |
|
|
86
|
+
|-----|------|-------------|
|
|
87
|
+
| 1 | Alien | `DEFAULT_ALIEN_EYES` (purple) |
|
|
88
|
+
| 2 | Pink | `DEFAULT_STANDARD_EYES` |
|
|
89
|
+
| 3 | Orange | `DEFAULT_STANDARD_EYES` |
|
|
90
|
+
| 4 | Original | `DEFAULT_STANDARD_EYES` |
|
|
91
|
+
|
|
92
|
+
## Events
|
|
93
|
+
|
|
94
|
+
| Event | When |
|
|
95
|
+
|-------|------|
|
|
96
|
+
| `DecorateBanny(hook, bannyBodyId, backgroundId, outfitIds, caller)` | Body decorated with new outfits/background |
|
|
97
|
+
| `SetMetadata(description, externalUrl, baseUri, caller)` | Metadata updated |
|
|
98
|
+
| `SetProductName(upc, name, caller)` | Product name set |
|
|
99
|
+
| `SetSvgContent(upc, svgContent, caller)` | SVG content stored on-chain |
|
|
100
|
+
| `SetSvgHash(upc, svgHash, caller)` | SVG hash registered |
|
|
101
|
+
|
|
102
|
+
## Errors
|
|
103
|
+
|
|
104
|
+
| Error | When |
|
|
105
|
+
|-------|------|
|
|
106
|
+
| `Banny721TokenUriResolver_ArrayLengthMismatch` | Batch setter called with mismatched array lengths |
|
|
107
|
+
| `Banny721TokenUriResolver_BannyBodyNotBodyCategory` | Passing a non-body-category token as bannyBodyId to decorateBannyWith |
|
|
108
|
+
| `Banny721TokenUriResolver_CantAccelerateTheLock` | Trying to lock a body that's already locked for longer |
|
|
109
|
+
| `Banny721TokenUriResolver_ContentsAlreadyStored` | SVG content already exists for this UPC |
|
|
110
|
+
| `Banny721TokenUriResolver_ContentsMismatch` | Uploaded content doesn't match registered hash |
|
|
111
|
+
| `Banny721TokenUriResolver_HashAlreadyStored` | Hash already registered for this UPC |
|
|
112
|
+
| `Banny721TokenUriResolver_HashNotFound` | No hash registered for UPC (must register before uploading content) |
|
|
113
|
+
| `Banny721TokenUriResolver_HeadAlreadyAdded` | Trying to add eyes/glasses/mouth/headtop when head is already attached |
|
|
114
|
+
| `Banny721TokenUriResolver_OutfitChangesLocked` | Body is locked (7-day lock active) |
|
|
115
|
+
| `Banny721TokenUriResolver_SuitAlreadyAdded` | Trying to add suit top/bottom when full suit is already attached |
|
|
116
|
+
| `Banny721TokenUriResolver_UnauthorizedBannyBody` | Caller doesn't own the body |
|
|
117
|
+
| `Banny721TokenUriResolver_UnauthorizedOutfit` | Caller doesn't own the outfit or the body wearing it |
|
|
118
|
+
| `Banny721TokenUriResolver_UnauthorizedBackground` | Caller doesn't own the background or the body using it |
|
|
119
|
+
| `Banny721TokenUriResolver_UnorderedCategories` | Outfit IDs not in ascending category order |
|
|
120
|
+
| `Banny721TokenUriResolver_UnrecognizedCategory` | Category ID not in valid range (0-17) |
|
|
121
|
+
| `Banny721TokenUriResolver_UnrecognizedBackground` | Token is not a background category |
|
|
122
|
+
| `Banny721TokenUriResolver_UnrecognizedProduct` | Token's UPC doesn't map to a known product |
|
|
123
|
+
| `Banny721TokenUriResolver_UnauthorizedTransfer` | `onERC721Received` called by non-self operator |
|
|
124
|
+
|
|
125
|
+
## Constants
|
|
126
|
+
|
|
127
|
+
| Constant | Value | Purpose |
|
|
128
|
+
|----------|-------|---------|
|
|
129
|
+
| `_LOCK_DURATION` | 7 days | Fixed outfit lock period |
|
|
130
|
+
| `_ONE_BILLION` | 1,000,000,000 | Token ID encoding: `tokenId = upc * 1B + sequenceNumber` |
|
|
131
|
+
| `_BODY_CATEGORY` | 0 | Category ID for base Banny body |
|
|
132
|
+
| `_BACKGROUND_CATEGORY` | 1 | Category ID for backgrounds |
|
|
133
|
+
| Categories 2-17 | 2-17 | Backside, necklace, head, eyes, glasses, mouth, legs, suit, suit bottom, suit top, headtop, hand, specials |
|
|
134
|
+
|
|
135
|
+
## Storage
|
|
136
|
+
|
|
137
|
+
| Mapping | Type | Purpose |
|
|
138
|
+
|---------|------|---------|
|
|
139
|
+
| `_attachedOutfitIdsOf` | `hook => bannyBodyId => uint256[]` | Outfit token IDs attached to a body |
|
|
140
|
+
| `_attachedBackgroundIdOf` | `hook => bannyBodyId => uint256` | Background token ID attached to a body |
|
|
141
|
+
| `_svgContentOf` | `upc => string` | On-chain SVG content (immutable once set) |
|
|
142
|
+
| `svgHashOf` | `upc => bytes32` | Expected SVG content hash (immutable once set) |
|
|
143
|
+
| `_customProductNameOf` | `upc => string` | Human-readable product name |
|
|
144
|
+
| `outfitLockedUntil` | `hook => upc => uint256` | Timestamp until outfit changes locked |
|
|
145
|
+
| `_userOf` | `hook => backgroundId => uint256` | Which body uses a background |
|
|
146
|
+
| `_wearerOf` | `hook => outfitId => uint256` | Which body wears an outfit |
|
|
147
|
+
| `svgBaseUri` | `string` | IPFS/HTTP base URI for fallback SVG loading |
|
|
148
|
+
| `svgDescription` | `string` | Token metadata description |
|
|
149
|
+
| `svgExternalUrl` | `string` | Token metadata external URL |
|
|
57
150
|
|
|
58
151
|
## Gotchas
|
|
59
152
|
|
|
60
|
-
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
- `
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
153
|
+
1. **7-day lock is fixed and non-cancellable.** `lockOutfitChangesFor` always sets `outfitLockedUntil = block.timestamp + 7 days`. Cannot be shortened, cancelled, or accelerated. Only extended.
|
|
154
|
+
2. **SVG content is immutable.** Once `setSvgContentsOf` stores content for a UPC, it cannot be changed. A mistake requires deploying a new resolver.
|
|
155
|
+
3. **SVG hashes are also immutable.** `setSvgHashesOf` reverts if a hash already exists for a UPC. Register carefully.
|
|
156
|
+
4. **Hash registration is owner-only, but content upload is permissionless.** Anyone can call `setSvgContentsOf` as long as the content matches the registered hash. This enables community-driven lazy uploads.
|
|
157
|
+
5. **Strict ascending category order.** `decorateBannyWith` requires outfits passed in ascending category order. Reverts with `UnorderedCategories` if violated.
|
|
158
|
+
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
|
+
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).
|
|
161
|
+
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
|
+
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
|
+
11. **`onERC721Received` only accepts self-transfers.** Reverts unless `operator == address(this)`. The contract calls `safeTransferFrom` on itself during decoration, triggering this callback.
|
|
164
|
+
12. **Via-IR required.** `foundry.toml` must have `via_ir = true` due to stack-too-deep in the SVG composition logic.
|
|
165
|
+
13. **SVG fallback chain.** If on-chain content exists: use it. Else if category <= 17: fall back to `svgBaseUri + IPFS URI`. Else: use the hook's `baseURI() + IPFS URI`.
|
|
166
|
+
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
|
+
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
|
+
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.
|
|
68
169
|
|
|
69
170
|
## Example Integration
|
|
70
171
|
|
|
71
172
|
```solidity
|
|
72
173
|
import {IBanny721TokenUriResolver} from "@bannynet/core-v6/src/interfaces/IBanny721TokenUriResolver.sol";
|
|
73
174
|
|
|
74
|
-
// Get the composed SVG for a dressed Banny
|
|
175
|
+
// --- Get the composed SVG for a dressed Banny ---
|
|
176
|
+
|
|
75
177
|
string memory svg = resolver.svgOf(
|
|
76
178
|
hookAddress,
|
|
77
179
|
bannyBodyTokenId,
|
|
@@ -79,7 +181,8 @@ string memory svg = resolver.svgOf(
|
|
|
79
181
|
true // include the attached background
|
|
80
182
|
);
|
|
81
183
|
|
|
82
|
-
// Dress a Banny body with outfits
|
|
184
|
+
// --- Dress a Banny body with outfits ---
|
|
185
|
+
|
|
83
186
|
uint256[] memory outfitIds = new uint256[](2);
|
|
84
187
|
outfitIds[0] = hatTokenId; // must be category 4 (head)
|
|
85
188
|
outfitIds[1] = shirtTokenId; // must be category 11 (suit top)
|
|
@@ -89,6 +192,24 @@ resolver.decorateBannyWith(
|
|
|
89
192
|
hookAddress,
|
|
90
193
|
bannyBodyTokenId,
|
|
91
194
|
backgroundTokenId,
|
|
92
|
-
outfitIds //
|
|
195
|
+
outfitIds // MUST be in ascending category order
|
|
93
196
|
);
|
|
197
|
+
|
|
198
|
+
// --- Lock outfit changes for 7 days ---
|
|
199
|
+
|
|
200
|
+
resolver.lockOutfitChangesFor(hookAddress, bannyBodyTokenId);
|
|
201
|
+
|
|
202
|
+
// --- Register and upload SVG content ---
|
|
203
|
+
|
|
204
|
+
// Step 1: Owner registers content hashes
|
|
205
|
+
uint256[] memory upcs = new uint256[](1);
|
|
206
|
+
upcs[0] = 42;
|
|
207
|
+
bytes32[] memory hashes = new bytes32[](1);
|
|
208
|
+
hashes[0] = keccak256(bytes(svgContent));
|
|
209
|
+
resolver.setSvgHashesOf(upcs, hashes);
|
|
210
|
+
|
|
211
|
+
// Step 2: Anyone uploads matching content
|
|
212
|
+
string[] memory contents = new string[](1);
|
|
213
|
+
contents[0] = svgContent;
|
|
214
|
+
resolver.setSvgContentsOf(upcs, contents);
|
|
94
215
|
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bannynet/core-v6",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.4",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -20,15 +20,15 @@
|
|
|
20
20
|
"artifacts": "source ./.env && npx sphinx artifacts --org-id 'ea165b21-7cdc-4d7b-be59-ecdd4c26bee4' --project-name 'banny-core-v6'"
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"@bananapus/721-hook-v6": "^0.0.
|
|
24
|
-
"@bananapus/core-v6": "^0.0.
|
|
25
|
-
"@bananapus/
|
|
26
|
-
"@bananapus/
|
|
27
|
-
"@openzeppelin/contracts": "
|
|
28
|
-
"@rev-net/core-v6": "^0.0.
|
|
23
|
+
"@bananapus/721-hook-v6": "^0.0.9",
|
|
24
|
+
"@bananapus/core-v6": "^0.0.9",
|
|
25
|
+
"@bananapus/router-terminal-v6": "^0.0.6",
|
|
26
|
+
"@bananapus/suckers-v6": "^0.0.6",
|
|
27
|
+
"@openzeppelin/contracts": "5.2.0",
|
|
28
|
+
"@rev-net/core-v6": "^0.0.6",
|
|
29
29
|
"keccak": "^3.0.4"
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|
|
32
|
-
"@sphinx-labs/plugins": "0.33.
|
|
32
|
+
"@sphinx-labs/plugins": "0.33.3"
|
|
33
33
|
}
|
|
34
34
|
}
|
package/script/Add.Denver.s.sol
CHANGED
|
@@ -23,7 +23,7 @@ contract Drop1Script is Script, Sphinx {
|
|
|
23
23
|
|
|
24
24
|
function configureSphinx() public override {
|
|
25
25
|
// TODO: Update to contain revnet devs.
|
|
26
|
-
sphinxConfig.projectName = "banny-core";
|
|
26
|
+
sphinxConfig.projectName = "banny-core-v6";
|
|
27
27
|
sphinxConfig.mainnets = ["ethereum", "optimism", "base", "arbitrum"];
|
|
28
28
|
sphinxConfig.testnets = ["ethereum_sepolia", "optimism_sepolia", "base_sepolia", "arbitrum_sepolia"];
|
|
29
29
|
}
|
|
@@ -37,8 +37,10 @@ contract Drop1Script is Script, Sphinx {
|
|
|
37
37
|
);
|
|
38
38
|
|
|
39
39
|
// Get the deployment addresses for the 721 hook contracts for this chain.
|
|
40
|
-
bannyverse =
|
|
41
|
-
|
|
40
|
+
bannyverse = BannyverseDeploymentLib.getDeployment(
|
|
41
|
+
vm.envOr("BANNYVERSE_CORE_DEPLOYMENT_PATH", string("deployments/")),
|
|
42
|
+
vm.envOr("BANNYVERSE_REVNET_ID", uint256(4))
|
|
43
|
+
);
|
|
42
44
|
|
|
43
45
|
// Get the hook address by using the deployer.
|
|
44
46
|
hook = JB721TiersHook(address(revnet.basic_deployer.tiered721HookOf(bannyverse.revnetId)));
|
|
@@ -73,6 +75,16 @@ contract Drop1Script is Script, Sphinx {
|
|
|
73
75
|
splits: new JBSplit[](0)
|
|
74
76
|
});
|
|
75
77
|
|
|
78
|
+
// Get the next tier ID so we can set names and hashes for the new product.
|
|
79
|
+
uint256 nextTierId = hook.STORE().maxTierIdOf(address(hook)) + 1;
|
|
80
|
+
|
|
76
81
|
hook.adjustTiers(products, new uint256[](0));
|
|
82
|
+
|
|
83
|
+
// Build the product IDs array for the newly added tier(s).
|
|
84
|
+
uint256[] memory productIds = new uint256[](1);
|
|
85
|
+
productIds[0] = nextTierId;
|
|
86
|
+
|
|
87
|
+
bannyverse.resolver.setSvgHashesOf(productIds, svgHashes);
|
|
88
|
+
bannyverse.resolver.setProductNames(productIds, names);
|
|
77
89
|
}
|
|
78
90
|
}
|
package/script/Drop1.s.sol
CHANGED
|
@@ -37,8 +37,10 @@ contract Drop1Script is Script, Sphinx {
|
|
|
37
37
|
);
|
|
38
38
|
|
|
39
39
|
// Get the deployment addresses for the 721 hook contracts for this chain.
|
|
40
|
-
bannyverse =
|
|
41
|
-
|
|
40
|
+
bannyverse = BannyverseDeploymentLib.getDeployment(
|
|
41
|
+
vm.envOr("BANNYVERSE_CORE_DEPLOYMENT_PATH", string("deployments/")),
|
|
42
|
+
vm.envOr("BANNYVERSE_REVNET_ID", uint256(4))
|
|
43
|
+
);
|
|
42
44
|
|
|
43
45
|
// Get the hook address by using the deployer.
|
|
44
46
|
hook = JB721TiersHook(address(revnet.basic_deployer.tiered721HookOf(bannyverse.revnetId)));
|
|
@@ -1045,29 +1047,8 @@ contract Drop1Script is Script, Sphinx {
|
|
|
1045
1047
|
productIds[i] = i + 5;
|
|
1046
1048
|
}
|
|
1047
1049
|
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
string.concat("./", vm.toString(block.chainid), "-adjustTiers.hex.txt"), vm.toString(adjustTiersData)
|
|
1052
|
-
);
|
|
1053
|
-
|
|
1054
|
-
bytes memory setSvgHashData =
|
|
1055
|
-
abi.encodeCall(Banny721TokenUriResolver.setSvgHashesOf, (productIds, svgHashes));
|
|
1056
|
-
|
|
1057
|
-
vm.writeFile(
|
|
1058
|
-
string.concat("./", vm.toString(block.chainid), "-setSvgHashOf.hex.txt"), vm.toString(setSvgHashData)
|
|
1059
|
-
);
|
|
1060
|
-
|
|
1061
|
-
bytes memory setProductNamesData =
|
|
1062
|
-
abi.encodeCall(Banny721TokenUriResolver.setProductNames, (productIds, names));
|
|
1063
|
-
vm.writeFile(
|
|
1064
|
-
string.concat("./", vm.toString(block.chainid), "-setProductNames.hex.txt"),
|
|
1065
|
-
vm.toString(setProductNamesData)
|
|
1066
|
-
);
|
|
1067
|
-
} else {
|
|
1068
|
-
hook.adjustTiers(products, new uint256[](0));
|
|
1069
|
-
bannyverse.resolver.setSvgHashesOf(productIds, svgHashes);
|
|
1070
|
-
bannyverse.resolver.setProductNames(productIds, names);
|
|
1071
|
-
}
|
|
1050
|
+
hook.adjustTiers(products, new uint256[](0));
|
|
1051
|
+
bannyverse.resolver.setSvgHashesOf(productIds, svgHashes);
|
|
1052
|
+
bannyverse.resolver.setProductNames(productIds, names);
|
|
1072
1053
|
}
|
|
1073
1054
|
}
|
|
@@ -18,7 +18,13 @@ library BannyverseDeploymentLib {
|
|
|
18
18
|
address internal constant VM_ADDRESS = address(uint160(uint256(keccak256("hevm cheat code"))));
|
|
19
19
|
Vm internal constant vm = Vm(VM_ADDRESS);
|
|
20
20
|
|
|
21
|
-
function getDeployment(
|
|
21
|
+
function getDeployment(
|
|
22
|
+
string memory path,
|
|
23
|
+
uint256 revnetId
|
|
24
|
+
)
|
|
25
|
+
internal
|
|
26
|
+
returns (BannyverseDeployment memory deployment)
|
|
27
|
+
{
|
|
22
28
|
// get chainId for which we need to get the deployment.
|
|
23
29
|
uint256 chainId = block.chainid;
|
|
24
30
|
|
|
@@ -29,7 +35,7 @@ library BannyverseDeploymentLib {
|
|
|
29
35
|
|
|
30
36
|
for (uint256 _i; _i < networks.length; _i++) {
|
|
31
37
|
if (networks[_i].chainId == chainId) {
|
|
32
|
-
return getDeployment(path, networks[_i].name);
|
|
38
|
+
return getDeployment(path, networks[_i].name, revnetId);
|
|
33
39
|
}
|
|
34
40
|
}
|
|
35
41
|
|
|
@@ -38,7 +44,8 @@ library BannyverseDeploymentLib {
|
|
|
38
44
|
|
|
39
45
|
function getDeployment(
|
|
40
46
|
string memory path,
|
|
41
|
-
string memory network_name
|
|
47
|
+
string memory network_name,
|
|
48
|
+
uint256 revnetId
|
|
42
49
|
)
|
|
43
50
|
internal
|
|
44
51
|
view
|
|
@@ -48,7 +55,7 @@ library BannyverseDeploymentLib {
|
|
|
48
55
|
_getDeploymentAddress(path, "banny-core-v6", network_name, "Banny721TokenUriResolver")
|
|
49
56
|
);
|
|
50
57
|
|
|
51
|
-
deployment.revnetId =
|
|
58
|
+
deployment.revnetId = revnetId;
|
|
52
59
|
}
|
|
53
60
|
|
|
54
61
|
/// @notice Get the address of a contract that was deployed by the Deploy script.
|