@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.
- package/AUDIT_INSTRUCTIONS.md +327 -0
- package/CHANGE_LOG.md +222 -0
- package/RISKS.md +30 -148
- package/USER_JOURNEYS.md +523 -0
- package/package.json +8 -8
- package/script/Add.Denver.s.sol +6 -4
- package/script/Deploy.s.sol +5 -8
- package/script/Drop1.s.sol +10 -2
- package/script/helpers/BannyverseDeploymentLib.sol +2 -2
- package/src/Banny721TokenUriResolver.sol +28 -10
- package/test/Banny721TokenUriResolver.t.sol +12 -10
- package/test/BannyAttacks.t.sol +2 -0
- package/test/DecorateFlow.t.sol +2 -0
- package/test/Fork.t.sol +12 -9
- package/test/OutfitTransferLifecycle.t.sol +391 -0
- package/test/TestAuditGaps.sol +720 -0
- package/test/TestQALastMile.t.sol +443 -0
- package/test/regression/BodyCategoryValidation.t.sol +1 -0
- package/test/regression/BurnedTokenCheck.t.sol +1 -0
- package/test/regression/CEIReorder.t.sol +1 -0
- package/test/regression/MsgSenderEvents.t.sol +1 -0
- package/test/regression/RemovedTierDesync.t.sol +1 -0
|
@@ -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
|
|
611
|
-
_setupTier(
|
|
612
|
-
hook.setOwner(
|
|
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] =
|
|
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(
|
|
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
|
|
package/test/BannyAttacks.t.sol
CHANGED
|
@@ -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
|
}
|
package/test/DecorateFlow.t.sol
CHANGED
|
@@ -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
|