@bannynet/core-v6 0.0.24 → 0.0.25
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/foundry.toml +2 -1
- package/package.json +22 -12
- package/src/Banny721TokenUriResolver.sol +6 -0
- package/ADMINISTRATION.md +0 -87
- package/ARCHITECTURE.md +0 -101
- package/AUDIT_INSTRUCTIONS.md +0 -78
- package/RISKS.md +0 -80
- package/SKILLS.md +0 -42
- package/STYLE_GUIDE.md +0 -610
- package/USER_JOURNEYS.md +0 -190
- package/foundry.lock +0 -14
- package/slither-ci.config.json +0 -10
- package/sphinx.lock +0 -521
- package/test/Banny721TokenUriResolver.t.sol +0 -694
- package/test/BannyAttacks.t.sol +0 -326
- package/test/DecorateFlow.t.sol +0 -1091
- package/test/Fork.t.sol +0 -2026
- package/test/OutfitTransferLifecycle.t.sol +0 -395
- package/test/TestAuditGaps.sol +0 -724
- package/test/TestQALastMile.t.sol +0 -447
- package/test/audit/AntiStrandingRetention.t.sol +0 -422
- package/test/audit/BurnedBodyStrandsAssets.t.sol +0 -163
- package/test/audit/DuplicateCategoryRetention.t.sol +0 -163
- package/test/audit/MergedOutfitExclusivity.t.sol +0 -228
- package/test/audit/MigrationHelperVerificationBypass.t.sol +0 -102
- package/test/audit/TryTransferFromStrandsAssets.t.sol +0 -197
- package/test/regression/ArrayLengthValidation.t.sol +0 -57
- package/test/regression/BodyCategoryValidation.t.sol +0 -147
- package/test/regression/BurnedTokenCheck.t.sol +0 -186
- package/test/regression/CEIReorder.t.sol +0 -209
- package/test/regression/ClearMetadata.t.sol +0 -52
- package/test/regression/MsgSenderEvents.t.sol +0 -153
- package/test/regression/RemovedTierDesync.t.sol +0 -346
|
@@ -1,422 +0,0 @@
|
|
|
1
|
-
// SPDX-License-Identifier: MIT
|
|
2
|
-
pragma solidity 0.8.28;
|
|
3
|
-
|
|
4
|
-
import {Test} from "forge-std/Test.sol";
|
|
5
|
-
import {JB721Tier} from "@bananapus/721-hook-v6/src/structs/JB721Tier.sol";
|
|
6
|
-
import {JB721TierFlags} from "@bananapus/721-hook-v6/src/structs/JB721TierFlags.sol";
|
|
7
|
-
import {IERC721Receiver} from "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol";
|
|
8
|
-
|
|
9
|
-
import {Banny721TokenUriResolver} from "../../src/Banny721TokenUriResolver.sol";
|
|
10
|
-
|
|
11
|
-
/// @dev Mock hook that supports safeTransferFrom with ERC721Receiver checks.
|
|
12
|
-
contract RetentionMockHook {
|
|
13
|
-
mapping(uint256 tokenId => address) public ownerOf;
|
|
14
|
-
mapping(address owner => mapping(address operator => bool)) public isApprovedForAll;
|
|
15
|
-
|
|
16
|
-
address public immutable MOCK_STORE;
|
|
17
|
-
|
|
18
|
-
constructor(address store) {
|
|
19
|
-
MOCK_STORE = store;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function STORE() external view returns (address) {
|
|
23
|
-
return MOCK_STORE;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function setOwner(uint256 tokenId, address owner) external {
|
|
27
|
-
ownerOf[tokenId] = owner;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function setApprovalForAll(address operator, bool approved) external {
|
|
31
|
-
isApprovedForAll[msg.sender][operator] = approved;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function safeTransferFrom(address from, address to, uint256 tokenId) external {
|
|
35
|
-
require(
|
|
36
|
-
msg.sender == ownerOf[tokenId] || msg.sender == from || isApprovedForAll[from][msg.sender],
|
|
37
|
-
"RetentionMockHook: not authorized"
|
|
38
|
-
);
|
|
39
|
-
|
|
40
|
-
ownerOf[tokenId] = to;
|
|
41
|
-
|
|
42
|
-
if (to.code.length > 0) {
|
|
43
|
-
bytes4 retval = IERC721Receiver(to).onERC721Received(msg.sender, from, tokenId, "");
|
|
44
|
-
require(retval == IERC721Receiver.onERC721Received.selector, "RetentionMockHook: receiver rejected");
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function pricingContext() external pure returns (uint256, uint256, uint256) {
|
|
49
|
-
return (1, 18, 0);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function baseURI() external pure returns (string memory) {
|
|
53
|
-
return "ipfs://";
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/// @dev Mock store that returns tier data.
|
|
58
|
-
contract RetentionMockStore {
|
|
59
|
-
mapping(address hook => mapping(uint256 tokenId => JB721Tier)) public tiers;
|
|
60
|
-
|
|
61
|
-
function setTier(address hook, uint256 tokenId, JB721Tier memory tier) external {
|
|
62
|
-
tiers[hook][tokenId] = tier;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function tierOfTokenId(address hook, uint256 tokenId, bool) external view returns (JB721Tier memory) {
|
|
66
|
-
return tiers[hook][tokenId];
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// forge-lint: disable-next-line(mixed-case-function)
|
|
70
|
-
function encodedIPFSUriOf(address, uint256) external pure returns (bytes32) {
|
|
71
|
-
return bytes32(0);
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/// @dev Contract that does NOT implement IERC721Receiver --transfers to it will revert.
|
|
76
|
-
contract NonReceiverContract {
|
|
77
|
-
function approveResolver(RetentionMockHook hook, address resolver) external {
|
|
78
|
-
hook.setApprovalForAll(resolver, true);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
function decorate(
|
|
82
|
-
Banny721TokenUriResolver resolver,
|
|
83
|
-
address hook,
|
|
84
|
-
uint256 bannyBodyId,
|
|
85
|
-
uint256 backgroundId,
|
|
86
|
-
uint256[] memory outfitIds
|
|
87
|
-
)
|
|
88
|
-
external
|
|
89
|
-
{
|
|
90
|
-
resolver.decorateBannyWith(hook, bannyBodyId, backgroundId, outfitIds);
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/// @dev Contract that DOES implement IERC721Receiver --can receive NFTs.
|
|
95
|
-
contract ReceiverContract is IERC721Receiver {
|
|
96
|
-
bool public canReceive = true;
|
|
97
|
-
|
|
98
|
-
function setCanReceive(bool _canReceive) external {
|
|
99
|
-
canReceive = _canReceive;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
function approveResolver(RetentionMockHook hook, address resolver) external {
|
|
103
|
-
hook.setApprovalForAll(resolver, true);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
function decorate(
|
|
107
|
-
Banny721TokenUriResolver resolver,
|
|
108
|
-
address hook,
|
|
109
|
-
uint256 bannyBodyId,
|
|
110
|
-
uint256 backgroundId,
|
|
111
|
-
uint256[] memory outfitIds
|
|
112
|
-
)
|
|
113
|
-
external
|
|
114
|
-
{
|
|
115
|
-
resolver.decorateBannyWith(hook, bannyBodyId, backgroundId, outfitIds);
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
function onERC721Received(address, address, uint256, bytes calldata) external view override returns (bytes4) {
|
|
119
|
-
require(canReceive, "ReceiverContract: rejecting");
|
|
120
|
-
return IERC721Receiver.onERC721Received.selector;
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
contract AntiStrandingRetentionTest is Test {
|
|
125
|
-
Banny721TokenUriResolver resolver;
|
|
126
|
-
RetentionMockHook hook;
|
|
127
|
-
RetentionMockStore store;
|
|
128
|
-
NonReceiverContract nonReceiver;
|
|
129
|
-
ReceiverContract receiverContract;
|
|
130
|
-
|
|
131
|
-
uint256 constant BODY_TOKEN = 4_000_000_001;
|
|
132
|
-
uint256 constant BG_TOKEN_1 = 5_000_000_001;
|
|
133
|
-
uint256 constant BG_TOKEN_2 = 5_000_000_002;
|
|
134
|
-
uint256 constant NECKLACE_TOKEN = 10_000_000_001;
|
|
135
|
-
uint256 constant HEAD_TOKEN = 11_000_000_001;
|
|
136
|
-
uint256 constant EYES_TOKEN = 12_000_000_001;
|
|
137
|
-
|
|
138
|
-
function setUp() public {
|
|
139
|
-
store = new RetentionMockStore();
|
|
140
|
-
hook = new RetentionMockHook(address(store));
|
|
141
|
-
nonReceiver = new NonReceiverContract();
|
|
142
|
-
receiverContract = new ReceiverContract();
|
|
143
|
-
|
|
144
|
-
resolver = new Banny721TokenUriResolver(
|
|
145
|
-
"<path/>", "<necklace/>", "<mouth/>", "<eyes/>", "<alieneyes/>", address(this), address(0)
|
|
146
|
-
);
|
|
147
|
-
|
|
148
|
-
// Set up tiers: body(cat 0), backgrounds(cat 1), necklace(cat 3), head(cat 4), eyes(cat 5)
|
|
149
|
-
_setupTier(BODY_TOKEN, 4, 0);
|
|
150
|
-
_setupTier(BG_TOKEN_1, 5, 1);
|
|
151
|
-
_setupTier(BG_TOKEN_2, 6, 1);
|
|
152
|
-
_setupTier(NECKLACE_TOKEN, 10, 3);
|
|
153
|
-
_setupTier(HEAD_TOKEN, 11, 4);
|
|
154
|
-
_setupTier(EYES_TOKEN, 12, 5);
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
// -----------------------------------------------------------------------
|
|
158
|
-
// Test 1: Background equip aborted on failed return
|
|
159
|
-
// -----------------------------------------------------------------------
|
|
160
|
-
function test_backgroundEquipAbortedOnFailedReturn() public {
|
|
161
|
-
// Give NonReceiverContract ownership of body and both backgrounds.
|
|
162
|
-
_setOwnerForAll(address(nonReceiver));
|
|
163
|
-
nonReceiver.approveResolver(hook, address(resolver));
|
|
164
|
-
|
|
165
|
-
// Equip BG_TOKEN_1.
|
|
166
|
-
uint256[] memory empty = new uint256[](0);
|
|
167
|
-
nonReceiver.decorate(resolver, address(hook), BODY_TOKEN, BG_TOKEN_1, empty);
|
|
168
|
-
|
|
169
|
-
assertEq(hook.ownerOf(BG_TOKEN_1), address(resolver), "bg1 in resolver custody");
|
|
170
|
-
assertEq(resolver.userOf(address(hook), BG_TOKEN_1), BODY_TOKEN, "bg1 attached to body");
|
|
171
|
-
|
|
172
|
-
// Try to replace with BG_TOKEN_2 --returning BG_TOKEN_1 fails (NonReceiverContract).
|
|
173
|
-
nonReceiver.decorate(resolver, address(hook), BODY_TOKEN, BG_TOKEN_2, empty);
|
|
174
|
-
|
|
175
|
-
// Old background stays attached (return aborted the background change).
|
|
176
|
-
assertEq(resolver.userOf(address(hook), BG_TOKEN_1), BODY_TOKEN, "bg1 still attached --change was aborted");
|
|
177
|
-
|
|
178
|
-
// New background was NOT equipped.
|
|
179
|
-
(uint256 bgId,) = resolver.assetIdsOf(address(hook), BODY_TOKEN);
|
|
180
|
-
assertEq(bgId, BG_TOKEN_1, "body still has bg1, not bg2");
|
|
181
|
-
|
|
182
|
-
// BG_TOKEN_2 stays with the nonReceiver (never transferred in).
|
|
183
|
-
assertEq(hook.ownerOf(BG_TOKEN_2), address(nonReceiver), "bg2 never transferred to resolver");
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
// -----------------------------------------------------------------------
|
|
187
|
-
// Test 2: Background removal preserved on failed return
|
|
188
|
-
// -----------------------------------------------------------------------
|
|
189
|
-
function test_backgroundRemovalPreservedOnFailedReturn() public {
|
|
190
|
-
_setOwnerForAll(address(nonReceiver));
|
|
191
|
-
nonReceiver.approveResolver(hook, address(resolver));
|
|
192
|
-
|
|
193
|
-
// Equip BG_TOKEN_1.
|
|
194
|
-
uint256[] memory empty = new uint256[](0);
|
|
195
|
-
nonReceiver.decorate(resolver, address(hook), BODY_TOKEN, BG_TOKEN_1, empty);
|
|
196
|
-
|
|
197
|
-
assertEq(resolver.userOf(address(hook), BG_TOKEN_1), BODY_TOKEN, "bg1 attached");
|
|
198
|
-
|
|
199
|
-
// Try to remove background (pass 0) --return to NonReceiverContract fails.
|
|
200
|
-
nonReceiver.decorate(resolver, address(hook), BODY_TOKEN, 0, empty);
|
|
201
|
-
|
|
202
|
-
// Background stays attached because the transfer failed and state was preserved.
|
|
203
|
-
assertEq(resolver.userOf(address(hook), BG_TOKEN_1), BODY_TOKEN, "bg1 still attached --removal failed");
|
|
204
|
-
(uint256 bgId,) = resolver.assetIdsOf(address(hook), BODY_TOKEN);
|
|
205
|
-
assertEq(bgId, BG_TOKEN_1, "body still has bg1");
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
// -----------------------------------------------------------------------
|
|
209
|
-
// Test 3: Outfit retained on failed return
|
|
210
|
-
// -----------------------------------------------------------------------
|
|
211
|
-
function test_outfitRetainedOnFailedReturn() public {
|
|
212
|
-
_setOwnerForAll(address(nonReceiver));
|
|
213
|
-
nonReceiver.approveResolver(hook, address(resolver));
|
|
214
|
-
|
|
215
|
-
// Equip necklace outfit.
|
|
216
|
-
uint256[] memory outfits = new uint256[](1);
|
|
217
|
-
outfits[0] = NECKLACE_TOKEN;
|
|
218
|
-
nonReceiver.decorate(resolver, address(hook), BODY_TOKEN, 0, outfits);
|
|
219
|
-
|
|
220
|
-
assertEq(resolver.wearerOf(address(hook), NECKLACE_TOKEN), BODY_TOKEN, "necklace worn by body");
|
|
221
|
-
|
|
222
|
-
// Try to remove outfit (pass empty) --return to NonReceiverContract fails.
|
|
223
|
-
uint256[] memory empty = new uint256[](0);
|
|
224
|
-
nonReceiver.decorate(resolver, address(hook), BODY_TOKEN, 0, empty);
|
|
225
|
-
|
|
226
|
-
// Outfit retained in the attached list.
|
|
227
|
-
assertEq(resolver.wearerOf(address(hook), NECKLACE_TOKEN), BODY_TOKEN, "necklace still worn --retained");
|
|
228
|
-
(, uint256[] memory currentOutfits) = resolver.assetIdsOf(address(hook), BODY_TOKEN);
|
|
229
|
-
assertEq(currentOutfits.length, 1, "one outfit retained");
|
|
230
|
-
assertEq(currentOutfits[0], NECKLACE_TOKEN, "retained outfit is the necklace");
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
// -----------------------------------------------------------------------
|
|
234
|
-
// Test 4: Mixed outfit success/failure --EOA succeeds, contract fails
|
|
235
|
-
// -----------------------------------------------------------------------
|
|
236
|
-
function test_mixedOutfitSuccessAndFailure() public {
|
|
237
|
-
// Set up: body owned by nonReceiver, outfits owned by nonReceiver.
|
|
238
|
-
hook.setOwner(BODY_TOKEN, address(nonReceiver));
|
|
239
|
-
hook.setOwner(NECKLACE_TOKEN, address(nonReceiver));
|
|
240
|
-
hook.setOwner(HEAD_TOKEN, address(nonReceiver));
|
|
241
|
-
hook.setOwner(BG_TOKEN_1, address(nonReceiver));
|
|
242
|
-
hook.setOwner(BG_TOKEN_2, address(nonReceiver));
|
|
243
|
-
hook.setOwner(EYES_TOKEN, address(nonReceiver));
|
|
244
|
-
nonReceiver.approveResolver(hook, address(resolver));
|
|
245
|
-
|
|
246
|
-
// Equip necklace and head.
|
|
247
|
-
uint256[] memory outfits = new uint256[](2);
|
|
248
|
-
outfits[0] = NECKLACE_TOKEN;
|
|
249
|
-
outfits[1] = HEAD_TOKEN;
|
|
250
|
-
nonReceiver.decorate(resolver, address(hook), BODY_TOKEN, 0, outfits);
|
|
251
|
-
|
|
252
|
-
assertEq(resolver.wearerOf(address(hook), NECKLACE_TOKEN), BODY_TOKEN, "necklace worn");
|
|
253
|
-
assertEq(resolver.wearerOf(address(hook), HEAD_TOKEN), BODY_TOKEN, "head worn");
|
|
254
|
-
|
|
255
|
-
// Now try to remove all outfits --both transfers will fail (NonReceiverContract).
|
|
256
|
-
uint256[] memory empty = new uint256[](0);
|
|
257
|
-
nonReceiver.decorate(resolver, address(hook), BODY_TOKEN, 0, empty);
|
|
258
|
-
|
|
259
|
-
// Both should be retained.
|
|
260
|
-
(, uint256[] memory currentOutfits) = resolver.assetIdsOf(address(hook), BODY_TOKEN);
|
|
261
|
-
assertEq(currentOutfits.length, 2, "both outfits retained");
|
|
262
|
-
|
|
263
|
-
// Order: retained items are appended after new (empty), so they appear in order.
|
|
264
|
-
assertEq(resolver.wearerOf(address(hook), NECKLACE_TOKEN), BODY_TOKEN, "necklace still worn");
|
|
265
|
-
assertEq(resolver.wearerOf(address(hook), HEAD_TOKEN), BODY_TOKEN, "head still worn");
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
// -----------------------------------------------------------------------
|
|
269
|
-
// Test 4b: Retained outfits are re-sorted before being stored
|
|
270
|
-
// -----------------------------------------------------------------------
|
|
271
|
-
function test_retainedOutfitsAreResortedBeforeStorage() public {
|
|
272
|
-
// Give the rejecting contract ownership so returns to the owner will fail and force retention.
|
|
273
|
-
_setOwnerForAll(address(nonReceiver));
|
|
274
|
-
nonReceiver.approveResolver(hook, address(resolver));
|
|
275
|
-
|
|
276
|
-
// Equip a lower-category outfit first so it becomes the retained item in the next decoration.
|
|
277
|
-
uint256[] memory necklaceOnly = new uint256[](1);
|
|
278
|
-
necklaceOnly[0] = NECKLACE_TOKEN;
|
|
279
|
-
nonReceiver.decorate(resolver, address(hook), BODY_TOKEN, 0, necklaceOnly);
|
|
280
|
-
|
|
281
|
-
// Add a higher-category outfit while the necklace return fails, forcing the stored list to be merged.
|
|
282
|
-
uint256[] memory headOnly = new uint256[](1);
|
|
283
|
-
headOnly[0] = HEAD_TOKEN;
|
|
284
|
-
nonReceiver.decorate(resolver, address(hook), BODY_TOKEN, 0, headOnly);
|
|
285
|
-
|
|
286
|
-
// The merged list should be stored in ascending category order, not append order.
|
|
287
|
-
(, uint256[] memory currentOutfits) = resolver.assetIdsOf(address(hook), BODY_TOKEN);
|
|
288
|
-
assertEq(currentOutfits.length, 2, "both outfits should remain attached");
|
|
289
|
-
assertEq(currentOutfits[0], NECKLACE_TOKEN, "lower-category retained outfit should stay first");
|
|
290
|
-
assertEq(currentOutfits[1], HEAD_TOKEN, "higher-category new outfit should stay second");
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
// -----------------------------------------------------------------------
|
|
294
|
-
// Test 5: Recovery path --make contract receivable, retry decoration
|
|
295
|
-
// -----------------------------------------------------------------------
|
|
296
|
-
function test_recoveryAfterMakingContractReceivable() public {
|
|
297
|
-
// Use ReceiverContract with canReceive initially set to false.
|
|
298
|
-
hook.setOwner(BODY_TOKEN, address(receiverContract));
|
|
299
|
-
hook.setOwner(NECKLACE_TOKEN, address(receiverContract));
|
|
300
|
-
hook.setOwner(BG_TOKEN_1, address(receiverContract));
|
|
301
|
-
hook.setOwner(BG_TOKEN_2, address(receiverContract));
|
|
302
|
-
hook.setOwner(HEAD_TOKEN, address(receiverContract));
|
|
303
|
-
hook.setOwner(EYES_TOKEN, address(receiverContract));
|
|
304
|
-
receiverContract.approveResolver(hook, address(resolver));
|
|
305
|
-
|
|
306
|
-
// Equip necklace with receiver accepting.
|
|
307
|
-
uint256[] memory outfits = new uint256[](1);
|
|
308
|
-
outfits[0] = NECKLACE_TOKEN;
|
|
309
|
-
receiverContract.decorate(resolver, address(hook), BODY_TOKEN, BG_TOKEN_1, outfits);
|
|
310
|
-
|
|
311
|
-
assertEq(resolver.wearerOf(address(hook), NECKLACE_TOKEN), BODY_TOKEN, "necklace worn");
|
|
312
|
-
assertEq(resolver.userOf(address(hook), BG_TOKEN_1), BODY_TOKEN, "bg1 attached");
|
|
313
|
-
|
|
314
|
-
// Now disable receiving --simulates the "contract can't receive" scenario.
|
|
315
|
-
receiverContract.setCanReceive(false);
|
|
316
|
-
|
|
317
|
-
// Try to undress --transfers will fail.
|
|
318
|
-
uint256[] memory empty = new uint256[](0);
|
|
319
|
-
receiverContract.decorate(resolver, address(hook), BODY_TOKEN, 0, empty);
|
|
320
|
-
|
|
321
|
-
// Assets retained.
|
|
322
|
-
assertEq(resolver.wearerOf(address(hook), NECKLACE_TOKEN), BODY_TOKEN, "necklace retained");
|
|
323
|
-
assertEq(resolver.userOf(address(hook), BG_TOKEN_1), BODY_TOKEN, "bg1 retained");
|
|
324
|
-
|
|
325
|
-
// Re-enable receiving.
|
|
326
|
-
receiverContract.setCanReceive(true);
|
|
327
|
-
|
|
328
|
-
// Retry undress --now transfers should succeed.
|
|
329
|
-
receiverContract.decorate(resolver, address(hook), BODY_TOKEN, 0, empty);
|
|
330
|
-
|
|
331
|
-
// Background successfully removed.
|
|
332
|
-
(uint256 bgId, uint256[] memory currentOutfits) = resolver.assetIdsOf(address(hook), BODY_TOKEN);
|
|
333
|
-
assertEq(bgId, 0, "bg removed after recovery");
|
|
334
|
-
assertEq(currentOutfits.length, 0, "outfit removed after recovery");
|
|
335
|
-
|
|
336
|
-
// NFTs returned to owner.
|
|
337
|
-
assertEq(hook.ownerOf(BG_TOKEN_1), address(receiverContract), "bg1 returned to owner");
|
|
338
|
-
assertEq(hook.ownerOf(NECKLACE_TOKEN), address(receiverContract), "necklace returned to owner");
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
// -----------------------------------------------------------------------
|
|
342
|
-
// Test 6: Happy path unchanged --EOA owner equip/unequip works as before
|
|
343
|
-
// -----------------------------------------------------------------------
|
|
344
|
-
function test_happyPathUnchanged_EOAOwner() public {
|
|
345
|
-
address alice = makeAddr("alice");
|
|
346
|
-
|
|
347
|
-
// Set up: Alice (EOA) owns everything.
|
|
348
|
-
hook.setOwner(BODY_TOKEN, alice);
|
|
349
|
-
hook.setOwner(NECKLACE_TOKEN, alice);
|
|
350
|
-
hook.setOwner(HEAD_TOKEN, alice);
|
|
351
|
-
hook.setOwner(BG_TOKEN_1, alice);
|
|
352
|
-
hook.setOwner(BG_TOKEN_2, alice);
|
|
353
|
-
hook.setOwner(EYES_TOKEN, alice);
|
|
354
|
-
|
|
355
|
-
vm.startPrank(alice);
|
|
356
|
-
hook.setApprovalForAll(address(resolver), true);
|
|
357
|
-
|
|
358
|
-
// Equip background + necklace.
|
|
359
|
-
uint256[] memory outfits = new uint256[](1);
|
|
360
|
-
outfits[0] = NECKLACE_TOKEN;
|
|
361
|
-
resolver.decorateBannyWith(address(hook), BODY_TOKEN, BG_TOKEN_1, outfits);
|
|
362
|
-
|
|
363
|
-
assertEq(hook.ownerOf(BG_TOKEN_1), address(resolver), "bg in resolver custody");
|
|
364
|
-
assertEq(hook.ownerOf(NECKLACE_TOKEN), address(resolver), "necklace in resolver custody");
|
|
365
|
-
assertEq(resolver.userOf(address(hook), BG_TOKEN_1), BODY_TOKEN, "bg attached");
|
|
366
|
-
assertEq(resolver.wearerOf(address(hook), NECKLACE_TOKEN), BODY_TOKEN, "necklace worn");
|
|
367
|
-
|
|
368
|
-
// Undress --should work fine for EOA.
|
|
369
|
-
uint256[] memory empty = new uint256[](0);
|
|
370
|
-
resolver.decorateBannyWith(address(hook), BODY_TOKEN, 0, empty);
|
|
371
|
-
|
|
372
|
-
// NFTs returned to Alice.
|
|
373
|
-
assertEq(hook.ownerOf(BG_TOKEN_1), alice, "bg returned to alice");
|
|
374
|
-
assertEq(hook.ownerOf(NECKLACE_TOKEN), alice, "necklace returned to alice");
|
|
375
|
-
|
|
376
|
-
// State cleared.
|
|
377
|
-
(uint256 bgId, uint256[] memory currentOutfits) = resolver.assetIdsOf(address(hook), BODY_TOKEN);
|
|
378
|
-
assertEq(bgId, 0, "no bg attached");
|
|
379
|
-
assertEq(currentOutfits.length, 0, "no outfits attached");
|
|
380
|
-
|
|
381
|
-
vm.stopPrank();
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
// -----------------------------------------------------------------------
|
|
385
|
-
// Helpers
|
|
386
|
-
// -----------------------------------------------------------------------
|
|
387
|
-
|
|
388
|
-
function _setOwnerForAll(address owner) internal {
|
|
389
|
-
hook.setOwner(BODY_TOKEN, owner);
|
|
390
|
-
hook.setOwner(BG_TOKEN_1, owner);
|
|
391
|
-
hook.setOwner(BG_TOKEN_2, owner);
|
|
392
|
-
hook.setOwner(NECKLACE_TOKEN, owner);
|
|
393
|
-
hook.setOwner(HEAD_TOKEN, owner);
|
|
394
|
-
hook.setOwner(EYES_TOKEN, owner);
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
function _setupTier(uint256 tokenId, uint32 tierId, uint24 category) internal {
|
|
398
|
-
JB721Tier memory tier = JB721Tier({
|
|
399
|
-
id: tierId,
|
|
400
|
-
price: 0.01 ether,
|
|
401
|
-
remainingSupply: 100,
|
|
402
|
-
initialSupply: 100,
|
|
403
|
-
votingUnits: 0,
|
|
404
|
-
reserveFrequency: 0,
|
|
405
|
-
reserveBeneficiary: address(0),
|
|
406
|
-
encodedIPFSUri: bytes32(0),
|
|
407
|
-
category: category,
|
|
408
|
-
discountPercent: 0,
|
|
409
|
-
flags: JB721TierFlags({
|
|
410
|
-
allowOwnerMint: false,
|
|
411
|
-
transfersPausable: false,
|
|
412
|
-
cantBeRemoved: false,
|
|
413
|
-
cantIncreaseDiscountPercent: false,
|
|
414
|
-
cantBuyWithCredits: false
|
|
415
|
-
}),
|
|
416
|
-
splitPercent: 0,
|
|
417
|
-
resolvedUri: ""
|
|
418
|
-
});
|
|
419
|
-
|
|
420
|
-
store.setTier(address(hook), tokenId, tier);
|
|
421
|
-
}
|
|
422
|
-
}
|
|
@@ -1,163 +0,0 @@
|
|
|
1
|
-
// SPDX-License-Identifier: MIT
|
|
2
|
-
pragma solidity 0.8.28;
|
|
3
|
-
|
|
4
|
-
import {Test} from "forge-std/Test.sol";
|
|
5
|
-
import {JB721Tier} from "@bananapus/721-hook-v6/src/structs/JB721Tier.sol";
|
|
6
|
-
import {JB721TierFlags} from "@bananapus/721-hook-v6/src/structs/JB721TierFlags.sol";
|
|
7
|
-
import {IERC721Receiver} from "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol";
|
|
8
|
-
|
|
9
|
-
import {Banny721TokenUriResolver} from "../../src/Banny721TokenUriResolver.sol";
|
|
10
|
-
|
|
11
|
-
contract MockBurnableHook is Test {
|
|
12
|
-
mapping(uint256 => address) public owners;
|
|
13
|
-
mapping(address => mapping(address => bool)) public isApprovedForAll;
|
|
14
|
-
address public immutable MOCK_STORE;
|
|
15
|
-
|
|
16
|
-
constructor(address store) {
|
|
17
|
-
MOCK_STORE = store;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function STORE() external view returns (address) {
|
|
21
|
-
return MOCK_STORE;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function setOwner(uint256 tokenId, address owner) external {
|
|
25
|
-
owners[tokenId] = owner;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function ownerOf(uint256 tokenId) external view returns (address) {
|
|
29
|
-
address owner = owners[tokenId];
|
|
30
|
-
require(owner != address(0), "ERC721: token does not exist");
|
|
31
|
-
return owner;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function burn(uint256 tokenId) external {
|
|
35
|
-
owners[tokenId] = address(0);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function setApprovalForAll(address operator, bool approved) external {
|
|
39
|
-
isApprovedForAll[msg.sender][operator] = approved;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function safeTransferFrom(address from, address to, uint256 tokenId) external {
|
|
43
|
-
address owner = owners[tokenId];
|
|
44
|
-
require(owner != address(0), "ERC721: token does not exist");
|
|
45
|
-
require(
|
|
46
|
-
msg.sender == owner || msg.sender == from || isApprovedForAll[from][msg.sender], "MockHook: not authorized"
|
|
47
|
-
);
|
|
48
|
-
owners[tokenId] = to;
|
|
49
|
-
if (to.code.length > 0) {
|
|
50
|
-
bytes4 retval = IERC721Receiver(to).onERC721Received(msg.sender, from, tokenId, "");
|
|
51
|
-
require(retval == IERC721Receiver.onERC721Received.selector, "MockHook: receiver rejected");
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function pricingContext() external pure returns (uint256, uint256) {
|
|
56
|
-
return (1, 18);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function baseURI() external pure returns (string memory) {
|
|
60
|
-
return "ipfs://";
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
contract MockBurnableStore {
|
|
65
|
-
mapping(address => mapping(uint256 => JB721Tier)) public tiers;
|
|
66
|
-
|
|
67
|
-
function setTier(address hook, uint256 tokenId, JB721Tier memory tier) external {
|
|
68
|
-
tiers[hook][tokenId] = tier;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function tierOfTokenId(address hook, uint256 tokenId, bool) external view returns (JB721Tier memory) {
|
|
72
|
-
return tiers[hook][tokenId];
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// forge-lint: disable-next-line(mixed-case-function)
|
|
76
|
-
function encodedIPFSUriOf(address, uint256) external pure returns (bytes32) {
|
|
77
|
-
return bytes32(0);
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
contract BurnedBodyStrandsAssetsTest is Test {
|
|
82
|
-
Banny721TokenUriResolver resolver;
|
|
83
|
-
MockBurnableHook hook;
|
|
84
|
-
MockBurnableStore store;
|
|
85
|
-
|
|
86
|
-
address deployer = makeAddr("deployer");
|
|
87
|
-
address alice = makeAddr("alice");
|
|
88
|
-
|
|
89
|
-
uint256 constant BODY1 = 1_000_000_001;
|
|
90
|
-
uint256 constant BODY2 = 1_000_000_002;
|
|
91
|
-
uint256 constant BACKGROUND = 2_000_000_001;
|
|
92
|
-
uint256 constant OUTFIT = 3_000_000_001;
|
|
93
|
-
|
|
94
|
-
function setUp() public {
|
|
95
|
-
store = new MockBurnableStore();
|
|
96
|
-
hook = new MockBurnableHook(address(store));
|
|
97
|
-
|
|
98
|
-
vm.prank(deployer);
|
|
99
|
-
resolver = new Banny721TokenUriResolver(
|
|
100
|
-
"<path/>", "<necklace/>", "<mouth/>", "<eyes/>", "<alieneyes/>", deployer, address(0)
|
|
101
|
-
);
|
|
102
|
-
|
|
103
|
-
_setupTier(BODY1, 1, 0);
|
|
104
|
-
_setupTier(BODY2, 1, 0);
|
|
105
|
-
_setupTier(BACKGROUND, 2, 1);
|
|
106
|
-
_setupTier(OUTFIT, 3, 2);
|
|
107
|
-
|
|
108
|
-
hook.setOwner(BODY1, alice);
|
|
109
|
-
hook.setOwner(BODY2, alice);
|
|
110
|
-
hook.setOwner(BACKGROUND, alice);
|
|
111
|
-
hook.setOwner(OUTFIT, alice);
|
|
112
|
-
|
|
113
|
-
vm.prank(alice);
|
|
114
|
-
hook.setApprovalForAll(address(resolver), true);
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
function test_burningDressedBodyPermanentlyStrandsAttachedAssets() public {
|
|
118
|
-
uint256[] memory outfitIds = new uint256[](1);
|
|
119
|
-
outfitIds[0] = OUTFIT;
|
|
120
|
-
|
|
121
|
-
vm.prank(alice);
|
|
122
|
-
resolver.decorateBannyWith(address(hook), BODY1, BACKGROUND, outfitIds);
|
|
123
|
-
|
|
124
|
-
assertEq(resolver.userOf(address(hook), BACKGROUND), BODY1);
|
|
125
|
-
assertEq(resolver.wearerOf(address(hook), OUTFIT), BODY1);
|
|
126
|
-
|
|
127
|
-
hook.burn(BODY1);
|
|
128
|
-
|
|
129
|
-
vm.expectRevert(bytes("ERC721: token does not exist"));
|
|
130
|
-
vm.prank(alice);
|
|
131
|
-
resolver.decorateBannyWith(address(hook), BODY2, BACKGROUND, outfitIds);
|
|
132
|
-
|
|
133
|
-
uint256[] memory emptyOutfits = new uint256[](0);
|
|
134
|
-
vm.expectRevert(bytes("ERC721: token does not exist"));
|
|
135
|
-
vm.prank(alice);
|
|
136
|
-
resolver.decorateBannyWith(address(hook), BODY1, 0, emptyOutfits);
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
function _setupTier(uint256 tokenId, uint32 tierId, uint24 category) internal {
|
|
140
|
-
JB721Tier memory tier = JB721Tier({
|
|
141
|
-
id: tierId,
|
|
142
|
-
price: 0.01 ether,
|
|
143
|
-
remainingSupply: 100,
|
|
144
|
-
initialSupply: 100,
|
|
145
|
-
votingUnits: 0,
|
|
146
|
-
reserveFrequency: 0,
|
|
147
|
-
reserveBeneficiary: address(0),
|
|
148
|
-
encodedIPFSUri: bytes32(0),
|
|
149
|
-
category: category,
|
|
150
|
-
discountPercent: 0,
|
|
151
|
-
flags: JB721TierFlags({
|
|
152
|
-
allowOwnerMint: false,
|
|
153
|
-
transfersPausable: false,
|
|
154
|
-
cantBeRemoved: false,
|
|
155
|
-
cantIncreaseDiscountPercent: false,
|
|
156
|
-
cantBuyWithCredits: false
|
|
157
|
-
}),
|
|
158
|
-
splitPercent: 0,
|
|
159
|
-
resolvedUri: ""
|
|
160
|
-
});
|
|
161
|
-
store.setTier(address(hook), tokenId, tier);
|
|
162
|
-
}
|
|
163
|
-
}
|