@bannynet/core-v6 0.0.10 → 0.0.11

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/ARCHITECTURE.md CHANGED
@@ -17,25 +17,27 @@ src/
17
17
 
18
18
  ### Asset Storage
19
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
20
+ Owner → setSvgHashesOf(upcs, hashes)
21
+ Register content hashes for UPCs (owner-only)
23
22
 
24
- OwneraddProduct(category, hash)
25
- Register a product (body/background/head/suit) for a category
26
- Products linked to stored SVG content
23
+ AnyonesetSvgContentsOf(upcs, contents)
24
+ Upload SVG content matching registered hashes
25
+ Content validated against hash before storage
26
+
27
+ Owner → setProductNames(upcs, names)
28
+ → Register product names for UPCs
27
29
  ```
28
30
 
29
31
  ### Outfit Composition
30
32
  ```
31
- NFT Holderdress(tokenId, outfitTokenIds[])
32
- → Attach outfit NFTs (head, suit, background) to a body NFT
33
- → Outfit NFTs transferred to resolver contract (locked)
33
+ Body OwnerdecorateBannyWith(hook, bodyId, backgroundId, outfitIds)
34
+ → Attach outfit and background NFTs to a body NFT
35
+ → Outfit/background NFTs transferred to resolver contract
36
+ → Previous outfits returned to owner
34
37
  → Composite SVG generated from layered components
35
38
 
36
- NFT Holderundress(tokenId, outfitTokenIds[])
37
- Remove outfit NFTs from body
38
- → Outfit NFTs returned to holder
39
+ Body OwnerlockOutfitChangesFor(hook, bodyId)
40
+ Lock outfit changes for 7 days
39
41
  ```
40
42
 
41
43
  ### Token URI Generation
package/README.md CHANGED
@@ -23,7 +23,8 @@ Banny is a composable NFT character system built on top of Juicebox 721 hooks. E
23
23
  → Body's tokenURI now renders the full dressed composition
24
24
  |
25
25
  4. Outfit lock (optional): lockOutfitChangesFor(hook, bodyId)
26
- → Freezes outfit changes for 7 days
26
+ → Freezes outfit and background changes for 7 days
27
+ → Prevents moving currently equipped assets away through another body's decoration call
27
28
  → Proves the Banny's look is stable (useful for PFPs, displays)
28
29
  |
29
30
  5. SVG content is stored on-chain via a two-step process:
@@ -92,20 +93,32 @@ Requires `via_ir = true` in foundry.toml due to stack depth in SVG composition.
92
93
  | Command | Description |
93
94
  |---------|-------------|
94
95
  | `forge build` | Compile contracts (requires via-IR) |
95
- | `forge test` | Run all tests (3 test files: functionality, attacks, decoration flows) |
96
+ | `forge test` | Run all tests (14 test files: functionality, attacks, decoration flows, fork integration, transfer lifecycle, audit gaps, QA, regressions) |
96
97
  | `forge test -vvv` | Run tests with full trace |
97
98
 
98
99
  ## Repository Layout
99
100
 
100
101
  ```
101
102
  src/
102
- Banny721TokenUriResolver.sol # Sole contract (~1,331 lines)
103
+ Banny721TokenUriResolver.sol # Sole contract (~1,428 lines)
103
104
  interfaces/
104
105
  IBanny721TokenUriResolver.sol # Public interface + events
105
106
  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)
107
+ Banny721TokenUriResolver.t.sol # Unit tests
108
+ BannyAttacks.t.sol # Security/adversarial tests
109
+ DecorateFlow.t.sol # Decoration flow tests
110
+ Fork.t.sol # Fork integration tests
111
+ OutfitTransferLifecycle.t.sol # Transfer lifecycle tests
112
+ TestAuditGaps.sol # Audit gap coverage (meta-tx, SVG edge cases)
113
+ TestQALastMile.t.sol # QA tests (gas, round-trip, fallback)
114
+ regression/
115
+ ArrayLengthValidation.t.sol # Input validation regression
116
+ BodyCategoryValidation.t.sol # Body category regression
117
+ BurnedTokenCheck.t.sol # Burned token regression
118
+ CEIReorder.t.sol # CEI ordering regression
119
+ ClearMetadata.t.sol # Metadata clearing regression
120
+ MsgSenderEvents.t.sol # Event emission regression
121
+ RemovedTierDesync.t.sol # Removed tier desync regression
109
122
  script/
