@bannynet/core-v6 0.0.24 → 0.0.26

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 (35) 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 +10 -1
  5. package/src/interfaces/IBanny721TokenUriResolver.sol +3 -2
  6. package/ADMINISTRATION.md +0 -87
  7. package/ARCHITECTURE.md +0 -101
  8. package/AUDIT_INSTRUCTIONS.md +0 -78
  9. package/RISKS.md +0 -80
  10. package/SKILLS.md +0 -42
  11. package/STYLE_GUIDE.md +0 -610
  12. package/USER_JOURNEYS.md +0 -190
  13. package/foundry.lock +0 -14
  14. package/slither-ci.config.json +0 -10
  15. package/sphinx.lock +0 -521
  16. package/test/Banny721TokenUriResolver.t.sol +0 -694
  17. package/test/BannyAttacks.t.sol +0 -326
  18. package/test/DecorateFlow.t.sol +0 -1091
  19. package/test/Fork.t.sol +0 -2026
  20. package/test/OutfitTransferLifecycle.t.sol +0 -395
  21. package/test/TestAuditGaps.sol +0 -724
  22. package/test/TestQALastMile.t.sol +0 -447
  23. package/test/audit/AntiStrandingRetention.t.sol +0 -422
  24. package/test/audit/BurnedBodyStrandsAssets.t.sol +0 -163
  25. package/test/audit/DuplicateCategoryRetention.t.sol +0 -163
  26. package/test/audit/MergedOutfitExclusivity.t.sol +0 -228
  27. package/test/audit/MigrationHelperVerificationBypass.t.sol +0 -102
  28. package/test/audit/TryTransferFromStrandsAssets.t.sol +0 -197
  29. package/test/regression/ArrayLengthValidation.t.sol +0 -57
  30. package/test/regression/BodyCategoryValidation.t.sol +0 -147
  31. package/test/regression/BurnedTokenCheck.t.sol +0 -186
  32. package/test/regression/CEIReorder.t.sol +0 -209
  33. package/test/regression/ClearMetadata.t.sol +0 -52
  34. package/test/regression/MsgSenderEvents.t.sol +0 -153
  35. 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
- }