@bannynet/core-v6 0.0.24 → 0.0.25

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.
Files changed (34) hide show
  1. package/README.md +2 -2
  2. package/foundry.toml +2 -1
  3. package/package.json +22 -12
  4. package/src/Banny721TokenUriResolver.sol +6 -0
  5. package/ADMINISTRATION.md +0 -87
  6. package/ARCHITECTURE.md +0 -101
  7. package/AUDIT_INSTRUCTIONS.md +0 -78
  8. package/RISKS.md +0 -80
  9. package/SKILLS.md +0 -42
  10. package/STYLE_GUIDE.md +0 -610
  11. package/USER_JOURNEYS.md +0 -190
  12. package/foundry.lock +0 -14
  13. package/slither-ci.config.json +0 -10
  14. package/sphinx.lock +0 -521
  15. package/test/Banny721TokenUriResolver.t.sol +0 -694
  16. package/test/BannyAttacks.t.sol +0 -326
  17. package/test/DecorateFlow.t.sol +0 -1091
  18. package/test/Fork.t.sol +0 -2026
  19. package/test/OutfitTransferLifecycle.t.sol +0 -395
  20. package/test/TestAuditGaps.sol +0 -724
  21. package/test/TestQALastMile.t.sol +0 -447
  22. package/test/audit/AntiStrandingRetention.t.sol +0 -422
  23. package/test/audit/BurnedBodyStrandsAssets.t.sol +0 -163
  24. package/test/audit/DuplicateCategoryRetention.t.sol +0 -163
  25. package/test/audit/MergedOutfitExclusivity.t.sol +0 -228
  26. package/test/audit/MigrationHelperVerificationBypass.t.sol +0 -102
  27. package/test/audit/TryTransferFromStrandsAssets.t.sol +0 -197
  28. package/test/regression/ArrayLengthValidation.t.sol +0 -57
  29. package/test/regression/BodyCategoryValidation.t.sol +0 -147
  30. package/test/regression/BurnedTokenCheck.t.sol +0 -186
  31. package/test/regression/CEIReorder.t.sol +0 -209
  32. package/test/regression/ClearMetadata.t.sol +0 -52
  33. package/test/regression/MsgSenderEvents.t.sol +0 -153
  34. package/test/regression/RemovedTierDesync.t.sol +0 -346