110
123
  Deploy.s.sol # Sphinx multi-chain deployment
111
124
  Drop1.s.sol # Outfit drop deployment
package/RISKS.md CHANGED
@@ -11,7 +11,7 @@
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
13
  - **try-catch silent failures.** `_tryTransferFrom` silently catches all transfer failures. If an outfit NFT is burned or its tier removed, the transfer fails silently. The outfit remains logically "equipped" in state but the NFT is lost. This can create phantom outfits that show in SVG rendering but cannot be recovered.
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.
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
 
package/USER_JOURNEYS.md CHANGED
@@ -311,7 +311,7 @@ Where `outfitId` is currently equipped on `oldBodyId`, and the caller owns both
311
311
 
312
312
  ### Edge Cases
313
313
 
314
- - **Old body is locked**: The lock is on `oldBodyId`, but the caller is calling `decorateBannyWith` on `newBodyId`. The lock only prevents changes to the locked body, not removal of its outfits via a different body's decoration call. **Wait -- verify this**: The outfit's `wearerOf` returns `oldBodyId`. The caller owns `oldBodyId`. The authorization check at line 1266 checks `ownerOf(wearerId)` which is the caller. So this succeeds. However, `oldBodyId`'s `_attachedOutfitIdsOf` still contains the outfit. The outfit has been moved at the `_wearerOf` level, but the old array is stale. This is handled by `assetIdsOf` which filters by checking `wearerOf` (line 383). **Auditors should verify the lock on `oldBodyId` does not prevent this path.** The lock check is only in `decorateBannyWith` at line 995, and it checks the body being decorated (`bannyBodyId`), not the body being undressed.
314
+ - **Old body is locked**: Reverts `OutfitChangesLocked`. A locked source body keeps its currently equipped outfits and background until the lock expires, even if the caller owns both bodies.
315
315
 
316
316
  ---
317
317
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bannynet/core-v6",
3
- "version": "0.0.10",
3
+ "version": "0.0.11",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -20,14 +20,14 @@
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.17",
24
- "@bananapus/core-v6": "^0.0.17",
23
+ "@bananapus/721-hook-v6": "^0.0.19",
24
+ "@bananapus/core-v6": "^0.0.24",
25
25
  "@bananapus/permission-ids-v6": "^0.0.10",
26
- "@bananapus/router-terminal-v6": "^0.0.13",
27
- "@bananapus/suckers-v6": "^0.0.11",
28
- "@croptop/core-v6": "^0.0.18",
26
+ "@bananapus/router-terminal-v6": "^0.0.17",
27
+ "@bananapus/suckers-v6": "^0.0.13",
28
+ "@croptop/core-v6": "^0.0.20",
29
29
  "@openzeppelin/contracts": "^5.6.1",
30
- "@rev-net/core-v6": "^0.0.13",
30
+ "@rev-net/core-v6": "^0.0.15",
31
31
  "keccak": "^3.0.4"
32
32
  },
