@bannynet/core-v6 0.0.8 → 0.0.10

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.
@@ -127,6 +127,12 @@ contract Banny721TokenUriResolver is
127
127
  /// @dev Naked Banny's will only be shown with outfits currently owned by the owner of the banny body.
128
128
  /// @dev NOTE: Equipped outfits travel with the banny body NFT on transfer. When a body is transferred,
129
129
  /// the new owner inherits all equipped outfits and can unequip them to receive the outfit NFTs.
130
+ // The _attachedOutfitIdsOf array grows with each attachment. Gas cost for operations
131
+ // iterating this array increases linearly. In practice, Bannys have a small, bounded number of outfit slots
132
+ // (< 20), making gas cost manageable. No explicit cap is needed given the natural slot limit.
133
+ // This array may contain stale entries (e.g. outfits transferred away externally). Stale entries are
134
+ // filtered at read time via `outfitsOf` and `wearerOf`, which check current ownership/attachment status.
135
+ // This lazy reconciliation avoids extra storage writes on every transfer.
130
136
  /// @custom:param hook The hook address of the collection.
131
137
  /// @custom:param bannyBodyId The ID of the banny body of the outfits.
132
138
  mapping(address hook => mapping(uint256 bannyBodyId => uint256[])) internal _attachedOutfitIdsOf;
@@ -616,6 +622,9 @@ contract Banny721TokenUriResolver is
616
622
  }
617
623
 
618
624
  /// @notice Encode the token URI JSON with base64.
625
+ // Metadata strings (name, description, external_url) are set by the contract owner, not by users.
626
+ // No JSON escaping is applied — the owner is trusted to provide valid values. On-chain JSON is consumed
627
+ // by off-chain indexers and UIs, not rendered in a browser context where XSS would apply.
619
628
  function _encodeTokenUri(
620
629
  uint256 tokenId,
621
630
  JB721Tier memory product,
@@ -1025,6 +1034,9 @@ contract Banny721TokenUriResolver is
1025
1034
  }
1026
1035
 
1027
1036
  /// @dev Make sure tokens can be received if the transaction was initiated by this contract.
1037
+ // NFTs sent via transferFrom (not safeTransferFrom) bypass onERC721Received and cannot be
1038
+ // tracked or recovered. This is an inherent ERC-721 limitation — the contract cannot prevent non-safe
1039
+ // transfers. Users and UIs should always use safeTransferFrom.
1028
1040
  /// @param operator The address that initiated the transaction.
1029
1041
  /// @param from The address that initiated the transfer.
1030
1042
  /// @param tokenId The ID of the token being transferred.
@@ -1073,6 +1085,8 @@ contract Banny721TokenUriResolver is
1073
1085
  }
1074
1086
 
1075
1087
  /// @notice Allows the owner to set the product's name.
1088
+ /// @dev Product names are mutable — the owner can update them at any time. This is intentional to allow
1089
+ /// corrections and localization. Names are only used in metadata and do not affect on-chain logic.
1076
1090
  /// @param upcs The universal product codes of the products having their name stored.
1077
1091
  /// @param names The names of the products.
1078
1092
  function setProductNames(uint256[] memory upcs, string[] memory names) external override onlyOwner {
@@ -1362,6 +1376,16 @@ contract Banny721TokenUriResolver is
1362
1376
  _attachedOutfitIdsOf[hook][bannyBodyId] = outfitIds;
1363
1377
  }
1364
1378
 
1379
+ /// @notice Check if a value is present in an array.
1380
+ /// @param value The value to search for.
1381
+ /// @param array The array to search in.
1382
+ /// @return found True if the value was found.
1383
+ function _isInArray(uint256 value, uint256[] memory array) internal pure returns (bool found) {
1384
+ for (uint256 i; i < array.length; i++) {
1385
+ if (array[i] == value) return true;
1386
+ }
1387
+ }
1388
+
1365
1389
  /// @notice Transfer a token from one address to another.
1366
1390
  /// @param hook The 721 contract of the token being transferred.
1367
1391
  /// @param from The address to transfer the token from.
@@ -1373,6 +1397,10 @@ contract Banny721TokenUriResolver is
1373
1397
 
1374
1398
  /// @notice Try to transfer a token, silently succeeding if the transfer fails (e.g. token was burned).
1375
1399
  /// @dev Used when returning previously equipped items that may no longer exist.
1400
+ // `_tryTransferFrom` may silently fail to transfer outfit NFTs, leaving them attached to the
1401
+ // Banny but owned by a different address. This is by design — the try-catch pattern prevents a single failing
1402
+ // outfit transfer from blocking the entire Banny transfer. Orphaned outfits can be recovered by the original
1403
+ // owner.
1376
1404
  /// @param hook The 721 contract of the token being transferred.
