@bannynet/core-v6 0.0.4 → 0.0.6
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/ADMINISTRATION.md +97 -0
- package/ARCHITECTURE.md +58 -0
- package/RISKS.md +160 -0
- package/STYLE_GUIDE.md +482 -0
- package/foundry.toml +2 -2
- package/package.json +5 -5
- package/script/Deploy.s.sol +2 -1
- package/src/Banny721TokenUriResolver.sol +178 -158
- package/src/interfaces/IBanny721TokenUriResolver.sol +88 -61
- package/test/Banny721TokenUriResolver.t.sol +1 -1
- package/test/Fork.t.sol +2020 -0
- package/test/regression/I25_CEIReorder.t.sol +1 -1
- package/test/regression/L56_MsgSenderEvents.t.sol +1 -1
- package/test/regression/L57_BodyCategoryValidation.t.sol +1 -1
- package/test/regression/L58_ArrayLengthValidation.t.sol +1 -1
- package/test/regression/L59_ClearMetadata.t.sol +1 -1
- package/test/regression/L62_BurnedTokenCheck.t.sol +1 -1
- package/test/regression/M8_RemovedTierDesync.t.sol +341 -0
package/test/Fork.t.sol
ADDED
|
@@ -0,0 +1,2020 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity 0.8.26;
|
|
3
|
+
|
|
4
|
+
import "forge-std/Test.sol";
|
|
5
|
+
|
|
6
|
+
// JB core — deploy fresh within fork.
|
|
7
|
+
import {JBPermissions} from "@bananapus/core-v6/src/JBPermissions.sol";
|
|
8
|
+
import {JBProjects} from "@bananapus/core-v6/src/JBProjects.sol";
|
|
9
|
+
import {JBDirectory} from "@bananapus/core-v6/src/JBDirectory.sol";
|
|
10
|
+
import {JBRulesets} from "@bananapus/core-v6/src/JBRulesets.sol";
|
|
11
|
+
import {JBTokens} from "@bananapus/core-v6/src/JBTokens.sol";
|
|
12
|
+
import {JBERC20} from "@bananapus/core-v6/src/JBERC20.sol";
|
|
13
|
+
import {JBSplits} from "@bananapus/core-v6/src/JBSplits.sol";
|
|
14
|
+
import {JBPrices} from "@bananapus/core-v6/src/JBPrices.sol";
|
|
15
|
+
import {JBController} from "@bananapus/core-v6/src/JBController.sol";
|
|
16
|
+
import {JBFundAccessLimits} from "@bananapus/core-v6/src/JBFundAccessLimits.sol";
|
|
17
|
+
import {JBConstants} from "@bananapus/core-v6/src/libraries/JBConstants.sol";
|
|
18
|
+
|
|
19
|
+
// 721 hook — deploy fresh within fork.
|
|
20
|
+
import {JB721TiersHookStore} from "@bananapus/721-hook-v6/src/JB721TiersHookStore.sol";
|
|
21
|
+
import {JB721TiersHook} from "@bananapus/721-hook-v6/src/JB721TiersHook.sol";
|
|
22
|
+
import {JB721TiersHookDeployer} from "@bananapus/721-hook-v6/src/JB721TiersHookDeployer.sol";
|
|
23
|
+
import {JBAddressRegistry} from "@bananapus/address-registry-v6/src/JBAddressRegistry.sol";
|
|
24
|
+
import {IJB721TiersHook} from "@bananapus/721-hook-v6/src/interfaces/IJB721TiersHook.sol";
|
|
25
|
+
import {IJB721TiersHookStore} from "@bananapus/721-hook-v6/src/interfaces/IJB721TiersHookStore.sol";
|
|
26
|
+
import {JB721TierConfig} from "@bananapus/721-hook-v6/src/structs/JB721TierConfig.sol";
|
|
27
|
+
import {JB721InitTiersConfig} from "@bananapus/721-hook-v6/src/structs/JB721InitTiersConfig.sol";
|
|
28
|
+
import {JB721TiersHookFlags} from "@bananapus/721-hook-v6/src/structs/JB721TiersHookFlags.sol";
|
|
29
|
+
import {JBDeploy721TiersHookConfig} from "@bananapus/721-hook-v6/src/structs/JBDeploy721TiersHookConfig.sol";
|
|
30
|
+
import {JB721Tier} from "@bananapus/721-hook-v6/src/structs/JB721Tier.sol";
|
|
31
|
+
import {IJB721TokenUriResolver} from "@bananapus/721-hook-v6/src/interfaces/IJB721TokenUriResolver.sol";
|
|
32
|
+
import {IJBPrices} from "@bananapus/core-v6/src/interfaces/IJBPrices.sol";
|
|
33
|
+
import {JBCurrencyIds} from "@bananapus/core-v6/src/libraries/JBCurrencyIds.sol";
|
|
34
|
+
import {JBSplit} from "@bananapus/core-v6/src/structs/JBSplit.sol";
|
|
35
|
+
|
|
36
|
+
// OpenZeppelin.
|
|
37
|
+
import {IERC721} from "@bananapus/721-hook-v6/src/abstract/ERC721.sol";
|
|
38
|
+
import {IERC721Receiver} from "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol";
|
|
39
|
+
|
|
40
|
+
// Banny.
|
|
41
|
+
import {Banny721TokenUriResolver} from "../src/Banny721TokenUriResolver.sol";
|
|
42
|
+
import {IBanny721TokenUriResolver} from "../src/interfaces/IBanny721TokenUriResolver.sol";
|
|
43
|
+
|
|
44
|
+
/// @notice Malicious hook for reentrancy testing. Re-enters the resolver during safeTransferFrom.
|
|
45
|
+
contract ReentrantHook {
|
|
46
|
+
Banny721TokenUriResolver public resolver;
|
|
47
|
+
address public hookTarget;
|
|
48
|
+
uint256 public bodyId;
|
|
49
|
+
bool public armed;
|
|
50
|
+
|
|
51
|
+
mapping(uint256 tokenId => address) public ownerOf;
|
|
52
|
+
mapping(uint256 tokenId => uint32) public tierIdOf;
|
|
53
|
+
mapping(address owner => mapping(address operator => bool)) public isApprovedForAll;
|
|
54
|
+
address public immutable MOCK_STORE;
|
|
55
|
+
|
|
56
|
+
constructor(address store) {
|
|
57
|
+
MOCK_STORE = store;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function STORE() external view returns (address) {
|
|
61
|
+
return MOCK_STORE;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function setOwner(uint256 tokenId, address _owner) external {
|
|
65
|
+
ownerOf[tokenId] = _owner;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function setApprovalForAll(address operator, bool approved) external {
|
|
69
|
+
isApprovedForAll[msg.sender][operator] = approved;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function arm(Banny721TokenUriResolver _resolver, address _hookTarget, uint256 _bodyId) external {
|
|
73
|
+
resolver = _resolver;
|
|
74
|
+
hookTarget = _hookTarget;
|
|
75
|
+
bodyId = _bodyId;
|
|
76
|
+
armed = true;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function disarm() external {
|
|
80
|
+
armed = false;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function safeTransferFrom(address from, address to, uint256 tokenId) external {
|
|
84
|
+
ownerOf[tokenId] = to;
|
|
85
|
+
|
|
86
|
+
// Re-enter if armed.
|
|
87
|
+
if (armed) {
|
|
88
|
+
armed = false; // prevent infinite loop
|
|
89
|
+
uint256[] memory emptyOutfits = new uint256[](0);
|
|
90
|
+
try resolver.decorateBannyWith(hookTarget, bodyId, 0, emptyOutfits) {} catch {}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (to.code.length > 0) {
|
|
94
|
+
IERC721Receiver(to).onERC721Received(msg.sender, from, tokenId, "");
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function pricingContext() external pure returns (uint256, uint256, uint256) {
|
|
99
|
+
return (1, 18, 0);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function baseURI() external pure returns (string memory) {
|
|
103
|
+
return "ipfs://";
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/// @notice Mock store for the reentrancy hook.
|
|
108
|
+
contract ReentrantMockStore {
|
|
109
|
+
mapping(address hook => mapping(uint256 tokenId => JB721Tier)) public tiers;
|
|
110
|
+
|
|
111
|
+
function setTier(address hook, uint256 tokenId, JB721Tier memory tier) external {
|
|
112
|
+
tiers[hook][tokenId] = tier;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function tierOfTokenId(address hook, uint256 tokenId, bool) external view returns (JB721Tier memory) {
|
|
116
|
+
return tiers[hook][tokenId];
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function encodedTierIPFSUriOf(address, uint256) external pure returns (bytes32) {
|
|
120
|
+
return bytes32(0);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function encodedIPFSUriOf(address, uint256) external pure returns (bytes32) {
|
|
124
|
+
return bytes32(0);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/// @notice Fork tests for Banny721TokenUriResolver against real JB infrastructure.
|
|
129
|
+
/// @dev Deploys all JB core + 721 hook contracts fresh within a mainnet fork, then tests
|
|
130
|
+
/// the full decoration lifecycle with adversarial conditions.
|
|
131
|
+
contract BannyForkTest is Test {
|
|
132
|
+
// ───────────────────────── JB core (deployed fresh)
|
|
133
|
+
// ────────────────────
|
|
134
|
+
|
|
135
|
+
address multisig = address(0xBEEF);
|
|
136
|
+
address trustedForwarder = address(0);
|
|
137
|
+
|
|
138
|
+
JBPermissions jbPermissions;
|
|
139
|
+
JBProjects jbProjects;
|
|
140
|
+
JBDirectory jbDirectory;
|
|
141
|
+
JBRulesets jbRulesets;
|
|
142
|
+
JBTokens jbTokens;
|
|
143
|
+
JBSplits jbSplits;
|
|
144
|
+
JBPrices jbPrices;
|
|
145
|
+
JBFundAccessLimits jbFundAccessLimits;
|
|
146
|
+
JBController jbController;
|
|
147
|
+
|
|
148
|
+
// ───────────────────────── 721 hook (deployed fresh)
|
|
149
|
+
// ──────────────────
|
|
150
|
+
|
|
151
|
+
JB721TiersHookDeployer hookDeployer;
|
|
152
|
+
IJB721TiersHook bannyHook;
|
|
153
|
+
|
|
154
|
+
// ───────────────────────── Banny resolver
|
|
155
|
+
// ──────────────────────────
|
|
156
|
+
|
|
157
|
+
Banny721TokenUriResolver resolver;
|
|
158
|
+
|
|
159
|
+
// ───────────────────────── Actors
|
|
160
|
+
// ──────────────────────────────────
|
|
161
|
+
|
|
162
|
+
address alice = makeAddr("alice");
|
|
163
|
+
address bob = makeAddr("bob");
|
|
164
|
+
address charlie = makeAddr("charlie");
|
|
165
|
+
address attacker = makeAddr("attacker");
|
|
166
|
+
|
|
167
|
+
// ───────────────────────── Tier IDs
|
|
168
|
+
// ──────────────────────────────────
|
|
169
|
+
|
|
170
|
+
// Tiers sorted by category (ascending) as required by the store.
|
|
171
|
+
// Tier 1-4: Bodies (category 0) — Alien, Pink, Orange, Original
|
|
172
|
+
// Tier 5: Background (category 1)
|
|
173
|
+
// Tier 6: Necklace (category 3)
|
|
174
|
+
// Tier 7: Head (category 4)
|
|
175
|
+
// Tier 8: Eyes (category 5)
|
|
176
|
+
// Tier 9: Glasses (category 6)
|
|
177
|
+
// Tier 10: Mouth (category 7)
|
|
178
|
+
// Tier 11: Legs (category 8)
|
|
179
|
+
// Tier 12: Suit (category 9)
|
|
180
|
+
// Tier 13: Suit Bottom (category 10)
|
|
181
|
+
// Tier 14: Suit Top (category 11)
|
|
182
|
+
// Tier 15: Headtop (category 12)
|
|
183
|
+
// Tier 16: Hand (category 13)
|
|
184
|
+
|
|
185
|
+
uint16 constant TIER_ALIEN_BODY = 1;
|
|
186
|
+
uint16 constant TIER_PINK_BODY = 2;
|
|
187
|
+
uint16 constant TIER_ORANGE_BODY = 3;
|
|
188
|
+
uint16 constant TIER_ORIGINAL_BODY = 4;
|
|
189
|
+
uint16 constant TIER_BACKGROUND = 5;
|
|
190
|
+
uint16 constant TIER_NECKLACE = 6;
|
|
191
|
+
uint16 constant TIER_HEAD = 7;
|
|
192
|
+
uint16 constant TIER_EYES = 8;
|
|
193
|
+
uint16 constant TIER_GLASSES = 9;
|
|
194
|
+
uint16 constant TIER_MOUTH = 10;
|
|
195
|
+
uint16 constant TIER_LEGS = 11;
|
|
196
|
+
uint16 constant TIER_SUIT = 12;
|
|
197
|
+
uint16 constant TIER_SUIT_BOTTOM = 13;
|
|
198
|
+
uint16 constant TIER_SUIT_TOP = 14;
|
|
199
|
+
uint16 constant TIER_HEADTOP = 15;
|
|
200
|
+
uint16 constant TIER_HAND = 16;
|
|
201
|
+
|
|
202
|
+
// Pre-computed token IDs for the first mint of each tier.
|
|
203
|
+
// Formula: tierId * 1_000_000_000 + sequenceNumber (starts at 1).
|
|
204
|
+
uint256 constant ALIEN_BODY_1 = 1_000_000_001;
|
|
205
|
+
uint256 constant PINK_BODY_1 = 2_000_000_001;
|
|
206
|
+
uint256 constant ORIGINAL_BODY_1 = 4_000_000_001;
|
|
207
|
+
uint256 constant ORIGINAL_BODY_2 = 4_000_000_002;
|
|
208
|
+
uint256 constant BACKGROUND_1 = 5_000_000_001;
|
|
209
|
+
uint256 constant BACKGROUND_2 = 5_000_000_002;
|
|
210
|
+
uint256 constant NECKLACE_1 = 6_000_000_001;
|
|
211
|
+
uint256 constant NECKLACE_2 = 6_000_000_002;
|
|
212
|
+
uint256 constant HEAD_1 = 7_000_000_001;
|
|
213
|
+
uint256 constant EYES_1 = 8_000_000_001;
|
|
214
|
+
uint256 constant GLASSES_1 = 9_000_000_001;
|
|
215
|
+
uint256 constant MOUTH_1 = 10_000_000_001;
|
|
216
|
+
uint256 constant LEGS_1 = 11_000_000_001;
|
|
217
|
+
uint256 constant SUIT_1 = 12_000_000_001;
|
|
218
|
+
uint256 constant SUIT_BOTTOM_1 = 13_000_000_001;
|
|
219
|
+
uint256 constant SUIT_TOP_1 = 14_000_000_001;
|
|
220
|
+
uint256 constant HEADTOP_1 = 15_000_000_001;
|
|
221
|
+
uint256 constant HAND_1 = 16_000_000_001;
|
|
222
|
+
|
|
223
|
+
// Second mints for multi-actor tests.
|
|
224
|
+
uint256 constant EYES_2 = 8_000_000_002;
|
|
225
|
+
uint256 constant MOUTH_2 = 10_000_000_002;
|
|
226
|
+
uint256 constant HEAD_2 = 7_000_000_002;
|
|
227
|
+
|
|
228
|
+
// Third mints for redressing cycle tests (minted to alice in setUp).
|
|
229
|
+
uint256 constant GLASSES_2 = 9_000_000_002;
|
|
230
|
+
uint256 constant LEGS_2 = 11_000_000_002;
|
|
231
|
+
uint256 constant NECKLACE_3 = 6_000_000_003;
|
|
232
|
+
uint256 constant HEADTOP_2 = 15_000_000_002;
|
|
233
|
+
|
|
234
|
+
// ───────────────────────── Setup
|
|
235
|
+
// ──────────────────────────────────────
|
|
236
|
+
|
|
237
|
+
function setUp() public {
|
|
238
|
+
// Skip fork tests when no RPC URL is configured.
|
|
239
|
+
string memory rpcUrl = vm.envOr("RPC_ETHEREUM_MAINNET", string(""));
|
|
240
|
+
if (bytes(rpcUrl).length == 0) {
|
|
241
|
+
vm.skip(true);
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
vm.createSelectFork(rpcUrl);
|
|
245
|
+
|
|
246
|
+
// Clear any mainnet code at actor addresses (makeAddr may collide with deployed contracts).
|
|
247
|
+
vm.etch(alice, "");
|
|
248
|
+
vm.etch(bob, "");
|
|
249
|
+
vm.etch(charlie, "");
|
|
250
|
+
vm.etch(attacker, "");
|
|
251
|
+
|
|
252
|
+
// Deploy all JB core contracts fresh within the fork.
|
|
253
|
+
_deployJBCore();
|
|
254
|
+
|
|
255
|
+
// Deploy the 721 hook infrastructure.
|
|
256
|
+
_deploy721Hook();
|
|
257
|
+
|
|
258
|
+
// Deploy the Banny resolver.
|
|
259
|
+
_deployBannyResolver();
|
|
260
|
+
|
|
261
|
+
// Deploy the 721 hook with production-like tiers.
|
|
262
|
+
_deployBannyHook();
|
|
263
|
+
|
|
264
|
+
// Mint NFTs to test actors.
|
|
265
|
+
_mintInitialNFTs();
|
|
266
|
+
|
|
267
|
+
// Labels for trace readability.
|
|
268
|
+
vm.label(alice, "alice");
|
|
269
|
+
vm.label(bob, "bob");
|
|
270
|
+
vm.label(charlie, "charlie");
|
|
271
|
+
vm.label(attacker, "attacker");
|
|
272
|
+
vm.label(address(resolver), "BannyResolver");
|
|
273
|
+
vm.label(address(bannyHook), "BannyHook");
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
277
|
+
// 1. E2E HAPPY PATH: Mint → Decorate → Verify Token URI
|
|
278
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
279
|
+
|
|
280
|
+
function test_fork_e2e_mintDecorateRender() public {
|
|
281
|
+
// Alice owns ORIGINAL_BODY_1, NECKLACE_1, EYES_1, MOUTH_1, BACKGROUND_1.
|
|
282
|
+
// Decorate with necklace, eyes, mouth, and background.
|
|
283
|
+
uint256[] memory outfitIds = new uint256[](3);
|
|
284
|
+
outfitIds[0] = NECKLACE_1; // cat 3
|
|
285
|
+
outfitIds[1] = EYES_1; // cat 5
|
|
286
|
+
outfitIds[2] = MOUTH_1; // cat 7
|
|
287
|
+
|
|
288
|
+
vm.prank(alice);
|
|
289
|
+
resolver.decorateBannyWith(address(bannyHook), ORIGINAL_BODY_1, BACKGROUND_1, outfitIds);
|
|
290
|
+
|
|
291
|
+
// Verify outfits are worn.
|
|
292
|
+
assertEq(resolver.wearerOf(address(bannyHook), NECKLACE_1), ORIGINAL_BODY_1);
|
|
293
|
+
assertEq(resolver.wearerOf(address(bannyHook), EYES_1), ORIGINAL_BODY_1);
|
|
294
|
+
assertEq(resolver.wearerOf(address(bannyHook), MOUTH_1), ORIGINAL_BODY_1);
|
|
295
|
+
assertEq(resolver.userOf(address(bannyHook), BACKGROUND_1), ORIGINAL_BODY_1);
|
|
296
|
+
|
|
297
|
+
// Verify token URI is non-empty (SVG rendered on-chain).
|
|
298
|
+
string memory uri = resolver.tokenUriOf(address(bannyHook), ORIGINAL_BODY_1);
|
|
299
|
+
assertGt(bytes(uri).length, 0, "token URI should not be empty");
|
|
300
|
+
|
|
301
|
+
// Verify outfits are held by the resolver.
|
|
302
|
+
assertEq(IERC721(address(bannyHook)).ownerOf(NECKLACE_1), address(resolver));
|
|
303
|
+
assertEq(IERC721(address(bannyHook)).ownerOf(EYES_1), address(resolver));
|
|
304
|
+
assertEq(IERC721(address(bannyHook)).ownerOf(MOUTH_1), address(resolver));
|
|
305
|
+
assertEq(IERC721(address(bannyHook)).ownerOf(BACKGROUND_1), address(resolver));
|
|
306
|
+
|
|
307
|
+
// Body still owned by alice.
|
|
308
|
+
assertEq(IERC721(address(bannyHook)).ownerOf(ORIGINAL_BODY_1), alice);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function test_fork_e2e_alienBodyDefaultEyes() public {
|
|
312
|
+
// Alice owns ALIEN_BODY_1. Naked alien body should inject alien eyes in SVG.
|
|
313
|
+
string memory svg = resolver.svgOf(address(bannyHook), ALIEN_BODY_1, true, false);
|
|
314
|
+
assertGt(bytes(svg).length, 0, "alien body SVG should render");
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function test_fork_e2e_outfitRenderedOnMannequin() public {
|
|
318
|
+
// Unequipped outfit token should render on mannequin.
|
|
319
|
+
string memory uri = resolver.tokenUriOf(address(bannyHook), NECKLACE_1);
|
|
320
|
+
assertGt(bytes(uri).length, 0, "outfit URI should render on mannequin");
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
324
|
+
// 2. DECORATION FLOWS
|
|
325
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
326
|
+
|
|
327
|
+
function test_fork_decorateWithBackgroundOnly() public {
|
|
328
|
+
uint256[] memory emptyOutfits = new uint256[](0);
|
|
329
|
+
|
|
330
|
+
vm.prank(alice);
|
|
331
|
+
resolver.decorateBannyWith(address(bannyHook), ORIGINAL_BODY_1, BACKGROUND_1, emptyOutfits);
|
|
332
|
+
|
|
333
|
+
(uint256 bgId,) = resolver.assetIdsOf(address(bannyHook), ORIGINAL_BODY_1);
|
|
334
|
+
assertEq(bgId, BACKGROUND_1);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function test_fork_decorateReplaceOutfit() public {
|
|
338
|
+
// Equip necklace 1.
|
|
339
|
+
uint256[] memory outfits1 = new uint256[](1);
|
|
340
|
+
outfits1[0] = NECKLACE_1;
|
|
341
|
+
vm.prank(alice);
|
|
342
|
+
resolver.decorateBannyWith(address(bannyHook), ORIGINAL_BODY_1, 0, outfits1);
|
|
343
|
+
|
|
344
|
+
assertEq(resolver.wearerOf(address(bannyHook), NECKLACE_1), ORIGINAL_BODY_1);
|
|
345
|
+
assertEq(IERC721(address(bannyHook)).ownerOf(NECKLACE_1), address(resolver));
|
|
346
|
+
|
|
347
|
+
// Replace with necklace 2.
|
|
348
|
+
uint256[] memory outfits2 = new uint256[](1);
|
|
349
|
+
outfits2[0] = NECKLACE_2;
|
|
350
|
+
vm.prank(alice);
|
|
351
|
+
resolver.decorateBannyWith(address(bannyHook), ORIGINAL_BODY_1, 0, outfits2);
|
|
352
|
+
|
|
353
|
+
// Old necklace returned to alice, new one held by resolver.
|
|
354
|
+
assertEq(IERC721(address(bannyHook)).ownerOf(NECKLACE_1), alice);
|
|
355
|
+
assertEq(IERC721(address(bannyHook)).ownerOf(NECKLACE_2), address(resolver));
|
|
356
|
+
assertEq(resolver.wearerOf(address(bannyHook), NECKLACE_2), ORIGINAL_BODY_1);
|
|
357
|
+
assertEq(resolver.wearerOf(address(bannyHook), NECKLACE_1), 0);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function test_fork_decorateStripAllOutfits() public {
|
|
361
|
+
// Equip multiple outfits.
|
|
362
|
+
uint256[] memory outfits = new uint256[](2);
|
|
363
|
+
outfits[0] = NECKLACE_1; // cat 3
|
|
364
|
+
outfits[1] = EYES_1; // cat 5
|
|
365
|
+
vm.prank(alice);
|
|
366
|
+
resolver.decorateBannyWith(address(bannyHook), ORIGINAL_BODY_1, BACKGROUND_1, outfits);
|
|
367
|
+
|
|
368
|
+
// Strip all by passing empty arrays.
|
|
369
|
+
uint256[] memory empty = new uint256[](0);
|
|
370
|
+
vm.prank(alice);
|
|
371
|
+
resolver.decorateBannyWith(address(bannyHook), ORIGINAL_BODY_1, 0, empty);
|
|
372
|
+
|
|
373
|
+
// All returned.
|
|
374
|
+
assertEq(IERC721(address(bannyHook)).ownerOf(NECKLACE_1), alice);
|
|
375
|
+
assertEq(IERC721(address(bannyHook)).ownerOf(EYES_1), alice);
|
|
376
|
+
assertEq(IERC721(address(bannyHook)).ownerOf(BACKGROUND_1), alice);
|
|
377
|
+
|
|
378
|
+
(uint256 bgId, uint256[] memory outfitIds) = resolver.assetIdsOf(address(bannyHook), ORIGINAL_BODY_1);
|
|
379
|
+
assertEq(bgId, 0);
|
|
380
|
+
assertEq(outfitIds.length, 0);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function test_fork_decorateReplaceBackground() public {
|
|
384
|
+
uint256[] memory empty = new uint256[](0);
|
|
385
|
+
|
|
386
|
+
// Set background 1.
|
|
387
|
+
vm.prank(alice);
|
|
388
|
+
resolver.decorateBannyWith(address(bannyHook), ORIGINAL_BODY_1, BACKGROUND_1, empty);
|
|
389
|
+
|
|
390
|
+
// Replace with background 2.
|
|
391
|
+
vm.prank(alice);
|
|
392
|
+
resolver.decorateBannyWith(address(bannyHook), ORIGINAL_BODY_1, BACKGROUND_2, empty);
|
|
393
|
+
|
|
394
|
+
// Background 1 returned, background 2 held.
|
|
395
|
+
assertEq(IERC721(address(bannyHook)).ownerOf(BACKGROUND_1), alice);
|
|
396
|
+
assertEq(IERC721(address(bannyHook)).ownerOf(BACKGROUND_2), address(resolver));
|
|
397
|
+
assertEq(resolver.userOf(address(bannyHook), BACKGROUND_2), ORIGINAL_BODY_1);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function test_fork_decorateMaxOutfits() public {
|
|
401
|
+
// Equip one of every non-conflicting category: necklace(3), eyes(5), glasses(6), mouth(7),
|
|
402
|
+
// legs(8), suit_bottom(10), suit_top(11), headtop(12), hand(13).
|
|
403
|
+
uint256[] memory outfits = new uint256[](9);
|
|
404
|
+
outfits[0] = NECKLACE_1; // cat 3
|
|
405
|
+
outfits[1] = EYES_1; // cat 5
|
|
406
|
+
outfits[2] = GLASSES_1; // cat 6
|
|
407
|
+
outfits[3] = MOUTH_1; // cat 7
|
|
408
|
+
outfits[4] = LEGS_1; // cat 8
|
|
409
|
+
outfits[5] = SUIT_BOTTOM_1; // cat 10
|
|
410
|
+
outfits[6] = SUIT_TOP_1; // cat 11
|
|
411
|
+
outfits[7] = HEADTOP_1; // cat 12
|
|
412
|
+
outfits[8] = HAND_1; // cat 13
|
|
413
|
+
|
|
414
|
+
vm.prank(alice);
|
|
415
|
+
resolver.decorateBannyWith(address(bannyHook), ORIGINAL_BODY_1, BACKGROUND_1, outfits);
|
|
416
|
+
|
|
417
|
+
(, uint256[] memory attachedOutfits) = resolver.assetIdsOf(address(bannyHook), ORIGINAL_BODY_1);
|
|
418
|
+
assertEq(attachedOutfits.length, 9, "should have 9 outfits attached");
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function test_fork_outfitReuseBetweenOwnBodies() public {
|
|
422
|
+
// Alice owns both ORIGINAL_BODY_1 and ORIGINAL_BODY_2.
|
|
423
|
+
// Equip necklace on body 1.
|
|
424
|
+
uint256[] memory outfits = new uint256[](1);
|
|
425
|
+
outfits[0] = NECKLACE_1;
|
|
426
|
+
vm.prank(alice);
|
|
427
|
+
resolver.decorateBannyWith(address(bannyHook), ORIGINAL_BODY_1, 0, outfits);
|
|
428
|
+
|
|
429
|
+
// Move necklace to body 2 (alice owns both).
|
|
430
|
+
vm.prank(alice);
|
|
431
|
+
resolver.decorateBannyWith(address(bannyHook), ORIGINAL_BODY_2, 0, outfits);
|
|
432
|
+
|
|
433
|
+
assertEq(resolver.wearerOf(address(bannyHook), NECKLACE_1), ORIGINAL_BODY_2);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
437
|
+
// 3. AUTHORIZATION (ADVERSARIAL)
|
|
438
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
439
|
+
|
|
440
|
+
function test_fork_auth_nonOwnerCantDecorate() public {
|
|
441
|
+
uint256[] memory empty = new uint256[](0);
|
|
442
|
+
|
|
443
|
+
vm.prank(attacker);
|
|
444
|
+
vm.expectRevert(Banny721TokenUriResolver.Banny721TokenUriResolver_UnauthorizedBannyBody.selector);
|
|
445
|
+
resolver.decorateBannyWith(address(bannyHook), ORIGINAL_BODY_1, 0, empty);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function test_fork_auth_cantEquipOthersOutfit() public {
|
|
449
|
+
// Bob owns his own body. He tries to equip alice's necklace (which she owns).
|
|
450
|
+
uint256[] memory outfits = new uint256[](1);
|
|
451
|
+
outfits[0] = NECKLACE_1; // Owned by alice
|
|
452
|
+
|
|
453
|
+
// Bob has his own body but necklace belongs to alice.
|
|
454
|
+
vm.prank(bob);
|
|
455
|
+
vm.expectRevert(); // UnauthorizedOutfit — bob doesn't own the necklace
|
|
456
|
+
resolver.decorateBannyWith(address(bannyHook), PINK_BODY_1, 0, outfits);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
function test_fork_auth_cantEquipOthersBackground() public {
|
|
460
|
+
uint256[] memory empty = new uint256[](0);
|
|
461
|
+
|
|
462
|
+
// Bob tries to use alice's background.
|
|
463
|
+
vm.prank(bob);
|
|
464
|
+
vm.expectRevert(); // UnauthorizedBackground
|
|
465
|
+
resolver.decorateBannyWith(address(bannyHook), PINK_BODY_1, BACKGROUND_1, empty);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
function test_fork_auth_attackerCantStealOutfit() public {
|
|
469
|
+
// Alice equips necklace on her body.
|
|
470
|
+
uint256[] memory outfits = new uint256[](1);
|
|
471
|
+
outfits[0] = NECKLACE_1;
|
|
472
|
+
vm.prank(alice);
|
|
473
|
+
resolver.decorateBannyWith(address(bannyHook), ORIGINAL_BODY_1, 0, outfits);
|
|
474
|
+
|
|
475
|
+
// Attacker has their own body but tries to steal alice's worn necklace.
|
|
476
|
+
// Attacker doesn't own the banny body the necklace is worn by.
|
|
477
|
+
uint256[] memory stealOutfits = new uint256[](1);
|
|
478
|
+
stealOutfits[0] = NECKLACE_1;
|
|
479
|
+
|
|
480
|
+
// Give attacker a body for this test.
|
|
481
|
+
_mintTo(attacker, TIER_ORIGINAL_BODY); // ORIGINAL_BODY_3 = 4_000_000_003
|
|
482
|
+
|
|
483
|
+
vm.prank(attacker);
|
|
484
|
+
vm.expectRevert(); // UnauthorizedOutfit — attacker doesn't own body wearing the necklace
|
|
485
|
+
resolver.decorateBannyWith(address(bannyHook), 4_000_000_003, 0, stealOutfits);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
function test_fork_auth_nonOwnerCantLock() public {
|
|
489
|
+
vm.prank(attacker);
|
|
490
|
+
vm.expectRevert(Banny721TokenUriResolver.Banny721TokenUriResolver_UnauthorizedBannyBody.selector);
|
|
491
|
+
resolver.lockOutfitChangesFor(address(bannyHook), ORIGINAL_BODY_1);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
function test_fork_auth_bodyAsOutfitReverts() public {
|
|
495
|
+
// Try to use a body token as an outfit.
|
|
496
|
+
uint256[] memory outfits = new uint256[](1);
|
|
497
|
+
outfits[0] = ORIGINAL_BODY_2;
|
|
498
|
+
|
|
499
|
+
vm.prank(alice);
|
|
500
|
+
vm.expectRevert(Banny721TokenUriResolver.Banny721TokenUriResolver_UnrecognizedCategory.selector);
|
|
501
|
+
resolver.decorateBannyWith(address(bannyHook), ORIGINAL_BODY_1, 0, outfits);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
function test_fork_auth_backgroundAsOutfitReverts() public {
|
|
505
|
+
uint256[] memory outfits = new uint256[](1);
|
|
506
|
+
outfits[0] = BACKGROUND_1; // cat 1
|
|
507
|
+
|
|
508
|
+
vm.prank(alice);
|
|
509
|
+
vm.expectRevert(Banny721TokenUriResolver.Banny721TokenUriResolver_UnrecognizedCategory.selector);
|
|
510
|
+
resolver.decorateBannyWith(address(bannyHook), ORIGINAL_BODY_1, 0, outfits);
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function test_fork_auth_nonBodyAsBodyReverts() public {
|
|
514
|
+
// Try to decorate a necklace token as if it were a body.
|
|
515
|
+
uint256[] memory empty = new uint256[](0);
|
|
516
|
+
|
|
517
|
+
vm.prank(alice);
|
|
518
|
+
vm.expectRevert(Banny721TokenUriResolver.Banny721TokenUriResolver_BannyBodyNotBodyCategory.selector);
|
|
519
|
+
resolver.decorateBannyWith(address(bannyHook), NECKLACE_1, 0, empty);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
523
|
+
// 4. LOCK MECHANISM
|
|
524
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
525
|
+
|
|
526
|
+
function test_fork_lock_preventsDecoration() public {
|
|
527
|
+
vm.prank(alice);
|
|
528
|
+
resolver.lockOutfitChangesFor(address(bannyHook), ORIGINAL_BODY_1);
|
|
529
|
+
|
|
530
|
+
uint256[] memory empty = new uint256[](0);
|
|
531
|
+
vm.prank(alice);
|
|
532
|
+
vm.expectRevert(Banny721TokenUriResolver.Banny721TokenUriResolver_OutfitChangesLocked.selector);
|
|
533
|
+
resolver.decorateBannyWith(address(bannyHook), ORIGINAL_BODY_1, 0, empty);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
function test_fork_lock_expiresAfter7Days() public {
|
|
537
|
+
vm.prank(alice);
|
|
538
|
+
resolver.lockOutfitChangesFor(address(bannyHook), ORIGINAL_BODY_1);
|
|
539
|
+
|
|
540
|
+
// Warp past lock.
|
|
541
|
+
vm.warp(block.timestamp + 7 days + 1);
|
|
542
|
+
|
|
543
|
+
// Should succeed.
|
|
544
|
+
uint256[] memory outfits = new uint256[](1);
|
|
545
|
+
outfits[0] = NECKLACE_1;
|
|
546
|
+
vm.prank(alice);
|
|
547
|
+
resolver.decorateBannyWith(address(bannyHook), ORIGINAL_BODY_1, 0, outfits);
|
|
548
|
+
|
|
549
|
+
assertEq(resolver.wearerOf(address(bannyHook), NECKLACE_1), ORIGINAL_BODY_1);
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
function test_fork_lock_extendsOnRelock() public {
|
|
553
|
+
vm.prank(alice);
|
|
554
|
+
resolver.lockOutfitChangesFor(address(bannyHook), ORIGINAL_BODY_1);
|
|
555
|
+
uint256 firstLock = resolver.outfitLockedUntil(address(bannyHook), ORIGINAL_BODY_1);
|
|
556
|
+
|
|
557
|
+
// Warp forward 3 days (within lock period).
|
|
558
|
+
vm.warp(block.timestamp + 3 days);
|
|
559
|
+
|
|
560
|
+
// Re-lock extends from now + 7 days.
|
|
561
|
+
vm.prank(alice);
|
|
562
|
+
resolver.lockOutfitChangesFor(address(bannyHook), ORIGINAL_BODY_1);
|
|
563
|
+
uint256 secondLock = resolver.outfitLockedUntil(address(bannyHook), ORIGINAL_BODY_1);
|
|
564
|
+
|
|
565
|
+
assertGt(secondLock, firstLock, "lock should be extended");
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
function test_fork_lock_cantAccelerate() public {
|
|
569
|
+
vm.prank(alice);
|
|
570
|
+
resolver.lockOutfitChangesFor(address(bannyHook), ORIGINAL_BODY_1);
|
|
571
|
+
uint256 originalLock = resolver.outfitLockedUntil(address(bannyHook), ORIGINAL_BODY_1);
|
|
572
|
+
|
|
573
|
+
// Same-block re-lock doesn't shorten.
|
|
574
|
+
vm.prank(alice);
|
|
575
|
+
resolver.lockOutfitChangesFor(address(bannyHook), ORIGINAL_BODY_1);
|
|
576
|
+
uint256 afterRelock = resolver.outfitLockedUntil(address(bannyHook), ORIGINAL_BODY_1);
|
|
577
|
+
|
|
578
|
+
assertEq(afterRelock, originalLock, "lock should not accelerate");
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
function test_fork_lock_persistsAcrossTransfer() public {
|
|
582
|
+
// Alice locks her body.
|
|
583
|
+
vm.prank(alice);
|
|
584
|
+
resolver.lockOutfitChangesFor(address(bannyHook), ORIGINAL_BODY_1);
|
|
585
|
+
|
|
586
|
+
// Alice transfers body to bob.
|
|
587
|
+
vm.prank(alice);
|
|
588
|
+
IERC721(address(bannyHook)).safeTransferFrom(alice, bob, ORIGINAL_BODY_1);
|
|
589
|
+
|
|
590
|
+
// Bob owns the body but it's still locked.
|
|
591
|
+
assertEq(IERC721(address(bannyHook)).ownerOf(ORIGINAL_BODY_1), bob);
|
|
592
|
+
|
|
593
|
+
uint256[] memory empty = new uint256[](0);
|
|
594
|
+
vm.prank(bob);
|
|
595
|
+
vm.expectRevert(Banny721TokenUriResolver.Banny721TokenUriResolver_OutfitChangesLocked.selector);
|
|
596
|
+
resolver.decorateBannyWith(address(bannyHook), ORIGINAL_BODY_1, 0, empty);
|
|
597
|
+
|
|
598
|
+
// After lock expires, bob can decorate.
|
|
599
|
+
vm.warp(block.timestamp + 7 days + 1);
|
|
600
|
+
|
|
601
|
+
vm.prank(bob);
|
|
602
|
+
resolver.decorateBannyWith(address(bannyHook), ORIGINAL_BODY_1, 0, empty);
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
606
|
+
// 5. CATEGORY CONFLICTS
|
|
607
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
608
|
+
|
|
609
|
+
function test_fork_conflict_headBlocksEyes() public {
|
|
610
|
+
uint256[] memory outfits = new uint256[](2);
|
|
611
|
+
outfits[0] = HEAD_1; // cat 4
|
|
612
|
+
outfits[1] = EYES_1; // cat 5
|
|
613
|
+
|
|
614
|
+
vm.prank(alice);
|
|
615
|
+
vm.expectRevert(Banny721TokenUriResolver.Banny721TokenUriResolver_HeadAlreadyAdded.selector);
|
|
616
|
+
resolver.decorateBannyWith(address(bannyHook), ORIGINAL_BODY_1, 0, outfits);
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
function test_fork_conflict_headBlocksGlasses() public {
|
|
620
|
+
uint256[] memory outfits = new uint256[](2);
|
|
621
|
+
outfits[0] = HEAD_1; // cat 4
|
|
622
|
+
outfits[1] = GLASSES_1; // cat 6
|
|
623
|
+
|
|
624
|
+
vm.prank(alice);
|
|
625
|
+
vm.expectRevert(Banny721TokenUriResolver.Banny721TokenUriResolver_HeadAlreadyAdded.selector);
|
|
626
|
+
resolver.decorateBannyWith(address(bannyHook), ORIGINAL_BODY_1, 0, outfits);
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
function test_fork_conflict_headBlocksMouth() public {
|
|
630
|
+
uint256[] memory outfits = new uint256[](2);
|
|
631
|
+
outfits[0] = HEAD_1; // cat 4
|
|
632
|
+
outfits[1] = MOUTH_1; // cat 7
|
|
633
|
+
|
|
634
|
+
vm.prank(alice);
|
|
635
|
+
vm.expectRevert(Banny721TokenUriResolver.Banny721TokenUriResolver_HeadAlreadyAdded.selector);
|
|
636
|
+
resolver.decorateBannyWith(address(bannyHook), ORIGINAL_BODY_1, 0, outfits);
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
function test_fork_conflict_headBlocksHeadtop() public {
|
|
640
|
+
uint256[] memory outfits = new uint256[](2);
|
|
641
|
+
outfits[0] = HEAD_1; // cat 4
|
|
642
|
+
outfits[1] = HEADTOP_1; // cat 12
|
|
643
|
+
|
|
644
|
+
vm.prank(alice);
|
|
645
|
+
vm.expectRevert(Banny721TokenUriResolver.Banny721TokenUriResolver_HeadAlreadyAdded.selector);
|
|
646
|
+
resolver.decorateBannyWith(address(bannyHook), ORIGINAL_BODY_1, 0, outfits);
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
function test_fork_conflict_suitBlocksSuitBottom() public {
|
|
650
|
+
uint256[] memory outfits = new uint256[](2);
|
|
651
|
+
outfits[0] = SUIT_1; // cat 9
|
|
652
|
+
outfits[1] = SUIT_BOTTOM_1; // cat 10
|
|
653
|
+
|
|
654
|
+
vm.prank(alice);
|
|
655
|
+
vm.expectRevert(Banny721TokenUriResolver.Banny721TokenUriResolver_SuitAlreadyAdded.selector);
|
|
656
|
+
resolver.decorateBannyWith(address(bannyHook), ORIGINAL_BODY_1, 0, outfits);
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
function test_fork_conflict_suitBlocksSuitTop() public {
|
|
660
|
+
uint256[] memory outfits = new uint256[](2);
|
|
661
|
+
outfits[0] = SUIT_1; // cat 9
|
|
662
|
+
outfits[1] = SUIT_TOP_1; // cat 11
|
|
663
|
+
|
|
664
|
+
vm.prank(alice);
|
|
665
|
+
vm.expectRevert(Banny721TokenUriResolver.Banny721TokenUriResolver_SuitAlreadyAdded.selector);
|
|
666
|
+
resolver.decorateBannyWith(address(bannyHook), ORIGINAL_BODY_1, 0, outfits);
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
function test_fork_conflict_unorderedCategoriesRevert() public {
|
|
670
|
+
uint256[] memory outfits = new uint256[](2);
|
|
671
|
+
outfits[0] = MOUTH_1; // cat 7
|
|
672
|
+
outfits[1] = NECKLACE_1; // cat 3 — out of order!
|
|
673
|
+
|
|
674
|
+
vm.prank(alice);
|
|
675
|
+
vm.expectRevert(Banny721TokenUriResolver.Banny721TokenUriResolver_UnorderedCategories.selector);
|
|
676
|
+
resolver.decorateBannyWith(address(bannyHook), ORIGINAL_BODY_1, 0, outfits);
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
function test_fork_conflict_suitBottomAndTopAllowed() public {
|
|
680
|
+
// Suit bottom (10) + suit top (11) without full suit — should work.
|
|
681
|
+
uint256[] memory outfits = new uint256[](2);
|
|
682
|
+
outfits[0] = SUIT_BOTTOM_1; // cat 10
|
|
683
|
+
outfits[1] = SUIT_TOP_1; // cat 11
|
|
684
|
+
|
|
685
|
+
vm.prank(alice);
|
|
686
|
+
resolver.decorateBannyWith(address(bannyHook), ORIGINAL_BODY_1, 0, outfits);
|
|
687
|
+
|
|
688
|
+
assertEq(resolver.wearerOf(address(bannyHook), SUIT_BOTTOM_1), ORIGINAL_BODY_1);
|
|
689
|
+
assertEq(resolver.wearerOf(address(bannyHook), SUIT_TOP_1), ORIGINAL_BODY_1);
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
693
|
+
// 6. SVG STORAGE (IMMUTABILITY + PERMISSIONLESS UPLOAD)
|
|
694
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
695
|
+
|
|
696
|
+
function test_fork_svg_hashSetAndContentUpload() public {
|
|
697
|
+
string memory content = "<g class='test'>test-svg</g>";
|
|
698
|
+
uint256 testUpc = 100;
|
|
699
|
+
|
|
700
|
+
// Owner sets hash.
|
|
701
|
+
uint256[] memory upcs = new uint256[](1);
|
|
702
|
+
upcs[0] = testUpc;
|
|
703
|
+
bytes32[] memory hashes = new bytes32[](1);
|
|
704
|
+
hashes[0] = keccak256(abi.encodePacked(content));
|
|
705
|
+
|
|
706
|
+
vm.prank(multisig);
|
|
707
|
+
resolver.setSvgHashesOf(upcs, hashes);
|
|
708
|
+
|
|
709
|
+
assertEq(resolver.svgHashOf(testUpc), hashes[0]);
|
|
710
|
+
|
|
711
|
+
// Anyone can upload content if hash matches.
|
|
712
|
+
string[] memory contents = new string[](1);
|
|
713
|
+
contents[0] = content;
|
|
714
|
+
|
|
715
|
+
vm.prank(charlie); // charlie is random — permissionless upload
|
|
716
|
+
resolver.setSvgContentsOf(upcs, contents);
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
function test_fork_svg_wrongContentRejected() public {
|
|
720
|
+
uint256 testUpc = 101;
|
|
721
|
+
uint256[] memory upcs = new uint256[](1);
|
|
722
|
+
upcs[0] = testUpc;
|
|
723
|
+
bytes32[] memory hashes = new bytes32[](1);
|
|
724
|
+
hashes[0] = keccak256(abi.encodePacked("correct"));
|
|
725
|
+
|
|
726
|
+
vm.prank(multisig);
|
|
727
|
+
resolver.setSvgHashesOf(upcs, hashes);
|
|
728
|
+
|
|
729
|
+
string[] memory contents = new string[](1);
|
|
730
|
+
contents[0] = "wrong";
|
|
731
|
+
|
|
732
|
+
vm.expectRevert(Banny721TokenUriResolver.Banny721TokenUriResolver_ContentsMismatch.selector);
|
|
733
|
+
resolver.setSvgContentsOf(upcs, contents);
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
function test_fork_svg_doubleHashReverts() public {
|
|
737
|
+
uint256[] memory upcs = new uint256[](1);
|
|
738
|
+
upcs[0] = 102;
|
|
739
|
+
bytes32[] memory hashes = new bytes32[](1);
|
|
740
|
+
hashes[0] = keccak256("test");
|
|
741
|
+
|
|
742
|
+
vm.startPrank(multisig);
|
|
743
|
+
resolver.setSvgHashesOf(upcs, hashes);
|
|
744
|
+
|
|
745
|
+
vm.expectRevert(Banny721TokenUriResolver.Banny721TokenUriResolver_HashAlreadyStored.selector);
|
|
746
|
+
resolver.setSvgHashesOf(upcs, hashes);
|
|
747
|
+
vm.stopPrank();
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
function test_fork_svg_doubleUploadReverts() public {
|
|
751
|
+
string memory content = "svg-content";
|
|
752
|
+
uint256[] memory upcs = new uint256[](1);
|
|
753
|
+
upcs[0] = 103;
|
|
754
|
+
bytes32[] memory hashes = new bytes32[](1);
|
|
755
|
+
hashes[0] = keccak256(abi.encodePacked(content));
|
|
756
|
+
|
|
757
|
+
vm.prank(multisig);
|
|
758
|
+
resolver.setSvgHashesOf(upcs, hashes);
|
|
759
|
+
|
|
760
|
+
string[] memory contents = new string[](1);
|
|
761
|
+
contents[0] = content;
|
|
762
|
+
resolver.setSvgContentsOf(upcs, contents);
|
|
763
|
+
|
|
764
|
+
vm.expectRevert(Banny721TokenUriResolver.Banny721TokenUriResolver_ContentsAlreadyStored.selector);
|
|
765
|
+
resolver.setSvgContentsOf(upcs, contents);
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
function test_fork_svg_nonOwnerCantSetHash() public {
|
|
769
|
+
uint256[] memory upcs = new uint256[](1);
|
|
770
|
+
upcs[0] = 104;
|
|
771
|
+
bytes32[] memory hashes = new bytes32[](1);
|
|
772
|
+
hashes[0] = keccak256("test");
|
|
773
|
+
|
|
774
|
+
vm.prank(attacker);
|
|
775
|
+
vm.expectRevert();
|
|
776
|
+
resolver.setSvgHashesOf(upcs, hashes);
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
function test_fork_svg_uploadWithoutHashReverts() public {
|
|
780
|
+
uint256[] memory upcs = new uint256[](1);
|
|
781
|
+
upcs[0] = 999;
|
|
782
|
+
string[] memory contents = new string[](1);
|
|
783
|
+
contents[0] = "anything";
|
|
784
|
+
|
|
785
|
+
vm.expectRevert(Banny721TokenUriResolver.Banny721TokenUriResolver_HashNotFound.selector);
|
|
786
|
+
resolver.setSvgContentsOf(upcs, contents);
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
790
|
+
// 7. TOKEN URI RENDERING
|
|
791
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
792
|
+
|
|
793
|
+
function test_fork_render_nakedBodyHasDefaultInjections() public {
|
|
794
|
+
// A naked body should still render with default necklace, eyes, mouth.
|
|
795
|
+
string memory svg = resolver.svgOf(address(bannyHook), ORIGINAL_BODY_1, true, false);
|
|
796
|
+
assertGt(bytes(svg).length, 0, "naked body should render");
|
|
797
|
+
// The SVG should contain the body path and defaults.
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
function test_fork_render_allFourBodyTypes() public {
|
|
801
|
+
// Each body type should render.
|
|
802
|
+
string memory alienSvg = resolver.svgOf(address(bannyHook), ALIEN_BODY_1, true, false);
|
|
803
|
+
string memory pinkSvg = resolver.svgOf(address(bannyHook), PINK_BODY_1, true, false);
|
|
804
|
+
string memory originalSvg = resolver.svgOf(address(bannyHook), ORIGINAL_BODY_1, true, false);
|
|
805
|
+
|
|
806
|
+
assertGt(bytes(alienSvg).length, 0);
|
|
807
|
+
assertGt(bytes(pinkSvg).length, 0);
|
|
808
|
+
assertGt(bytes(originalSvg).length, 0);
|
|
809
|
+
|
|
810
|
+
// SVGs should differ (different body colors).
|
|
811
|
+
assertTrue(
|
|
812
|
+
keccak256(bytes(alienSvg)) != keccak256(bytes(originalSvg)), "alien and original should have different SVGs"
|
|
813
|
+
);
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
function test_fork_render_headSuppressesDefaultEyesMouth() public {
|
|
817
|
+
// Equip only a head — defaults for eyes and mouth should NOT be injected.
|
|
818
|
+
uint256[] memory outfits = new uint256[](1);
|
|
819
|
+
outfits[0] = HEAD_1; // cat 4
|
|
820
|
+
|
|
821
|
+
vm.prank(alice);
|
|
822
|
+
resolver.decorateBannyWith(address(bannyHook), ORIGINAL_BODY_1, 0, outfits);
|
|
823
|
+
|
|
824
|
+
// Should still render without error.
|
|
825
|
+
string memory svg = resolver.svgOf(address(bannyHook), ORIGINAL_BODY_1, true, false);
|
|
826
|
+
assertGt(bytes(svg).length, 0);
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
function test_fork_render_nonexistentTierReverts() public {
|
|
830
|
+
// A token ID that doesn't belong to any tier should revert with UnrecognizedProduct.
|
|
831
|
+
vm.expectRevert(Banny721TokenUriResolver.Banny721TokenUriResolver_UnrecognizedProduct.selector);
|
|
832
|
+
resolver.tokenUriOf(address(bannyHook), 999_000_000_001);
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
function test_fork_render_dressedBodyIncludesBackground() public {
|
|
836
|
+
// Equip background.
|
|
837
|
+
uint256[] memory empty = new uint256[](0);
|
|
838
|
+
vm.prank(alice);
|
|
839
|
+
resolver.decorateBannyWith(address(bannyHook), ORIGINAL_BODY_1, BACKGROUND_1, empty);
|
|
840
|
+
|
|
841
|
+
// Render with background.
|
|
842
|
+
string memory svgWithBg = resolver.svgOf(address(bannyHook), ORIGINAL_BODY_1, true, true);
|
|
843
|
+
string memory svgWithoutBg = resolver.svgOf(address(bannyHook), ORIGINAL_BODY_1, true, false);
|
|
844
|
+
|
|
845
|
+
assertGt(bytes(svgWithBg).length, 0);
|
|
846
|
+
assertGt(bytes(svgWithoutBg).length, 0);
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
850
|
+
// 8. REENTRANCY ATTACKS
|
|
851
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
852
|
+
|
|
853
|
+
function test_fork_reentrancy_reentrantHookBlocked() public {
|
|
854
|
+
// Deploy a malicious mock hook that re-enters during safeTransferFrom.
|
|
855
|
+
ReentrantMockStore reStore = new ReentrantMockStore();
|
|
856
|
+
ReentrantHook reHook = new ReentrantHook(address(reStore));
|
|
857
|
+
|
|
858
|
+
// Set up a body and outfit on the malicious hook.
|
|
859
|
+
uint256 bodyId = 4_000_000_001;
|
|
860
|
+
uint256 outfitId = 6_000_000_001;
|
|
861
|
+
|
|
862
|
+
reStore.setTier(
|
|
863
|
+
address(reHook),
|
|
864
|
+
bodyId,
|
|
865
|
+
JB721Tier({
|
|
866
|
+
id: 4,
|
|
867
|
+
price: 0,
|
|
868
|
+
remainingSupply: 100,
|
|
869
|
+
initialSupply: 100,
|
|
870
|
+
votingUnits: 0,
|
|
871
|
+
reserveFrequency: 0,
|
|
872
|
+
reserveBeneficiary: address(0),
|
|
873
|
+
encodedIPFSUri: bytes32(0),
|
|
874
|
+
category: 0, // body
|
|
875
|
+
discountPercent: 0,
|
|
876
|
+
allowOwnerMint: false,
|
|
877
|
+
transfersPausable: false,
|
|
878
|
+
cannotBeRemoved: false,
|
|
879
|
+
cannotIncreaseDiscountPercent: false,
|
|
880
|
+
splitPercent: 0,
|
|
881
|
+
resolvedUri: ""
|
|
882
|
+
})
|
|
883
|
+
);
|
|
884
|
+
reStore.setTier(
|
|
885
|
+
address(reHook),
|
|
886
|
+
outfitId,
|
|
887
|
+
JB721Tier({
|
|
888
|
+
id: 6,
|
|
889
|
+
price: 0,
|
|
890
|
+
remainingSupply: 100,
|
|
891
|
+
initialSupply: 100,
|
|
892
|
+
votingUnits: 0,
|
|
893
|
+
reserveFrequency: 0,
|
|
894
|
+
reserveBeneficiary: address(0),
|
|
895
|
+
encodedIPFSUri: bytes32(0),
|
|
896
|
+
category: 3, // necklace
|
|
897
|
+
discountPercent: 0,
|
|
898
|
+
allowOwnerMint: false,
|
|
899
|
+
transfersPausable: false,
|
|
900
|
+
cannotBeRemoved: false,
|
|
901
|
+
cannotIncreaseDiscountPercent: false,
|
|
902
|
+
splitPercent: 0,
|
|
903
|
+
resolvedUri: ""
|
|
904
|
+
})
|
|
905
|
+
);
|
|
906
|
+
|
|
907
|
+
reHook.setOwner(bodyId, alice);
|
|
908
|
+
reHook.setOwner(outfitId, alice);
|
|
909
|
+
|
|
910
|
+
vm.prank(alice);
|
|
911
|
+
reHook.setApprovalForAll(address(resolver), true);
|
|
912
|
+
|
|
913
|
+
// Arm the reentrancy attack: when outfit transfers, re-enter decorateBannyWith.
|
|
914
|
+
reHook.arm(resolver, address(reHook), bodyId);
|
|
915
|
+
|
|
916
|
+
// The reentrancy should be caught by ReentrancyGuard (silent try-catch in the hook).
|
|
917
|
+
uint256[] memory outfits = new uint256[](1);
|
|
918
|
+
outfits[0] = outfitId;
|
|
919
|
+
|
|
920
|
+
vm.prank(alice);
|
|
921
|
+
resolver.decorateBannyWith(address(reHook), bodyId, 0, outfits);
|
|
922
|
+
|
|
923
|
+
// The initial decoration should still succeed.
|
|
924
|
+
assertEq(resolver.wearerOf(address(reHook), outfitId), bodyId);
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
928
|
+
// 9. MULTI-ACTOR SCENARIOS
|
|
929
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
930
|
+
|
|
931
|
+
function test_fork_multiActor_bodyTransferOutfitsStay() public {
|
|
932
|
+
// Alice equips outfits on her body.
|
|
933
|
+
uint256[] memory outfits = new uint256[](1);
|
|
934
|
+
outfits[0] = NECKLACE_1;
|
|
935
|
+
vm.prank(alice);
|
|
936
|
+
resolver.decorateBannyWith(address(bannyHook), ORIGINAL_BODY_1, BACKGROUND_1, outfits);
|
|
937
|
+
|
|
938
|
+
// Alice transfers body to bob. Outfits stay.
|
|
939
|
+
vm.prank(alice);
|
|
940
|
+
IERC721(address(bannyHook)).safeTransferFrom(alice, bob, ORIGINAL_BODY_1);
|
|
941
|
+
|
|
942
|
+
// Outfits still attached.
|
|
943
|
+
assertEq(resolver.wearerOf(address(bannyHook), NECKLACE_1), ORIGINAL_BODY_1);
|
|
944
|
+
assertEq(resolver.userOf(address(bannyHook), BACKGROUND_1), ORIGINAL_BODY_1);
|
|
945
|
+
|
|
946
|
+
// Bob can redecorate (strip the necklace to himself).
|
|
947
|
+
vm.prank(bob);
|
|
948
|
+
IERC721(address(bannyHook)).setApprovalForAll(address(resolver), true);
|
|
949
|
+
|
|
950
|
+
uint256[] memory empty = new uint256[](0);
|
|
951
|
+
vm.prank(bob);
|
|
952
|
+
resolver.decorateBannyWith(address(bannyHook), ORIGINAL_BODY_1, 0, empty);
|
|
953
|
+
|
|
954
|
+
// Necklace returned to bob (new body owner), not alice.
|
|
955
|
+
assertEq(IERC721(address(bannyHook)).ownerOf(NECKLACE_1), bob);
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
function test_fork_multiActor_bobDecoratesWithOwnOutfit() public {
|
|
959
|
+
// Bob equips his own eyes on his own body.
|
|
960
|
+
uint256[] memory outfits = new uint256[](1);
|
|
961
|
+
outfits[0] = EYES_2; // Bob's eyes
|
|
962
|
+
|
|
963
|
+
vm.prank(bob);
|
|
964
|
+
resolver.decorateBannyWith(address(bannyHook), PINK_BODY_1, 0, outfits);
|
|
965
|
+
|
|
966
|
+
assertEq(resolver.wearerOf(address(bannyHook), EYES_2), PINK_BODY_1);
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
function test_fork_multiActor_twoUsersCompeteForSameCategory() public {
|
|
970
|
+
// Both alice and bob have bodies and eyes. They both equip eyes.
|
|
971
|
+
uint256[] memory aliceOutfits = new uint256[](1);
|
|
972
|
+
aliceOutfits[0] = EYES_1;
|
|
973
|
+
vm.prank(alice);
|
|
974
|
+
resolver.decorateBannyWith(address(bannyHook), ORIGINAL_BODY_1, 0, aliceOutfits);
|
|
975
|
+
|
|
976
|
+
uint256[] memory bobOutfits = new uint256[](1);
|
|
977
|
+
bobOutfits[0] = EYES_2;
|
|
978
|
+
vm.prank(bob);
|
|
979
|
+
resolver.decorateBannyWith(address(bannyHook), PINK_BODY_1, 0, bobOutfits);
|
|
980
|
+
|
|
981
|
+
assertEq(resolver.wearerOf(address(bannyHook), EYES_1), ORIGINAL_BODY_1);
|
|
982
|
+
assertEq(resolver.wearerOf(address(bannyHook), EYES_2), PINK_BODY_1);
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
function test_fork_multiActor_threePartyInteraction() public {
|
|
986
|
+
// Alice owns body, bob owns outfit worn by alice's body, charlie tries to interact.
|
|
987
|
+
// Step 1: Alice equips necklace.
|
|
988
|
+
uint256[] memory outfits = new uint256[](1);
|
|
989
|
+
outfits[0] = NECKLACE_1;
|
|
990
|
+
vm.prank(alice);
|
|
991
|
+
resolver.decorateBannyWith(address(bannyHook), ORIGINAL_BODY_1, 0, outfits);
|
|
992
|
+
|
|
993
|
+
// Necklace is now held by resolver, worn by ORIGINAL_BODY_1.
|
|
994
|
+
// Charlie has no body or outfit — any interaction should fail.
|
|
995
|
+
uint256[] memory empty = new uint256[](0);
|
|
996
|
+
vm.prank(charlie);
|
|
997
|
+
vm.expectRevert(Banny721TokenUriResolver.Banny721TokenUriResolver_UnauthorizedBannyBody.selector);
|
|
998
|
+
resolver.decorateBannyWith(address(bannyHook), ORIGINAL_BODY_1, 0, empty);
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
1002
|
+
// 10. EDGE CASES
|
|
1003
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
1004
|
+
|
|
1005
|
+
function test_fork_edge_onERC721ReceivedRejectsDirectTransfer() public {
|
|
1006
|
+
// Direct transfer to resolver should revert (only self-transfers allowed).
|
|
1007
|
+
vm.prank(alice);
|
|
1008
|
+
vm.expectRevert(Banny721TokenUriResolver.Banny721TokenUriResolver_UnauthorizedTransfer.selector);
|
|
1009
|
+
IERC721(address(bannyHook)).safeTransferFrom(alice, address(resolver), NECKLACE_2);
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
function test_fork_edge_decorateEmptyOutfitsAndZeroBackground() public {
|
|
1013
|
+
// Empty decoration should succeed (no-op).
|
|
1014
|
+
uint256[] memory empty = new uint256[](0);
|
|
1015
|
+
vm.prank(alice);
|
|
1016
|
+
resolver.decorateBannyWith(address(bannyHook), ORIGINAL_BODY_1, 0, empty);
|
|
1017
|
+
|
|
1018
|
+
(uint256 bgId, uint256[] memory outfitIds) = resolver.assetIdsOf(address(bannyHook), ORIGINAL_BODY_1);
|
|
1019
|
+
assertEq(bgId, 0);
|
|
1020
|
+
assertEq(outfitIds.length, 0);
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
function test_fork_edge_assetIdsEmptyInitially() public {
|
|
1024
|
+
(uint256 bgId, uint256[] memory outfitIds) = resolver.assetIdsOf(address(bannyHook), ORIGINAL_BODY_1);
|
|
1025
|
+
assertEq(bgId, 0);
|
|
1026
|
+
assertEq(outfitIds.length, 0);
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
function test_fork_edge_wearerOfUnwornReturnsZero() public {
|
|
1030
|
+
assertEq(resolver.wearerOf(address(bannyHook), NECKLACE_1), 0);
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
function test_fork_edge_userOfUnusedBackgroundReturnsZero() public {
|
|
1034
|
+
assertEq(resolver.userOf(address(bannyHook), BACKGROUND_1), 0);
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
function test_fork_edge_setMetadata() public {
|
|
1038
|
+
vm.prank(multisig);
|
|
1039
|
+
resolver.setMetadata("Test desc", "https://test.url", "https://test.base/");
|
|
1040
|
+
|
|
1041
|
+
assertEq(resolver.svgDescription(), "Test desc");
|
|
1042
|
+
assertEq(resolver.svgExternalUrl(), "https://test.url");
|
|
1043
|
+
assertEq(resolver.svgBaseUri(), "https://test.base/");
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
function test_fork_edge_setMetadataNonOwnerReverts() public {
|
|
1047
|
+
vm.prank(attacker);
|
|
1048
|
+
vm.expectRevert();
|
|
1049
|
+
resolver.setMetadata("evil", "https://evil", "https://evil");
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
function test_fork_edge_setProductNames() public {
|
|
1053
|
+
uint256[] memory upcs = new uint256[](1);
|
|
1054
|
+
upcs[0] = 200;
|
|
1055
|
+
string[] memory names = new string[](1);
|
|
1056
|
+
names[0] = "Cool Hat";
|
|
1057
|
+
|
|
1058
|
+
vm.prank(multisig);
|
|
1059
|
+
resolver.setProductNames(upcs, names);
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
function test_fork_edge_arrayLengthMismatchReverts() public {
|
|
1063
|
+
uint256[] memory upcs = new uint256[](2);
|
|
1064
|
+
upcs[0] = 1;
|
|
1065
|
+
upcs[1] = 2;
|
|
1066
|
+
bytes32[] memory hashes = new bytes32[](1);
|
|
1067
|
+
hashes[0] = keccak256("test");
|
|
1068
|
+
|
|
1069
|
+
vm.prank(multisig);
|
|
1070
|
+
vm.expectRevert(Banny721TokenUriResolver.Banny721TokenUriResolver_ArrayLengthMismatch.selector);
|
|
1071
|
+
resolver.setSvgHashesOf(upcs, hashes);
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
function test_fork_edge_clearMetadataWithEmptyStrings() public {
|
|
1075
|
+
vm.startPrank(multisig);
|
|
1076
|
+
resolver.setMetadata("desc", "https://url", "https://base/");
|
|
1077
|
+
resolver.setMetadata("", "", "");
|
|
1078
|
+
vm.stopPrank();
|
|
1079
|
+
|
|
1080
|
+
assertEq(resolver.svgDescription(), "");
|
|
1081
|
+
assertEq(resolver.svgExternalUrl(), "");
|
|
1082
|
+
assertEq(resolver.svgBaseUri(), "");
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
function test_fork_edge_crossHookIsolation() public {
|
|
1086
|
+
// Deploy a second hook for a different project. Outfits should not cross hooks.
|
|
1087
|
+
uint256 projectId2 = jbProjects.count() + 1;
|
|
1088
|
+
|
|
1089
|
+
JBDeploy721TiersHookConfig memory hookConfig2 = _buildHookConfig();
|
|
1090
|
+
|
|
1091
|
+
vm.prank(multisig);
|
|
1092
|
+
IJB721TiersHook hook2 = hookDeployer.deployHookFor(projectId2, hookConfig2, bytes32(uint256(2)));
|
|
1093
|
+
|
|
1094
|
+
// Mint a body on hook2 for alice.
|
|
1095
|
+
uint16[] memory tierIds = new uint16[](1);
|
|
1096
|
+
tierIds[0] = TIER_ORIGINAL_BODY;
|
|
1097
|
+
vm.prank(multisig);
|
|
1098
|
+
hook2.mintFor(tierIds, alice);
|
|
1099
|
+
uint256 hook2Body = 4_000_000_001;
|
|
1100
|
+
|
|
1101
|
+
// Alice has resolver approval for hook2.
|
|
1102
|
+
vm.prank(alice);
|
|
1103
|
+
IERC721(address(hook2)).setApprovalForAll(address(resolver), true);
|
|
1104
|
+
|
|
1105
|
+
// Try to decorate hook2 body with hook1 necklace — different hooks, should revert.
|
|
1106
|
+
uint256[] memory outfits = new uint256[](1);
|
|
1107
|
+
outfits[0] = NECKLACE_1; // belongs to hook1
|
|
1108
|
+
|
|
1109
|
+
// This should fail because NECKLACE_1 doesn't exist on hook2
|
|
1110
|
+
// (its tier data won't match, and ownership is on hook1 not hook2).
|
|
1111
|
+
vm.prank(alice);
|
|
1112
|
+
vm.expectRevert();
|
|
1113
|
+
resolver.decorateBannyWith(address(hook2), hook2Body, 0, outfits);
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
function test_fork_edge_approvalRevocationPreventsDecoration() public {
|
|
1117
|
+
// Alice approves resolver, then revokes before decorating.
|
|
1118
|
+
vm.prank(alice);
|
|
1119
|
+
IERC721(address(bannyHook)).setApprovalForAll(address(resolver), false);
|
|
1120
|
+
|
|
1121
|
+
uint256[] memory outfits = new uint256[](1);
|
|
1122
|
+
outfits[0] = NECKLACE_1;
|
|
1123
|
+
|
|
1124
|
+
vm.prank(alice);
|
|
1125
|
+
vm.expectRevert(); // transfer fails due to no approval
|
|
1126
|
+
resolver.decorateBannyWith(address(bannyHook), ORIGINAL_BODY_1, 0, outfits);
|
|
1127
|
+
|
|
1128
|
+
// Re-approve for other tests.
|
|
1129
|
+
vm.prank(alice);
|
|
1130
|
+
IERC721(address(bannyHook)).setApprovalForAll(address(resolver), true);
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
function test_fork_edge_duplicateCategoryInSingleCallReverts() public {
|
|
1134
|
+
// Mint a second necklace (same category 3) to alice.
|
|
1135
|
+
_mintTo(alice, TIER_NECKLACE);
|
|
1136
|
+
uint256 necklace3 = 6_000_000_003; // Third necklace minted overall
|
|
1137
|
+
|
|
1138
|
+
// Try to equip two necklaces (same category).
|
|
1139
|
+
uint256[] memory outfits = new uint256[](2);
|
|
1140
|
+
outfits[0] = NECKLACE_1; // cat 3
|
|
1141
|
+
outfits[1] = necklace3; // cat 3
|
|
1142
|
+
|
|
1143
|
+
vm.prank(alice);
|
|
1144
|
+
vm.expectRevert(Banny721TokenUriResolver.Banny721TokenUriResolver_UnorderedCategories.selector);
|
|
1145
|
+
resolver.decorateBannyWith(address(bannyHook), ORIGINAL_BODY_1, 0, outfits);
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
function test_fork_edge_namesReturnsCorrectData() public {
|
|
1149
|
+
// Verify namesOf returns correct product name for each body type.
|
|
1150
|
+
(string memory alienFull,,) = resolver.namesOf(address(bannyHook), ALIEN_BODY_1);
|
|
1151
|
+
assertGt(bytes(alienFull).length, 0, "alien name should not be empty");
|
|
1152
|
+
|
|
1153
|
+
(string memory originalFull,,) = resolver.namesOf(address(bannyHook), ORIGINAL_BODY_1);
|
|
1154
|
+
assertGt(bytes(originalFull).length, 0, "original name should not be empty");
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
1158
|
+
// 11. GRIEFING & FRONT-RUNNING VECTORS
|
|
1159
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
1160
|
+
|
|
1161
|
+
function test_fork_grief_lockDoesNotPreventTokenTransfer() public {
|
|
1162
|
+
// Lock should prevent redecoration but NOT prevent body NFT transfer.
|
|
1163
|
+
vm.prank(alice);
|
|
1164
|
+
resolver.lockOutfitChangesFor(address(bannyHook), ORIGINAL_BODY_1);
|
|
1165
|
+
|
|
1166
|
+
// Alice can still transfer the body.
|
|
1167
|
+
vm.prank(alice);
|
|
1168
|
+
IERC721(address(bannyHook)).safeTransferFrom(alice, bob, ORIGINAL_BODY_1);
|
|
1169
|
+
assertEq(IERC721(address(bannyHook)).ownerOf(ORIGINAL_BODY_1), bob);
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
function test_fork_grief_frontRunStripBeforeSale() public {
|
|
1173
|
+
// Scenario: Alice equips valuable outfits, then sells body to bob.
|
|
1174
|
+
// Alice strips outfits first (simulating front-run).
|
|
1175
|
+
uint256[] memory outfits = new uint256[](1);
|
|
1176
|
+
outfits[0] = NECKLACE_1;
|
|
1177
|
+
vm.prank(alice);
|
|
1178
|
+
resolver.decorateBannyWith(address(bannyHook), ORIGINAL_BODY_1, 0, outfits);
|
|
1179
|
+
|
|
1180
|
+
// Alice strips before transfer.
|
|
1181
|
+
uint256[] memory empty = new uint256[](0);
|
|
1182
|
+
vm.prank(alice);
|
|
1183
|
+
resolver.decorateBannyWith(address(bannyHook), ORIGINAL_BODY_1, 0, empty);
|
|
1184
|
+
|
|
1185
|
+
// Transfer to bob — body is naked.
|
|
1186
|
+
vm.prank(alice);
|
|
1187
|
+
IERC721(address(bannyHook)).safeTransferFrom(alice, bob, ORIGINAL_BODY_1);
|
|
1188
|
+
|
|
1189
|
+
// Necklace is back with alice, not bob.
|
|
1190
|
+
assertEq(IERC721(address(bannyHook)).ownerOf(NECKLACE_1), alice);
|
|
1191
|
+
assertEq(IERC721(address(bannyHook)).ownerOf(ORIGINAL_BODY_1), bob);
|
|
1192
|
+
|
|
1193
|
+
// This is expected behavior — sellers should be warned to unequip.
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
function test_fork_grief_lockPreventsFrontRunStrip() public {
|
|
1197
|
+
// Counter: Lock outfits so they can't be stripped before sale.
|
|
1198
|
+
uint256[] memory outfits = new uint256[](1);
|
|
1199
|
+
outfits[0] = NECKLACE_1;
|
|
1200
|
+
vm.prank(alice);
|
|
1201
|
+
resolver.decorateBannyWith(address(bannyHook), ORIGINAL_BODY_1, 0, outfits);
|
|
1202
|
+
|
|
1203
|
+
// Lock the body.
|
|
1204
|
+
vm.prank(alice);
|
|
1205
|
+
resolver.lockOutfitChangesFor(address(bannyHook), ORIGINAL_BODY_1);
|
|
1206
|
+
|
|
1207
|
+
// Alice can't strip during lock period.
|
|
1208
|
+
uint256[] memory empty = new uint256[](0);
|
|
1209
|
+
vm.prank(alice);
|
|
1210
|
+
vm.expectRevert(Banny721TokenUriResolver.Banny721TokenUriResolver_OutfitChangesLocked.selector);
|
|
1211
|
+
resolver.decorateBannyWith(address(bannyHook), ORIGINAL_BODY_1, 0, empty);
|
|
1212
|
+
|
|
1213
|
+
// Transfer to bob — body still has outfit.
|
|
1214
|
+
vm.prank(alice);
|
|
1215
|
+
IERC721(address(bannyHook)).safeTransferFrom(alice, bob, ORIGINAL_BODY_1);
|
|
1216
|
+
|
|
1217
|
+
assertEq(resolver.wearerOf(address(bannyHook), NECKLACE_1), ORIGINAL_BODY_1);
|
|
1218
|
+
assertEq(IERC721(address(bannyHook)).ownerOf(ORIGINAL_BODY_1), bob);
|
|
1219
|
+
|
|
1220
|
+
// Bob can unequip after lock expires.
|
|
1221
|
+
vm.warp(block.timestamp + 7 days + 1);
|
|
1222
|
+
vm.prank(bob);
|
|
1223
|
+
IERC721(address(bannyHook)).setApprovalForAll(address(resolver), true);
|
|
1224
|
+
vm.prank(bob);
|
|
1225
|
+
resolver.decorateBannyWith(address(bannyHook), ORIGINAL_BODY_1, 0, empty);
|
|
1226
|
+
|
|
1227
|
+
assertEq(IERC721(address(bannyHook)).ownerOf(NECKLACE_1), bob);
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
function test_fork_grief_doubleEquipSameOutfitReverts() public {
|
|
1231
|
+
// Try to pass the same outfit ID twice.
|
|
1232
|
+
uint256[] memory outfits = new uint256[](2);
|
|
1233
|
+
outfits[0] = NECKLACE_1;
|
|
1234
|
+
outfits[1] = NECKLACE_1; // duplicate
|
|
1235
|
+
|
|
1236
|
+
vm.prank(alice);
|
|
1237
|
+
vm.expectRevert(); // UnorderedCategories (same category = not ascending)
|
|
1238
|
+
resolver.decorateBannyWith(address(bannyHook), ORIGINAL_BODY_1, 0, outfits);
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
1242
|
+
// 12. REDRESSING CYCLES — exhaustive outfit rotation, re-equip, partial swap
|
|
1243
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
1244
|
+
|
|
1245
|
+
/// @dev Helper: assert ownership and wearer state for an outfit on a body held by resolver.
|
|
1246
|
+
function _assertWorn(uint256 outfitId, uint256 bodyId) internal view {
|
|
1247
|
+
assertEq(resolver.wearerOf(address(bannyHook), outfitId), bodyId, "wrong wearer");
|
|
1248
|
+
assertEq(IERC721(address(bannyHook)).ownerOf(outfitId), address(resolver), "outfit not held by resolver");
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
/// @dev Helper: assert outfit returned to owner.
|
|
1252
|
+
function _assertReturned(uint256 outfitId, address owner) internal view {
|
|
1253
|
+
assertEq(IERC721(address(bannyHook)).ownerOf(outfitId), owner, "outfit not returned");
|
|
1254
|
+
assertEq(resolver.wearerOf(address(bannyHook), outfitId), 0, "wearer should be cleared");
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
/// @notice Full wardrobe cycle: dress → strip → re-equip same → swap some → strip → dress different.
|
|
1258
|
+
function test_fork_redress_fullWardrobeCycle() public {
|
|
1259
|
+
// --- Round 1: Equip necklace + eyes + mouth ---
|
|
1260
|
+
uint256[] memory r1 = new uint256[](3);
|
|
1261
|
+
r1[0] = NECKLACE_1; // cat 3
|
|
1262
|
+
r1[1] = EYES_1; // cat 5
|
|
1263
|
+
r1[2] = MOUTH_1; // cat 7
|
|
1264
|
+
vm.prank(alice);
|
|
1265
|
+
resolver.decorateBannyWith(address(bannyHook), ORIGINAL_BODY_1, BACKGROUND_1, r1);
|
|
1266
|
+
_assertWorn(NECKLACE_1, ORIGINAL_BODY_1);
|
|
1267
|
+
_assertWorn(EYES_1, ORIGINAL_BODY_1);
|
|
1268
|
+
_assertWorn(MOUTH_1, ORIGINAL_BODY_1);
|
|
1269
|
+
|
|
1270
|
+
// --- Round 2: Strip everything ---
|
|
1271
|
+
uint256[] memory empty = new uint256[](0);
|
|
1272
|
+
vm.prank(alice);
|
|
1273
|
+
resolver.decorateBannyWith(address(bannyHook), ORIGINAL_BODY_1, 0, empty);
|
|
1274
|
+
_assertReturned(NECKLACE_1, alice);
|
|
1275
|
+
_assertReturned(EYES_1, alice);
|
|
1276
|
+
_assertReturned(MOUTH_1, alice);
|
|
1277
|
+
_assertReturned(BACKGROUND_1, alice);
|
|
1278
|
+
|
|
1279
|
+
// --- Round 3: Re-equip the EXACT SAME outfits ---
|
|
1280
|
+
vm.prank(alice);
|
|
1281
|
+
resolver.decorateBannyWith(address(bannyHook), ORIGINAL_BODY_1, BACKGROUND_1, r1);
|
|
1282
|
+
_assertWorn(NECKLACE_1, ORIGINAL_BODY_1);
|
|
1283
|
+
_assertWorn(EYES_1, ORIGINAL_BODY_1);
|
|
1284
|
+
_assertWorn(MOUTH_1, ORIGINAL_BODY_1);
|
|
1285
|
+
|
|
1286
|
+
// --- Round 4: Swap to entirely different categories (glasses + legs + headtop) ---
|
|
1287
|
+
uint256[] memory r4 = new uint256[](3);
|
|
1288
|
+
r4[0] = GLASSES_1; // cat 6
|
|
1289
|
+
r4[1] = LEGS_1; // cat 8
|
|
1290
|
+
r4[2] = HEADTOP_1; // cat 12
|
|
1291
|
+
vm.prank(alice);
|
|
1292
|
+
resolver.decorateBannyWith(address(bannyHook), ORIGINAL_BODY_1, BACKGROUND_2, r4);
|
|
1293
|
+
// Old outfits returned.
|
|
1294
|
+
_assertReturned(NECKLACE_1, alice);
|
|
1295
|
+
_assertReturned(EYES_1, alice);
|
|
1296
|
+
_assertReturned(MOUTH_1, alice);
|
|
1297
|
+
_assertReturned(BACKGROUND_1, alice);
|
|
1298
|
+
// New outfits worn.
|
|
1299
|
+
_assertWorn(GLASSES_1, ORIGINAL_BODY_1);
|
|
1300
|
+
_assertWorn(LEGS_1, ORIGINAL_BODY_1);
|
|
1301
|
+
_assertWorn(HEADTOP_1, ORIGINAL_BODY_1);
|
|
1302
|
+
|
|
1303
|
+
// --- Round 5: Strip all again ---
|
|
1304
|
+
vm.prank(alice);
|
|
1305
|
+
resolver.decorateBannyWith(address(bannyHook), ORIGINAL_BODY_1, 0, empty);
|
|
1306
|
+
_assertReturned(GLASSES_1, alice);
|
|
1307
|
+
_assertReturned(LEGS_1, alice);
|
|
1308
|
+
_assertReturned(HEADTOP_1, alice);
|
|
1309
|
+
_assertReturned(BACKGROUND_2, alice);
|
|
1310
|
+
|
|
1311
|
+
// --- Round 6: Dress with original set one more time ---
|
|
1312
|
+
vm.prank(alice);
|
|
1313
|
+
resolver.decorateBannyWith(address(bannyHook), ORIGINAL_BODY_1, BACKGROUND_1, r1);
|
|
1314
|
+
_assertWorn(NECKLACE_1, ORIGINAL_BODY_1);
|
|
1315
|
+
_assertWorn(EYES_1, ORIGINAL_BODY_1);
|
|
1316
|
+
_assertWorn(MOUTH_1, ORIGINAL_BODY_1);
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
/// @notice Partial redress: keep some outfits, swap others in a single call.
|
|
1320
|
+
function test_fork_redress_partialSwap() public {
|
|
1321
|
+
// Round 1: necklace + eyes + legs (cats 3, 5, 8).
|
|
1322
|
+
uint256[] memory r1 = new uint256[](3);
|
|
1323
|
+
r1[0] = NECKLACE_1;
|
|
1324
|
+
r1[1] = EYES_1;
|
|
1325
|
+
r1[2] = LEGS_1;
|
|
1326
|
+
vm.prank(alice);
|
|
1327
|
+
resolver.decorateBannyWith(address(bannyHook), ORIGINAL_BODY_1, 0, r1);
|
|
1328
|
+
|
|
1329
|
+
// Round 2: necklace + mouth + legs (cats 3, 7, 8) — eyes out, mouth in, necklace+legs stay.
|
|
1330
|
+
uint256[] memory r2 = new uint256[](3);
|
|
1331
|
+
r2[0] = NECKLACE_1; // SAME — should be re-equipped (no-op transfer)
|
|
1332
|
+
r2[1] = MOUTH_1; // NEW — cat 7 replaces nothing (eyes at cat 5 gets swept)
|
|
1333
|
+
r2[2] = LEGS_1; // SAME
|
|
1334
|
+
vm.prank(alice);
|
|
1335
|
+
resolver.decorateBannyWith(address(bannyHook), ORIGINAL_BODY_1, 0, r2);
|
|
1336
|
+
|
|
1337
|
+
// Necklace stays worn (same body).
|
|
1338
|
+
_assertWorn(NECKLACE_1, ORIGINAL_BODY_1);
|
|
1339
|
+
// Eyes returned (category 5 was swept by category 7 advance).
|
|
1340
|
+
_assertReturned(EYES_1, alice);
|
|
1341
|
+
// Mouth now worn.
|
|
1342
|
+
_assertWorn(MOUTH_1, ORIGINAL_BODY_1);
|
|
1343
|
+
// Legs stays worn.
|
|
1344
|
+
_assertWorn(LEGS_1, ORIGINAL_BODY_1);
|
|
1345
|
+
|
|
1346
|
+
(, uint256[] memory attached) = resolver.assetIdsOf(address(bannyHook), ORIGINAL_BODY_1);
|
|
1347
|
+
assertEq(attached.length, 3, "should have 3 outfits");
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1350
|
+
/// @notice Expand then shrink outfit set across redressings.
|
|
1351
|
+
function test_fork_redress_expandAndShrink() public {
|
|
1352
|
+
// Round 1: 1 outfit.
|
|
1353
|
+
uint256[] memory r1 = new uint256[](1);
|
|
1354
|
+
r1[0] = NECKLACE_1;
|
|
1355
|
+
vm.prank(alice);
|
|
1356
|
+
resolver.decorateBannyWith(address(bannyHook), ORIGINAL_BODY_1, 0, r1);
|
|
1357
|
+
(, uint256[] memory a1) = resolver.assetIdsOf(address(bannyHook), ORIGINAL_BODY_1);
|
|
1358
|
+
assertEq(a1.length, 1);
|
|
1359
|
+
|
|
1360
|
+
// Round 2: expand to 4 outfits.
|
|
1361
|
+
uint256[] memory r2 = new uint256[](4);
|
|
1362
|
+
r2[0] = NECKLACE_1; // cat 3
|
|
1363
|
+
r2[1] = EYES_1; // cat 5
|
|
1364
|
+
r2[2] = MOUTH_1; // cat 7
|
|
1365
|
+
r2[3] = LEGS_1; // cat 8
|
|
1366
|
+
vm.prank(alice);
|
|
1367
|
+
resolver.decorateBannyWith(address(bannyHook), ORIGINAL_BODY_1, BACKGROUND_1, r2);
|
|
1368
|
+
(, uint256[] memory a2) = resolver.assetIdsOf(address(bannyHook), ORIGINAL_BODY_1);
|
|
1369
|
+
assertEq(a2.length, 4);
|
|
1370
|
+
|
|
1371
|
+
// Round 3: shrink to 2 outfits in different categories (glasses + hand).
|
|
1372
|
+
uint256[] memory r3 = new uint256[](2);
|
|
1373
|
+
r3[0] = GLASSES_1; // cat 6
|
|
1374
|
+
r3[1] = HAND_1; // cat 13
|
|
1375
|
+
vm.prank(alice);
|
|
1376
|
+
resolver.decorateBannyWith(address(bannyHook), ORIGINAL_BODY_1, 0, r3);
|
|
1377
|
+
(, uint256[] memory a3) = resolver.assetIdsOf(address(bannyHook), ORIGINAL_BODY_1);
|
|
1378
|
+
assertEq(a3.length, 2);
|
|
1379
|
+
|
|
1380
|
+
// All previous 4 outfits returned.
|
|
1381
|
+
_assertReturned(NECKLACE_1, alice);
|
|
1382
|
+
_assertReturned(EYES_1, alice);
|
|
1383
|
+
_assertReturned(MOUTH_1, alice);
|
|
1384
|
+
_assertReturned(LEGS_1, alice);
|
|
1385
|
+
_assertReturned(BACKGROUND_1, alice);
|
|
1386
|
+
|
|
1387
|
+
// Round 4: shrink to 0.
|
|
1388
|
+
uint256[] memory empty = new uint256[](0);
|
|
1389
|
+
vm.prank(alice);
|
|
1390
|
+
resolver.decorateBannyWith(address(bannyHook), ORIGINAL_BODY_1, 0, empty);
|
|
1391
|
+
(, uint256[] memory a4) = resolver.assetIdsOf(address(bannyHook), ORIGINAL_BODY_1);
|
|
1392
|
+
assertEq(a4.length, 0);
|
|
1393
|
+
_assertReturned(GLASSES_1, alice);
|
|
1394
|
+
_assertReturned(HAND_1, alice);
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
/// @notice Same outfit worn by body A, stripped, worn by body B, stripped, re-worn by body A.
|
|
1398
|
+
function test_fork_redress_outfitPingPongBetweenBodies() public {
|
|
1399
|
+
uint256[] memory outfits = new uint256[](1);
|
|
1400
|
+
outfits[0] = NECKLACE_1;
|
|
1401
|
+
uint256[] memory empty = new uint256[](0);
|
|
1402
|
+
|
|
1403
|
+
// Round 1: Body 1 wears necklace.
|
|
1404
|
+
vm.prank(alice);
|
|
1405
|
+
resolver.decorateBannyWith(address(bannyHook), ORIGINAL_BODY_1, 0, outfits);
|
|
1406
|
+
_assertWorn(NECKLACE_1, ORIGINAL_BODY_1);
|
|
1407
|
+
|
|
1408
|
+
// Round 2: Strip from body 1.
|
|
1409
|
+
vm.prank(alice);
|
|
1410
|
+
resolver.decorateBannyWith(address(bannyHook), ORIGINAL_BODY_1, 0, empty);
|
|
1411
|
+
_assertReturned(NECKLACE_1, alice);
|
|
1412
|
+
|
|
1413
|
+
// Round 3: Body 2 wears necklace.
|
|
1414
|
+
vm.prank(alice);
|
|
1415
|
+
resolver.decorateBannyWith(address(bannyHook), ORIGINAL_BODY_2, 0, outfits);
|
|
1416
|
+
_assertWorn(NECKLACE_1, ORIGINAL_BODY_2);
|
|
1417
|
+
|
|
1418
|
+
// Round 4: Strip from body 2.
|
|
1419
|
+
vm.prank(alice);
|
|
1420
|
+
resolver.decorateBannyWith(address(bannyHook), ORIGINAL_BODY_2, 0, empty);
|
|
1421
|
+
_assertReturned(NECKLACE_1, alice);
|
|
1422
|
+
|
|
1423
|
+
// Round 5: Back to body 1 again.
|
|
1424
|
+
vm.prank(alice);
|
|
1425
|
+
resolver.decorateBannyWith(address(bannyHook), ORIGINAL_BODY_1, 0, outfits);
|
|
1426
|
+
_assertWorn(NECKLACE_1, ORIGINAL_BODY_1);
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
/// @notice Re-equip exact same outfit set that's already worn — should be a no-op.
|
|
1430
|
+
function test_fork_redress_reequipSameSetIsNoop() public {
|
|
1431
|
+
uint256[] memory outfits = new uint256[](3);
|
|
1432
|
+
outfits[0] = NECKLACE_1;
|
|
1433
|
+
outfits[1] = EYES_1;
|
|
1434
|
+
outfits[2] = MOUTH_1;
|
|
1435
|
+
|
|
1436
|
+
// First dress.
|
|
1437
|
+
vm.prank(alice);
|
|
1438
|
+
resolver.decorateBannyWith(address(bannyHook), ORIGINAL_BODY_1, BACKGROUND_1, outfits);
|
|
1439
|
+
|
|
1440
|
+
// Re-dress with exact same set.
|
|
1441
|
+
vm.prank(alice);
|
|
1442
|
+
resolver.decorateBannyWith(address(bannyHook), ORIGINAL_BODY_1, BACKGROUND_1, outfits);
|
|
1443
|
+
|
|
1444
|
+
// Everything still worn.
|
|
1445
|
+
_assertWorn(NECKLACE_1, ORIGINAL_BODY_1);
|
|
1446
|
+
_assertWorn(EYES_1, ORIGINAL_BODY_1);
|
|
1447
|
+
_assertWorn(MOUTH_1, ORIGINAL_BODY_1);
|
|
1448
|
+
assertEq(resolver.userOf(address(bannyHook), BACKGROUND_1), ORIGINAL_BODY_1);
|
|
1449
|
+
|
|
1450
|
+
(, uint256[] memory attached) = resolver.assetIdsOf(address(bannyHook), ORIGINAL_BODY_1);
|
|
1451
|
+
assertEq(attached.length, 3);
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
/// @notice Swap outfits within same category across rounds — tests replacement of same-category items.
|
|
1455
|
+
function test_fork_redress_sameCategoryRotation() public {
|
|
1456
|
+
// Round 1: NECKLACE_1.
|
|
1457
|
+
uint256[] memory r1 = new uint256[](1);
|
|
1458
|
+
r1[0] = NECKLACE_1;
|
|
1459
|
+
vm.prank(alice);
|
|
1460
|
+
resolver.decorateBannyWith(address(bannyHook), ORIGINAL_BODY_1, 0, r1);
|
|
1461
|
+
_assertWorn(NECKLACE_1, ORIGINAL_BODY_1);
|
|
1462
|
+
|
|
1463
|
+
// Round 2: NECKLACE_2 replaces NECKLACE_1 (same category 3).
|
|
1464
|
+
uint256[] memory r2 = new uint256[](1);
|
|
1465
|
+
r2[0] = NECKLACE_2;
|
|
1466
|
+
vm.prank(alice);
|
|
1467
|
+
resolver.decorateBannyWith(address(bannyHook), ORIGINAL_BODY_1, 0, r2);
|
|
1468
|
+
_assertWorn(NECKLACE_2, ORIGINAL_BODY_1);
|
|
1469
|
+
_assertReturned(NECKLACE_1, alice);
|
|
1470
|
+
|
|
1471
|
+
// Round 3: NECKLACE_3 replaces NECKLACE_2.
|
|
1472
|
+
uint256[] memory r3 = new uint256[](1);
|
|
1473
|
+
r3[0] = NECKLACE_3;
|
|
1474
|
+
vm.prank(alice);
|
|
1475
|
+
resolver.decorateBannyWith(address(bannyHook), ORIGINAL_BODY_1, 0, r3);
|
|
1476
|
+
_assertWorn(NECKLACE_3, ORIGINAL_BODY_1);
|
|
1477
|
+
_assertReturned(NECKLACE_2, alice);
|
|
1478
|
+
|
|
1479
|
+
// Round 4: Back to NECKLACE_1 — previously worn item re-equipped.
|
|
1480
|
+
uint256[] memory r4 = new uint256[](1);
|
|
1481
|
+
r4[0] = NECKLACE_1;
|
|
1482
|
+
vm.prank(alice);
|
|
1483
|
+
resolver.decorateBannyWith(address(bannyHook), ORIGINAL_BODY_1, 0, r4);
|
|
1484
|
+
_assertWorn(NECKLACE_1, ORIGINAL_BODY_1);
|
|
1485
|
+
_assertReturned(NECKLACE_3, alice);
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
/// @notice Category ordering varies across rounds — each round uses different category subsets.
|
|
1489
|
+
function test_fork_redress_varyingCategorySubsets() public {
|
|
1490
|
+
// Round 1: Low categories (necklace=3, eyes=5).
|
|
1491
|
+
uint256[] memory r1 = new uint256[](2);
|
|
1492
|
+
r1[0] = NECKLACE_1;
|
|
1493
|
+
r1[1] = EYES_1;
|
|
1494
|
+
vm.prank(alice);
|
|
1495
|
+
resolver.decorateBannyWith(address(bannyHook), ORIGINAL_BODY_1, 0, r1);
|
|
1496
|
+
|
|
1497
|
+
// Round 2: High categories only (headtop=12, hand=13) — all low returned.
|
|
1498
|
+
uint256[] memory r2 = new uint256[](2);
|
|
1499
|
+
r2[0] = HEADTOP_1;
|
|
1500
|
+
r2[1] = HAND_1;
|
|
1501
|
+
vm.prank(alice);
|
|
1502
|
+
resolver.decorateBannyWith(address(bannyHook), ORIGINAL_BODY_1, 0, r2);
|
|
1503
|
+
_assertReturned(NECKLACE_1, alice);
|
|
1504
|
+
_assertReturned(EYES_1, alice);
|
|
1505
|
+
_assertWorn(HEADTOP_1, ORIGINAL_BODY_1);
|
|
1506
|
+
_assertWorn(HAND_1, ORIGINAL_BODY_1);
|
|
1507
|
+
|
|
1508
|
+
// Round 3: Mid categories (glasses=6, mouth=7, legs=8) — all high returned.
|
|
1509
|
+
uint256[] memory r3 = new uint256[](3);
|
|
1510
|
+
r3[0] = GLASSES_1;
|
|
1511
|
+
r3[1] = MOUTH_1;
|
|
1512
|
+
r3[2] = LEGS_1;
|
|
1513
|
+
vm.prank(alice);
|
|
1514
|
+
resolver.decorateBannyWith(address(bannyHook), ORIGINAL_BODY_1, 0, r3);
|
|
1515
|
+
_assertReturned(HEADTOP_1, alice);
|
|
1516
|
+
_assertReturned(HAND_1, alice);
|
|
1517
|
+
_assertWorn(GLASSES_1, ORIGINAL_BODY_1);
|
|
1518
|
+
_assertWorn(MOUTH_1, ORIGINAL_BODY_1);
|
|
1519
|
+
_assertWorn(LEGS_1, ORIGINAL_BODY_1);
|
|
1520
|
+
|
|
1521
|
+
// Round 4: Spread across all ranges (necklace=3, legs=8, hand=13).
|
|
1522
|
+
uint256[] memory r4 = new uint256[](3);
|
|
1523
|
+
r4[0] = NECKLACE_1;
|
|
1524
|
+
r4[1] = LEGS_1; // same item stays
|
|
1525
|
+
r4[2] = HAND_1;
|
|
1526
|
+
vm.prank(alice);
|
|
1527
|
+
resolver.decorateBannyWith(address(bannyHook), ORIGINAL_BODY_1, 0, r4);
|
|
1528
|
+
_assertReturned(GLASSES_1, alice);
|
|
1529
|
+
_assertReturned(MOUTH_1, alice);
|
|
1530
|
+
_assertWorn(NECKLACE_1, ORIGINAL_BODY_1);
|
|
1531
|
+
_assertWorn(LEGS_1, ORIGINAL_BODY_1);
|
|
1532
|
+
_assertWorn(HAND_1, ORIGINAL_BODY_1);
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1535
|
+
/// @notice Interleave background changes with outfit changes across rounds.
|
|
1536
|
+
function test_fork_redress_backgroundAndOutfitInterleaved() public {
|
|
1537
|
+
uint256[] memory empty = new uint256[](0);
|
|
1538
|
+
|
|
1539
|
+
// Round 1: Background only.
|
|
1540
|
+
vm.prank(alice);
|
|
1541
|
+
resolver.decorateBannyWith(address(bannyHook), ORIGINAL_BODY_1, BACKGROUND_1, empty);
|
|
1542
|
+
assertEq(resolver.userOf(address(bannyHook), BACKGROUND_1), ORIGINAL_BODY_1);
|
|
1543
|
+
|
|
1544
|
+
// Round 2: Add outfits, keep background.
|
|
1545
|
+
uint256[] memory r2 = new uint256[](2);
|
|
1546
|
+
r2[0] = NECKLACE_1;
|
|
1547
|
+
r2[1] = EYES_1;
|
|
1548
|
+
vm.prank(alice);
|
|
1549
|
+
resolver.decorateBannyWith(address(bannyHook), ORIGINAL_BODY_1, BACKGROUND_1, r2);
|
|
1550
|
+
assertEq(resolver.userOf(address(bannyHook), BACKGROUND_1), ORIGINAL_BODY_1);
|
|
1551
|
+
_assertWorn(NECKLACE_1, ORIGINAL_BODY_1);
|
|
1552
|
+
|
|
1553
|
+
// Round 3: Swap background, keep outfits.
|
|
1554
|
+
vm.prank(alice);
|
|
1555
|
+
resolver.decorateBannyWith(address(bannyHook), ORIGINAL_BODY_1, BACKGROUND_2, r2);
|
|
1556
|
+
_assertReturned(BACKGROUND_1, alice);
|
|
1557
|
+
assertEq(resolver.userOf(address(bannyHook), BACKGROUND_2), ORIGINAL_BODY_1);
|
|
1558
|
+
_assertWorn(NECKLACE_1, ORIGINAL_BODY_1);
|
|
1559
|
+
_assertWorn(EYES_1, ORIGINAL_BODY_1);
|
|
1560
|
+
|
|
1561
|
+
// Round 4: Remove background, swap outfits.
|
|
1562
|
+
uint256[] memory r4 = new uint256[](1);
|
|
1563
|
+
r4[0] = MOUTH_1;
|
|
1564
|
+
vm.prank(alice);
|
|
1565
|
+
resolver.decorateBannyWith(address(bannyHook), ORIGINAL_BODY_1, 0, r4);
|
|
1566
|
+
_assertReturned(BACKGROUND_2, alice);
|
|
1567
|
+
_assertReturned(NECKLACE_1, alice);
|
|
1568
|
+
_assertReturned(EYES_1, alice);
|
|
1569
|
+
_assertWorn(MOUTH_1, ORIGINAL_BODY_1);
|
|
1570
|
+
|
|
1571
|
+
// Round 5: Add background back + outfit.
|
|
1572
|
+
uint256[] memory r5 = new uint256[](2);
|
|
1573
|
+
r5[0] = GLASSES_1;
|
|
1574
|
+
r5[1] = MOUTH_1; // keep from round 4
|
|
1575
|
+
vm.prank(alice);
|
|
1576
|
+
resolver.decorateBannyWith(address(bannyHook), ORIGINAL_BODY_1, BACKGROUND_1, r5);
|
|
1577
|
+
assertEq(resolver.userOf(address(bannyHook), BACKGROUND_1), ORIGINAL_BODY_1);
|
|
1578
|
+
_assertWorn(GLASSES_1, ORIGINAL_BODY_1);
|
|
1579
|
+
_assertWorn(MOUTH_1, ORIGINAL_BODY_1);
|
|
1580
|
+
}
|
|
1581
|
+
|
|
1582
|
+
/// @notice Move an outfit directly from one body to another without explicit strip.
|
|
1583
|
+
function test_fork_redress_directOutfitMoveToOtherBody() public {
|
|
1584
|
+
// Equip necklace on body 1.
|
|
1585
|
+
uint256[] memory outfits = new uint256[](1);
|
|
1586
|
+
outfits[0] = NECKLACE_1;
|
|
1587
|
+
vm.prank(alice);
|
|
1588
|
+
resolver.decorateBannyWith(address(bannyHook), ORIGINAL_BODY_1, 0, outfits);
|
|
1589
|
+
_assertWorn(NECKLACE_1, ORIGINAL_BODY_1);
|
|
1590
|
+
|
|
1591
|
+
// Move to body 2 without stripping body 1 first.
|
|
1592
|
+
// Alice owns both bodies, and since she owns the body wearing the necklace,
|
|
1593
|
+
// she can re-equip it on her other body.
|
|
1594
|
+
vm.prank(alice);
|
|
1595
|
+
resolver.decorateBannyWith(address(bannyHook), ORIGINAL_BODY_2, 0, outfits);
|
|
1596
|
+
_assertWorn(NECKLACE_1, ORIGINAL_BODY_2);
|
|
1597
|
+
|
|
1598
|
+
// Body 1 should now have no outfits (the old necklace moved away).
|
|
1599
|
+
(, uint256[] memory body1Outfits) = resolver.assetIdsOf(address(bannyHook), ORIGINAL_BODY_1);
|
|
1600
|
+
assertEq(body1Outfits.length, 0, "body 1 should have 0 outfits after move");
|
|
1601
|
+
}
|
|
1602
|
+
|
|
1603
|
+
/// @notice Swap items across categories where old and new overlap partially.
|
|
1604
|
+
/// Old: [necklace(3), eyes(5), legs(8)]. New: [necklace(3), glasses(6), legs(8), headtop(12)].
|
|
1605
|
+
/// Expected: necklace stays, eyes returned, glasses added, legs stays, headtop added.
|
|
1606
|
+
function test_fork_redress_overlappingCategorySwap() public {
|
|
1607
|
+
// Round 1: necklace + eyes + legs.
|
|
1608
|
+
uint256[] memory r1 = new uint256[](3);
|
|
1609
|
+
r1[0] = NECKLACE_1;
|
|
1610
|
+
r1[1] = EYES_1;
|
|
1611
|
+
r1[2] = LEGS_1;
|
|
1612
|
+
vm.prank(alice);
|
|
1613
|
+
resolver.decorateBannyWith(address(bannyHook), ORIGINAL_BODY_1, 0, r1);
|
|
1614
|
+
|
|
1615
|
+
// Round 2: necklace + glasses + legs + headtop.
|
|
1616
|
+
uint256[] memory r2 = new uint256[](4);
|
|
1617
|
+
r2[0] = NECKLACE_1; // same
|
|
1618
|
+
r2[1] = GLASSES_1; // new, cat 6 > cat 5 so eyes swept
|
|
1619
|
+
r2[2] = LEGS_1; // same
|
|
1620
|
+
r2[3] = HEADTOP_1; // new
|
|
1621
|
+
vm.prank(alice);
|
|
1622
|
+
resolver.decorateBannyWith(address(bannyHook), ORIGINAL_BODY_1, 0, r2);
|
|
1623
|
+
|
|
1624
|
+
_assertWorn(NECKLACE_1, ORIGINAL_BODY_1);
|
|
1625
|
+
_assertReturned(EYES_1, alice);
|
|
1626
|
+
_assertWorn(GLASSES_1, ORIGINAL_BODY_1);
|
|
1627
|
+
_assertWorn(LEGS_1, ORIGINAL_BODY_1);
|
|
1628
|
+
_assertWorn(HEADTOP_1, ORIGINAL_BODY_1);
|
|
1629
|
+
|
|
1630
|
+
(, uint256[] memory attached) = resolver.assetIdsOf(address(bannyHook), ORIGINAL_BODY_1);
|
|
1631
|
+
assertEq(attached.length, 4);
|
|
1632
|
+
}
|
|
1633
|
+
|
|
1634
|
+
/// @notice Replace items within same categories using different tokens (e.g., EYES_1 → GLASSES_2 in cat 6).
|
|
1635
|
+
function test_fork_redress_replaceSameCategoryDifferentTokens() public {
|
|
1636
|
+
// Round 1: glasses_1 (cat 6).
|
|
1637
|
+
uint256[] memory r1 = new uint256[](1);
|
|
1638
|
+
r1[0] = GLASSES_1;
|
|
1639
|
+
vm.prank(alice);
|
|
1640
|
+
resolver.decorateBannyWith(address(bannyHook), ORIGINAL_BODY_1, 0, r1);
|
|
1641
|
+
_assertWorn(GLASSES_1, ORIGINAL_BODY_1);
|
|
1642
|
+
|
|
1643
|
+
// Round 2: glasses_2 replaces glasses_1 (both cat 6).
|
|
1644
|
+
uint256[] memory r2 = new uint256[](1);
|
|
1645
|
+
r2[0] = GLASSES_2;
|
|
1646
|
+
vm.prank(alice);
|
|
1647
|
+
resolver.decorateBannyWith(address(bannyHook), ORIGINAL_BODY_1, 0, r2);
|
|
1648
|
+
_assertWorn(GLASSES_2, ORIGINAL_BODY_1);
|
|
1649
|
+
_assertReturned(GLASSES_1, alice);
|
|
1650
|
+
|
|
1651
|
+
// Round 3: back to glasses_1.
|
|
1652
|
+
vm.prank(alice);
|
|
1653
|
+
resolver.decorateBannyWith(address(bannyHook), ORIGINAL_BODY_1, 0, r1);
|
|
1654
|
+
_assertWorn(GLASSES_1, ORIGINAL_BODY_1);
|
|
1655
|
+
_assertReturned(GLASSES_2, alice);
|
|
1656
|
+
}
|
|
1657
|
+
|
|
1658
|
+
/// @notice Rapid-fire redressing: 10 rounds in one test, alternating between two outfit sets.
|
|
1659
|
+
function test_fork_redress_rapidAlternation() public {
|
|
1660
|
+
uint256[] memory setA = new uint256[](2);
|
|
1661
|
+
setA[0] = NECKLACE_1; // cat 3
|
|
1662
|
+
setA[1] = EYES_1; // cat 5
|
|
1663
|
+
|
|
1664
|
+
uint256[] memory setB = new uint256[](2);
|
|
1665
|
+
setB[0] = GLASSES_1; // cat 6
|
|
1666
|
+
setB[1] = LEGS_1; // cat 8
|
|
1667
|
+
|
|
1668
|
+
for (uint256 i; i < 10; i++) {
|
|
1669
|
+
uint256[] memory set = (i % 2 == 0) ? setA : setB;
|
|
1670
|
+
vm.prank(alice);
|
|
1671
|
+
resolver.decorateBannyWith(address(bannyHook), ORIGINAL_BODY_1, 0, set);
|
|
1672
|
+
|
|
1673
|
+
(, uint256[] memory attached) = resolver.assetIdsOf(address(bannyHook), ORIGINAL_BODY_1);
|
|
1674
|
+
assertEq(attached.length, 2, "should always have 2 outfits");
|
|
1675
|
+
|
|
1676
|
+
if (i % 2 == 0) {
|
|
1677
|
+
_assertWorn(NECKLACE_1, ORIGINAL_BODY_1);
|
|
1678
|
+
_assertWorn(EYES_1, ORIGINAL_BODY_1);
|
|
1679
|
+
} else {
|
|
1680
|
+
_assertWorn(GLASSES_1, ORIGINAL_BODY_1);
|
|
1681
|
+
_assertWorn(LEGS_1, ORIGINAL_BODY_1);
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1684
|
+
}
|
|
1685
|
+
|
|
1686
|
+
/// @notice Redress where new set has categories both below and above the old set's range.
|
|
1687
|
+
/// Old: [glasses(6), mouth(7)]. New: [necklace(3), headtop(12)].
|
|
1688
|
+
function test_fork_redress_newSetBracketsOldRange() public {
|
|
1689
|
+
// Round 1: middle categories.
|
|
1690
|
+
uint256[] memory r1 = new uint256[](2);
|
|
1691
|
+
r1[0] = GLASSES_1; // cat 6
|
|
1692
|
+
r1[1] = MOUTH_1; // cat 7
|
|
1693
|
+
vm.prank(alice);
|
|
1694
|
+
resolver.decorateBannyWith(address(bannyHook), ORIGINAL_BODY_1, 0, r1);
|
|
1695
|
+
|
|
1696
|
+
// Round 2: new set brackets old range — lower and higher.
|
|
1697
|
+
uint256[] memory r2 = new uint256[](2);
|
|
1698
|
+
r2[0] = NECKLACE_1; // cat 3 — below old range
|
|
1699
|
+
r2[1] = HEADTOP_1; // cat 12 — above old range
|
|
1700
|
+
vm.prank(alice);
|
|
1701
|
+
resolver.decorateBannyWith(address(bannyHook), ORIGINAL_BODY_1, 0, r2);
|
|
1702
|
+
|
|
1703
|
+
_assertReturned(GLASSES_1, alice);
|
|
1704
|
+
_assertReturned(MOUTH_1, alice);
|
|
1705
|
+
_assertWorn(NECKLACE_1, ORIGINAL_BODY_1);
|
|
1706
|
+
_assertWorn(HEADTOP_1, ORIGINAL_BODY_1);
|
|
1707
|
+
}
|
|
1708
|
+
|
|
1709
|
+
/// @notice Redress from max outfits to completely different max outfits — tests full sweep + re-equip.
|
|
1710
|
+
function test_fork_redress_maxToMaxSwap() public {
|
|
1711
|
+
// Round 1: 5 outfits (necklace, eyes, mouth, legs, hand).
|
|
1712
|
+
uint256[] memory r1 = new uint256[](5);
|
|
1713
|
+
r1[0] = NECKLACE_1; // cat 3
|
|
1714
|
+
r1[1] = EYES_1; // cat 5
|
|
1715
|
+
r1[2] = MOUTH_1; // cat 7
|
|
1716
|
+
r1[3] = LEGS_1; // cat 8
|
|
1717
|
+
r1[4] = HAND_1; // cat 13
|
|
1718
|
+
vm.prank(alice);
|
|
1719
|
+
resolver.decorateBannyWith(address(bannyHook), ORIGINAL_BODY_1, BACKGROUND_1, r1);
|
|
1720
|
+
|
|
1721
|
+
// Round 2: 5 different outfits (necklace2, glasses, legs2, suit_bottom, headtop).
|
|
1722
|
+
uint256[] memory r2 = new uint256[](5);
|
|
1723
|
+
r2[0] = NECKLACE_2; // cat 3 — replaces NECKLACE_1
|
|
1724
|
+
r2[1] = GLASSES_1; // cat 6 — eyes swept
|
|
1725
|
+
r2[2] = LEGS_2; // cat 8 — replaces LEGS_1
|
|
1726
|
+
r2[3] = SUIT_BOTTOM_1; // cat 10 — new
|
|
1727
|
+
r2[4] = HEADTOP_1; // cat 12 — new, hand (13) swept by tail loop
|
|
1728
|
+
vm.prank(alice);
|
|
1729
|
+
resolver.decorateBannyWith(address(bannyHook), ORIGINAL_BODY_1, BACKGROUND_2, r2);
|
|
1730
|
+
|
|
1731
|
+
// All old items returned.
|
|
1732
|
+
_assertReturned(NECKLACE_1, alice);
|
|
1733
|
+
_assertReturned(EYES_1, alice);
|
|
1734
|
+
_assertReturned(MOUTH_1, alice);
|
|
1735
|
+
_assertReturned(LEGS_1, alice);
|
|
1736
|
+
_assertReturned(HAND_1, alice);
|
|
1737
|
+
_assertReturned(BACKGROUND_1, alice);
|
|
1738
|
+
|
|
1739
|
+
// All new items worn.
|
|
1740
|
+
_assertWorn(NECKLACE_2, ORIGINAL_BODY_1);
|
|
1741
|
+
_assertWorn(GLASSES_1, ORIGINAL_BODY_1);
|
|
1742
|
+
_assertWorn(LEGS_2, ORIGINAL_BODY_1);
|
|
1743
|
+
_assertWorn(SUIT_BOTTOM_1, ORIGINAL_BODY_1);
|
|
1744
|
+
_assertWorn(HEADTOP_1, ORIGINAL_BODY_1);
|
|
1745
|
+
assertEq(resolver.userOf(address(bannyHook), BACKGROUND_2), ORIGINAL_BODY_1);
|
|
1746
|
+
|
|
1747
|
+
(, uint256[] memory attached) = resolver.assetIdsOf(address(bannyHook), ORIGINAL_BODY_1);
|
|
1748
|
+
assertEq(attached.length, 5);
|
|
1749
|
+
}
|
|
1750
|
+
|
|
1751
|
+
/// @notice Single outfit survives multiple rounds where other categories rotate around it.
|
|
1752
|
+
function test_fork_redress_anchorOutfitSurvivesRotation() public {
|
|
1753
|
+
// LEGS_1 (cat 8) is the anchor. Other categories change each round.
|
|
1754
|
+
|
|
1755
|
+
// Round 1: necklace + legs.
|
|
1756
|
+
uint256[] memory r1 = new uint256[](2);
|
|
1757
|
+
r1[0] = NECKLACE_1;
|
|
1758
|
+
r1[1] = LEGS_1;
|
|
1759
|
+
vm.prank(alice);
|
|
1760
|
+
resolver.decorateBannyWith(address(bannyHook), ORIGINAL_BODY_1, 0, r1);
|
|
1761
|
+
_assertWorn(LEGS_1, ORIGINAL_BODY_1);
|
|
1762
|
+
|
|
1763
|
+
// Round 2: eyes + legs.
|
|
1764
|
+
uint256[] memory r2 = new uint256[](2);
|
|
1765
|
+
r2[0] = EYES_1;
|
|
1766
|
+
r2[1] = LEGS_1;
|
|
1767
|
+
vm.prank(alice);
|
|
1768
|
+
resolver.decorateBannyWith(address(bannyHook), ORIGINAL_BODY_1, 0, r2);
|
|
1769
|
+
_assertReturned(NECKLACE_1, alice);
|
|
1770
|
+
_assertWorn(EYES_1, ORIGINAL_BODY_1);
|
|
1771
|
+
_assertWorn(LEGS_1, ORIGINAL_BODY_1);
|
|
1772
|
+
|
|
1773
|
+
// Round 3: glasses + legs + headtop.
|
|
1774
|
+
uint256[] memory r3 = new uint256[](3);
|
|
1775
|
+
r3[0] = GLASSES_1;
|
|
1776
|
+
r3[1] = LEGS_1;
|
|
1777
|
+
r3[2] = HEADTOP_1;
|
|
1778
|
+
vm.prank(alice);
|
|
1779
|
+
resolver.decorateBannyWith(address(bannyHook), ORIGINAL_BODY_1, 0, r3);
|
|
1780
|
+
_assertReturned(EYES_1, alice);
|
|
1781
|
+
_assertWorn(GLASSES_1, ORIGINAL_BODY_1);
|
|
1782
|
+
_assertWorn(LEGS_1, ORIGINAL_BODY_1);
|
|
1783
|
+
_assertWorn(HEADTOP_1, ORIGINAL_BODY_1);
|
|
1784
|
+
|
|
1785
|
+
// Round 4: just legs alone.
|
|
1786
|
+
uint256[] memory r4 = new uint256[](1);
|
|
1787
|
+
r4[0] = LEGS_1;
|
|
1788
|
+
vm.prank(alice);
|
|
1789
|
+
resolver.decorateBannyWith(address(bannyHook), ORIGINAL_BODY_1, 0, r4);
|
|
1790
|
+
_assertReturned(GLASSES_1, alice);
|
|
1791
|
+
_assertReturned(HEADTOP_1, alice);
|
|
1792
|
+
_assertWorn(LEGS_1, ORIGINAL_BODY_1);
|
|
1793
|
+
|
|
1794
|
+
(, uint256[] memory attached) = resolver.assetIdsOf(address(bannyHook), ORIGINAL_BODY_1);
|
|
1795
|
+
assertEq(attached.length, 1);
|
|
1796
|
+
assertEq(attached[0], LEGS_1);
|
|
1797
|
+
}
|
|
1798
|
+
|
|
1799
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
1800
|
+
// Internal helpers
|
|
1801
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
1802
|
+
|
|
1803
|
+
function _deployJBCore() internal {
|
|
1804
|
+
jbPermissions = new JBPermissions(trustedForwarder);
|
|
1805
|
+
jbProjects = new JBProjects(multisig, address(0), trustedForwarder);
|
|
1806
|
+
jbDirectory = new JBDirectory(jbPermissions, jbProjects, multisig);
|
|
1807
|
+
JBERC20 jbErc20 = new JBERC20();
|
|
1808
|
+
jbTokens = new JBTokens(jbDirectory, jbErc20);
|
|
1809
|
+
jbRulesets = new JBRulesets(jbDirectory);
|
|
1810
|
+
jbPrices = new JBPrices(jbDirectory, jbPermissions, jbProjects, multisig, trustedForwarder);
|
|
1811
|
+
jbSplits = new JBSplits(jbDirectory);
|
|
1812
|
+
jbFundAccessLimits = new JBFundAccessLimits(jbDirectory);
|
|
1813
|
+
|
|
1814
|
+
jbController = new JBController(
|
|
1815
|
+
jbDirectory,
|
|
1816
|
+
jbFundAccessLimits,
|
|
1817
|
+
jbPermissions,
|
|
1818
|
+
jbPrices,
|
|
1819
|
+
jbProjects,
|
|
1820
|
+
jbRulesets,
|
|
1821
|
+
jbSplits,
|
|
1822
|
+
jbTokens,
|
|
1823
|
+
address(0), // omnichainRulesetOperator
|
|
1824
|
+
trustedForwarder
|
|
1825
|
+
);
|
|
1826
|
+
|
|
1827
|
+
vm.prank(multisig);
|
|
1828
|
+
jbDirectory.setIsAllowedToSetFirstController(address(jbController), true);
|
|
1829
|
+
}
|
|
1830
|
+
|
|
1831
|
+
function _deploy721Hook() internal {
|
|
1832
|
+
JB721TiersHookStore store = new JB721TiersHookStore();
|
|
1833
|
+
JBAddressRegistry addressRegistry = new JBAddressRegistry();
|
|
1834
|
+
|
|
1835
|
+
JB721TiersHook hookImpl = new JB721TiersHook(jbDirectory, jbPermissions, jbRulesets, store, trustedForwarder);
|
|
1836
|
+
|
|
1837
|
+
hookDeployer = new JB721TiersHookDeployer(hookImpl, store, addressRegistry, trustedForwarder);
|
|
1838
|
+
}
|
|
1839
|
+
|
|
1840
|
+
function _deployBannyResolver() internal {
|
|
1841
|
+
// Deploy with production SVG constants (abbreviated for tests).
|
|
1842
|
+
string memory bannyBody =
|
|
1843
|
+
'<g class="b1"><path d="M173 53h4v17h-4z"/></g><g class="b2"><path d="M167 57h3v10h-3z"/></g><g class="o"><path d="M177 53h3v17h-3z"/></g>';
|
|
1844
|
+
string memory defaultNecklace = '<g class="o"><path d="M190 173h-37v-3h-10v-4h-6v4h3v3h-3v4h6v3h10v4h37"/></g>';
|
|
1845
|
+
string memory defaultMouth = '<g class="o"><path d="M183 160v-4h-20v4h-3v3h3v4h24v-7h-4z" fill="#ad71c8"/></g>';
|
|
1846
|
+
string memory defaultEyes = '<g class="o"><path d="M177 140v3h6v11h10v-11h4v-3h-20z"/></g>';
|
|
1847
|
+
string memory defaultAlienEyes = '<g class="o"><path d="M190 127h3v3h-3z"/></g>';
|
|
1848
|
+
|
|
1849
|
+
vm.prank(multisig);
|
|
1850
|
+
resolver = new Banny721TokenUriResolver(
|
|
1851
|
+
bannyBody, defaultNecklace, defaultMouth, defaultEyes, defaultAlienEyes, multisig, trustedForwarder
|
|
1852
|
+
);
|
|
1853
|
+
|
|
1854
|
+
// Set metadata.
|
|
1855
|
+
vm.prank(multisig);
|
|
1856
|
+
resolver.setMetadata("A piece of Banny Retail.", "https://retail.banny.eth.shop", "https://bannyverse.test/");
|
|
1857
|
+
}
|
|
1858
|
+
|
|
1859
|
+
function _buildHookConfig() internal view returns (JBDeploy721TiersHookConfig memory) {
|
|
1860
|
+
// 16 tiers sorted by category (ascending).
|
|
1861
|
+
JB721TierConfig[] memory tiers = new JB721TierConfig[](16);
|
|
1862
|
+
|
|
1863
|
+
// Tier 1-4: Bodies (category 0) — different prices.
|
|
1864
|
+
tiers[0] = _tierConfig(1 ether, 100, 0); // Alien
|
|
1865
|
+
tiers[1] = _tierConfig(0.1 ether, 1000, 0); // Pink
|
|
1866
|
+
tiers[2] = _tierConfig(0.01 ether, 10_000, 0); // Orange
|
|
1867
|
+
tiers[3] = _tierConfig(0.0001 ether, 999_999_999, 0); // Original
|
|
1868
|
+
|
|
1869
|
+
// Tier 5: Background (category 1)
|
|
1870
|
+
tiers[4] = _tierConfig(0.01 ether, 1000, 1);
|
|
1871
|
+
|
|
1872
|
+
// Tier 6: Necklace (category 3)
|
|
1873
|
+
tiers[5] = _tierConfig(0.01 ether, 1000, 3);
|
|
1874
|
+
|
|
1875
|
+
// Tier 7: Head (category 4)
|
|
1876
|
+
tiers[6] = _tierConfig(0.01 ether, 1000, 4);
|
|
1877
|
+
|
|
1878
|
+
// Tier 8: Eyes (category 5)
|
|
1879
|
+
tiers[7] = _tierConfig(0.01 ether, 1000, 5);
|
|
1880
|
+
|
|
1881
|
+
// Tier 9: Glasses (category 6)
|
|
1882
|
+
tiers[8] = _tierConfig(0.01 ether, 1000, 6);
|
|
1883
|
+
|
|
1884
|
+
// Tier 10: Mouth (category 7)
|
|
1885
|
+
tiers[9] = _tierConfig(0.01 ether, 1000, 7);
|
|
1886
|
+
|
|
1887
|
+
// Tier 11: Legs (category 8)
|
|
1888
|
+
tiers[10] = _tierConfig(0.01 ether, 1000, 8);
|
|
1889
|
+
|
|
1890
|
+
// Tier 12: Suit (category 9)
|
|
1891
|
+
tiers[11] = _tierConfig(0.01 ether, 1000, 9);
|
|
1892
|
+
|
|
1893
|
+
// Tier 13: Suit Bottom (category 10)
|
|
1894
|
+
tiers[12] = _tierConfig(0.01 ether, 1000, 10);
|
|
1895
|
+
|
|
1896
|
+
// Tier 14: Suit Top (category 11)
|
|
1897
|
+
tiers[13] = _tierConfig(0.01 ether, 1000, 11);
|
|
1898
|
+
|
|
1899
|
+
// Tier 15: Headtop (category 12)
|
|
1900
|
+
tiers[14] = _tierConfig(0.01 ether, 1000, 12);
|
|
1901
|
+
|
|
1902
|
+
// Tier 16: Hand (category 13)
|
|
1903
|
+
tiers[15] = _tierConfig(0.01 ether, 1000, 13);
|
|
1904
|
+
|
|
1905
|
+
return JBDeploy721TiersHookConfig({
|
|
1906
|
+
name: "Banny Retail",
|
|
1907
|
+
symbol: "BANNY",
|
|
1908
|
+
baseUri: "ipfs://",
|
|
1909
|
+
tokenUriResolver: IJB721TokenUriResolver(address(resolver)),
|
|
1910
|
+
contractUri: "",
|
|
1911
|
+
tiersConfig: JB721InitTiersConfig({
|
|
1912
|
+
tiers: tiers, currency: JBCurrencyIds.ETH, decimals: 18, prices: IJBPrices(address(0))
|
|
1913
|
+
}),
|
|
1914
|
+
reserveBeneficiary: address(0),
|
|
1915
|
+
flags: JB721TiersHookFlags({
|
|
1916
|
+
noNewTiersWithReserves: false,
|
|
1917
|
+
noNewTiersWithVotes: false,
|
|
1918
|
+
noNewTiersWithOwnerMinting: false,
|
|
1919
|
+
preventOverspending: false,
|
|
1920
|
+
issueTokensForSplits: false
|
|
1921
|
+
})
|
|
1922
|
+
});
|
|
1923
|
+
}
|
|
1924
|
+
|
|
1925
|
+
function _deployBannyHook() internal {
|
|
1926
|
+
// Compute the next project ID.
|
|
1927
|
+
uint256 projectId = jbProjects.count() + 1;
|
|
1928
|
+
|
|
1929
|
+
JBDeploy721TiersHookConfig memory hookConfig = _buildHookConfig();
|
|
1930
|
+
|
|
1931
|
+
// Deploy hook for this project. multisig becomes the owner.
|
|
1932
|
+
vm.prank(multisig);
|
|
1933
|
+
bannyHook = hookDeployer.deployHookFor(projectId, hookConfig, bytes32(uint256(1)));
|
|
1934
|
+
}
|
|
1935
|
+
|
|
1936
|
+
function _tierConfig(uint104 price, uint32 supply, uint24 category) internal pure returns (JB721TierConfig memory) {
|
|
1937
|
+
return JB721TierConfig({
|
|
1938
|
+
price: price,
|
|
1939
|
+
initialSupply: supply,
|
|
1940
|
+
votingUnits: 0,
|
|
1941
|
+
reserveFrequency: 0,
|
|
1942
|
+
reserveBeneficiary: address(0),
|
|
1943
|
+
encodedIPFSUri: bytes32(0),
|
|
1944
|
+
category: category,
|
|
1945
|
+
discountPercent: 0,
|
|
1946
|
+
allowOwnerMint: true,
|
|
1947
|
+
useReserveBeneficiaryAsDefault: false,
|
|
1948
|
+
transfersPausable: false,
|
|
1949
|
+
useVotingUnits: false,
|
|
1950
|
+
cannotBeRemoved: false,
|
|
1951
|
+
cannotIncreaseDiscountPercent: false,
|
|
1952
|
+
splitPercent: 0,
|
|
1953
|
+
splits: new JBSplit[](0)
|
|
1954
|
+
});
|
|
1955
|
+
}
|
|
1956
|
+
|
|
1957
|
+
function _mintInitialNFTs() internal {
|
|
1958
|
+
// Mint bodies and outfits to alice, bob.
|
|
1959
|
+
vm.startPrank(multisig); // hook owner can mint
|
|
1960
|
+
|
|
1961
|
+
// Alice: alien body, original body x2, background x2, necklace x2,
|
|
1962
|
+
// head, eyes, glasses, mouth, legs, suit, suit bottom, suit top, headtop, hand.
|
|
1963
|
+
uint16[] memory aliceTiers = new uint16[](18);
|
|
1964
|
+
aliceTiers[0] = TIER_ALIEN_BODY; // ALIEN_BODY_1
|
|
1965
|
+
aliceTiers[1] = TIER_PINK_BODY; // PINK_BODY_1 — will give to bob
|
|
1966
|
+
aliceTiers[2] = TIER_ORIGINAL_BODY; // ORIGINAL_BODY_1
|
|
1967
|
+
aliceTiers[3] = TIER_ORIGINAL_BODY; // ORIGINAL_BODY_2
|
|
1968
|
+
aliceTiers[4] = TIER_BACKGROUND; // BACKGROUND_1
|
|
1969
|
+
aliceTiers[5] = TIER_BACKGROUND; // BACKGROUND_2
|
|
1970
|
+
aliceTiers[6] = TIER_NECKLACE; // NECKLACE_1
|
|
1971
|
+
aliceTiers[7] = TIER_NECKLACE; // NECKLACE_2
|
|
1972
|
+
aliceTiers[8] = TIER_HEAD; // HEAD_1
|
|
1973
|
+
aliceTiers[9] = TIER_EYES; // EYES_1
|
|
1974
|
+
aliceTiers[10] = TIER_GLASSES; // GLASSES_1
|
|
1975
|
+
aliceTiers[11] = TIER_MOUTH; // MOUTH_1
|
|
1976
|
+
aliceTiers[12] = TIER_LEGS; // LEGS_1
|
|
1977
|
+
aliceTiers[13] = TIER_SUIT; // SUIT_1
|
|
1978
|
+
aliceTiers[14] = TIER_SUIT_BOTTOM; // SUIT_BOTTOM_1
|
|
1979
|
+
aliceTiers[15] = TIER_SUIT_TOP; // SUIT_TOP_1
|
|
1980
|
+
aliceTiers[16] = TIER_HEADTOP; // HEADTOP_1
|
|
1981
|
+
aliceTiers[17] = TIER_HAND; // HAND_1
|
|
1982
|
+
bannyHook.mintFor(aliceTiers, alice);
|
|
1983
|
+
|
|
1984
|
+
// Alice extra: glasses2, legs2, necklace3, headtop2 (for redressing cycle tests).
|
|
1985
|
+
uint16[] memory aliceExtra = new uint16[](4);
|
|
1986
|
+
aliceExtra[0] = TIER_NECKLACE; // NECKLACE_3
|
|
1987
|
+
aliceExtra[1] = TIER_GLASSES; // GLASSES_2
|
|
1988
|
+
aliceExtra[2] = TIER_LEGS; // LEGS_2
|
|
1989
|
+
aliceExtra[3] = TIER_HEADTOP; // HEADTOP_2
|
|
1990
|
+
bannyHook.mintFor(aliceExtra, alice);
|
|
1991
|
+
|
|
1992
|
+
// Bob: eyes, mouth, head (for multi-actor tests).
|
|
1993
|
+
uint16[] memory bobTiers = new uint16[](3);
|
|
1994
|
+
bobTiers[0] = TIER_HEAD; // HEAD_2
|
|
1995
|
+
bobTiers[1] = TIER_EYES; // EYES_2
|
|
1996
|
+
bobTiers[2] = TIER_MOUTH; // MOUTH_2
|
|
1997
|
+
bannyHook.mintFor(bobTiers, bob);
|
|
1998
|
+
|
|
1999
|
+
vm.stopPrank();
|
|
2000
|
+
|
|
2001
|
+
// Transfer PINK_BODY_1 from alice to bob for multi-actor tests.
|
|
2002
|
+
vm.prank(alice);
|
|
2003
|
+
IERC721(address(bannyHook)).safeTransferFrom(alice, bob, PINK_BODY_1);
|
|
2004
|
+
|
|
2005
|
+
// Approve resolver for alice and bob.
|
|
2006
|
+
vm.prank(alice);
|
|
2007
|
+
IERC721(address(bannyHook)).setApprovalForAll(address(resolver), true);
|
|
2008
|
+
|
|
2009
|
+
vm.prank(bob);
|
|
2010
|
+
IERC721(address(bannyHook)).setApprovalForAll(address(resolver), true);
|
|
2011
|
+
}
|
|
2012
|
+
|
|
2013
|
+
/// @dev Mint a single NFT of the given tier to the given address.
|
|
2014
|
+
function _mintTo(address to, uint16 tierId) internal {
|
|
2015
|
+
uint16[] memory tierIds = new uint16[](1);
|
|
2016
|
+
tierIds[0] = tierId;
|
|
2017
|
+
vm.prank(multisig);
|
|
2018
|
+
bannyHook.mintFor(tierIds, to);
|
|
2019
|
+
}
|
|
2020
|
+
}
|