@bannynet/core-v6 0.0.11 → 0.0.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ADMINISTRATION.md +42 -31
- package/ARCHITECTURE.md +41 -3
- package/AUDIT_INSTRUCTIONS.md +68 -41
- package/CHANGE_LOG.md +28 -7
- package/README.md +53 -1
- package/RISKS.md +33 -7
- package/SKILLS.md +44 -3
- package/STYLE_GUIDE.md +2 -2
- package/USER_JOURNEYS.md +327 -325
- package/foundry.toml +1 -1
- package/package.json +8 -8
- package/script/Add.Denver.s.sol +1 -1
- package/script/Deploy.s.sol +1 -1
- package/script/Drop1.s.sol +1 -1
- package/script/helpers/BannyverseDeploymentLib.sol +1 -1
- package/script/helpers/MigrationHelper.sol +1 -1
- package/src/Banny721TokenUriResolver.sol +132 -24
- package/test/Banny721TokenUriResolver.t.sol +1 -1
- package/test/BannyAttacks.t.sol +1 -1
- package/test/DecorateFlow.t.sol +1 -1
- package/test/Fork.t.sol +1 -1
- package/test/OutfitTransferLifecycle.t.sol +1 -1
- package/test/TestAuditGaps.sol +1 -1
- package/test/TestQALastMile.t.sol +1 -1
- package/test/audit/AntiStrandingRetention.t.sol +392 -0
- package/test/audit/MergedOutfitExclusivity.t.sol +223 -0
- package/test/audit/TryTransferFromStrandsAssets.t.sol +192 -0
- package/test/regression/ArrayLengthValidation.t.sol +1 -1
- package/test/regression/BodyCategoryValidation.t.sol +1 -1
- package/test/regression/BurnedTokenCheck.t.sol +1 -1
- package/test/regression/CEIReorder.t.sol +1 -1
- package/test/regression/ClearMetadata.t.sol +1 -1
- package/test/regression/MsgSenderEvents.t.sol +1 -1
- package/test/regression/RemovedTierDesync.t.sol +1 -1
- package/deployments/banny-core-v5/arbitrum/Banny721TokenUriResolver.json +0 -1809
- package/deployments/banny-core-v5/arbitrum_sepolia/Banny721TokenUriResolver.json +0 -1795
- package/deployments/banny-core-v5/base/Banny721TokenUriResolver.json +0 -1810
- package/deployments/banny-core-v5/base_sepolia/Banny721TokenUriResolver.json +0 -1796
- package/deployments/banny-core-v5/ethereum/Banny721TokenUriResolver.json +0 -1795
- package/deployments/banny-core-v5/optimism/Banny721TokenUriResolver.json +0 -1810
- package/deployments/banny-core-v5/optimism_sepolia/Banny721TokenUriResolver.json +0 -1796
- package/deployments/banny-core-v5/sepolia/Banny721TokenUriResolver.json +0 -1795
package/RISKS.md
CHANGED
|
@@ -10,20 +10,19 @@
|
|
|
10
10
|
## 2. Economic / Manipulation Risks
|
|
11
11
|
|
|
12
12
|
- **Outfit theft via banny body transfer.** Equipped outfits and backgrounds travel with the banny body NFT on transfer. If a banny body is sold with valuable outfits equipped, the buyer gains control of all equipped items. Sellers must unequip before selling. Marketplaces may not surface this risk.
|
|
13
|
-
- **try-catch silent failures.** `_tryTransferFrom` silently catches all transfer failures
|
|
13
|
+
- **try-catch silent failures with retention.** `_tryTransferFrom` silently catches all transfer failures and returns `false`. When a transfer fails, the resolver preserves the attachment record instead of clearing state. For backgrounds, the entire background change is aborted. For outfits, failed-to-return items are retained in the attached list via `_storeOutfitsWithRetained`. This prevents NFT stranding — assets remain tracked and recoverable once the transfer issue is resolved (e.g., the owner contract becomes receivable). However, if an outfit NFT is burned or its tier removed, the retained record refers to a non-existent asset, creating a phantom entry in the SVG rendering.
|
|
14
14
|
- **Lock griefing.** `lockOutfitChangesFor` extends the lock to `block.timestamp + 7 days`. Locking just before selling prevents the buyer from changing outfits for up to 7 days. The lock now also freezes reassignment of currently equipped outfits/backgrounds away from that body during the lock window.
|
|
15
15
|
|
|
16
16
|
## 3. Access Control
|
|
17
17
|
|
|
18
|
-
- **No hook validation.** Any address can be passed as `hook`. A malicious hook can return `_msgSender()` from `ownerOf()` to pass authorization checks, execute arbitrary code during `safeTransferFrom`, or return manipulated tier data from `STORE().tierOfTokenId()`.
|
|
18
|
+
- **No hook validation (HIGH impact).** Any address can be passed as `hook`. A malicious hook can return `_msgSender()` from `ownerOf()` to pass authorization checks, execute arbitrary code during `safeTransferFrom`, or return manipulated tier data from `STORE().tierOfTokenId()`.
|
|
19
19
|
- **SVG content upload is permissionless (with hash).** `setSvgContentsOf` only requires the content to match a pre-committed hash. Safe if hashes are correctly committed.
|
|
20
20
|
- **onERC721Received restriction.** Only accepts NFTs when `operator == address(this)`. `transferFrom` (non-safe) bypasses this -- NFTs sent via `transferFrom` are permanently locked with no rescue function.
|
|
21
21
|
|
|
22
22
|
## 4. DoS Vectors
|
|
23
23
|
|
|
24
|
-
- **
|
|
25
|
-
- **
|
|
26
|
-
- **External hook calls in view functions.** `tokenUriOf` and `svgOf` call into the hook's store multiple times per outfit. A malicious hook that consumes excessive gas or reverts can make token metadata unretrievable.
|
|
24
|
+
- **External call iteration scales with outfit count.** `_attachedOutfitIdsOf[hook][bannyBodyId]` is replaced wholesale on each `decorateBannyWith` call (not appended to), so the array is bounded by the number of currently equipped outfits, not cumulative history. However, `decorateBannyWith` iterates over both the previous and new outfit arrays to diff them (transferring removed outfits back and new outfits in), so gas cost scales with the number of outfits being equipped/unequipped in a single call.
|
|
25
|
+
- **External hook calls in view functions.** `tokenUriOf` and `svgOf` call into the hook's store multiple times per outfit. A malicious hook that consumes excessive gas or reverts can make token metadata unretrievable. Measured: `tokenUriOf` with a well-behaved hook and 9 equipped outfits costs ~609k gas (see `test_tokenUri_gasSnapshot_9outfits`). The practical ceiling for a malicious hook is bounded only by the caller's gas limit — RPC nodes typically cap `eth_call` at 30M+ gas, so even expensive hooks won't fail for off-chain reads, but on-chain consumers (e.g., other contracts calling `tokenURI`) could revert.
|
|
27
26
|
|
|
28
27
|
## 5. Integration Risks
|
|
29
28
|
|
|
@@ -37,6 +36,33 @@
|
|
|
37
36
|
- Every outfit held by this contract has a corresponding `_wearerOf[hook][outfitId]` pointing to a valid banny body.
|
|
38
37
|
- Every background held by this contract has a corresponding `_userOf[hook][backgroundId]` pointing to a valid banny body.
|
|
39
38
|
- `outfitLockedUntil[hook][bannyBodyId]` is monotonically non-decreasing per banny body (lock can only be extended, never shortened).
|
|
40
|
-
- After `decorateBannyWith`, all previously equipped outfits not in the new set are transferred back to `_msgSender()`
|
|
41
|
-
- `_attachedOutfitIdsOf[hook][bannyBodyId]`
|
|
39
|
+
- After `decorateBannyWith`, all previously equipped outfits not in the new set are either transferred back to `_msgSender()` or retained in the attached list if the transfer failed.
|
|
40
|
+
- `_attachedOutfitIdsOf[hook][bannyBodyId]` contains the outfitIds passed to the most recent `decorateBannyWith` call, plus any retained outfits whose return transfer failed. Category exclusivity is enforced on the merged set (retained + new outfits), not just the new outfit set alone.
|
|
42
41
|
- SVG content integrity: `keccak256(_svgContentOf[upc]) == svgHashOf[upc]` for all populated entries.
|
|
42
|
+
- NFT custody balance: the number of outfit NFTs held by this contract (`IERC721(hook).balanceOf(address(this))`) equals the total number of outfits currently equipped across all banny bodies for that hook. Violations indicate phantom outfits (equipped in state but NFT lost via try-catch silent failure) or orphaned NFTs (held by contract but not tracked in `_wearerOf`).
|
|
43
|
+
|
|
44
|
+
## 7. Accepted Behaviors
|
|
45
|
+
|
|
46
|
+
### 7.1 Failed transfers retain attachment records (anti-stranding)
|
|
47
|
+
|
|
48
|
+
`_tryTransferFrom` catches all transfer failures and returns `false`. When returning a previously equipped item fails, the resolver preserves the attachment record rather than clearing state:
|
|
49
|
+
|
|
50
|
+
- **Backgrounds**: If returning the old background fails, the entire background change is aborted (`return` in `_decorateBannyWithBackground`). The old background stays attached and the new one is not equipped.
|
|
51
|
+
- **Background removal**: If returning the background fails during removal (backgroundId=0), `_attachedBackgroundIdOf` is not cleared. The background stays attached.
|
|
52
|
+
- **Outfits**: Failed-to-return outfits remain non-zero in the `previousOutfitIds` array. `_storeOutfitsWithRetained` appends them to the new outfit list, preserving their attachment record.
|
|
53
|
+
|
|
54
|
+
This prevents NFT stranding — assets held by the resolver stay tracked and recoverable. Once the transfer issue is resolved (e.g., the owner contract implements `IERC721Receiver`), a subsequent `decorateBannyWith` call will successfully return the retained items.
|
|
55
|
+
|
|
56
|
+
For permanently unrecoverable assets (burned NFTs, removed tiers), the retained record creates a phantom entry in the SVG rendering and attached list. This is cosmetically incorrect but not economically exploitable — phantom entries cannot be transferred or sold. The alternative — reverting on any failed transfer — would make `decorateBannyWith` fragile: a single burned outfit would prevent the banny owner from changing ANY outfits.
|
|
57
|
+
|
|
58
|
+
### 7.2 Lock griefing window is bounded at 7 days
|
|
59
|
+
|
|
60
|
+
`lockOutfitChangesFor` extends the lock to `block.timestamp + 7 days`. A seller who locks just before transferring the banny forces the buyer to wait up to 7 days. This is accepted because: (1) marketplaces can check `outfitLockedUntil` before displaying the item, (2) the lock duration is fixed (not owner-configurable), and (3) the lock prevents a more severe attack where a buyer immediately strips valuable outfits — the lock gives the previous owner time to arrange the sale intentionally.
|
|
61
|
+
|
|
62
|
+
### 7.3 On-chain SVG rendering gas is well within limits
|
|
63
|
+
|
|
64
|
+
`tokenUriOf` constructs full SVGs on-chain with string concatenation. Measured gas ceiling: ~609K gas for the worst case (9 non-conflicting outfits + background with on-chain SVG content), well within typical RPC node limits (30M+). Regression test: `test_tokenUri_gasSnapshot_9outfits` in `test/TestQALastMile.t.sol`.
|
|
65
|
+
|
|
66
|
+
### 7.4 Reentrancy in non-guarded functions is harmless
|
|
67
|
+
|
|
68
|
+
`lockOutfitChangesFor` and all view functions (`tokenUriOf`, `svgOf`) are not protected by `nonReentrant`. A malicious hook's `STORE().tierOfTokenId()` could re-enter `lockOutfitChangesFor` during a `tokenUriOf` call, but this is harmless -- `lockOutfitChangesFor` only extends the lock timestamp (monotonically non-decreasing) and has no state that could be corrupted by reentrancy. The view functions themselves are read-only at the contract level (no storage writes), so reentrancy through them cannot extract value.
|
package/SKILLS.md
CHANGED
|
@@ -23,7 +23,7 @@ On-chain composable NFT avatar system that renders Banny characters with layered
|
|
|
23
23
|
|
|
24
24
|
| Function | What it does |
|
|
25
25
|
|----------|-------------|
|
|
26
|
-
| `decorateBannyWith(hook, bannyBodyId, backgroundId, outfitIds)` | Attaches background and outfits to a body. Transfers assets to contract custody. Validates ownership, lock status, category ordering, slot conflicts.
|
|
26
|
+
| `decorateBannyWith(hook, bannyBodyId, backgroundId, outfitIds)` | Attaches background and outfits to a body. Transfers assets to contract custody. Validates ownership, lock status, category ordering, slot conflicts. Attempts to return previously attached items to caller — if a return fails, the item is retained (not stranded). Emits `DecorateBanny`. |
|
|
27
27
|
| `lockOutfitChangesFor(hook, bannyBodyId)` | Locks a body's outfit for 7 days. Cannot accelerate an existing lock. Caller must own the body NFT. |
|
|
28
28
|
|
|
29
29
|
### Views
|
|
@@ -58,6 +58,46 @@ On-chain composable NFT avatar system that renders Banny characters with layered
|
|
|
58
58
|
| `@openzeppelin/contracts` | `Ownable`, `ReentrancyGuard`, `ERC2771Context`, `IERC721Receiver`, `Strings` | Access control, reentrancy protection, meta-transactions, safe NFT receipt, string utilities |
|
|
59
59
|
| `lib/base64` | `Base64` | Base64 encoding for on-chain SVG and JSON metadata |
|
|
60
60
|
|
|
61
|
+
### Deployment / Setup Sequence
|
|
62
|
+
|
|
63
|
+
The resolver is connected to a 721 hook in one of two ways:
|
|
64
|
+
|
|
65
|
+
1. **At hook deployment**: Pass the resolver address as `tokenUriResolver` in `JBDeploy721TiersHookConfig` when calling `IJB721TiersHookDeployer.deployHookFor(...)`. The hook's `initialize()` stores it in the hook store via `recordSetTokenUriResolver`.
|
|
66
|
+
2. **After deployment**: Call `hook.setMetadata(...)` with the resolver address as the `tokenUriResolver` parameter. Requires `SET_721_METADATA` permission. Pass `IJB721TokenUriResolver(address(this))` as a sentinel value to leave it unchanged, since `address(0)` clears the resolver.
|
|
67
|
+
|
|
68
|
+
Once set, the hook's store maps `tokenUriResolverOf[hook]` to the resolver address. Any `tokenURI(tokenId)` call on the hook delegates to `resolver.tokenUriOf(hook, tokenId)`, which composes the on-chain SVG.
|
|
69
|
+
|
|
70
|
+
## Rendering Order
|
|
71
|
+
|
|
72
|
+
The composed SVG layers elements back-to-front. Later layers are drawn on top of earlier layers. The full rendering order for a dressed body is:
|
|
73
|
+
|
|
74
|
+
1. **Background** (category 1) -- if attached and `shouldIncludeBackgroundOnBannyBody` is true
|
|
75
|
+
2. **Body** (category 0) -- the base Banny character with color-fill styles (`b1`-`b4`, `a1`-`a3`)
|
|
76
|
+
3. **Backside** (category 2) -- rendered behind the body in the SVG source but layered by the SVG's internal z-ordering
|
|
77
|
+
4. **Necklace** (category 3) -- default injected if none attached; custom necklaces are deferred and rendered after suit top (category 11)
|
|
78
|
+
5. **Head** (category 4) -- if present, suppresses default injection of eyes, mouth
|
|
79
|
+
6. **Eyes** (category 5) -- default alien or standard eyes injected if no head and no explicit eyes
|
|
80
|
+
7. **Glasses** (category 6)
|
|
81
|
+
8. **Mouth** (category 7) -- default mouth injected if no head and no explicit mouth
|
|
82
|
+
9. **Legs** (category 8)
|
|
83
|
+
10. **Suit** (category 9)
|
|
84
|
+
11. **Suit Bottom** (category 10)
|
|
85
|
+
12. **Suit Top** (category 11)
|
|
86
|
+
13. **Custom Necklace** -- rendered here (after suit top) if a custom necklace was provided, so it layers on top of clothing
|
|
87
|
+
14. **Headtop** (category 12)
|
|
88
|
+
15. **Hand** (category 13)
|
|
89
|
+
16. **Special overlays** (categories 14-17) -- suit, legs, head, body specials
|
|
90
|
+
|
|
91
|
+
For non-body tokens (outfits, backgrounds), the item is rendered on a grayscale mannequin Banny (`fill:#808080`) for preview.
|
|
92
|
+
|
|
93
|
+
## SVG Format Requirements
|
|
94
|
+
|
|
95
|
+
- **Canvas**: All SVGs are rendered within a `400x400` viewport: `<svg width="400" height="400" viewBox="0 0 400 400">`.
|
|
96
|
+
- **Fragment, not document**: SVG content stored via `setSvgContentsOf` must NOT include the parent `<svg>` element. The resolver wraps all fragments in the outer SVG element with shared styles (`.o{fill:#050505;}.w{fill:#f9f9f9;}`).
|
|
97
|
+
- **Hash verification only**: The contract validates uploaded SVG content against a pre-registered `keccak256` hash but performs no structural validation (no dimension checks, no viewBox enforcement). Ensuring content renders correctly at 400x400 is the uploader's responsibility.
|
|
98
|
+
- **Body color classes**: Body SVGs use CSS classes `b1`-`b4` (body fills) and `a1`-`a3` (accent fills) which are injected as `<style>` elements based on the body's UPC. Outfit SVGs can reference `.o` (dark, #050505) and `.w` (light, #f9f9f9) classes provided by the wrapper.
|
|
99
|
+
- **Immutability**: Once stored, SVG content cannot be changed. A content or hash error requires deploying a new resolver instance.
|
|
100
|
+
|
|
61
101
|
## Key Types
|
|
62
102
|
|
|
63
103
|
### Asset Categories
|
|
@@ -141,7 +181,7 @@ On-chain composable NFT avatar system that renders Banny characters with layered
|
|
|
141
181
|
| `_svgContentOf` | `upc => string` | On-chain SVG content (immutable once set) |
|
|
142
182
|
| `svgHashOf` | `upc => bytes32` | Expected SVG content hash (immutable once set) |
|
|
143
183
|
| `_customProductNameOf` | `upc => string` | Human-readable product name |
|
|
144
|
-
| `outfitLockedUntil` | `hook =>
|
|
184
|
+
| `outfitLockedUntil` | `hook => bannyBodyId => uint256` | Timestamp until outfit changes locked (declared as `upc` but keyed by token ID) |
|
|
145
185
|
| `_userOf` | `hook => backgroundId => uint256` | Which body uses a background |
|
|
146
186
|
| `_wearerOf` | `hook => outfitId => uint256` | Which body wears an outfit |
|
|
147
187
|
| `svgBaseUri` | `string` | IPFS/HTTP base URI for fallback SVG loading |
|
|
@@ -157,7 +197,7 @@ On-chain composable NFT avatar system that renders Banny characters with layered
|
|
|
157
197
|
5. **Strict ascending category order.** `decorateBannyWith` requires outfits passed in ascending category order. Reverts with `UnorderedCategories` if violated.
|
|
158
198
|
6. **Slot conflicts block combinations.** Head (4) blocks eyes (5), glasses (6), mouth (7), and headtop (12). Full suit (9) blocks suit top (11) and suit bottom (10). These are enforced at decoration time.
|
|
159
199
|
7. **Default injection.** If no explicit necklace, eyes, or mouth outfit is attached, the resolver auto-injects defaults during SVG rendering. Alien bodies get `DEFAULT_ALIEN_EYES`; others get `DEFAULT_STANDARD_EYES`.
|
|
160
|
-
8. **Outfits are held in contract custody.** Attached outfits and backgrounds are transferred to `address(this)` via `safeTransferFrom`. They are returned to the caller when detached (by passing a new outfit set that excludes them).
|
|
200
|
+
8. **Outfits are held in contract custody.** Attached outfits and backgrounds are transferred to `address(this)` via `safeTransferFrom`. They are returned to the caller when detached (by passing a new outfit set that excludes them). If the return transfer fails (e.g., owner is a non-receiver contract), the asset is **retained** in the attached list rather than stranded — the owner can retry once the transfer issue is resolved.
|
|
161
201
|
9. **Complex outfit ownership rules.** If an outfit is unworn: only its owner can attach it. If already worn by another body: the caller must own THAT body to reassign the outfit. This allows body owners to swap outfits between their own bodies.
|
|
162
202
|
10. **Token ID encoding.** `tokenId = upc * 1_000_000_000 + sequenceNumber`. The resolver extracts UPC via integer division and sequence via modulo to display inventory counts like "42/100".
|
|
163
203
|
11. **`onERC721Received` only accepts self-transfers.** Reverts unless `operator == address(this)`. The contract calls `safeTransferFrom` on itself during decoration, triggering this callback.
|
|
@@ -166,6 +206,7 @@ On-chain composable NFT avatar system that renders Banny characters with layered
|
|
|
166
206
|
14. **Mannequin rendering.** Outfit and background tokens (not bodies) are rendered on a grayscale mannequin Banny for preview purposes. The mannequin has `fill:#808080` styling.
|
|
167
207
|
15. **ERC2771 meta-transaction support.** Constructor takes a `trustedForwarder` address. All `_msgSender()` calls use `ERC2771Context`, allowing relayers to submit decoration transactions on behalf of users.
|
|
168
208
|
16. **Empty metadata fields clear the value.** `setMetadata` always writes all three fields. Passing an empty string clears that field. To keep a field unchanged, pass its current value.
|
|
209
|
+
17. **Outfit locks persist through transfers.** The `outfitLockedUntil` mapping is keyed by `(hook, bannyBodyId)` and is never cleared on transfer. When a locked body NFT is transferred, the new owner inherits the lock and cannot change outfits until it expires. Equipped outfits also travel with the body -- the new owner can unequip them after the lock expires. Sellers should unequip valuable outfits before transferring a body.
|
|
169
210
|
|
|
170
211
|
## Example Integration
|
|
171
212
|
|
package/STYLE_GUIDE.md
CHANGED
|
@@ -21,7 +21,7 @@ One contract/interface/struct/enum per file. Name the file after the type it con
|
|
|
21
21
|
|
|
22
22
|
```solidity
|
|
23
23
|
// Contracts — pin to exact version
|
|
24
|
-
pragma solidity 0.8.
|
|
24
|
+
pragma solidity 0.8.28;
|
|
25
25
|
|
|
26
26
|
// Interfaces, structs, enums — caret for forward compatibility
|
|
27
27
|
pragma solidity ^0.8.0;
|
|
@@ -326,7 +326,7 @@ Standard config across all repos:
|
|
|
326
326
|
|
|
327
327
|
```toml
|
|
328
328
|
[profile.default]
|
|
329
|
-
solc = '0.8.
|
|
329
|
+
solc = '0.8.28'
|
|
330
330
|
evm_version = 'cancun'
|
|
331
331
|
optimizer_runs = 200
|
|
332
332
|
libs = ["node_modules", "lib"]
|