@bannynet/core-v6 0.0.1
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/README.md +53 -0
- package/SKILLS.md +94 -0
- package/deployments/banny-core-v5/arbitrum/Banny721TokenUriResolver.json +1809 -0
- package/deployments/banny-core-v5/arbitrum_sepolia/Banny721TokenUriResolver.json +1795 -0
- package/deployments/banny-core-v5/base/Banny721TokenUriResolver.json +1810 -0
- package/deployments/banny-core-v5/base_sepolia/Banny721TokenUriResolver.json +1796 -0
- package/deployments/banny-core-v5/ethereum/Banny721TokenUriResolver.json +1795 -0
- package/deployments/banny-core-v5/optimism/Banny721TokenUriResolver.json +1810 -0
- package/deployments/banny-core-v5/optimism_sepolia/Banny721TokenUriResolver.json +1796 -0
- package/deployments/banny-core-v5/sepolia/Banny721TokenUriResolver.json +1795 -0
- package/foundry.toml +22 -0
- package/package.json +53 -0
- package/remappings.txt +1 -0
- package/script/1.Fix.s.sol +290 -0
- package/script/Add.Denver.s.sol +75 -0
- package/script/AirdropOutfits.s.sol +2302 -0
- package/script/Deploy.s.sol +440 -0
- package/script/Drop1.s.sol +979 -0
- package/script/MigrationContractArbitrum.sol +494 -0
- package/script/MigrationContractArbitrum1.sol +170 -0
- package/script/MigrationContractArbitrum2.sol +204 -0
- package/script/MigrationContractArbitrum3.sol +174 -0
- package/script/MigrationContractArbitrum4.sol +478 -0
- package/script/MigrationContractBase1.sol +444 -0
- package/script/MigrationContractBase2.sol +175 -0
- package/script/MigrationContractBase3.sol +309 -0
- package/script/MigrationContractBase4.sol +350 -0
- package/script/MigrationContractBase5.sol +259 -0
- package/script/MigrationContractEthereum1.sol +468 -0
- package/script/MigrationContractEthereum2.sol +306 -0
- package/script/MigrationContractEthereum3.sol +349 -0
- package/script/MigrationContractEthereum4.sol +352 -0
- package/script/MigrationContractEthereum5.sol +354 -0
- package/script/MigrationContractEthereum6.sol +270 -0
- package/script/MigrationContractEthereum7.sol +439 -0
- package/script/MigrationContractEthereum8.sol +385 -0
- package/script/MigrationContractOptimism.sol +196 -0
- package/script/helpers/BannyverseDeploymentLib.sol +73 -0
- package/script/helpers/MigrationHelper.sol +155 -0
- package/script/outfit_drop/generate-migration.js +3441 -0
- package/script/outfit_drop/raw.json +43276 -0
- package/slither-ci.config.json +10 -0
- package/sphinx.lock +521 -0
- package/src/Banny721TokenUriResolver.sol +1288 -0
- package/src/interfaces/IBanny721TokenUriResolver.sol +137 -0
- package/test/Banny721TokenUriResolver.t.sol +669 -0
- package/test/BannyAttacks.t.sol +322 -0
- package/test/DecorateFlow.t.sol +1056 -0
|
@@ -0,0 +1,669 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity 0.8.23;
|
|
3
|
+
|
|
4
|
+
import "forge-std/Test.sol";
|
|
5
|
+
import {IERC721} from "@bananapus/721-hook-v5/src/abstract/ERC721.sol";
|
|
6
|
+
import {IJB721TiersHook} from "@bananapus/721-hook-v5/src/interfaces/IJB721TiersHook.sol";
|
|
7
|
+
import {IJB721TiersHookStore} from "@bananapus/721-hook-v5/src/interfaces/IJB721TiersHookStore.sol";
|
|
8
|
+
import {JB721Tier} from "@bananapus/721-hook-v5/src/structs/JB721Tier.sol";
|
|
9
|
+
import {IERC721Receiver} from "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol";
|
|
10
|
+
|
|
11
|
+
import {Banny721TokenUriResolver} from "../src/Banny721TokenUriResolver.sol";
|
|
12
|
+
|
|
13
|
+
/// @notice Minimal mock hook for testing. Tracks owners and categories for mock 721 tokens.
|
|
14
|
+
contract MockHook {
|
|
15
|
+
// Mock owner tracking.
|
|
16
|
+
mapping(uint256 tokenId => address) public ownerOf;
|
|
17
|
+
|
|
18
|
+
// Mock tier data: tokenId => (tierId, category).
|
|
19
|
+
mapping(uint256 tokenId => uint32) public tierIdOf;
|
|
20
|
+
mapping(uint256 tokenId => uint24) public categoryOf;
|
|
21
|
+
|
|
22
|
+
// Mock store.
|
|
23
|
+
address public immutable MOCK_STORE;
|
|
24
|
+
|
|
25
|
+
// Approval tracking.
|
|
26
|
+
mapping(address owner => mapping(address operator => bool)) public isApprovedForAll;
|
|
27
|
+
|
|
28
|
+
constructor(address store) {
|
|
29
|
+
MOCK_STORE = store;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function STORE() external view returns (address) {
|
|
33
|
+
return MOCK_STORE;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function setOwner(uint256 tokenId, address owner) external {
|
|
37
|
+
ownerOf[tokenId] = owner;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function setTier(uint256 tokenId, uint32 tierId, uint24 category) external {
|
|
41
|
+
tierIdOf[tokenId] = tierId;
|
|
42
|
+
categoryOf[tokenId] = category;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function setApprovalForAll(address operator, bool approved) external {
|
|
46
|
+
isApprovedForAll[msg.sender][operator] = approved;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Minimal safeTransferFrom mock.
|
|
50
|
+
function safeTransferFrom(address from, address to, uint256 tokenId) external {
|
|
51
|
+
require(
|
|
52
|
+
msg.sender == ownerOf[tokenId] || msg.sender == from || isApprovedForAll[from][msg.sender],
|
|
53
|
+
"MockHook: not authorized"
|
|
54
|
+
);
|
|
55
|
+
ownerOf[tokenId] = to;
|
|
56
|
+
|
|
57
|
+
// Call onERC721Received if to is a contract.
|
|
58
|
+
if (to.code.length > 0) {
|
|
59
|
+
bytes4 retval = IERC721Receiver(to).onERC721Received(msg.sender, from, tokenId, "");
|
|
60
|
+
require(retval == IERC721Receiver.onERC721Received.selector, "MockHook: receiver rejected");
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function pricingContext() external pure returns (uint256 currency, uint256 decimals, uint256) {
|
|
65
|
+
return (1, 18, 0);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function baseURI() external pure returns (string memory) {
|
|
69
|
+
return "ipfs://";
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/// @notice Minimal mock store.
|
|
74
|
+
contract MockStore {
|
|
75
|
+
mapping(address hook => mapping(uint256 tokenId => JB721Tier)) public tiers;
|
|
76
|
+
|
|
77
|
+
function setTier(address hook, uint256 tokenId, JB721Tier memory tier) external {
|
|
78
|
+
tiers[hook][tokenId] = tier;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function tierOfTokenId(address hook, uint256 tokenId, bool) external view returns (JB721Tier memory) {
|
|
82
|
+
return tiers[hook][tokenId];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function encodedTierIPFSUriOf(address, uint256) external pure returns (bytes32) {
|
|
86
|
+
return bytes32(0);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function encodedIPFSUriOf(address, uint256) external pure returns (bytes32) {
|
|
90
|
+
return bytes32(0);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/// @notice Tests for Banny721TokenUriResolver.
|
|
95
|
+
contract TestBanny721TokenUriResolver is Test {
|
|
96
|
+
Banny721TokenUriResolver resolver;
|
|
97
|
+
MockHook hook;
|
|
98
|
+
MockStore store;
|
|
99
|
+
|
|
100
|
+
address deployer = makeAddr("deployer");
|
|
101
|
+
address alice = makeAddr("alice");
|
|
102
|
+
address bob = makeAddr("bob");
|
|
103
|
+
|
|
104
|
+
// Token IDs: product ID * 1_000_000_000 + sequence.
|
|
105
|
+
// Product ID 1 = Alien body (UPC 1, category 0)
|
|
106
|
+
// Product ID 4 = Original body (UPC 4, category 0)
|
|
107
|
+
// Product ID 5 = background (category 1)
|
|
108
|
+
// Product ID 10 = necklace (category 3)
|
|
109
|
+
// Product ID 20 = head (category 4)
|
|
110
|
+
// Product ID 30 = eyes (category 5)
|
|
111
|
+
// Product ID 40 = mouth (category 7)
|
|
112
|
+
// Product ID 50 = suit (category 9)
|
|
113
|
+
// Product ID 51 = suit_bottom (category 10)
|
|
114
|
+
// Product ID 52 = suit_top (category 11)
|
|
115
|
+
|
|
116
|
+
uint256 constant BODY_TOKEN = 4_000_000_001; // Original body, token 1
|
|
117
|
+
uint256 constant BACKGROUND_TOKEN = 5_000_000_001;
|
|
118
|
+
uint256 constant NECKLACE_TOKEN = 10_000_000_001;
|
|
119
|
+
uint256 constant HEAD_TOKEN = 20_000_000_001;
|
|
120
|
+
uint256 constant EYES_TOKEN = 30_000_000_001;
|
|
121
|
+
uint256 constant MOUTH_TOKEN = 40_000_000_001;
|
|
122
|
+
uint256 constant SUIT_TOKEN = 50_000_000_001;
|
|
123
|
+
uint256 constant SUIT_BOTTOM_TOKEN = 51_000_000_001;
|
|
124
|
+
uint256 constant SUIT_TOP_TOKEN = 52_000_000_001;
|
|
125
|
+
|
|
126
|
+
function setUp() public {
|
|
127
|
+
store = new MockStore();
|
|
128
|
+
hook = new MockHook(address(store));
|
|
129
|
+
|
|
130
|
+
vm.prank(deployer);
|
|
131
|
+
resolver = new Banny721TokenUriResolver(
|
|
132
|
+
"<path/>", // bannyBody
|
|
133
|
+
"<necklace/>", // defaultNecklace
|
|
134
|
+
"<mouth/>", // defaultMouth
|
|
135
|
+
"<eyes/>", // defaultStandardEyes
|
|
136
|
+
"<alieneyes/>", // defaultAlienEyes
|
|
137
|
+
deployer, // owner
|
|
138
|
+
address(0) // trustedForwarder
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
// Set up tier data in mock store for each token.
|
|
142
|
+
_setupTier(BODY_TOKEN, 4, 0); // Original body
|
|
143
|
+
_setupTier(BACKGROUND_TOKEN, 5, 1); // Background
|
|
144
|
+
_setupTier(NECKLACE_TOKEN, 10, 3); // Necklace
|
|
145
|
+
_setupTier(HEAD_TOKEN, 20, 4); // Head
|
|
146
|
+
_setupTier(EYES_TOKEN, 30, 5); // Eyes
|
|
147
|
+
_setupTier(MOUTH_TOKEN, 40, 7); // Mouth
|
|
148
|
+
_setupTier(SUIT_TOKEN, 50, 9); // Suit
|
|
149
|
+
_setupTier(SUIT_BOTTOM_TOKEN, 51, 10); // Suit bottom
|
|
150
|
+
_setupTier(SUIT_TOP_TOKEN, 52, 11); // Suit top
|
|
151
|
+
|
|
152
|
+
// Give alice all tokens.
|
|
153
|
+
hook.setOwner(BODY_TOKEN, alice);
|
|
154
|
+
hook.setOwner(BACKGROUND_TOKEN, alice);
|
|
155
|
+
hook.setOwner(NECKLACE_TOKEN, alice);
|
|
156
|
+
hook.setOwner(HEAD_TOKEN, alice);
|
|
157
|
+
hook.setOwner(EYES_TOKEN, alice);
|
|
158
|
+
hook.setOwner(MOUTH_TOKEN, alice);
|
|
159
|
+
hook.setOwner(SUIT_TOKEN, alice);
|
|
160
|
+
hook.setOwner(SUIT_BOTTOM_TOKEN, alice);
|
|
161
|
+
hook.setOwner(SUIT_TOP_TOKEN, alice);
|
|
162
|
+
|
|
163
|
+
// Approve resolver to manage tokens for alice.
|
|
164
|
+
vm.prank(alice);
|
|
165
|
+
hook.setApprovalForAll(address(resolver), true);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
//*********************************************************************//
|
|
169
|
+
// --- Constructor --------------------------------------------------- //
|
|
170
|
+
//*********************************************************************//
|
|
171
|
+
|
|
172
|
+
function test_constructor_setsDefaults() public {
|
|
173
|
+
assertEq(resolver.BANNY_BODY(), "<path/>");
|
|
174
|
+
assertEq(resolver.DEFAULT_NECKLACE(), "<necklace/>");
|
|
175
|
+
assertEq(resolver.DEFAULT_MOUTH(), "<mouth/>");
|
|
176
|
+
assertEq(resolver.DEFAULT_STANDARD_EYES(), "<eyes/>");
|
|
177
|
+
assertEq(resolver.DEFAULT_ALIEN_EYES(), "<alieneyes/>");
|
|
178
|
+
assertEq(resolver.owner(), deployer);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
//*********************************************************************//
|
|
182
|
+
// --- Owner-Only: setSvgBaseUri ------------------------------------- //
|
|
183
|
+
//*********************************************************************//
|
|
184
|
+
|
|
185
|
+
function test_setSvgBaseUri() public {
|
|
186
|
+
vm.prank(deployer);
|
|
187
|
+
resolver.setSvgBaseUri("https://svg.example.com/");
|
|
188
|
+
assertEq(resolver.svgBaseUri(), "https://svg.example.com/");
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function test_setSvgBaseUri_revertsIfNotOwner() public {
|
|
192
|
+
vm.prank(alice);
|
|
193
|
+
vm.expectRevert();
|
|
194
|
+
resolver.setSvgBaseUri("https://evil.com/");
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
//*********************************************************************//
|
|
198
|
+
// --- Owner-Only: setProductNames ----------------------------------- //
|
|
199
|
+
//*********************************************************************//
|
|
200
|
+
|
|
201
|
+
function test_setProductNames() public {
|
|
202
|
+
uint256[] memory upcs = new uint256[](2);
|
|
203
|
+
upcs[0] = 100;
|
|
204
|
+
upcs[1] = 200;
|
|
205
|
+
|
|
206
|
+
string[] memory names = new string[](2);
|
|
207
|
+
names[0] = "Cool Hat";
|
|
208
|
+
names[1] = "Fancy Suit";
|
|
209
|
+
|
|
210
|
+
vm.prank(deployer);
|
|
211
|
+
resolver.setProductNames(upcs, names);
|
|
212
|
+
|
|
213
|
+
// Verify via namesOf (requires tier with that UPC).
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function test_setProductNames_revertsIfNotOwner() public {
|
|
217
|
+
uint256[] memory upcs = new uint256[](1);
|
|
218
|
+
upcs[0] = 100;
|
|
219
|
+
|
|
220
|
+
string[] memory names = new string[](1);
|
|
221
|
+
names[0] = "Hacked";
|
|
222
|
+
|
|
223
|
+
vm.prank(alice);
|
|
224
|
+
vm.expectRevert();
|
|
225
|
+
resolver.setProductNames(upcs, names);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
//*********************************************************************//
|
|
229
|
+
// --- Owner-Only: setSvgHashsOf ------------------------------------- //
|
|
230
|
+
//*********************************************************************//
|
|
231
|
+
|
|
232
|
+
function test_setSvgHashsOf() public {
|
|
233
|
+
uint256[] memory upcs = new uint256[](1);
|
|
234
|
+
upcs[0] = 100;
|
|
235
|
+
|
|
236
|
+
bytes32[] memory hashes = new bytes32[](1);
|
|
237
|
+
hashes[0] = keccak256("test-svg-content");
|
|
238
|
+
|
|
239
|
+
vm.prank(deployer);
|
|
240
|
+
resolver.setSvgHashsOf(upcs, hashes);
|
|
241
|
+
|
|
242
|
+
assertEq(resolver.svgHashOf(100), keccak256("test-svg-content"));
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function test_setSvgHashsOf_revertsIfAlreadyStored() public {
|
|
246
|
+
uint256[] memory upcs = new uint256[](1);
|
|
247
|
+
upcs[0] = 100;
|
|
248
|
+
bytes32[] memory hashes = new bytes32[](1);
|
|
249
|
+
hashes[0] = keccak256("test");
|
|
250
|
+
|
|
251
|
+
vm.startPrank(deployer);
|
|
252
|
+
resolver.setSvgHashsOf(upcs, hashes);
|
|
253
|
+
|
|
254
|
+
// Second attempt should revert.
|
|
255
|
+
vm.expectRevert(Banny721TokenUriResolver.Banny721TokenUriResolver_HashAlreadyStored.selector);
|
|
256
|
+
resolver.setSvgHashsOf(upcs, hashes);
|
|
257
|
+
vm.stopPrank();
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function test_setSvgHashsOf_revertsIfNotOwner() public {
|
|
261
|
+
uint256[] memory upcs = new uint256[](1);
|
|
262
|
+
upcs[0] = 100;
|
|
263
|
+
bytes32[] memory hashes = new bytes32[](1);
|
|
264
|
+
hashes[0] = keccak256("test");
|
|
265
|
+
|
|
266
|
+
vm.prank(alice);
|
|
267
|
+
vm.expectRevert();
|
|
268
|
+
resolver.setSvgHashsOf(upcs, hashes);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
//*********************************************************************//
|
|
272
|
+
// --- setSvgContentsOf ---------------------------------------------- //
|
|
273
|
+
//*********************************************************************//
|
|
274
|
+
|
|
275
|
+
function test_setSvgContentsOf_matchingHash() public {
|
|
276
|
+
string memory content = "test-svg-content";
|
|
277
|
+
|
|
278
|
+
// Store hash first (owner-only).
|
|
279
|
+
uint256[] memory upcs = new uint256[](1);
|
|
280
|
+
upcs[0] = 100;
|
|
281
|
+
bytes32[] memory hashes = new bytes32[](1);
|
|
282
|
+
hashes[0] = keccak256(abi.encodePacked(content));
|
|
283
|
+
|
|
284
|
+
vm.prank(deployer);
|
|
285
|
+
resolver.setSvgHashsOf(upcs, hashes);
|
|
286
|
+
|
|
287
|
+
// Anyone can store content if hash matches.
|
|
288
|
+
string[] memory contents = new string[](1);
|
|
289
|
+
contents[0] = content;
|
|
290
|
+
|
|
291
|
+
vm.prank(alice);
|
|
292
|
+
resolver.setSvgContentsOf(upcs, contents);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function test_setSvgContentsOf_revertsIfNoHash() public {
|
|
296
|
+
uint256[] memory upcs = new uint256[](1);
|
|
297
|
+
upcs[0] = 999; // No hash set.
|
|
298
|
+
string[] memory contents = new string[](1);
|
|
299
|
+
contents[0] = "anything";
|
|
300
|
+
|
|
301
|
+
vm.expectRevert(Banny721TokenUriResolver.Banny721TokenUriResolver_HashNotFound.selector);
|
|
302
|
+
resolver.setSvgContentsOf(upcs, contents);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function test_setSvgContentsOf_revertsIfMismatch() public {
|
|
306
|
+
uint256[] memory upcs = new uint256[](1);
|
|
307
|
+
upcs[0] = 100;
|
|
308
|
+
bytes32[] memory hashes = new bytes32[](1);
|
|
309
|
+
hashes[0] = keccak256(abi.encodePacked("correct-content"));
|
|
310
|
+
|
|
311
|
+
vm.prank(deployer);
|
|
312
|
+
resolver.setSvgHashsOf(upcs, hashes);
|
|
313
|
+
|
|
314
|
+
string[] memory contents = new string[](1);
|
|
315
|
+
contents[0] = "wrong-content";
|
|
316
|
+
|
|
317
|
+
vm.expectRevert(Banny721TokenUriResolver.Banny721TokenUriResolver_ContentsMismatch.selector);
|
|
318
|
+
resolver.setSvgContentsOf(upcs, contents);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function test_setSvgContentsOf_revertsIfAlreadyStored() public {
|
|
322
|
+
string memory content = "test-svg";
|
|
323
|
+
uint256[] memory upcs = new uint256[](1);
|
|
324
|
+
upcs[0] = 100;
|
|
325
|
+
bytes32[] memory hashes = new bytes32[](1);
|
|
326
|
+
hashes[0] = keccak256(abi.encodePacked(content));
|
|
327
|
+
|
|
328
|
+
vm.prank(deployer);
|
|
329
|
+
resolver.setSvgHashsOf(upcs, hashes);
|
|
330
|
+
|
|
331
|
+
string[] memory contents = new string[](1);
|
|
332
|
+
contents[0] = content;
|
|
333
|
+
resolver.setSvgContentsOf(upcs, contents);
|
|
334
|
+
|
|
335
|
+
// Second time reverts.
|
|
336
|
+
vm.expectRevert(Banny721TokenUriResolver.Banny721TokenUriResolver_ContentsAlreadyStored.selector);
|
|
337
|
+
resolver.setSvgContentsOf(upcs, contents);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
//*********************************************************************//
|
|
341
|
+
// --- Lock Mechanism ------------------------------------------------ //
|
|
342
|
+
//*********************************************************************//
|
|
343
|
+
|
|
344
|
+
function test_lockOutfitChangesFor() public {
|
|
345
|
+
vm.prank(alice);
|
|
346
|
+
resolver.lockOutfitChangesFor(address(hook), BODY_TOKEN);
|
|
347
|
+
|
|
348
|
+
uint256 lockedUntil = resolver.outfitLockedUntil(address(hook), BODY_TOKEN);
|
|
349
|
+
assertEq(lockedUntil, block.timestamp + 7 days, "should lock for 7 days");
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function test_lockOutfitChangesFor_revertsIfNotOwner() public {
|
|
353
|
+
vm.prank(bob);
|
|
354
|
+
vm.expectRevert(Banny721TokenUriResolver.Banny721TokenUriResolver_UnauthorizedBannyBody.selector);
|
|
355
|
+
resolver.lockOutfitChangesFor(address(hook), BODY_TOKEN);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function test_lockOutfitChangesFor_extendsLock() public {
|
|
359
|
+
vm.prank(alice);
|
|
360
|
+
resolver.lockOutfitChangesFor(address(hook), BODY_TOKEN);
|
|
361
|
+
uint256 firstLock = resolver.outfitLockedUntil(address(hook), BODY_TOKEN);
|
|
362
|
+
|
|
363
|
+
// Warp forward 3 days and re-lock.
|
|
364
|
+
vm.warp(block.timestamp + 3 days);
|
|
365
|
+
vm.prank(alice);
|
|
366
|
+
resolver.lockOutfitChangesFor(address(hook), BODY_TOKEN);
|
|
367
|
+
uint256 secondLock = resolver.outfitLockedUntil(address(hook), BODY_TOKEN);
|
|
368
|
+
|
|
369
|
+
assertGt(secondLock, firstLock, "re-lock should extend the lock");
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function test_lockOutfitChangesFor_cantAccelerate() public {
|
|
373
|
+
vm.prank(alice);
|
|
374
|
+
resolver.lockOutfitChangesFor(address(hook), BODY_TOKEN);
|
|
375
|
+
|
|
376
|
+
// Try to lock again immediately — new lock would be the same as current, not earlier.
|
|
377
|
+
// The contract checks: currentLockedUntil > newLockUntil.
|
|
378
|
+
// Same value passes (not strictly >). So this should succeed.
|
|
379
|
+
vm.prank(alice);
|
|
380
|
+
resolver.lockOutfitChangesFor(address(hook), BODY_TOKEN);
|
|
381
|
+
// No revert expected since equal is allowed.
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function test_decorateBannyWith_revertsWhenLocked() public {
|
|
385
|
+
vm.prank(alice);
|
|
386
|
+
resolver.lockOutfitChangesFor(address(hook), BODY_TOKEN);
|
|
387
|
+
|
|
388
|
+
uint256[] memory outfitIds = new uint256[](0);
|
|
389
|
+
|
|
390
|
+
vm.prank(alice);
|
|
391
|
+
vm.expectRevert(Banny721TokenUriResolver.Banny721TokenUriResolver_OutfitChangesLocked.selector);
|
|
392
|
+
resolver.decorateBannyWith(address(hook), BODY_TOKEN, 0, outfitIds);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function test_decorateBannyWith_succeedsAfterLockExpires() public {
|
|
396
|
+
vm.prank(alice);
|
|
397
|
+
resolver.lockOutfitChangesFor(address(hook), BODY_TOKEN);
|
|
398
|
+
|
|
399
|
+
// Warp past lock.
|
|
400
|
+
vm.warp(block.timestamp + 7 days + 1);
|
|
401
|
+
|
|
402
|
+
uint256[] memory outfitIds = new uint256[](0);
|
|
403
|
+
vm.prank(alice);
|
|
404
|
+
resolver.decorateBannyWith(address(hook), BODY_TOKEN, 0, outfitIds);
|
|
405
|
+
// Should not revert.
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
//*********************************************************************//
|
|
409
|
+
// --- decorateBannyWith: Authorization ------------------------------- //
|
|
410
|
+
//*********************************************************************//
|
|
411
|
+
|
|
412
|
+
function test_decorateBannyWith_revertsIfNotBodyOwner() public {
|
|
413
|
+
uint256[] memory outfitIds = new uint256[](0);
|
|
414
|
+
|
|
415
|
+
vm.prank(bob);
|
|
416
|
+
vm.expectRevert(Banny721TokenUriResolver.Banny721TokenUriResolver_UnauthorizedBannyBody.selector);
|
|
417
|
+
resolver.decorateBannyWith(address(hook), BODY_TOKEN, 0, outfitIds);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
//*********************************************************************//
|
|
421
|
+
// --- decorateBannyWith: Category Ordering -------------------------- //
|
|
422
|
+
//*********************************************************************//
|
|
423
|
+
|
|
424
|
+
function test_decorateBannyWith_outfitCategoriesMustBeOrdered() public {
|
|
425
|
+
// Pass outfits out of order: mouth (7) before eyes (5).
|
|
426
|
+
uint256[] memory outfitIds = new uint256[](2);
|
|
427
|
+
outfitIds[0] = MOUTH_TOKEN; // category 7
|
|
428
|
+
outfitIds[1] = EYES_TOKEN; // category 5
|
|
429
|
+
|
|
430
|
+
vm.prank(alice);
|
|
431
|
+
vm.expectRevert(Banny721TokenUriResolver.Banny721TokenUriResolver_UnorderedCategories.selector);
|
|
432
|
+
resolver.decorateBannyWith(address(hook), BODY_TOKEN, 0, outfitIds);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
//*********************************************************************//
|
|
436
|
+
// --- decorateBannyWith: Head Conflicts ----------------------------- //
|
|
437
|
+
//*********************************************************************//
|
|
438
|
+
|
|
439
|
+
function test_decorateBannyWith_headConflictsWithEyes() public {
|
|
440
|
+
// Head (4) then Eyes (5) should conflict.
|
|
441
|
+
uint256[] memory outfitIds = new uint256[](2);
|
|
442
|
+
outfitIds[0] = HEAD_TOKEN; // category 4
|
|
443
|
+
outfitIds[1] = EYES_TOKEN; // category 5
|
|
444
|
+
|
|
445
|
+
vm.prank(alice);
|
|
446
|
+
vm.expectRevert(Banny721TokenUriResolver.Banny721TokenUriResolver_HeadAlreadyAdded.selector);
|
|
447
|
+
resolver.decorateBannyWith(address(hook), BODY_TOKEN, 0, outfitIds);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function test_decorateBannyWith_headConflictsWithMouth() public {
|
|
451
|
+
// Head (4) then Mouth (7) should conflict.
|
|
452
|
+
uint256[] memory outfitIds = new uint256[](2);
|
|
453
|
+
outfitIds[0] = HEAD_TOKEN; // category 4
|
|
454
|
+
outfitIds[1] = MOUTH_TOKEN; // category 7
|
|
455
|
+
|
|
456
|
+
vm.prank(alice);
|
|
457
|
+
vm.expectRevert(Banny721TokenUriResolver.Banny721TokenUriResolver_HeadAlreadyAdded.selector);
|
|
458
|
+
resolver.decorateBannyWith(address(hook), BODY_TOKEN, 0, outfitIds);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
//*********************************************************************//
|
|
462
|
+
// --- decorateBannyWith: Suit Conflicts ----------------------------- //
|
|
463
|
+
//*********************************************************************//
|
|
464
|
+
|
|
465
|
+
function test_decorateBannyWith_suitConflictsWithSuitBottom() public {
|
|
466
|
+
// Suit (9) then Suit bottom (10) should conflict.
|
|
467
|
+
uint256[] memory outfitIds = new uint256[](2);
|
|
468
|
+
outfitIds[0] = SUIT_TOKEN; // category 9
|
|
469
|
+
outfitIds[1] = SUIT_BOTTOM_TOKEN; // category 10
|
|
470
|
+
|
|
471
|
+
vm.prank(alice);
|
|
472
|
+
vm.expectRevert(Banny721TokenUriResolver.Banny721TokenUriResolver_SuitAlreadyAdded.selector);
|
|
473
|
+
resolver.decorateBannyWith(address(hook), BODY_TOKEN, 0, outfitIds);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function test_decorateBannyWith_suitConflictsWithSuitTop() public {
|
|
477
|
+
// Suit (9) then Suit top (11) should conflict.
|
|
478
|
+
uint256[] memory outfitIds = new uint256[](2);
|
|
479
|
+
outfitIds[0] = SUIT_TOKEN; // category 9
|
|
480
|
+
outfitIds[1] = SUIT_TOP_TOKEN; // category 11
|
|
481
|
+
|
|
482
|
+
vm.prank(alice);
|
|
483
|
+
vm.expectRevert(Banny721TokenUriResolver.Banny721TokenUriResolver_SuitAlreadyAdded.selector);
|
|
484
|
+
resolver.decorateBannyWith(address(hook), BODY_TOKEN, 0, outfitIds);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
//*********************************************************************//
|
|
488
|
+
// --- decorateBannyWith: Unrecognized Category ---------------------- //
|
|
489
|
+
//*********************************************************************//
|
|
490
|
+
|
|
491
|
+
function test_decorateBannyWith_revertsOnBodyCategory() public {
|
|
492
|
+
// Create a token with category 0 (body) — not allowed as outfit.
|
|
493
|
+
uint256 fakeBadToken = 99_000_000_001;
|
|
494
|
+
_setupTier(fakeBadToken, 99, 0); // category 0
|
|
495
|
+
hook.setOwner(fakeBadToken, alice);
|
|
496
|
+
|
|
497
|
+
uint256[] memory outfitIds = new uint256[](1);
|
|
498
|
+
outfitIds[0] = fakeBadToken;
|
|
499
|
+
|
|
500
|
+
vm.prank(alice);
|
|
501
|
+
vm.expectRevert(Banny721TokenUriResolver.Banny721TokenUriResolver_UnrecognizedCategory.selector);
|
|
502
|
+
resolver.decorateBannyWith(address(hook), BODY_TOKEN, 0, outfitIds);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
function test_decorateBannyWith_revertsOnBackgroundCategory() public {
|
|
506
|
+
// Category 1 (background) is not allowed as an outfit.
|
|
507
|
+
uint256[] memory outfitIds = new uint256[](1);
|
|
508
|
+
outfitIds[0] = BACKGROUND_TOKEN; // category 1
|
|
509
|
+
|
|
510
|
+
vm.prank(alice);
|
|
511
|
+
vm.expectRevert(Banny721TokenUriResolver.Banny721TokenUriResolver_UnrecognizedCategory.selector);
|
|
512
|
+
resolver.decorateBannyWith(address(hook), BODY_TOKEN, 0, outfitIds);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
//*********************************************************************//
|
|
516
|
+
// --- decorateBannyWith: Background --------------------------------- //
|
|
517
|
+
//*********************************************************************//
|
|
518
|
+
|
|
519
|
+
function test_decorateBannyWith_setsBackground() public {
|
|
520
|
+
uint256[] memory outfitIds = new uint256[](0);
|
|
521
|
+
|
|
522
|
+
vm.prank(alice);
|
|
523
|
+
resolver.decorateBannyWith(address(hook), BODY_TOKEN, BACKGROUND_TOKEN, outfitIds);
|
|
524
|
+
|
|
525
|
+
// Verify background is attached.
|
|
526
|
+
(uint256 backgroundId,) = resolver.assetIdsOf(address(hook), BODY_TOKEN);
|
|
527
|
+
assertEq(backgroundId, BACKGROUND_TOKEN, "background should be attached");
|
|
528
|
+
|
|
529
|
+
// Verify the background is "used by" this banny.
|
|
530
|
+
assertEq(resolver.userOf(address(hook), BACKGROUND_TOKEN), BODY_TOKEN, "background user should be banny");
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
function test_decorateBannyWith_removesBackground() public {
|
|
534
|
+
// First attach background.
|
|
535
|
+
uint256[] memory outfitIds = new uint256[](0);
|
|
536
|
+
vm.prank(alice);
|
|
537
|
+
resolver.decorateBannyWith(address(hook), BODY_TOKEN, BACKGROUND_TOKEN, outfitIds);
|
|
538
|
+
|
|
539
|
+
// Now remove by passing 0.
|
|
540
|
+
vm.prank(alice);
|
|
541
|
+
resolver.decorateBannyWith(address(hook), BODY_TOKEN, 0, outfitIds);
|
|
542
|
+
|
|
543
|
+
(uint256 backgroundId,) = resolver.assetIdsOf(address(hook), BODY_TOKEN);
|
|
544
|
+
assertEq(backgroundId, 0, "background should be removed");
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
//*********************************************************************//
|
|
548
|
+
// --- decorateBannyWith: Valid Outfits ------------------------------- //
|
|
549
|
+
//*********************************************************************//
|
|
550
|
+
|
|
551
|
+
function test_decorateBannyWith_singleOutfit() public {
|
|
552
|
+
uint256[] memory outfitIds = new uint256[](1);
|
|
553
|
+
outfitIds[0] = NECKLACE_TOKEN; // category 3
|
|
554
|
+
|
|
555
|
+
vm.prank(alice);
|
|
556
|
+
resolver.decorateBannyWith(address(hook), BODY_TOKEN, 0, outfitIds);
|
|
557
|
+
|
|
558
|
+
// Verify outfit is worn.
|
|
559
|
+
assertEq(resolver.wearerOf(address(hook), NECKLACE_TOKEN), BODY_TOKEN, "necklace should be worn by banny");
|
|
560
|
+
|
|
561
|
+
// Verify token was transferred to resolver.
|
|
562
|
+
assertEq(hook.ownerOf(NECKLACE_TOKEN), address(resolver), "necklace should be held by resolver");
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
function test_decorateBannyWith_multipleOutfits() public {
|
|
566
|
+
// Necklace (3), Eyes (5), Mouth (7) — all ordered correctly.
|
|
567
|
+
uint256[] memory outfitIds = new uint256[](3);
|
|
568
|
+
outfitIds[0] = NECKLACE_TOKEN; // 3
|
|
569
|
+
outfitIds[1] = EYES_TOKEN; // 5
|
|
570
|
+
outfitIds[2] = MOUTH_TOKEN; // 7
|
|
571
|
+
|
|
572
|
+
vm.prank(alice);
|
|
573
|
+
resolver.decorateBannyWith(address(hook), BODY_TOKEN, 0, outfitIds);
|
|
574
|
+
|
|
575
|
+
assertEq(resolver.wearerOf(address(hook), NECKLACE_TOKEN), BODY_TOKEN);
|
|
576
|
+
assertEq(resolver.wearerOf(address(hook), EYES_TOKEN), BODY_TOKEN);
|
|
577
|
+
assertEq(resolver.wearerOf(address(hook), MOUTH_TOKEN), BODY_TOKEN);
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
//*********************************************************************//
|
|
581
|
+
// --- decorateBannyWith: Replace Outfits ----------------------------- //
|
|
582
|
+
//*********************************************************************//
|
|
583
|
+
|
|
584
|
+
function test_decorateBannyWith_replacingOutfitsReturnsOld() public {
|
|
585
|
+
// First: equip necklace.
|
|
586
|
+
uint256[] memory outfitIds1 = new uint256[](1);
|
|
587
|
+
outfitIds1[0] = NECKLACE_TOKEN;
|
|
588
|
+
vm.prank(alice);
|
|
589
|
+
resolver.decorateBannyWith(address(hook), BODY_TOKEN, 0, outfitIds1);
|
|
590
|
+
|
|
591
|
+
// Create a new necklace token.
|
|
592
|
+
uint256 NECKLACE_TOKEN_2 = 11_000_000_001;
|
|
593
|
+
_setupTier(NECKLACE_TOKEN_2, 11, 3); // Same category (3)
|
|
594
|
+
hook.setOwner(NECKLACE_TOKEN_2, alice);
|
|
595
|
+
|
|
596
|
+
// Replace with new necklace. Old one should be returned.
|
|
597
|
+
uint256[] memory outfitIds2 = new uint256[](1);
|
|
598
|
+
outfitIds2[0] = NECKLACE_TOKEN_2;
|
|
599
|
+
vm.prank(alice);
|
|
600
|
+
resolver.decorateBannyWith(address(hook), BODY_TOKEN, 0, outfitIds2);
|
|
601
|
+
|
|
602
|
+
// Old necklace returned to alice.
|
|
603
|
+
assertEq(hook.ownerOf(NECKLACE_TOKEN), alice, "old necklace should be returned");
|
|
604
|
+
// New necklace held by resolver.
|
|
605
|
+
assertEq(hook.ownerOf(NECKLACE_TOKEN_2), address(resolver), "new necklace should be held");
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
//*********************************************************************//
|
|
609
|
+
// --- onERC721Received ---------------------------------------------- //
|
|
610
|
+
//*********************************************************************//
|
|
611
|
+
|
|
612
|
+
function test_onERC721Received_acceptsFromSelf() public {
|
|
613
|
+
bytes4 result = resolver.onERC721Received(address(resolver), alice, 1, "");
|
|
614
|
+
assertEq(result, IERC721Receiver.onERC721Received.selector, "should accept from self");
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
function test_onERC721Received_revertsIfNotSelf() public {
|
|
618
|
+
vm.expectRevert(Banny721TokenUriResolver.Banny721TokenUriResolver_UnauthorizedTransfer.selector);
|
|
619
|
+
resolver.onERC721Received(alice, alice, 1, "");
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
//*********************************************************************//
|
|
623
|
+
// --- View: assetIdsOf with no outfits ------------------------------ //
|
|
624
|
+
//*********************************************************************//
|
|
625
|
+
|
|
626
|
+
function test_assetIdsOf_empty() public {
|
|
627
|
+
(uint256 backgroundId, uint256[] memory outfitIds) = resolver.assetIdsOf(address(hook), BODY_TOKEN);
|
|
628
|
+
assertEq(backgroundId, 0, "no background initially");
|
|
629
|
+
assertEq(outfitIds.length, 0, "no outfits initially");
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
//*********************************************************************//
|
|
633
|
+
// --- View: userOf / wearerOf --------------------------------------- //
|
|
634
|
+
//*********************************************************************//
|
|
635
|
+
|
|
636
|
+
function test_userOf_returnsZeroIfNotAttached() public {
|
|
637
|
+
assertEq(resolver.userOf(address(hook), BACKGROUND_TOKEN), 0, "no user initially");
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
function test_wearerOf_returnsZeroIfNotWorn() public {
|
|
641
|
+
assertEq(resolver.wearerOf(address(hook), NECKLACE_TOKEN), 0, "no wearer initially");
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
//*********************************************************************//
|
|
645
|
+
// --- Helpers ------------------------------------------------------- //
|
|
646
|
+
//*********************************************************************//
|
|
647
|
+
|
|
648
|
+
function _setupTier(uint256 tokenId, uint32 tierId, uint24 category) internal {
|
|
649
|
+
JB721Tier memory tier = JB721Tier({
|
|
650
|
+
id: tierId,
|
|
651
|
+
price: 0.01 ether,
|
|
652
|
+
remainingSupply: 100,
|
|
653
|
+
initialSupply: 100,
|
|
654
|
+
votingUnits: 0,
|
|
655
|
+
reserveFrequency: 0,
|
|
656
|
+
reserveBeneficiary: address(0),
|
|
657
|
+
encodedIPFSUri: bytes32(0),
|
|
658
|
+
category: category,
|
|
659
|
+
discountPercent: 0,
|
|
660
|
+
allowOwnerMint: false,
|
|
661
|
+
transfersPausable: false,
|
|
662
|
+
cannotBeRemoved: false,
|
|
663
|
+
cannotIncreaseDiscountPercent: false,
|
|
664
|
+
resolvedUri: ""
|
|
665
|
+
});
|
|
666
|
+
store.setTier(address(hook), tokenId, tier);
|
|
667
|
+
hook.setTier(tokenId, tierId, category);
|
|
668
|
+
}
|
|
669
|
+
}
|