@bannynet/core-v6 0.0.5 → 0.0.7
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 +97 -0
- package/ARCHITECTURE.md +58 -0
- package/RISKS.md +160 -0
- package/STYLE_GUIDE.md +558 -0
- package/foundry.toml +4 -4
- package/package.json +8 -6
- package/remappings.txt +1 -1
- package/script/Add.Denver.s.sol +1 -1
- package/script/Deploy.s.sol +5 -5
- package/script/Drop1.s.sol +1 -1
- package/script/helpers/BannyverseDeploymentLib.sol +1 -1
- package/script/outfit_drop/generate-migration.js +2 -2
- package/src/Banny721TokenUriResolver.sol +18 -2
- package/test/Banny721TokenUriResolver.t.sol +2 -5
- package/test/BannyAttacks.t.sol +1 -4
- package/test/DecorateFlow.t.sol +1 -4
- package/test/Fork.t.sol +2012 -0
- package/test/regression/{L58_ArrayLengthValidation.t.sol → ArrayLengthValidation.t.sol} +3 -4
- package/test/regression/{L57_BodyCategoryValidation.t.sol → BodyCategoryValidation.t.sol} +3 -3
- package/test/regression/{L62_BurnedTokenCheck.t.sol → BurnedTokenCheck.t.sol} +3 -3
- package/test/regression/{I25_CEIReorder.t.sol → CEIReorder.t.sol} +3 -3
- package/test/regression/{L59_ClearMetadata.t.sol → ClearMetadata.t.sol} +3 -3
- package/test/regression/{L56_MsgSenderEvents.t.sol → MsgSenderEvents.t.sol} +3 -3
- package/test/regression/RemovedTierDesync.t.sol +341 -0
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# Administration
|
|
2
|
+
|
|
3
|
+
Admin privileges and their scope in banny-retail-v6. The contract (`Banny721TokenUriResolver`) is a single-file system with one admin role (Ownable) and per-token owner privileges.
|
|
4
|
+
|
|
5
|
+
## Roles
|
|
6
|
+
|
|
7
|
+
| Role | How Assigned | Scope |
|
|
8
|
+
|------|-------------|-------|
|
|
9
|
+
| **Contract Owner** | Set via constructor `Ownable(owner)` (line 174). 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)` (line 605). | Per-token: decoration, locking |
|
|
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
|
+
| **Anyone (Permissionless)** | No restriction. | Upload SVG content (if matching hash exists) |
|
|
13
|
+
| **Trusted Forwarder** | Set at construction via `ERC2771Context(trustedForwarder)` (line 175). Immutable after deploy. | Meta-transaction relay: `_msgSender()` resolves to the relayed sender |
|
|
14
|
+
|
|
15
|
+
## Privileged Functions
|
|
16
|
+
|
|
17
|
+
### Banny721TokenUriResolver -- Owner-Only Functions
|
|
18
|
+
|
|
19
|
+
| Function | Line | Guard | What It Does |
|
|
20
|
+
|----------|------|-------|-------------|
|
|
21
|
+
| `setMetadata(description, url, baseUri)` | 1054-1068 | `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)` | 1073-1084 | `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)` | 1119-1134 | `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
|
+
|
|
25
|
+
### Banny721TokenUriResolver -- Permissionless Functions
|
|
26
|
+
|
|
27
|
+
| Function | Line | Guard | What It Does |
|
|
28
|
+
|----------|------|-------|-------------|
|
|
29
|
+
| `setSvgContentsOf(upcs, svgContents)` | 1089-1113 | 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
|
+
|
|
31
|
+
### Banny721TokenUriResolver -- NFT-Owner Functions
|
|
32
|
+
|
|
33
|
+
| Function | Line | Guard | What It Does |
|
|
34
|
+
|----------|------|-------|-------------|
|
|
35
|
+
| `decorateBannyWith(hook, bannyBodyId, backgroundId, outfitIds)` | 969-999 | `_checkIfSenderIsOwner` (line 979) + `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)` | 1005-1019 | `_checkIfSenderIsOwner` (line 1007) | Locks a banny body's outfit for 7 days (`_LOCK_DURATION`). Lock can only be extended, never shortened (`CantAccelerateTheLock`, line 1016). Prevents `decorateBannyWith` during the lock period. |
|
|
37
|
+
|
|
38
|
+
### Banny721TokenUriResolver -- Restricted Receiver
|
|
39
|
+
|
|
40
|
+
| Function | Line | Guard | What It Does |
|
|
41
|
+
|----------|------|-------|-------------|
|
|
42
|
+
| `onERC721Received(operator, from, tokenId, data)` | 1027-1046 | `operator == address(this)` (line 1043) | Only accepts incoming NFT transfers when the resolver itself initiated the transfer. Rejects all direct user transfers to the resolver contract with `UnauthorizedTransfer`. |
|
|
43
|
+
|
|
44
|
+
## Asset Management
|
|
45
|
+
|
|
46
|
+
**Who can add SVG assets:**
|
|
47
|
+
|
|
48
|
+
1. The contract **owner** commits SVG hashes via `setSvgHashesOf()`. This is the only gatekeeping step -- only the owner decides which UPCs get artwork.
|
|
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
|
+
|
|
51
|
+
**Immutability of stored content:**
|
|
52
|
+
- Once a hash is set for a UPC, it cannot be changed (line 1127: `HashAlreadyStored`).
|
|
53
|
+
- Once content is uploaded for a UPC, it cannot be replaced (line 1097: `ContentsAlreadyStored`).
|
|
54
|
+
- There is no function to delete or modify stored SVG hashes or content.
|
|
55
|
+
|
|
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()` (lines 888-902) and cannot be overridden.
|
|
58
|
+
|
|
59
|
+
**Metadata:**
|
|
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
|
+
|
|
62
|
+
## Immutable Configuration
|
|
63
|
+
|
|
64
|
+
The following are set at construction and cannot be changed:
|
|
65
|
+
|
|
66
|
+
| Property | Set At | Value |
|
|
67
|
+
|----------|--------|-------|
|
|
68
|
+
| `BANNY_BODY` | Constructor (line 177) | Base SVG path for all banny body rendering |
|
|
69
|
+
| `DEFAULT_NECKLACE` | Constructor (line 178) | Default necklace SVG injected when no custom necklace equipped |
|
|
70
|
+
| `DEFAULT_MOUTH` | Constructor (line 179) | Default mouth SVG injected when no custom mouth equipped |
|
|
71
|
+
| `DEFAULT_STANDARD_EYES` | Constructor (line 180) | Default eyes SVG for non-alien bodies |
|
|
72
|
+
| `DEFAULT_ALIEN_EYES` | Constructor (line 181) | Default eyes SVG for alien bodies |
|
|
73
|
+
| `trustedForwarder` | Constructor (line 175) | ERC-2771 forwarder address for meta-transactions |
|
|
74
|
+
| `_LOCK_DURATION` | Constant (line 63) | 7 days -- hardcoded, not configurable |
|
|
75
|
+
| Body color fills | Hardcoded in `_fillsFor()` (lines 661-685) | Color palettes for Alien, Pink, Orange, Original body types |
|
|
76
|
+
| Category IDs | Constants (lines 65-82) | 18 category slots (0-17), hardcoded |
|
|
77
|
+
|
|
78
|
+
## Admin Boundaries
|
|
79
|
+
|
|
80
|
+
**What the owner CANNOT do:**
|
|
81
|
+
|
|
82
|
+
- **Cannot move or steal user NFTs.** The resolver only holds custody of outfit/background NFTs that users voluntarily equip. The `onERC721Received` guard (line 1043) ensures only self-initiated transfers are accepted.
|
|
83
|
+
- **Cannot modify stored SVG content.** Once hash + content are committed, they are permanent. No delete or update function exists.
|
|
84
|
+
- **Cannot modify stored SVG hashes.** Each UPC's hash is write-once.
|
|
85
|
+
- **Cannot change the lock duration.** The 7-day lock is a compile-time constant.
|
|
86
|
+
- **Cannot force-equip or force-unequip outfits.** Only the body NFT's owner can call `decorateBannyWith` and `lockOutfitChangesFor`.
|
|
87
|
+
- **Cannot override hardcoded body names.** UPCs 1-4 always resolve to Alien, Pink, Orange, Original regardless of `_customProductNameOf`.
|
|
88
|
+
- **Cannot change the trusted forwarder.** It is immutable after construction.
|
|
89
|
+
- **Cannot pause the contract.** There is no pause mechanism.
|
|
90
|
+
- **Cannot upgrade the contract.** It is not upgradeable.
|
|
91
|
+
|
|
92
|
+
**What the owner CAN do that affects users:**
|
|
93
|
+
|
|
94
|
+
- **Change metadata** (`svgDescription`, `svgExternalUrl`, `svgBaseUri`). This affects how all tokens render in wallets/marketplaces. Clearing `svgBaseUri` would break IPFS-based fallback rendering for products without on-chain SVG content.
|
|
95
|
+
- **Rename products.** Custom product names (UPC > 4) can be changed at any time, altering how NFTs display.
|
|
96
|
+
- **Commit new SVG hashes.** This controls which new artwork can be uploaded, but cannot affect already-stored content.
|
|
97
|
+
- **Transfer ownership.** Via OpenZeppelin `transferOwnership()`, the owner can hand off all admin privileges to a new address (including a multisig, DAO, or malicious actor).
|
package/ARCHITECTURE.md
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# banny-retail-v6 — Architecture
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
Banny NFT asset manager for Juicebox V6. Stores on-chain SVG artwork for Banny characters and generates fully on-chain token URI metadata. Supports outfit composition (bodies, backgrounds, heads, suits) with lockable outfit changes.
|
|
6
|
+
|
|
7
|
+
## Contract Map
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
src/
|
|
11
|
+
├── Banny721TokenUriResolver.sol — Token URI resolver: SVG generation, outfit management, asset storage
|
|
12
|
+
└── interfaces/
|
|
13
|
+
└── IBanny721TokenUriResolver.sol — Interface for outfit and asset operations
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Key Operations
|
|
17
|
+
|
|
18
|
+
### Asset Storage
|
|
19
|
+
```
|
|
20
|
+
Owner → storeContents(hash, contents)
|
|
21
|
+
→ Store SVG content chunks on-chain (keyed by hash)
|
|
22
|
+
→ Content stored in multiple chunks for large SVGs
|
|
23
|
+
|
|
24
|
+
Owner → addProduct(category, hash)
|
|
25
|
+
→ Register a product (body/background/head/suit) for a category
|
|
26
|
+
→ Products linked to stored SVG content
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### Outfit Composition
|
|
30
|
+
```
|
|
31
|
+
NFT Holder → dress(tokenId, outfitTokenIds[])
|
|
32
|
+
→ Attach outfit NFTs (head, suit, background) to a body NFT
|
|
33
|
+
→ Outfit NFTs transferred to resolver contract (locked)
|
|
34
|
+
→ Composite SVG generated from layered components
|
|
35
|
+
|
|
36
|
+
NFT Holder → undress(tokenId, outfitTokenIds[])
|
|
37
|
+
→ Remove outfit NFTs from body
|
|
38
|
+
→ Outfit NFTs returned to holder
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### Token URI Generation
|
|
42
|
+
```
|
|
43
|
+
JB721TiersHook → tokenURI(tokenId)
|
|
44
|
+
→ Banny721TokenUriResolver.tokenURI(hook, tokenId)
|
|
45
|
+
→ Look up body tier and any attached outfits
|
|
46
|
+
→ Compose SVG layers (background → body → outfits)
|
|
47
|
+
→ Encode as base64 data URI with JSON metadata
|
|
48
|
+
→ Return fully on-chain SVG
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Dependencies
|
|
52
|
+
- `@bananapus/721-hook-v6` — NFT tier system (IJB721TiersHook, IJB721TokenUriResolver)
|
|
53
|
+
- `@bananapus/core-v6` — Core protocol interfaces
|
|
54
|
+
- `@bananapus/router-terminal-v6` — Payment routing
|
|
55
|
+
- `@bananapus/suckers-v6` — Cross-chain support
|
|
56
|
+
- `@rev-net/core-v6` — Revnet integration
|
|
57
|
+
- `@openzeppelin/contracts` — Ownable, ERC2771, ReentrancyGuard, Strings
|
|
58
|
+
- `keccak` — Hashing utilities
|
package/RISKS.md
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# banny-retail-v6 -- Risks
|
|
2
|
+
|
|
3
|
+
Deep implementation-level risk analysis of `Banny721TokenUriResolver`. All line references are to `src/Banny721TokenUriResolver.sol` unless otherwise noted.
|
|
4
|
+
|
|
5
|
+
## Trust Assumptions
|
|
6
|
+
|
|
7
|
+
1. **Contract Owner (Ownable)** -- Can commit SVG hashes, set metadata, rename products. Cannot modify stored content or hashes once committed. Cannot touch user NFTs. See ADMINISTRATION.md for full scope.
|
|
8
|
+
2. **JB721TiersHook** -- The resolver treats the `hook` address parameter as a trusted 721 contract. It calls `IERC721(hook).ownerOf()`, `hook.STORE()`, and `hook.safeTransferFrom()` without verifying that `hook` implements any particular interface. A malicious hook could manipulate ownership returns or transfer behavior.
|
|
9
|
+
3. **Trusted Forwarder (ERC-2771)** -- Set immutably at construction (line 175). The forwarder can spoof `_msgSender()` for all authorization checks. If the forwarder is compromised or misconfigured, all ownership checks can be bypassed.
|
|
10
|
+
4. **On-Chain SVG Storage** -- Content is permanent and censorship-resistant once stored. Hash-then-reveal pattern prevents unauthorized content. Owner controls which hashes are committed.
|
|
11
|
+
|
|
12
|
+
## Risk Register
|
|
13
|
+
|
|
14
|
+
### CRITICAL -- Outfit Custody and Asset Loss
|
|
15
|
+
|
|
16
|
+
**Risk: Equipped NFTs held by resolver contract**
|
|
17
|
+
- Severity: **CRITICAL** | Tested: **YES** (Fork.t.sol lines 931-956, BannyAttacks.t.sol)
|
|
18
|
+
- When outfits or backgrounds are equipped via `decorateBannyWith()`, the NFT is transferred to the resolver contract via `safeTransferFrom` (lines 1191, 1325, 1366). The resolver holds custody until the body owner unequips.
|
|
19
|
+
- **Impact**: If the resolver contract has a bug, equipped NFTs could become permanently locked. All value of equipped outfits depends on the resolver's correctness.
|
|
20
|
+
- **Mitigation**: ReentrancyGuard on `decorateBannyWith` (line 977). `_tryTransferFrom` (lines 1375-1378) uses try-catch for returning old outfits, so burned/removed-tier tokens do not block redecoration. Tested in regression BurnedTokenCheck.t.sol.
|
|
21
|
+
- **Residual risk**: The resolver is not upgradeable. If a critical bug is found post-deployment, there is no admin mechanism to rescue stuck NFTs. The owner has no function to force-return assets.
|
|
22
|
+
|
|
23
|
+
**Risk: Body transfer transfers outfit control**
|
|
24
|
+
- Severity: **HIGH** | Tested: **YES** (Fork.t.sol lines 931-956)
|
|
25
|
+
- When a banny body NFT is transferred, all equipped outfits remain associated with that body. The new body owner can unequip all outfits and receive the outfit NFTs. This is by design but creates a significant gotcha for sellers.
|
|
26
|
+
- **Impact**: A seller who forgets to unequip before selling a body loses all equipped outfit NFTs to the buyer.
|
|
27
|
+
- **Mitigation**: Lock mechanism (`lockOutfitChangesFor`, line 1005) allows sellers to lock outfits to the body for 7 days, guaranteeing buyers receive the advertised outfits. The lock persists across transfers (tested in Fork.t.sol lines 581-603).
|
|
28
|
+
- **Residual risk**: No automated warning at the hook/marketplace level. Users must know to unequip or lock before listing.
|
|
29
|
+
|
|
30
|
+
### HIGH -- Front-Running and Griefing
|
|
31
|
+
|
|
32
|
+
**Risk: Outfit strip front-running before sale**
|
|
33
|
+
- Severity: **HIGH** | Tested: **YES** (Fork.t.sol lines 1172-1228)
|
|
34
|
+
- A seller can list a dressed banny, then front-run the sale by stripping all outfits. The buyer receives a naked body.
|
|
35
|
+
- **Mitigation**: The lock mechanism exists precisely for this. Tested in `test_fork_grief_lockPreventsFrontRunStrip` (Fork.t.sol line 1196). A locked body cannot have outfits changed during the lock period.
|
|
36
|
+
- **Residual risk**: Requires buyer awareness. Buyers should verify `outfitLockedUntil` before purchasing dressed bannys.
|
|
37
|
+
|
|
38
|
+
**Risk: Lock cannot be shortened (anti-griefing)**
|
|
39
|
+
- Severity: **LOW** | Tested: **YES** (Banny721TokenUriResolver.t.sol lines 393-403, Fork.t.sol lines 568-579)
|
|
40
|
+
- `lockOutfitChangesFor` checks `currentLockedUntil > newLockUntil` (line 1016) and reverts with `CantAccelerateTheLock` if the new lock would expire sooner. Equal values are allowed (same-block re-lock succeeds).
|
|
41
|
+
- **Impact**: A body owner who locks cannot undo the lock early. This is intentional -- it provides a guarantee to buyers.
|
|
42
|
+
- **Note**: The owner role has no ability to override locks. The 7-day `_LOCK_DURATION` constant (line 63) is not configurable.
|
|
43
|
+
|
|
44
|
+
### HIGH -- Reentrancy
|
|
45
|
+
|
|
46
|
+
**Risk: Reentrancy via safeTransferFrom callbacks**
|
|
47
|
+
- Severity: **HIGH** | Tested: **YES** (Fork.t.sol lines 853-925)
|
|
48
|
+
- `decorateBannyWith` calls `safeTransferFrom` (lines 1191, 1325, 1366) which triggers `onERC721Received` on receiving contracts. A malicious hook could attempt to re-enter `decorateBannyWith` during these callbacks.
|
|
49
|
+
- **Mitigation**: `decorateBannyWith` has `nonReentrant` modifier (line 977, OpenZeppelin ReentrancyGuard). Tested with a purpose-built `ReentrantHook` (Fork.t.sol line 46) that re-enters during `safeTransferFrom`. The reentrancy attempt is caught and silently fails via try-catch.
|
|
50
|
+
- **CEI pattern**: Background replacement follows Checks-Effects-Interactions. State updates (`_attachedBackgroundIdOf`, `_userOf`) happen at lines 1181-1182 before external transfers at lines 1186-1192. Verified in regression CEIReorder.t.sol.
|
|
51
|
+
- **Residual risk**: None identified. The ReentrancyGuard provides a hard block, and CEI ordering provides defense-in-depth.
|
|
52
|
+
|
|
53
|
+
### MEDIUM -- Hook Trust Boundary
|
|
54
|
+
|
|
55
|
+
**Risk: Untrusted hook address parameter**
|
|
56
|
+
- Severity: **MEDIUM** | Tested: **PARTIAL** (Fork.t.sol lines 1085-1114 for cross-hook isolation)
|
|
57
|
+
- The `hook` parameter in `decorateBannyWith`, `tokenUriOf`, and all view functions is caller-supplied. The resolver calls `IERC721(hook).ownerOf()`, `IJB721TiersHook(hook).STORE()`, and `IERC721(hook).safeTransferFrom()` on this address.
|
|
58
|
+
- **Impact**: A malicious hook contract could return arbitrary data from `ownerOf()`, `STORE()`, and `tierOfTokenId()`. However, since outfit custody is per-hook (the mapping is `address hook => mapping(...)` at lines 127-152), a malicious hook cannot access NFTs custodied from a different hook.
|
|
59
|
+
- **Mitigation**: Per-hook state isolation. Cross-hook interference tested in `test_fork_edge_crossHookIsolation` (Fork.t.sol line 1085).
|
|
60
|
+
- **Residual risk**: If a user interacts with a malicious hook, they could lose the NFTs they equip on that hook. The resolver cannot distinguish legitimate from malicious hooks.
|
|
61
|
+
|
|
62
|
+
**Risk: Removed tier desynchronization**
|
|
63
|
+
- Severity: **MEDIUM** | Tested: **YES** (RemovedTierDesync.t.sol, 6 test cases)
|
|
64
|
+
- When a tier is removed from the JB721TiersHookStore, `_productOfTokenId()` returns a zeroed struct (category=0, id=0). Previously equipped outfits from that tier have category 0 in the redecoration loop.
|
|
65
|
+
- **Impact**: Without proper handling, removed-tier outfits could cause the `_decorateBannyWithOutfits` loop to malfunction -- the category-0 entries would be processed incorrectly by the `while` loop (line 1297).
|
|
66
|
+
- **Mitigation**: The current code handles this correctly. Category-0 entries are processed and transferred out by the while loop. The `_tryTransferFrom` (line 1303, 1345) silently handles cases where the token no longer exists. Six regression tests verify: first/middle/last tier removal, all tiers removed, replacement after removal, and two consecutive removed tiers.
|
|
67
|
+
|
|
68
|
+
### MEDIUM -- SVG and Rendering
|
|
69
|
+
|
|
70
|
+
**Risk: Gas-intensive tokenURI view calls**
|
|
71
|
+
- Severity: **MEDIUM** | Tested: **YES** (Fork.t.sol lines 280-334, 793-847)
|
|
72
|
+
- `tokenUriOf()` (line 192) performs extensive string concatenation, Base64 encoding, and multi-layer SVG composition. For a fully dressed banny with 9 outfits + background, the view function iterates through all outfit layers, composes SVGs, builds JSON metadata with attributes, and encodes everything.
|
|
73
|
+
- **Impact**: Off-chain callers (marketplaces, wallets, indexers) may hit gas limits on `eth_call`. This does not affect on-chain operations since `tokenUriOf` is a view function.
|
|
74
|
+
- **Residual risk**: RPC providers may time out or gas-limit view calls for heavily dressed bannys. No on-chain mitigation needed.
|
|
75
|
+
|
|
76
|
+
**Risk: SVG injection via stored content**
|
|
77
|
+
- Severity: **LOW** | Tested: **NO** (content validation is hash-based, not sanitization-based)
|
|
78
|
+
- SVG content is stored verbatim in `_svgContentOf` (line 1109) after hash verification. The content is concatenated directly into the SVG output (lines 537-554, 928-934) without sanitization.
|
|
79
|
+
- **Impact**: If the owner commits a hash for malicious SVG content (e.g., containing `<script>` tags or external references), it would be embedded in all token URIs that reference that UPC. Modern SVG renderers in browsers and wallets typically sandbox SVG content, but some clients may be vulnerable.
|
|
80
|
+
- **Mitigation**: The hash-commit pattern means only intentionally chosen content can be stored. The owner is the trust boundary here. Content is also immutable once stored, so a compromised owner cannot retroactively inject malicious content into existing assets.
|
|
81
|
+
|
|
82
|
+
### MEDIUM -- Outfit Array Bounds
|
|
83
|
+
|
|
84
|
+
**Risk: Unbounded outfit iteration in _outfitContentsFor**
|
|
85
|
+
- Severity: **MEDIUM** | Tested: **YES** (Fork.t.sol line 400, max 9 outfits)
|
|
86
|
+
- `_outfitContentsFor()` (line 789) iterates `numberOfOutfits + 1` times. The outfit array is bounded by the number of valid categories (18 total, but only ~12 are usable as outfits due to body/background exclusion and conflict rules).
|
|
87
|
+
- **Impact**: Gas cost scales linearly with outfit count. With maximum 9 non-conflicting outfits (tested in `test_fork_decorateMaxOutfits`), gas is manageable. The category ordering constraint (line 1269-1271) and conflict rules (lines 1273-1289) naturally limit the array size.
|
|
88
|
+
- **Mitigation**: Category ordering enforcement means outfits cannot be duplicated or out of order. The practical maximum is ~12 outfits (one per non-body, non-background category minus conflicts).
|
|
89
|
+
|
|
90
|
+
**Risk: Unbounded _attachedOutfitIdsOf growth (theoretical)**
|
|
91
|
+
- Severity: **LOW** | Tested: **PARTIAL**
|
|
92
|
+
- The `_attachedOutfitIdsOf` array (line 127) is replaced wholesale on each `decorateBannyWith` call (line 1357). It is not appended to -- it is fully overwritten with the new `outfitIds` array. The old array's storage is not explicitly cleared but is overwritten.
|
|
93
|
+
- **Impact**: No unbounded growth risk in practice since the array is replaced, not extended.
|
|
94
|
+
|
|
95
|
+
### LOW -- ERC-2771 Meta-Transaction
|
|
96
|
+
|
|
97
|
+
**Risk: Trusted forwarder compromise**
|
|
98
|
+
- Severity: **LOW** (deployment-dependent) | Tested: **NO** (no meta-tx-specific tests)
|
|
99
|
+
- If `trustedForwarder` is set to a non-zero address at construction (line 175), that contract can append arbitrary sender addresses to calldata, causing `_msgSender()` to return any address.
|
|
100
|
+
- **Impact**: Complete bypass of all ownership checks (`_checkIfSenderIsOwner`, `onlyOwner`, outfit/background authorization).
|
|
101
|
+
- **Mitigation**: The constructor parameter is typically set to `address(0)` (as seen in all test setups), which disables meta-transactions entirely. If used, the forwarder must be a well-audited, immutable contract.
|
|
102
|
+
|
|
103
|
+
### LOW -- Content Immutability Edge Cases
|
|
104
|
+
|
|
105
|
+
**Risk: SVG hash set but content never uploaded**
|
|
106
|
+
- Severity: **LOW** | Tested: **YES** (Banny721TokenUriResolver.t.sol lines 316-324)
|
|
107
|
+
- If the owner sets a hash via `setSvgHashesOf` but nobody ever uploads matching content, `_svgContentOf[upc]` remains empty. The `_svgOf` function (line 922) falls back to IPFS resolution via `JBIpfsDecoder.decode()` (lines 928-934).
|
|
108
|
+
- **Impact**: Tokens render using IPFS fallback URI instead of on-chain SVG. Not a loss of functionality, just a different rendering path.
|
|
109
|
+
|
|
110
|
+
**Risk: Product name overwrite**
|
|
111
|
+
- Severity: **LOW** | Tested: **NO** (no test for overwriting names)
|
|
112
|
+
- `setProductNames()` (line 1073) can overwrite previously set custom product names. Unlike SVG hashes and content, names have no write-once protection.
|
|
113
|
+
- **Impact**: The owner can rename products at any time, changing how NFTs display in wallets. Could be used to mislead users about what they hold.
|
|
114
|
+
- **Mitigation**: Built-in names for UPCs 1-4 (Alien, Pink, Orange, Original) are hardcoded (lines 890-901) and cannot be overridden.
|
|
115
|
+
|
|
116
|
+
### LOW -- Authorization Edge Cases
|
|
117
|
+
|
|
118
|
+
**Risk: Outfit reuse across bodies by same owner**
|
|
119
|
+
- Severity: **LOW** (intended behavior) | Tested: **YES** (BannyAttacks.t.sol line 181, Fork.t.sol line 421)
|
|
120
|
+
- When a user owns multiple bodies, they can move an equipped outfit from one body to another in a single `decorateBannyWith` call. The authorization check at lines 1246-1258 allows this: if the caller owns the body currently wearing the outfit, they can authorize its use on a different body.
|
|
121
|
+
- **Impact**: Expected behavior. No security issue.
|
|
122
|
+
|
|
123
|
+
**Risk: L18 -- ownerOf(0) authorization bypass (FIXED)**
|
|
124
|
+
- Severity: **CRITICAL (historical)** | Tested: **YES** (DecorateFlow.t.sol lines 179-239)
|
|
125
|
+
- The old code called `IERC721(hook).ownerOf(wearerOf(hook, outfitId))` without first checking if `wearerOf` returned 0. When an outfit was unworn, this resolved to `ownerOf(0)`, and if an attacker owned token 0, they could pass the auth check and steal any unworn outfit.
|
|
126
|
+
- **Fix**: The current code (lines 1248-1257) checks `wearerId == 0` first and reverts with `UnauthorizedOutfit` before any `ownerOf` call. Six regression tests in DecorateFlow.t.sol verify the fix.
|
|
127
|
+
|
|
128
|
+
## Test Coverage Summary
|
|
129
|
+
|
|
130
|
+
| Test File | Tests | What It Covers |
|
|
131
|
+
|-----------|-------|----------------|
|
|
132
|
+
| `Banny721TokenUriResolver.t.sol` | 22 | Unit tests: constructor, owner-only functions, lock mechanism, category conflicts, background/outfit equip, onERC721Received |
|
|
133
|
+
| `DecorateFlow.t.sol` | ~40 | L18 vulnerability proof, multi-body outfit reuse, authorization flows, three-party interactions, background replacement, edge cases |
|
|
134
|
+
| `BannyAttacks.t.sol` | 8 | Adversarial: outfit reuse, lock bypass, category conflicts, unauthorized decoration, out-of-order categories, body/background as outfit |
|
|
135
|
+
| `Fork.t.sol` | 40+ | E2E against real JB infrastructure: full lifecycle, multi-actor, reentrancy (ReentrantHook), griefing/front-running, cross-hook isolation, redressing cycles |
|
|
136
|
+
| `regression/CEIReorder.t.sol` | 3 | CEI ordering in background replacement |
|
|
137
|
+
| `regression/RemovedTierDesync.t.sol` | 6 | Removed tier handling during redecoration |
|
|
138
|
+
| `regression/ArrayLengthValidation.t.sol` | 3 | Array length mismatch reverts |
|
|
139
|
+
| `regression/BodyCategoryValidation.t.sol` | 2 | Non-body token as bannyBodyId rejection |
|
|
140
|
+
| `regression/MsgSenderEvents.t.sol` | 4 | Events emit `_msgSender()` not `msg.sender` |
|
|
141
|
+
| `regression/BurnedTokenCheck.t.sol` | 2 | Burned equipped tokens do not lock the body |
|
|
142
|
+
| `regression/ClearMetadata.t.sol` | 2 | setMetadata can clear fields to empty strings |
|
|
143
|
+
|
|
144
|
+
## Untested Areas
|
|
145
|
+
|
|
146
|
+
1. **Meta-transaction flows** -- No tests exercise the `trustedForwarder` path with a real forwarder contract. All tests use `address(0)` as forwarder.
|
|
147
|
+
2. **SVG content sanitization** -- No tests verify that stored SVG content is safe for rendering. The hash-commit pattern is tested, but content safety is not.
|
|
148
|
+
3. **Product name overwriting** -- No test verifies behavior when a custom product name is overwritten with a different name.
|
|
149
|
+
4. **Extreme gas consumption** -- No test measures gas for `tokenUriOf` with maximum outfit count and large SVG payloads.
|
|
150
|
+
5. **Ownership transfer of resolver** -- No test exercises `transferOwnership()` and verifies admin functions still work with the new owner.
|
|
151
|
+
|
|
152
|
+
## Invariants (Implied by Tests)
|
|
153
|
+
|
|
154
|
+
- Outfit NFTs held by the resolver are always retrievable by the current body owner (or returned on redecoration).
|
|
155
|
+
- Burned or removed-tier equipped tokens do not prevent body redecoration (`_tryTransferFrom` silently handles failures).
|
|
156
|
+
- Category ordering is strictly enforced (ascending, no duplicates).
|
|
157
|
+
- Category conflicts (head blocks face pieces; suit blocks top/bottom) are enforced.
|
|
158
|
+
- Cross-hook state is fully isolated.
|
|
159
|
+
- Lock duration can only increase, never decrease.
|
|
160
|
+
- SVG hashes and content are write-once per UPC.
|