@bannynet/core-v6 0.0.2 → 0.0.4

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.
@@ -0,0 +1,142 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity 0.8.26;
3
+
4
+ import "forge-std/Test.sol";
5
+ import {JB721Tier} from "@bananapus/721-hook-v6/src/structs/JB721Tier.sol";
6
+ import {IERC721Receiver} from "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol";
7
+
8
+ import {Banny721TokenUriResolver} from "../../src/Banny721TokenUriResolver.sol";
9
+
10
+ /// @notice Minimal mock hook.
11
+ contract MockHook57 {
12
+ mapping(uint256 => address) public ownerOf;
13
+ address public immutable MOCK_STORE;
14
+ mapping(address => mapping(address => bool)) public isApprovedForAll;
15
+
16
+ constructor(address store) {
17
+ MOCK_STORE = store;
18
+ }
19
+
20
+ function STORE() external view returns (address) {
21
+ return MOCK_STORE;
22
+ }
23
+
24
+ function setOwner(uint256 tokenId, address owner) external {
25
+ ownerOf[tokenId] = owner;
26
+ }
27
+
28
+ function setApprovalForAll(address operator, bool approved) external {
29
+ isApprovedForAll[msg.sender][operator] = approved;
30
+ }
31
+
32
+ function safeTransferFrom(address from, address to, uint256 tokenId) external {
33
+ require(
34
+ msg.sender == ownerOf[tokenId] || msg.sender == from || isApprovedForAll[from][msg.sender],
35
+ "MockHook: not authorized"
36
+ );
37
+ ownerOf[tokenId] = to;
38
+ if (to.code.length > 0) {
39
+ bytes4 retval = IERC721Receiver(to).onERC721Received(msg.sender, from, tokenId, "");
40
+ require(retval == IERC721Receiver.onERC721Received.selector, "MockHook: receiver rejected");
41
+ }
42
+ }
43
+
44
+ function pricingContext() external pure returns (uint256, uint256, uint256) {
45
+ return (1, 18, 0);
46
+ }
47
+
48
+ function baseURI() external pure returns (string memory) {
49
+ return "ipfs://";
50
+ }
51
+ }
52
+
53
+ /// @notice Minimal mock store.
54
+ contract MockStore57 {
55
+ mapping(address => mapping(uint256 => JB721Tier)) public tiers;
56
+
57
+ function setTier(address hook, uint256 tokenId, JB721Tier memory tier) external {
58
+ tiers[hook][tokenId] = tier;
59
+ }
60
+
61
+ function tierOfTokenId(address hook, uint256 tokenId, bool) external view returns (JB721Tier memory) {
62
+ return tiers[hook][tokenId];
63
+ }
64
+
65
+ function encodedIPFSUriOf(address, uint256) external pure returns (bytes32) {
66
+ return bytes32(0);
67
+ }
68
+ }
69
+
70
+ /// @notice Regression test: L-57 -- decorateBannyWith should reject non-body-category tokens as bannyBodyId.
71
+ contract L57_BodyCategoryValidationTest is Test {
72
+ Banny721TokenUriResolver resolver;
73
+ MockHook57 hook;
74
+ MockStore57 store;
75
+
76
+ address deployer = makeAddr("deployer");
77
+ address alice = makeAddr("alice");
78
+
79
+ uint256 constant BODY_TOKEN = 4_000_000_001; // category 0 (body)
80
+ uint256 constant NECKLACE_TOKEN = 10_000_000_001; // category 3 (necklace)
81
+
82
+ function setUp() public {
83
+ store = new MockStore57();
84
+ hook = new MockHook57(address(store));
85
+
86
+ vm.prank(deployer);
87
+ resolver = new Banny721TokenUriResolver(
88
+ "<path/>", "<necklace/>", "<mouth/>", "<eyes/>", "<alieneyes/>", deployer, address(0)
89
+ );
90
+
91
+ // Set up body token (category 0).
92
+ _setupTier(BODY_TOKEN, 4, 0);
93
+ hook.setOwner(BODY_TOKEN, alice);
94
+
95
+ // Set up necklace token (category 3) -- NOT a body.
96
+ _setupTier(NECKLACE_TOKEN, 10, 3);
97
+ hook.setOwner(NECKLACE_TOKEN, alice);
98
+
99
+ vm.prank(alice);
100
+ hook.setApprovalForAll(address(resolver), true);
101
+ }
102
+
103
+ /// @notice Passing a non-body token as bannyBodyId should revert.
104
+ function test_decorateBannyWith_revertsIfNotBodyCategory() public {
105
+ uint256[] memory outfitIds = new uint256[](0);
106
+
107
+ vm.prank(alice);
108
+ vm.expectRevert(Banny721TokenUriResolver.Banny721TokenUriResolver_BannyBodyNotBodyCategory.selector);
109
+ resolver.decorateBannyWith(address(hook), NECKLACE_TOKEN, 0, outfitIds);
110
+ }
111
+
112
+ /// @notice Passing a valid body token should succeed.
113
+ function test_decorateBannyWith_succeedsWithBodyCategory() public {
114
+ uint256[] memory outfitIds = new uint256[](0);
115
+
116
+ vm.prank(alice);
117
+ resolver.decorateBannyWith(address(hook), BODY_TOKEN, 0, outfitIds);
118
+ // Should not revert.
119
+ }
120
+
121
+ function _setupTier(uint256 tokenId, uint32 tierId, uint24 category) internal {
122
+ JB721Tier memory tier = JB721Tier({
123
+ id: tierId,
124
+ price: 0.01 ether,
125
+ remainingSupply: 100,
126
+ initialSupply: 100,
127
+ votingUnits: 0,
128
+ reserveFrequency: 0,
129
+ reserveBeneficiary: address(0),
130
+ encodedIPFSUri: bytes32(0),
131
+ category: category,
132
+ discountPercent: 0,
133
+ allowOwnerMint: false,
134
+ transfersPausable: false,
135
+ cannotBeRemoved: false,
136
+ cannotIncreaseDiscountPercent: false,
137
+ splitPercent: 0,
138
+ resolvedUri: ""
139
+ });
140
+ store.setTier(address(hook), tokenId, tier);
141
+ }
142
+ }
@@ -0,0 +1,58 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity 0.8.26;
3
+
4
+ import "forge-std/Test.sol";
5
+ import {JB721Tier} from "@bananapus/721-hook-v6/src/structs/JB721Tier.sol";
6
+
7
+ import {Banny721TokenUriResolver} from "../../src/Banny721TokenUriResolver.sol";
8
+
9
+ /// @notice Regression test: L-58 -- mismatched array lengths should revert.
10
+ contract L58_ArrayLengthValidationTest is Test {
11
+ Banny721TokenUriResolver resolver;
12
+ address deployer = makeAddr("deployer");
13
+
14
+ function setUp() public {
15
+ vm.prank(deployer);
16
+ resolver = new Banny721TokenUriResolver(
17
+ "<path/>", "<necklace/>", "<mouth/>", "<eyes/>", "<alieneyes/>", deployer, address(0)
18
+ );
19
+ }
20
+
21
+ function test_setProductNames_revertsOnMismatchedLengths() public {
22
+ uint256[] memory upcs = new uint256[](2);
23
+ upcs[0] = 1;
24
+ upcs[1] = 2;
25
+
26
+ string[] memory names = new string[](1);
27
+ names[0] = "Only One";
28
+
29
+ vm.prank(deployer);
30
+ vm.expectRevert(Banny721TokenUriResolver.Banny721TokenUriResolver_ArrayLengthMismatch.selector);
31
+ resolver.setProductNames(upcs, names);
32
+ }
33
+
34
+ function test_setSvgContentsOf_revertsOnMismatchedLengths() public {
35
+ uint256[] memory upcs = new uint256[](2);
36
+ upcs[0] = 1;
37
+ upcs[1] = 2;
38
+
39
+ string[] memory contents = new string[](1);
40
+ contents[0] = "only one";
41
+
42
+ vm.expectRevert(Banny721TokenUriResolver.Banny721TokenUriResolver_ArrayLengthMismatch.selector);
43
+ resolver.setSvgContentsOf(upcs, contents);
44
+ }
45
+
46
+ function test_setSvgHashesOf_revertsOnMismatchedLengths() public {
47
+ uint256[] memory upcs = new uint256[](2);
48
+ upcs[0] = 1;
49
+ upcs[1] = 2;
50
+
51
+ bytes32[] memory hashes = new bytes32[](1);
52
+ hashes[0] = keccak256("test");
53
+
54
+ vm.prank(deployer);
55
+ vm.expectRevert(Banny721TokenUriResolver.Banny721TokenUriResolver_ArrayLengthMismatch.selector);
56
+ resolver.setSvgHashesOf(upcs, hashes);
57
+ }
58
+ }
@@ -0,0 +1,52 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity 0.8.26;
3
+
4
+ import "forge-std/Test.sol";
5
+
6
+ import {Banny721TokenUriResolver} from "../../src/Banny721TokenUriResolver.sol";
7
+
8
+ /// @notice Regression test: L-59 -- setMetadata should allow clearing fields to empty string.
9
+ contract L59_ClearMetadataTest is Test {
10
+ Banny721TokenUriResolver resolver;
11
+ address deployer = makeAddr("deployer");
12
+
13
+ function setUp() public {
14
+ vm.prank(deployer);
15
+ resolver = new Banny721TokenUriResolver(
16
+ "<path/>", "<necklace/>", "<mouth/>", "<eyes/>", "<alieneyes/>", deployer, address(0)
17
+ );
18
+ }
19
+
20
+ function test_setMetadata_canClearToEmpty() public {
21
+ // First, set some values.
22
+ vm.prank(deployer);
23
+ resolver.setMetadata("Initial desc", "https://initial.url", "https://initial.base/");
24
+
25
+ assertEq(resolver.svgDescription(), "Initial desc");
26
+ assertEq(resolver.svgExternalUrl(), "https://initial.url");
27
+ assertEq(resolver.svgBaseUri(), "https://initial.base/");
28
+
29
+ // Now clear all fields by passing empty strings.
30
+ vm.prank(deployer);
31
+ resolver.setMetadata("", "", "");
32
+
33
+ // Fields should now be empty (cleared), not still holding old values.
34
+ assertEq(resolver.svgDescription(), "", "description should be cleared");
35
+ assertEq(resolver.svgExternalUrl(), "", "url should be cleared");
36
+ assertEq(resolver.svgBaseUri(), "", "baseUri should be cleared");
37
+ }
38
+
39
+ function test_setMetadata_canClearIndividualField() public {
40
+ // Set all fields.
41
+ vm.prank(deployer);
42
+ resolver.setMetadata("desc", "https://url", "https://base/");
43
+
44
+ // Clear only description, keep others.
45
+ vm.prank(deployer);
46
+ resolver.setMetadata("", "https://url", "https://base/");
47
+
48
+ assertEq(resolver.svgDescription(), "", "description should be cleared");
49
+ assertEq(resolver.svgExternalUrl(), "https://url", "url should be unchanged");
50
+ assertEq(resolver.svgBaseUri(), "https://base/", "baseUri should be unchanged");
51
+ }
52
+ }
@@ -0,0 +1,181 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity 0.8.26;
3
+
4
+ import "forge-std/Test.sol";
5
+ import {JB721Tier} from "@bananapus/721-hook-v6/src/structs/JB721Tier.sol";
6
+ import {IERC721Receiver} from "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol";
7
+
8
+ import {Banny721TokenUriResolver} from "../../src/Banny721TokenUriResolver.sol";
9
+
10
+ /// @notice Mock hook that supports burning (setting owner to address(0) so ownerOf reverts).
11
+ contract MockHook62 {
12
+ mapping(uint256 => address) public owners;
13
+ address public immutable MOCK_STORE;
14
+ mapping(address => mapping(address => bool)) public isApprovedForAll;
15
+
16
+ constructor(address store) {
17
+ MOCK_STORE = store;
18
+ }
19
+
20
+ function STORE() external view returns (address) {
21
+ return MOCK_STORE;
22
+ }
23
+
24
+ function setOwner(uint256 tokenId, address owner) external {
25
+ owners[tokenId] = owner;
26
+ }
27
+
28
+ function ownerOf(uint256 tokenId) external view returns (address) {
29
+ address owner = owners[tokenId];
30
+ require(owner != address(0), "ERC721: token does not exist");
31
+ return owner;
32
+ }
33
+
34
+ function burn(uint256 tokenId) external {
35
+ owners[tokenId] = address(0);
36
+ }
37
+
38
+ function setApprovalForAll(address operator, bool approved) external {
39
+ isApprovedForAll[msg.sender][operator] = approved;
40
+ }
41
+
42
+ function safeTransferFrom(address from, address to, uint256 tokenId) external {
43
+ address owner = owners[tokenId];
44
+ require(owner != address(0), "ERC721: token does not exist");
45
+ require(
46
+ msg.sender == owner || msg.sender == from || isApprovedForAll[from][msg.sender], "MockHook: not authorized"
47
+ );
48
+ owners[tokenId] = to;
49
+ if (to.code.length > 0) {
50
+ bytes4 retval = IERC721Receiver(to).onERC721Received(msg.sender, from, tokenId, "");
51
+ require(retval == IERC721Receiver.onERC721Received.selector, "MockHook: receiver rejected");
52
+ }
53
+ }
54
+
55
+ function pricingContext() external pure returns (uint256, uint256, uint256) {
56
+ return (1, 18, 0);
57
+ }
58
+
59
+ function baseURI() external pure returns (string memory) {
60
+ return "ipfs://";
61
+ }
62
+ }
63
+
64
+ /// @notice Minimal mock store.
65
+ contract MockStore62 {
66
+ mapping(address => mapping(uint256 => JB721Tier)) public tiers;
67
+
68
+ function setTier(address hook, uint256 tokenId, JB721Tier memory tier) external {
69
+ tiers[hook][tokenId] = tier;
70
+ }
71
+
72
+ function tierOfTokenId(address hook, uint256 tokenId, bool) external view returns (JB721Tier memory) {
73
+ return tiers[hook][tokenId];
74
+ }
75
+
76
+ function encodedIPFSUriOf(address, uint256) external pure returns (bytes32) {
77
+ return bytes32(0);
78
+ }
79
+ }
80
+
81
+ /// @notice Regression test: L-62 -- burned equipped tokens should not lock the body.
82
+ contract L62_BurnedTokenCheckTest is Test {
83
+ Banny721TokenUriResolver resolver;
84
+ MockHook62 hook;
85
+ MockStore62 store;
86
+
87
+ address deployer = makeAddr("deployer");
88
+ address alice = makeAddr("alice");
89
+
90
+ uint256 constant BODY_TOKEN = 4_000_000_001;
91
+ uint256 constant NECKLACE_TOKEN = 10_000_000_001;
92
+ uint256 constant EYES_TOKEN = 30_000_000_001;
93
+
94
+ function setUp() public {
95
+ store = new MockStore62();
96
+ hook = new MockHook62(address(store));
97
+
98
+ vm.prank(deployer);
99
+ resolver = new Banny721TokenUriResolver(
100
+ "<path/>", "<necklace/>", "<mouth/>", "<eyes/>", "<alieneyes/>", deployer, address(0)
101
+ );
102
+
103
+ _setupTier(BODY_TOKEN, 4, 0);
104
+ _setupTier(NECKLACE_TOKEN, 10, 3);
105
+ _setupTier(EYES_TOKEN, 30, 5);
106
+
107
+ hook.setOwner(BODY_TOKEN, alice);
108
+ hook.setOwner(NECKLACE_TOKEN, alice);
109
+ hook.setOwner(EYES_TOKEN, alice);
110
+
111
+ vm.prank(alice);
112
+ hook.setApprovalForAll(address(resolver), true);
113
+ }
114
+
115
+ /// @notice If an equipped outfit is burned, the body should still be able to change outfits.
116
+ function test_decorateBannyWith_succeedsAfterEquippedOutfitBurned() public {
117
+ // Equip a necklace.
118
+ uint256[] memory outfitIds = new uint256[](1);
119
+ outfitIds[0] = NECKLACE_TOKEN;
120
+
121
+ vm.prank(alice);
122
+ resolver.decorateBannyWith(address(hook), BODY_TOKEN, 0, outfitIds);
123
+
124
+ // Verify necklace is equipped.
125
+ assertEq(resolver.wearerOf(address(hook), NECKLACE_TOKEN), BODY_TOKEN);
126
+
127
+ // Burn the necklace (simulate external burn while it's equipped).
128
+ hook.burn(NECKLACE_TOKEN);
129
+
130
+ // Now try to change outfits -- equip eyes instead. This should NOT revert.
131
+ uint256[] memory newOutfitIds = new uint256[](1);
132
+ newOutfitIds[0] = EYES_TOKEN;
133
+
134
+ vm.prank(alice);
135
+ resolver.decorateBannyWith(address(hook), BODY_TOKEN, 0, newOutfitIds);
136
+
137
+ // Verify new outfit is equipped.
138
+ assertEq(resolver.wearerOf(address(hook), EYES_TOKEN), BODY_TOKEN);
139
+ }
140
+
141
+ /// @notice If an equipped outfit is burned, the body should be able to clear all outfits.
142
+ function test_decorateBannyWith_canClearOutfitsAfterBurn() public {
143
+ // Equip a necklace.
144
+ uint256[] memory outfitIds = new uint256[](1);
145
+ outfitIds[0] = NECKLACE_TOKEN;
146
+
147
+ vm.prank(alice);
148
+ resolver.decorateBannyWith(address(hook), BODY_TOKEN, 0, outfitIds);
149
+
150
+ // Burn the necklace.
151
+ hook.burn(NECKLACE_TOKEN);
152
+
153
+ // Clear all outfits. This should NOT revert.
154
+ uint256[] memory emptyOutfits = new uint256[](0);
155
+
156
+ vm.prank(alice);
157
+ resolver.decorateBannyWith(address(hook), BODY_TOKEN, 0, emptyOutfits);
158
+ }
159
+
160
+ function _setupTier(uint256 tokenId, uint32 tierId, uint24 category) internal {
161
+ JB721Tier memory tier = JB721Tier({
162
+ id: tierId,
163
+ price: 0.01 ether,
164
+ remainingSupply: 100,
165
+ initialSupply: 100,
166
+ votingUnits: 0,
167
+ reserveFrequency: 0,
168
+ reserveBeneficiary: address(0),
169
+ encodedIPFSUri: bytes32(0),
170
+ category: category,
171
+ discountPercent: 0,
172
+ allowOwnerMint: false,
173
+ transfersPausable: false,
174
+ cannotBeRemoved: false,
175
+ cannotIncreaseDiscountPercent: false,
176
+ splitPercent: 0,
177
+ resolvedUri: ""
178
+ });
179
+ store.setTier(address(hook), tokenId, tier);
180
+ }
181
+ }