@@ -1,153 +0,0 @@
1
- // SPDX-License-Identifier: MIT
2
- pragma solidity 0.8.28;
3
-
4
- import {Test} from "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
- import {IBanny721TokenUriResolver} from "../../src/interfaces/IBanny721TokenUriResolver.sol";
10
-
11
- /// @notice Minimal mock hook.
12
- contract MockHook56 {
13
- mapping(uint256 => address) public ownerOf;
14
- address public immutable MOCK_STORE;
15
- mapping(address => mapping(address => bool)) public isApprovedForAll;
16
-
17
- constructor(address store) {
18
- MOCK_STORE = store;
19
- }
20
-
21
- function STORE() external view returns (address) {
22
- return MOCK_STORE;
23
- }
24
-
25
- function setOwner(uint256 tokenId, address owner) external {
26
- ownerOf[tokenId] = owner;
27
- }
28
-
29
- function setApprovalForAll(address operator, bool approved) external {
30
- isApprovedForAll[msg.sender][operator] = approved;
31
- }
32
-
33
- function safeTransferFrom(address from, address to, uint256 tokenId) external {
34
- require(
35
- msg.sender == ownerOf[tokenId] || msg.sender == from || isApprovedForAll[from][msg.sender],
36
- "MockHook: not authorized"
37
- );
38
- ownerOf[tokenId] = to;
39
- if (to.code.length > 0) {
40
- bytes4 retval = IERC721Receiver(to).onERC721Received(msg.sender, from, tokenId, "");
41
- require(retval == IERC721Receiver.onERC721Received.selector, "MockHook: receiver rejected");
42
- }
43
- }
44
-
45
- function pricingContext() external pure returns (uint256, uint256, uint256) {
46
- return (1, 18, 0);
47
- }
48
-
49
- function baseURI() external pure returns (string memory) {
50
- return "ipfs://";
51
- }
52
- }
53
-
54
- /// @notice Minimal mock store.
55
- contract MockStore56 {
56
- mapping(address => mapping(uint256 => JB721Tier)) public tiers;
57
-
58
- function setTier(address hook, uint256 tokenId, JB721Tier memory tier) external {
59
- tiers[hook][tokenId] = tier;
60
- }
61
-
62
- function tierOfTokenId(address hook, uint256 tokenId, bool) external view returns (JB721Tier memory) {
63
- return tiers[hook][tokenId];
64
- }
65
-
66
- // forge-lint: disable-next-line(mixed-case-function)
67
- function encodedIPFSUriOf(address, uint256) external pure returns (bytes32) {
68
- return bytes32(0);
69
- }
70
- }
71
-
72
- /// @notice Events should emit _msgSender(), not msg.sender.
73
- contract MsgSenderEventsTest is Test {
74
- Banny721TokenUriResolver resolver;
75
- MockHook56 hook;
76
- MockStore56 store;
77
-
78
- address deployer = makeAddr("deployer");
79
-
80
- function setUp() public {
81
- store = new MockStore56();
82
- hook = new MockHook56(address(store));
83
-
84
- vm.prank(deployer);
85
- resolver = new Banny721TokenUriResolver(
86
- "<path/>", "<necklace/>", "<mouth/>", "<eyes/>", "<alieneyes/>", deployer, address(0)
87
- );
88
- }
89
-
90
- /// @notice Verify SetProductName event emits _msgSender() (== deployer) not msg.sender.
91
- function test_setProductNames_emitsCorrectCaller() public {
92
- uint256[] memory upcs = new uint256[](1);
93
- upcs[0] = 100;
94
- string[] memory names = new string[](1);
95
- names[0] = "Test";
96
-
97
- vm.expectEmit(true, true, true, true);
98
- emit IBanny721TokenUriResolver.SetProductName({upc: 100, name: "Test", caller: deployer});
99
-
100
- vm.prank(deployer);
101
- resolver.setProductNames(upcs, names);
102
- }
103
-
104
- /// @notice Verify SetMetadata event emits _msgSender() (== deployer) not msg.sender.
105
- function test_setMetadata_emitsCorrectCaller() public {
106
- vm.expectEmit(true, true, true, true);
107
- emit IBanny721TokenUriResolver.SetMetadata({
108
- description: "desc", externalUrl: "url", baseUri: "base", caller: deployer
109
- });
110
-
111
- vm.prank(deployer);
112
- resolver.setMetadata("desc", "url", "base");
113
- }
114
-
115
- /// @notice Verify SetSvgHash event emits _msgSender() (== deployer) not msg.sender.
116
- function test_setSvgHashesOf_emitsCorrectCaller() public {
117
- uint256[] memory upcs = new uint256[](1);
118
- upcs[0] = 100;
119
- bytes32[] memory hashes = new bytes32[](1);
120
- hashes[0] = keccak256("test");
121
-
122
- vm.expectEmit(true, true, true, true);
123
- emit IBanny721TokenUriResolver.SetSvgHash({upc: 100, svgHash: keccak256("test"), caller: deployer});
124
-
125
- vm.prank(deployer);
126
- resolver.setSvgHashesOf(upcs, hashes);
127
- }
128
-
129
- /// @notice Verify SetSvgContent event emits _msgSender() not msg.sender.
130
- function test_setSvgContentsOf_emitsCorrectCaller() public {
131
- string memory content = "test-svg-content";
132
-
133
- // Store hash first.
134
- uint256[] memory upcs = new uint256[](1);
135
- upcs[0] = 100;
136
- bytes32[] memory hashes = new bytes32[](1);
137
- hashes[0] = keccak256(abi.encodePacked(content));
138
-
139
- vm.prank(deployer);
140
- resolver.setSvgHashesOf(upcs, hashes);
141
-
142
- // Now store content -- anyone can call this.
143
- string[] memory contents = new string[](1);
144
- contents[0] = content;
145
-
146
- address alice = makeAddr("alice");
147
- vm.expectEmit(true, true, true, true);
148
- emit IBanny721TokenUriResolver.SetSvgContent({upc: 100, svgContent: content, caller: alice});
149
-
150
- vm.prank(alice);
151
- resolver.setSvgContentsOf(upcs, contents);
152
- }
153
- }
@@ -1,346 +0,0 @@
1
- // SPDX-License-Identifier: MIT
2
- pragma solidity 0.8.28;
3
-
4
- import {Test} from "forge-std/Test.sol";
5
- import {JB721Tier} from "@bananapus/721-hook-v6/src/structs/JB721Tier.sol";
6
- import {JB721TierFlags} from "@bananapus/721-hook-v6/src/structs/JB721TierFlags.sol";
7
- import {IERC721Receiver} from "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol";
8
-
9
- import {Banny721TokenUriResolver} from "../../src/Banny721TokenUriResolver.sol";
10
-
11
- /// @notice Mock hook that supports tier removal (clearing tier data so tierOfTokenId returns defaults).
12
- contract MockHookM8 {
13
- mapping(uint256 => address) public ownerOf;
14
- mapping(uint256 => uint32) public tierIdOf;
15
- mapping(uint256 => uint24) public categoryOf;
16
- address public immutable MOCK_STORE;
17
- mapping(address => mapping(address => bool)) public isApprovedForAll;
18
-
19
- constructor(address store) {
20
- MOCK_STORE = store;
21
- }
22
-
23
- function STORE() external view returns (address) {
24
- return MOCK_STORE;
25
- }
26
-
27
- function setOwner(uint256 tokenId, address owner) external {
28
- ownerOf[tokenId] = owner;
29
- }
30
-
31
- function setTier(uint256 tokenId, uint32 tierId, uint24 category) external {
32
- tierIdOf[tokenId] = tierId;
33
- categoryOf[tokenId] = category;
34
- }
35
-
36
- function setApprovalForAll(address operator, bool approved) external {
37
- isApprovedForAll[msg.sender][operator] = approved;
38
- }
39
-
40
- function safeTransferFrom(address from, address to, uint256 tokenId) external {
41
- require(
42
- msg.sender == ownerOf[tokenId] || msg.sender == from || isApprovedForAll[from][msg.sender],
43
- "MockHook: not authorized"
44
- );
45
- ownerOf[tokenId] = to;
46
- if (to.code.length > 0) {
47
- bytes4 retval = IERC721Receiver(to).onERC721Received(msg.sender, from, tokenId, "");
48
- require(retval == IERC721Receiver.onERC721Received.selector, "MockHook: receiver rejected");
49
- }
50
- }
51
-
52
- function pricingContext() external pure returns (uint256, uint256, uint256) {
53
- return (1, 18, 0);
54
- }
55
-
56
- function baseURI() external pure returns (string memory) {
57
- return "ipfs://";
58
- }
59
- }
60
-
61
- /// @notice Mock store that supports tier removal (deleting tier data so tierOfTokenId returns a zeroed struct).
62
- contract MockStoreM8 {
63
- mapping(address => mapping(uint256 => JB721Tier)) public tiers;
64
- mapping(address => mapping(uint256 => bool)) public tierRemoved;
65
-
66
- function setTier(address hook, uint256 tokenId, JB721Tier memory tier) external {
67
- tiers[hook][tokenId] = tier;
68
- tierRemoved[hook][tokenId] = false;
69
- }
70
-
71
- /// @notice Simulate removing a tier — tierOfTokenId will return a zeroed struct (category = 0).
72
- function removeTier(address hook, uint256 tokenId) external {
73
- delete tiers[hook][tokenId];
74
- tierRemoved[hook][tokenId] = true;
75
- }
76
-
77
- function tierOfTokenId(address hook, uint256 tokenId, bool) external view returns (JB721Tier memory) {
78
- return tiers[hook][tokenId];
79
- }
80
-
81
- // forge-lint: disable-next-line(mixed-case-function)
82
- function encodedIPFSUriOf(address, uint256) external pure returns (bytes32) {
83
- return bytes32(0);
84
- }
85
- }
86
-
87
- /// @notice Removed tier causes outfit state desynchronization.
88
- /// @dev When a previously equipped outfit's tier is removed, `_productOfTokenId` returns category 0.
89
- /// Before the fix, this caused the first while loop to exit immediately (due to `!= 0` guard),
90
- /// and the second while loop would transfer out outfits that were being re-equipped.
91
- contract RemovedTierDesyncTest is Test {
92
- Banny721TokenUriResolver resolver;
93
- MockHookM8 hook;
94
- MockStoreM8 store;
95
-
96
- address deployer = makeAddr("deployer");
97
- address alice = makeAddr("alice");
98
-
99
- // Token IDs: tierId * 1_000_000_000 + sequence.
100
- uint256 constant BODY_TOKEN = 4_000_000_001;
101
- uint256 constant NECKLACE_TOKEN = 10_000_000_001; // category 3
102
- uint256 constant EYES_TOKEN = 30_000_000_001; // category 5
103
- uint256 constant MOUTH_TOKEN = 40_000_000_001; // category 7
104
-
105
- function setUp() public {
106
- store = new MockStoreM8();
107
- hook = new MockHookM8(address(store));
108
-
109
- vm.prank(deployer);
110
- resolver = new Banny721TokenUriResolver(
111
- "<path/>", "<necklace/>", "<mouth/>", "<eyes/>", "<alieneyes/>", deployer, address(0)
112
- );
113
-
114
- _setupTier(BODY_TOKEN, 4, 0);
115
- _setupTier(NECKLACE_TOKEN, 10, 3);
116
- _setupTier(EYES_TOKEN, 30, 5);
117
- _setupTier(MOUTH_TOKEN, 40, 7);
118
-
119
- hook.setOwner(BODY_TOKEN, alice);
120
- hook.setOwner(NECKLACE_TOKEN, alice);
121
- hook.setOwner(EYES_TOKEN, alice);
122
- hook.setOwner(MOUTH_TOKEN, alice);
123
-
124
- vm.prank(alice);
125
- hook.setApprovalForAll(address(resolver), true);
126
- }
127
-
128
- /// @notice Equip 3 outfits, remove first outfit's tier, re-equip remaining 2.
129
- /// The remaining outfits should stay properly equipped and not be transferred back.
130
- function test_reequipAfterTierRemoval_retainsValidOutfits() public {
131
- // Step 1: Equip necklace, eyes, and mouth.
132
- uint256[] memory outfitIds = new uint256[](3);
133
- outfitIds[0] = NECKLACE_TOKEN; // category 3
134
- outfitIds[1] = EYES_TOKEN; // category 5
135
- outfitIds[2] = MOUTH_TOKEN; // category 7
136
-
137
- vm.prank(alice);
138
- resolver.decorateBannyWith(address(hook), BODY_TOKEN, 0, outfitIds);
139
-
140
- // Verify all equipped.
141
- assertEq(resolver.wearerOf(address(hook), NECKLACE_TOKEN), BODY_TOKEN, "necklace should be worn");
142
- assertEq(resolver.wearerOf(address(hook), EYES_TOKEN), BODY_TOKEN, "eyes should be worn");
143
- assertEq(resolver.wearerOf(address(hook), MOUTH_TOKEN), BODY_TOKEN, "mouth should be worn");
144
-
145
- // All outfit tokens should be held by the resolver.
146
- assertEq(hook.ownerOf(NECKLACE_TOKEN), address(resolver), "necklace held by resolver");
147
- assertEq(hook.ownerOf(EYES_TOKEN), address(resolver), "eyes held by resolver");
148
- assertEq(hook.ownerOf(MOUTH_TOKEN), address(resolver), "mouth held by resolver");
149
-
150
- // Step 2: Admin removes the necklace tier.
151
- store.removeTier(address(hook), NECKLACE_TOKEN);
152
-
153
- // Step 3: Re-equip with only the remaining valid outfits (eyes + mouth).
154
- uint256[] memory newOutfitIds = new uint256[](2);
155
- newOutfitIds[0] = EYES_TOKEN; // category 5
156
- newOutfitIds[1] = MOUTH_TOKEN; // category 7
157
-
158
- vm.prank(alice);
159
- resolver.decorateBannyWith(address(hook), BODY_TOKEN, 0, newOutfitIds);
160
-
161
- // Step 4: Verify eyes and mouth are STILL properly equipped (not transferred out).
162
- assertEq(resolver.wearerOf(address(hook), EYES_TOKEN), BODY_TOKEN, "eyes should still be worn after re-equip");
163
- assertEq(resolver.wearerOf(address(hook), MOUTH_TOKEN), BODY_TOKEN, "mouth should still be worn after re-equip");
164
-
165
- // Eyes and mouth should still be held by the resolver.
166
- assertEq(hook.ownerOf(EYES_TOKEN), address(resolver), "eyes should still be held by resolver");
167
- assertEq(hook.ownerOf(MOUTH_TOKEN), address(resolver), "mouth should still be held by resolver");
168
-
169
- // Step 5: Verify the assets list only contains eyes and mouth.
170
- (, uint256[] memory currentOutfits) = resolver.assetIdsOf(address(hook), BODY_TOKEN);
171
- assertEq(currentOutfits.length, 2, "should have 2 outfits");
172
- assertEq(currentOutfits[0], EYES_TOKEN, "first outfit should be eyes");
173
- assertEq(currentOutfits[1], MOUTH_TOKEN, "second outfit should be mouth");
174
- }
175
-
176
- /// @notice Variant: remove a middle outfit's tier, re-equip first and last.
177
- function test_reequipAfterMiddleTierRemoval_retainsValidOutfits() public {
178
- // Equip necklace, eyes, and mouth.
179
- uint256[] memory outfitIds = new uint256[](3);
180
- outfitIds[0] = NECKLACE_TOKEN; // category 3
181
- outfitIds[1] = EYES_TOKEN; // category 5
182
- outfitIds[2] = MOUTH_TOKEN; // category 7
183
-
184
- vm.prank(alice);
185
- resolver.decorateBannyWith(address(hook), BODY_TOKEN, 0, outfitIds);
186
-
187
- // Remove the eyes tier (middle outfit).
188
- store.removeTier(address(hook), EYES_TOKEN);
189
-
190
- // Re-equip with necklace + mouth (skipping the removed eyes).
191
- uint256[] memory newOutfitIds = new uint256[](2);
192
- newOutfitIds[0] = NECKLACE_TOKEN; // category 3
193
- newOutfitIds[1] = MOUTH_TOKEN; // category 7
194
-
195
- vm.prank(alice);
196
- resolver.decorateBannyWith(address(hook), BODY_TOKEN, 0, newOutfitIds);
197
-
198
- // Necklace and mouth should still be equipped.
199
- assertEq(resolver.wearerOf(address(hook), NECKLACE_TOKEN), BODY_TOKEN, "necklace should still be worn");
200
- assertEq(resolver.wearerOf(address(hook), MOUTH_TOKEN), BODY_TOKEN, "mouth should still be worn");
201
- assertEq(hook.ownerOf(NECKLACE_TOKEN), address(resolver), "necklace should still be held by resolver");
202
- assertEq(hook.ownerOf(MOUTH_TOKEN), address(resolver), "mouth should still be held by resolver");
203
-
204
- (, uint256[] memory currentOutfits) = resolver.assetIdsOf(address(hook), BODY_TOKEN);
205
- assertEq(currentOutfits.length, 2, "should have 2 outfits");
206
- assertEq(currentOutfits[0], NECKLACE_TOKEN, "first outfit should be necklace");
207
- assertEq(currentOutfits[1], MOUTH_TOKEN, "second outfit should be mouth");
208
- }
209
-
210
- /// @notice Variant: remove the last outfit's tier, re-equip first two.
211
- function test_reequipAfterLastTierRemoval_retainsValidOutfits() public {
212
- // Equip necklace, eyes, and mouth.
213
- uint256[] memory outfitIds = new uint256[](3);
214
- outfitIds[0] = NECKLACE_TOKEN; // category 3
215
- outfitIds[1] = EYES_TOKEN; // category 5
216
- outfitIds[2] = MOUTH_TOKEN; // category 7
217
-
218
- vm.prank(alice);
219
- resolver.decorateBannyWith(address(hook), BODY_TOKEN, 0, outfitIds);
220
-
221
- // Remove the mouth tier (last outfit).
222
- store.removeTier(address(hook), MOUTH_TOKEN);
223
-
224
- // Re-equip with necklace + eyes only.
225
- uint256[] memory newOutfitIds = new uint256[](2);
226
- newOutfitIds[0] = NECKLACE_TOKEN; // category 3
227
- newOutfitIds[1] = EYES_TOKEN; // category 5
228
-
229
- vm.prank(alice);
230
- resolver.decorateBannyWith(address(hook), BODY_TOKEN, 0, newOutfitIds);
231
-
232
- // Necklace and eyes should still be equipped.
233
- assertEq(resolver.wearerOf(address(hook), NECKLACE_TOKEN), BODY_TOKEN, "necklace should still be worn");
234
- assertEq(resolver.wearerOf(address(hook), EYES_TOKEN), BODY_TOKEN, "eyes should still be worn");
235
- assertEq(hook.ownerOf(NECKLACE_TOKEN), address(resolver), "necklace should still be held by resolver");
236
- assertEq(hook.ownerOf(EYES_TOKEN), address(resolver), "eyes should still be held by resolver");
237
- }
238
-
239
- /// @notice Variant: all tiers removed, clear all outfits. Should not revert.
240
- function test_clearOutfitsAfterAllTiersRemoved() public {
241
- // Equip necklace and eyes.
242
- uint256[] memory outfitIds = new uint256[](2);
243
- outfitIds[0] = NECKLACE_TOKEN;
244
- outfitIds[1] = EYES_TOKEN;
245
-
246
- vm.prank(alice);
247
- resolver.decorateBannyWith(address(hook), BODY_TOKEN, 0, outfitIds);
248
-
249
- // Remove both tiers.
250
- store.removeTier(address(hook), NECKLACE_TOKEN);
251
- store.removeTier(address(hook), EYES_TOKEN);
252
-
253
- // Clear all outfits (empty array). Should not revert.
254
- uint256[] memory emptyOutfits = new uint256[](0);
255
-
256
- vm.prank(alice);
257
- resolver.decorateBannyWith(address(hook), BODY_TOKEN, 0, emptyOutfits);
258
-
259
- (, uint256[] memory currentOutfits) = resolver.assetIdsOf(address(hook), BODY_TOKEN);
260
- assertEq(currentOutfits.length, 0, "should have no outfits");
261
- }
262
-
263
- /// @notice Edge case: single outfit equipped, its tier removed, re-equip with a different outfit.
264
- function test_replaceRemovedTierOutfitWithNew() public {
265
- // Equip necklace.
266
- uint256[] memory outfitIds = new uint256[](1);
267
- outfitIds[0] = NECKLACE_TOKEN;
268
-
269
- vm.prank(alice);
270
- resolver.decorateBannyWith(address(hook), BODY_TOKEN, 0, outfitIds);
271
-
272
- // Remove necklace tier.
273
- store.removeTier(address(hook), NECKLACE_TOKEN);
274
-
275
- // Equip eyes instead.
276
- uint256[] memory newOutfitIds = new uint256[](1);
277
- newOutfitIds[0] = EYES_TOKEN;
278
-
279
- vm.prank(alice);
280
- resolver.decorateBannyWith(address(hook), BODY_TOKEN, 0, newOutfitIds);
281
-
282
- // Eyes should be equipped.
283
- assertEq(resolver.wearerOf(address(hook), EYES_TOKEN), BODY_TOKEN, "eyes should be worn");
284
- assertEq(hook.ownerOf(EYES_TOKEN), address(resolver), "eyes should be held by resolver");
285
-
286
- (, uint256[] memory currentOutfits) = resolver.assetIdsOf(address(hook), BODY_TOKEN);
287
- assertEq(currentOutfits.length, 1, "should have 1 outfit");
288
- assertEq(currentOutfits[0], EYES_TOKEN, "outfit should be eyes");
289
- }
290
-
291
- /// @notice Edge case: two consecutive removed tiers at the start.
292
- function test_reequipAfterTwoConsecutiveRemovedTiers() public {
293
- // Equip necklace, eyes, and mouth.
294
- uint256[] memory outfitIds = new uint256[](3);
295
- outfitIds[0] = NECKLACE_TOKEN; // category 3
296
- outfitIds[1] = EYES_TOKEN; // category 5
297
- outfitIds[2] = MOUTH_TOKEN; // category 7
298
-
299
- vm.prank(alice);
300
- resolver.decorateBannyWith(address(hook), BODY_TOKEN, 0, outfitIds);
301
-
302
- // Remove both necklace and eyes tiers.
303
- store.removeTier(address(hook), NECKLACE_TOKEN);
304
- store.removeTier(address(hook), EYES_TOKEN);
305
-
306
- // Re-equip with just mouth.
307
- uint256[] memory newOutfitIds = new uint256[](1);
308
- newOutfitIds[0] = MOUTH_TOKEN; // category 7
309
-
310
- vm.prank(alice);
311
- resolver.decorateBannyWith(address(hook), BODY_TOKEN, 0, newOutfitIds);
312
-
313
- // Mouth should still be equipped.
314
- assertEq(resolver.wearerOf(address(hook), MOUTH_TOKEN), BODY_TOKEN, "mouth should still be worn");
315
- assertEq(hook.ownerOf(MOUTH_TOKEN), address(resolver), "mouth should still be held by resolver");
316
-
317
- (, uint256[] memory currentOutfits) = resolver.assetIdsOf(address(hook), BODY_TOKEN);
318
- assertEq(currentOutfits.length, 1, "should have 1 outfit");
319
- assertEq(currentOutfits[0], MOUTH_TOKEN, "outfit should be mouth");
320
- }
321
-
322
- function _setupTier(uint256 tokenId, uint32 tierId, uint24 category) internal {
323
- JB721Tier memory tier = JB721Tier({
324
- id: tierId,
325
- price: 0.01 ether,
326
- remainingSupply: 100,
327
- initialSupply: 100,
328
- votingUnits: 0,
329
- reserveFrequency: 0,
330
- reserveBeneficiary: address(0),
331
- encodedIPFSUri: bytes32(0),
332
- category: category,
333
- discountPercent: 0,
334
- flags: JB721TierFlags({
335
- allowOwnerMint: false,
336
- transfersPausable: false,
337
- cantBeRemoved: false,
338
- cantIncreaseDiscountPercent: false,
339
- cantBuyWithCredits: false
340
- }),
341
- splitPercent: 0,
342
- resolvedUri: ""
343
- });
344
- store.setTier(address(hook), tokenId, tier);
345
- }
346
- }