1377
1405
  /// @param from The address to transfer the token from.
1378
1406
  /// @param to The address to transfer the token to.
@@ -1381,14 +1409,4 @@ contract Banny721TokenUriResolver is
1381
1409
  // slither-disable-next-line reentrancy-no-eth
1382
1410
  try IERC721(hook).safeTransferFrom({from: from, to: to, tokenId: assetId}) {} catch {}
1383
1411
  }
1384
-
1385
- /// @notice Check if a value is present in an array.
1386
- /// @param value The value to search for.
1387
- /// @param array The array to search in.
1388
- /// @return found True if the value was found.
1389
- function _isInArray(uint256 value, uint256[] memory array) internal pure returns (bool found) {
1390
- for (uint256 i; i < array.length; i++) {
1391
- if (array[i] == value) return true;
1392
- }
1393
- }
1394
1412
  }
@@ -79,10 +79,12 @@ contract MockStore {
79
79
  return tiers[hook][tokenId];
80
80
  }
81
81
 
82
+ // forge-lint: disable-next-line(mixed-case-function)
82
83
  function encodedTierIPFSUriOf(address, uint256) external pure returns (bytes32) {
83
84
  return bytes32(0);
84
85
  }
85
86
 
87
+ // forge-lint: disable-next-line(mixed-case-function)
86
88
  function encodedIPFSUriOf(address, uint256) external pure returns (bytes32) {
87
89
  return bytes32(0);
88
90
  }