33
33
  "devDependencies": {
@@ -1156,6 +1156,16 @@ contract Banny721TokenUriResolver is
1156
1156
  // ---------------------- internal transactions ---------------------- //
1157
1157
  //*********************************************************************//
1158
1158
 
1159
+ /// @notice Revert if an equipped asset is being reassigned away from a locked source body.
1160
+ /// @param hook The hook storing the assets.
1161
+ /// @param bannyBodyId The body currently using the asset.
1162
+ /// @param exemptBodyId The destination body currently being decorated.
1163
+ function _revertIfBodyLocked(address hook, uint256 bannyBodyId, uint256 exemptBodyId) internal view {
1164
+ if (bannyBodyId != 0 && bannyBodyId != exemptBodyId && outfitLockedUntil[hook][bannyBodyId] > block.timestamp) {
1165
+ revert Banny721TokenUriResolver_OutfitChangesLocked();
1166
+ }
1167
+ }
1168
+
1159
1169
  /// @notice Add a background to a banny body.
1160
1170
  /// @param hook The hook storing the assets.
1161
1171
  /// @param bannyBodyId The ID of the banny body being dressed.
@@ -1186,6 +1196,9 @@ contract Banny721TokenUriResolver is
1186
1196
  if (_msgSender() != IERC721(hook).ownerOf(userId)) {
1187
1197
  revert Banny721TokenUriResolver_UnauthorizedBackground();
1188
1198
  }
1199
+
1200
+ // A locked source body keeps its equipped background until the lock expires.
1201
+ _revertIfBodyLocked({hook: hook, bannyBodyId: userId, exemptBodyId: bannyBodyId});
1189
1202
  }
1190
1203
 
1191
1204
  // Get the background's product info.
@@ -1274,6 +1287,9 @@ contract Banny721TokenUriResolver is
1274
1287
  if (_msgSender() != IERC721(hook).ownerOf(wearerId)) {
1275
1288
  revert Banny721TokenUriResolver_UnauthorizedOutfit();
1276
1289
  }
1290
+
1291
+ // A locked source body keeps its equipped outfits until the lock expires.
1292
+ _revertIfBodyLocked({hook: hook, bannyBodyId: wearerId, exemptBodyId: bannyBodyId});
1277
1293
  }
1278
1294
 
1279
1295
  // Get the outfit's product info.
@@ -797,6 +797,37 @@ contract DecorateFlowTests is Test {
797
797
  assertEq(hook.ownerOf(NECKLACE_1), bob, "necklace returned to bob");
798
798
  }
799
799
 
800
+ /// @notice A locked body keeps its equipped background until the lock expires, even if the owner also controls an
801
+ /// unlocked destination body.
802
+ function test_lock_preventsMovingBackgroundFromLockedBody() public {
803
+ vm.prank(alice);
804
+ resolver.decorateBannyWith(address(hook), BODY_A, BACKGROUND_1, new uint256[](0));
805
+
806
+ vm.prank(alice);
807
+ resolver.lockOutfitChangesFor(address(hook), BODY_A);
808
+
809
+ vm.prank(alice);
810
+ vm.expectRevert(Banny721TokenUriResolver.Banny721TokenUriResolver_OutfitChangesLocked.selector);
811
+ resolver.decorateBannyWith(address(hook), BODY_B, BACKGROUND_1, new uint256[](0));
812
+ }
813
+
814
+ /// @notice A locked body keeps its equipped outfits until the lock expires, even if the owner also controls an
815
+ /// unlocked destination body.
816
+ function test_lock_preventsMovingOutfitFromLockedBody() public {
817
+ uint256[] memory outfits = new uint256[](1);
818
+ outfits[0] = NECKLACE_1;
819
+
820
+ vm.prank(alice);
821
+ resolver.decorateBannyWith(address(hook), BODY_A, 0, outfits);
822
+
823
+ vm.prank(alice);
824
+ resolver.lockOutfitChangesFor(address(hook), BODY_A);
825
+
826
+ vm.prank(alice);
827
+ vm.expectRevert(Banny721TokenUriResolver.Banny721TokenUriResolver_OutfitChangesLocked.selector);
828
+ resolver.decorateBannyWith(address(hook), BODY_B, 0, outfits);
829
+ }
830
+
800
831
  // =========================================================================
801
832
  // SECTION 8: Complex Multi-Step Scenarios
802
833
  // =========================================================================