@@ -166,7 +168,7 @@ contract TestBanny721TokenUriResolver is Test {
166
168
  // --- Constructor --------------------------------------------------- //
167
169
  //*********************************************************************//
168
170
 
169
- function test_constructor_setsDefaults() public {
171
+ function test_constructor_setsDefaults() public view {
170
172
  assertEq(resolver.BANNY_BODY(), "<path/>");
171
173
  assertEq(resolver.DEFAULT_NECKLACE(), "<necklace/>");
172
174
  assertEq(resolver.DEFAULT_MOUTH(), "<mouth/>");
@@ -607,27 +609,27 @@ contract TestBanny721TokenUriResolver is Test {
607
609
  resolver.decorateBannyWith(address(hook), BODY_TOKEN, 0, outfitIds1);
608
610
 
609
611
  // Create a new necklace token.
610
- uint256 NECKLACE_TOKEN_2 = 11_000_000_001;
611
- _setupTier(NECKLACE_TOKEN_2, 11, 3); // Same category (3)
612
- hook.setOwner(NECKLACE_TOKEN_2, alice);
612
+ uint256 necklaceToken2 = 11_000_000_001;
613
+ _setupTier(necklaceToken2, 11, 3); // Same category (3)
614
+ hook.setOwner(necklaceToken2, alice);
613
615
 
614
616
  // Replace with new necklace. Old one should be returned.
615
617
  uint256[] memory outfitIds2 = new uint256[](1);
616
- outfitIds2[0] = NECKLACE_TOKEN_2;
618
+ outfitIds2[0] = necklaceToken2;
617
619
  vm.prank(alice);
618
620
  resolver.decorateBannyWith(address(hook), BODY_TOKEN, 0, outfitIds2);
619
621
 
620
622
  // Old necklace returned to alice.
621
623
  assertEq(hook.ownerOf(NECKLACE_TOKEN), alice, "old necklace should be returned");
622
624
  // New necklace held by resolver.
623
- assertEq(hook.ownerOf(NECKLACE_TOKEN_2), address(resolver), "new necklace should be held");
625
+ assertEq(hook.ownerOf(necklaceToken2), address(resolver), "new necklace should be held");
624
626
  }
625
627
 
626
628
  //*********************************************************************//
627
629
  // --- onERC721Received ---------------------------------------------- //
628
630
  //*********************************************************************//
629
631
 
630
- function test_onERC721Received_acceptsFromSelf() public {
632
+ function test_onERC721Received_acceptsFromSelf() public view {
631
633
  bytes4 result = resolver.onERC721Received(address(resolver), alice, 1, "");
632
634
  assertEq(result, IERC721Receiver.onERC721Received.selector, "should accept from self");
633
635
  }
@@ -641,7 +643,7 @@ contract TestBanny721TokenUriResolver is Test {
641
643
  // --- View: assetIdsOf with no outfits ------------------------------ //
642
644
  //*********************************************************************//
643
645
 
644
- function test_assetIdsOf_empty() public {
646
+ function test_assetIdsOf_empty() public view {
645
647
  (uint256 backgroundId, uint256[] memory outfitIds) = resolver.assetIdsOf(address(hook), BODY_TOKEN);
646
648
  assertEq(backgroundId, 0, "no background initially");
647
649
  assertEq(outfitIds.length, 0, "no outfits initially");
@@ -651,11 +653,11 @@ contract TestBanny721TokenUriResolver is Test {
651
653
  // --- View: userOf / wearerOf --------------------------------------- //
652
654
  //*********************************************************************//
653
655
 
654
- function test_userOf_returnsZeroIfNotAttached() public {
656
+ function test_userOf_returnsZeroIfNotAttached() public view {
655
657
  assertEq(resolver.userOf(address(hook), BACKGROUND_TOKEN), 0, "no user initially");
656
658
  }
657
659
 
658
- function test_wearerOf_returnsZeroIfNotWorn() public {
660
+ function test_wearerOf_returnsZeroIfNotWorn() public view {
659
661
  assertEq(resolver.wearerOf(address(hook), NECKLACE_TOKEN), 0, "no wearer initially");
660
662
  }
661
663
 
@@ -70,10 +70,12 @@ contract AttackMockStore {
70
70
  return tiers[hook][tokenId];
71
71
  }
72
72
 
73
+ // forge-lint: disable-next-line(mixed-case-function)
73
74
  function encodedTierIPFSUriOf(address, uint256) external pure returns (bytes32) {
74
75
  return bytes32(0);
75
76
  }
76
77
 
78
+ // forge-lint: disable-next-line(mixed-case-function)
77
79
  function encodedIPFSUriOf(address, uint256) external pure returns (bytes32) {
78
80
  return bytes32(0);
79
81
  }
@@ -70,10 +70,12 @@ contract DecorateFlowMockStore {
70
70
  return tiers[hook][tokenId];
71
71
  }
72
72
 
73
+ // forge-lint: disable-next-line(mixed-case-function)
73
74
  function encodedTierIPFSUriOf(address, uint256) external pure returns (bytes32) {
74
75
  return bytes32(0);
75
76
  }
76
77
 
78
+ // forge-lint: disable-next-line(mixed-case-function)
77
79
  function encodedIPFSUriOf(address, uint256) external pure returns (bytes32) {
78
80
  return bytes32(0);
79
81
  }
package/test/Fork.t.sol CHANGED
@@ -27,7 +27,6 @@ import {JB721TiersHookFlags} from "@bananapus/721-hook-v6/src/structs/JB721Tiers
27
27
  import {JBDeploy721TiersHookConfig} from "@bananapus/721-hook-v6/src/structs/JBDeploy721TiersHookConfig.sol";
28
28
  import {JB721Tier} from "@bananapus/721-hook-v6/src/structs/JB721Tier.sol";
29
29
  import {IJB721TokenUriResolver} from "@bananapus/721-hook-v6/src/interfaces/IJB721TokenUriResolver.sol";
30
- import {IJBPrices} from "@bananapus/core-v6/src/interfaces/IJBPrices.sol";
31
30
  import {JBCurrencyIds} from "@bananapus/core-v6/src/libraries/JBCurrencyIds.sol";
32
31
  import {JBSplit} from "@bananapus/core-v6/src/structs/JBSplit.sol";
33
32
 
@@ -113,10 +112,12 @@ contract ReentrantMockStore {
113
112
  return tiers[hook][tokenId];
114
113
  }
115
114
 
115
+ // forge-lint: disable-next-line(mixed-case-function)
116
116
  function encodedTierIPFSUriOf(address, uint256) external pure returns (bytes32) {
117
117
  return bytes32(0);
118
118
  }
119
119
 
120
+ // forge-lint: disable-next-line(mixed-case-function)
120
121
  function encodedIPFSUriOf(address, uint256) external pure returns (bytes32) {
121
122
  return bytes32(0);
122
123
  }
@@ -299,13 +300,13 @@ contract BannyForkTest is Test {
299
300
  assertEq(IERC721(address(bannyHook)).ownerOf(ORIGINAL_BODY_1), alice);
300
301
  }
301
302
 
302
- function test_fork_e2e_alienBodyDefaultEyes() public {
303
+ function test_fork_e2e_alienBodyDefaultEyes() public view {
303
304
  // Alice owns ALIEN_BODY_1. Naked alien body should inject alien eyes in SVG.
304
305
  string memory svg = resolver.svgOf(address(bannyHook), ALIEN_BODY_1, true, false);
305
306
  assertGt(bytes(svg).length, 0, "alien body SVG should render");
306
307
  }
307
308
 
308
- function test_fork_e2e_outfitRenderedOnMannequin() public {
309
+ function test_fork_e2e_outfitRenderedOnMannequin() public view {
309
310
  // Unequipped outfit token should render on mannequin.
310
311
  string memory uri = resolver.tokenUriOf(address(bannyHook), NECKLACE_1);
311
312
  assertGt(bytes(uri).length, 0, "outfit URI should render on mannequin");
@@ -781,14 +782,14 @@ contract BannyForkTest is Test {
781
782
  // 7. TOKEN URI RENDERING
782
783
  // ═══════════════════════════════════════════════════════════════════════
783
784
 
784
- function test_fork_render_nakedBodyHasDefaultInjections() public {
785
+ function test_fork_render_nakedBodyHasDefaultInjections() public view {
785
786
  // A naked body should still render with default necklace, eyes, mouth.
786
787
  string memory svg = resolver.svgOf(address(bannyHook), ORIGINAL_BODY_1, true, false);
787
788
  assertGt(bytes(svg).length, 0, "naked body should render");
788
789
  // The SVG should contain the body path and defaults.
789
790
  }
790
791
 
791
- function test_fork_render_allFourBodyTypes() public {
792
+ function test_fork_render_allFourBodyTypes() public view {
792
793
  // Each body type should render.
793
794
  string memory alienSvg = resolver.svgOf(address(bannyHook), ALIEN_BODY_1, true, false);
794
795
  string memory pinkSvg = resolver.svgOf(address(bannyHook), PINK_BODY_1, true, false);
@@ -1011,17 +1012,17 @@ contract BannyForkTest is Test {
1011
1012
  assertEq(outfitIds.length, 0);
1012
1013
  }
1013
1014
 
1014
- function test_fork_edge_assetIdsEmptyInitially() public {
1015
+ function test_fork_edge_assetIdsEmptyInitially() public view {
1015
1016
  (uint256 bgId, uint256[] memory outfitIds) = resolver.assetIdsOf(address(bannyHook), ORIGINAL_BODY_1);
1016
1017
  assertEq(bgId, 0);
1017
1018
  assertEq(outfitIds.length, 0);
1018
1019
  }
1019
1020
 
1020
- function test_fork_edge_wearerOfUnwornReturnsZero() public {
1021
+ function test_fork_edge_wearerOfUnwornReturnsZero() public view {
1021
1022
  assertEq(resolver.wearerOf(address(bannyHook), NECKLACE_1), 0);
1022
1023
  }
1023
1024
 
1024
- function test_fork_edge_userOfUnusedBackgroundReturnsZero() public {
1025
+ function test_fork_edge_userOfUnusedBackgroundReturnsZero() public view {
1025
1026
  assertEq(resolver.userOf(address(bannyHook), BACKGROUND_1), 0);
1026
1027
  }
1027
1028
 
@@ -1136,7 +1137,7 @@ contract BannyForkTest is Test {
1136
1137
  resolver.decorateBannyWith(address(bannyHook), ORIGINAL_BODY_1, 0, outfits);
1137
1138
  }
1138
1139
 
1139
- function test_fork_edge_namesReturnsCorrectData() public {
1140
+ function test_fork_edge_namesReturnsCorrectData() public view {
1140
1141
  // Verify namesOf returns correct product name for each body type.
1141
1142
  (string memory alienFull,,) = resolver.namesOf(address(bannyHook), ALIEN_BODY_1);
1142
1143
  assertGt(bytes(alienFull).length, 0, "alien name should not be empty");
@@ -1791,6 +1792,7 @@ contract BannyForkTest is Test {
1791
1792
  // Internal helpers
1792
1793
  // ═══════════════════════════════════════════════════════════════════════
1793
1794
 
1795
+ // forge-lint: disable-next-line(mixed-case-function)
1794
1796
  function _deployJBCore() internal {
1795
1797
  jbPermissions = new JBPermissions(trustedForwarder);
1796
1798
  jbProjects = new JBProjects(multisig, address(0), trustedForwarder);
@@ -1944,6 +1946,7 @@ contract BannyForkTest is Test {
1944
1946
  });
1945
1947
  }
1946
1948
 
1949
+ // forge-lint: disable-next-line(mixed-case-function)
1947
1950
  function _mintInitialNFTs() internal {
1948
1951
  // Mint bodies and outfits to alice, bob.
1949
1952
  vm.startPrank(multisig); // hook owner can mint