@bannynet/core-v6 0.0.14 → 0.0.16

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/ARCHITECTURE.md CHANGED
@@ -96,3 +96,5 @@ For IPFS-backed assets without on-chain SVG content, the resolver falls back to
96
96
  **Fixed category ordering for outfit layering.** Outfits must be passed in ascending category order (2-17) and only one outfit per category is allowed. This constraint eliminates ambiguity in SVG z-ordering -- the category number directly determines the layer position. It also enables the resolver to insert default accessories (necklace, eyes, mouth) at the correct z-position when no custom one is equipped and no full-head item occludes them.
97
97
 
98
98
  **Equipped assets travel with the body.** When a body NFT is transferred, all equipped outfits and backgrounds remain attached. The new owner inherits them and can unequip to receive the outfit NFTs. This was chosen over auto-unequip to preserve the dressed Banny as a complete visual unit, but it means sellers should unequip valuable outfits before listing.
99
+
100
+ **Outfits burn with the body.** When a body NFT is burned, equipped outfits and backgrounds held by the resolver become permanently unrecoverable. There is no recovery function — outfits share the body's fate. Users must unequip outfits before burning the body if they want to keep them.
package/RISKS.md CHANGED
@@ -63,6 +63,10 @@ For permanently unrecoverable assets (burned NFTs, removed tiers), the retained
63
63
 
64
64
  `tokenUriOf` constructs full SVGs on-chain with string concatenation. Measured gas ceiling: ~609K gas for the worst case (9 non-conflicting outfits + background with on-chain SVG content), well within typical RPC node limits (30M+). Regression test: `test_tokenUri_gasSnapshot_9outfits` in `test/TestQALastMile.t.sol`.
65
65
 
66
- ### 7.4 Reentrancy in non-guarded functions is harmless
66
+ ### 7.4 Outfits burn alongside the body
67
+
68
+ When a banny body NFT is burned (e.g. via cash-out), any equipped outfits and backgrounds held by the resolver are permanently unrecoverable. The resolver has no recovery function and this is intentional — outfits are part of the body's identity and share its fate. Users who want to preserve outfits must unequip them before burning the body.
69
+
70
+ ### 7.5 Reentrancy in non-guarded functions is harmless
67
71
 
68
72
  `lockOutfitChangesFor` and all view functions (`tokenUriOf`, `svgOf`) are not protected by `nonReentrant`. A malicious hook's `STORE().tierOfTokenId()` could re-enter `lockOutfitChangesFor` during a `tokenUriOf` call, but this is harmless -- `lockOutfitChangesFor` only extends the lock timestamp (monotonically non-decreasing) and has no state that could be corrupted by reentrancy. The view functions themselves are read-only at the contract level (no storage writes), so reentrancy through them cannot extract value.
@@ -0,0 +1,34 @@
1
+ # 🔐 Security Review — banny-retail-v6
2
+
3
+ ---
4
+
5
+ ## Scope
6
+
7
+ | | |
8
+ | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------- |
9
+ | **Mode** | ALL / default |
10
+ | **Files reviewed** | `Add.Denver.s.sol` · `Deploy.s.sol` · `Drop1.s.sol`<br>`BannyverseDeploymentLib.sol` · `MigrationHelper.sol` · `Banny721TokenUriResolver.sol` |
11
+ | **Confidence threshold (1-100)** | 75 |
12
+
13
+ ---
14
+
15
+ ## Findings
16
+
17
+ _No confirmed findings._
18
+
19
+ ---
20
+
21
+ Findings List
22
+
23
+ | # | Confidence | Title |
24
+ |---|---|---|
25
+
26
+ ---
27
+
28
+ ## Leads
29
+
30
+ _None._
31
+
32
+ ---
33
+
34
+ > ⚠️ This review was performed by an AI assistant. AI analysis can never verify the complete absence of vulnerabilities and no guarantee of security is given. Team security reviews, bug bounty programs, and on-chain monitoring are strongly recommended. For a consultation regarding your projects' security, visit [https://www.pashov.com](https://www.pashov.com)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bannynet/core-v6",
3
- "version": "0.0.14",
3
+ "version": "0.0.16",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -20,14 +20,14 @@
20
20
  "artifacts": "source ./.env && npx sphinx artifacts --org-id 'ea165b21-7cdc-4d7b-be59-ecdd4c26bee4' --project-name 'banny-core-v6'"
21
21
  },
22
22
  "dependencies": {
23
- "@bananapus/721-hook-v6": "^0.0.22",
23
+ "@bananapus/721-hook-v6": "^0.0.26",
24
24
  "@bananapus/core-v6": "^0.0.28",
25
25
  "@bananapus/permission-ids-v6": "^0.0.14",
26
26
  "@bananapus/router-terminal-v6": "^0.0.21",
27
27
  "@bananapus/suckers-v6": "^0.0.18",
28
- "@croptop/core-v6": "^0.0.23",
28
+ "@croptop/core-v6": "^0.0.26",
29
29
  "@openzeppelin/contracts": "^5.6.1",
30
- "@rev-net/core-v6": "^0.0.18",
30
+ "@rev-net/core-v6": "^0.0.24",
31
31
  "keccak": "^3.0.4"
32
32
  },
33
33
  "devDependencies": {
@@ -44,7 +44,7 @@ contract Drop1Script is Script, Sphinx {
44
44
  );
45
45
 
46
46
  // Get the hook address by using the deployer.
47
- hook = JB721TiersHook(address(revnet.basic_deployer.tiered721HookOf(bannyverse.revnetId)));
47
+ hook = JB721TiersHook(address(revnet.owner.tiered721HookOf(bannyverse.revnetId)));
48
48
  deploy();
49
49
  }
50
50
 
@@ -67,6 +67,7 @@ contract Drop1Script is Script, Sphinx {
67
67
  category: 1,
68
68
  discountPercent: 0,
69
69
  cannotIncreaseDiscountPercent: false,
70
+ cantBuyWithCredits: false,
70
71
  allowOwnerMint: false,
71
72
  useReserveBeneficiaryAsDefault: false,
72
73
  transfersPausable: false,
@@ -230,6 +230,7 @@ contract DeployScript is Script, Sphinx {
230
230
  category: BANNY_BODY_CATEGORY,
231
231
  discountPercent: 0,
232
232
  cannotIncreaseDiscountPercent: true,
233
+ cantBuyWithCredits: false,
233
234
  allowOwnerMint: false,
234
235
  useReserveBeneficiaryAsDefault: false,
235
236
  transfersPausable: false,
@@ -248,6 +249,7 @@ contract DeployScript is Script, Sphinx {
248
249
  category: BANNY_BODY_CATEGORY,
249
250
  discountPercent: 0,
250
251
  cannotIncreaseDiscountPercent: true,
252
+ cantBuyWithCredits: false,
251
253
  allowOwnerMint: false,
252
254
  useReserveBeneficiaryAsDefault: false,
253
255
  transfersPausable: false,
@@ -266,6 +268,7 @@ contract DeployScript is Script, Sphinx {
266
268
  category: BANNY_BODY_CATEGORY,
267
269
  discountPercent: 0,
268
270
  cannotIncreaseDiscountPercent: true,
271
+ cantBuyWithCredits: false,
269
272
  allowOwnerMint: false,
270
273
  useReserveBeneficiaryAsDefault: false,
271
274
  transfersPausable: false,
@@ -284,6 +287,7 @@ contract DeployScript is Script, Sphinx {
284
287
  category: BANNY_BODY_CATEGORY,
285
288
  discountPercent: 0,
286
289
  cannotIncreaseDiscountPercent: true,
290
+ cantBuyWithCredits: false,
287
291
  allowOwnerMint: false,
288
292
  useReserveBeneficiaryAsDefault: false,
289
293
  transfersPausable: false,
@@ -44,7 +44,7 @@ contract Drop1Script is Script, Sphinx {
44
44
  );
45
45
 
46
46
  // Get the hook address by using the deployer.
47
- hook = JB721TiersHook(address(revnet.basic_deployer.tiered721HookOf(bannyverse.revnetId)));
47
+ hook = JB721TiersHook(address(revnet.owner.tiered721HookOf(bannyverse.revnetId)));
48
48
  deploy();
49
49
  }
50
50
 
@@ -68,6 +68,7 @@ contract Drop1Script is Script, Sphinx {
68
68
  category: 1,
69
69
  discountPercent: 0,
70
70
  cannotIncreaseDiscountPercent: false,
71
+ cantBuyWithCredits: false,
71
72
  allowOwnerMint: false,
72
73
  useReserveBeneficiaryAsDefault: false,
73
74
  transfersPausable: false,
@@ -89,6 +90,7 @@ contract Drop1Script is Script, Sphinx {
89
90
  category: 1,
90
91
  discountPercent: 0,
91
92
  cannotIncreaseDiscountPercent: false,
93
+ cantBuyWithCredits: false,
92
94
  allowOwnerMint: false,
93
95
  useReserveBeneficiaryAsDefault: false,
94
96
  transfersPausable: false,
@@ -110,6 +112,7 @@ contract Drop1Script is Script, Sphinx {
110
112
  category: 2,
111
113
  discountPercent: 0,
112
114
  cannotIncreaseDiscountPercent: false,
115
+ cantBuyWithCredits: false,
113
116
  allowOwnerMint: false,
114
117
  useReserveBeneficiaryAsDefault: false,
115
118
  transfersPausable: false,
@@ -131,6 +134,7 @@ contract Drop1Script is Script, Sphinx {
131
134
  category: 2,
132
135
  discountPercent: 0,
133
136
  cannotIncreaseDiscountPercent: false,
137
+ cantBuyWithCredits: false,
134
138
  allowOwnerMint: false,
135
139
  useReserveBeneficiaryAsDefault: false,
136
140
  transfersPausable: false,
@@ -152,6 +156,7 @@ contract Drop1Script is Script, Sphinx {
152
156
  category: 3,
153
157
  discountPercent: 0,
154
158
  cannotIncreaseDiscountPercent: false,
159
+ cantBuyWithCredits: false,
155
160
  allowOwnerMint: false,
156
161
  useReserveBeneficiaryAsDefault: false,
157
162
  transfersPausable: false,
@@ -173,6 +178,7 @@ contract Drop1Script is Script, Sphinx {
173
178
  category: 4,
174
179
  discountPercent: 0,
175
180
  cannotIncreaseDiscountPercent: false,
181
+ cantBuyWithCredits: false,
176
182
  allowOwnerMint: false,
177
183
  useReserveBeneficiaryAsDefault: false,
178
184
  transfersPausable: false,
@@ -194,6 +200,7 @@ contract Drop1Script is Script, Sphinx {
194
200
  category: 6,
195
201
  discountPercent: 0,
196
202
  cannotIncreaseDiscountPercent: false,
203
+ cantBuyWithCredits: false,
197
204
  allowOwnerMint: false,
198
205
  useReserveBeneficiaryAsDefault: false,
199
206
  transfersPausable: false,
@@ -215,6 +222,7 @@ contract Drop1Script is Script, Sphinx {
215
222
  category: 6,
216
223
  discountPercent: 0,
217
224
  cannotIncreaseDiscountPercent: false,
225
+ cantBuyWithCredits: false,
218
226
  allowOwnerMint: false,
219
227
  useReserveBeneficiaryAsDefault: true,
220
228
  transfersPausable: false,
@@ -236,6 +244,7 @@ contract Drop1Script is Script, Sphinx {
236
244
  category: 6,
237
245
  discountPercent: 0,
238
246
  cannotIncreaseDiscountPercent: false,
247
+ cantBuyWithCredits: false,
239
248
  allowOwnerMint: false,
240
249
  useReserveBeneficiaryAsDefault: false,
241
250
  transfersPausable: false,
@@ -257,6 +266,7 @@ contract Drop1Script is Script, Sphinx {
257
266
  category: 6,
258
267
  discountPercent: 0,
259
268
  cannotIncreaseDiscountPercent: false,
269
+ cantBuyWithCredits: false,
260
270
  allowOwnerMint: false,
261
271
  useReserveBeneficiaryAsDefault: false,
262
272
  transfersPausable: false,
@@ -278,6 +288,7 @@ contract Drop1Script is Script, Sphinx {
278
288
  category: 6,
279
289
  discountPercent: 0,
280
290
  cannotIncreaseDiscountPercent: false,
291
+ cantBuyWithCredits: false,
281
292
  allowOwnerMint: false,
282
293
  useReserveBeneficiaryAsDefault: false,
283
294
  transfersPausable: false,
@@ -299,6 +310,7 @@ contract Drop1Script is Script, Sphinx {
299
310
  category: 7,
300
311
  discountPercent: 0,
301
312
  cannotIncreaseDiscountPercent: false,
313
+ cantBuyWithCredits: false,
302
314
  allowOwnerMint: false,
303
315
  useReserveBeneficiaryAsDefault: false,
304
316
  transfersPausable: false,
@@ -320,6 +332,7 @@ contract Drop1Script is Script, Sphinx {
320
332
  category: 8,
321
333
  discountPercent: 0,
322
334
  cannotIncreaseDiscountPercent: false,
335
+ cantBuyWithCredits: false,
323
336
  allowOwnerMint: false,
324
337
  useReserveBeneficiaryAsDefault: false,
325
338
  transfersPausable: false,
@@ -341,6 +354,7 @@ contract Drop1Script is Script, Sphinx {
341
354
  category: 8,
342
355
  discountPercent: 0,
343
356
  cannotIncreaseDiscountPercent: false,
357
+ cantBuyWithCredits: false,
344
358
  allowOwnerMint: false,
345
359
  useReserveBeneficiaryAsDefault: false,
346
360
  transfersPausable: false,
@@ -362,6 +376,7 @@ contract Drop1Script is Script, Sphinx {
362
376
  category: 8,
363
377
  discountPercent: 0,
364
378
  cannotIncreaseDiscountPercent: false,
379
+ cantBuyWithCredits: false,
365
380
  allowOwnerMint: false,
366
381
  useReserveBeneficiaryAsDefault: false,
367
382
  transfersPausable: false,
@@ -383,6 +398,7 @@ contract Drop1Script is Script, Sphinx {
383
398
  category: 9,
384
399
  discountPercent: 0,
385
400
  cannotIncreaseDiscountPercent: false,
401
+ cantBuyWithCredits: false,
386
402
  allowOwnerMint: false,
387
403
  useReserveBeneficiaryAsDefault: false,
388
404
  transfersPausable: false,
@@ -404,6 +420,7 @@ contract Drop1Script is Script, Sphinx {
404
420
  category: 9,
405
421
  discountPercent: 0,
406
422
  cannotIncreaseDiscountPercent: false,
423
+ cantBuyWithCredits: false,
407
424
  allowOwnerMint: false,
408
425
  useReserveBeneficiaryAsDefault: false,
409
426
  transfersPausable: false,
@@ -425,6 +442,7 @@ contract Drop1Script is Script, Sphinx {
425
442
  category: 9,
426
443
  discountPercent: 0,
427
444
  cannotIncreaseDiscountPercent: false,
445
+ cantBuyWithCredits: false,
428
446
  allowOwnerMint: false,
429
447
  useReserveBeneficiaryAsDefault: false,
430
448
  transfersPausable: false,
@@ -446,6 +464,7 @@ contract Drop1Script is Script, Sphinx {
446
464
  category: 9,
447
465
  discountPercent: 0,
448
466
  cannotIncreaseDiscountPercent: false,
467
+ cantBuyWithCredits: false,
449
468
  allowOwnerMint: false,
450
469
  useReserveBeneficiaryAsDefault: false,
451
470
  transfersPausable: false,
@@ -467,6 +486,7 @@ contract Drop1Script is Script, Sphinx {
467
486
  category: 10,
468
487
  discountPercent: 0,
469
488
  cannotIncreaseDiscountPercent: false,
489
+ cantBuyWithCredits: false,
470
490
  allowOwnerMint: false,
471
491
  useReserveBeneficiaryAsDefault: false,
472
492
  transfersPausable: false,
@@ -488,6 +508,7 @@ contract Drop1Script is Script, Sphinx {
488
508
  category: 11,
489
509
  discountPercent: 0,
490
510
  cannotIncreaseDiscountPercent: false,
511
+ cantBuyWithCredits: false,
491
512
  allowOwnerMint: false,
492
513
  useReserveBeneficiaryAsDefault: false,
493
514
  transfersPausable: false,
@@ -509,6 +530,7 @@ contract Drop1Script is Script, Sphinx {
509
530
  category: 11,
510
531
  discountPercent: 0,
511
532
  cannotIncreaseDiscountPercent: false,
533
+ cantBuyWithCredits: false,
512
534
  allowOwnerMint: false,
513
535
  useReserveBeneficiaryAsDefault: false,
514
536
  transfersPausable: false,
@@ -530,6 +552,7 @@ contract Drop1Script is Script, Sphinx {
530
552
  category: 11,
531
553
  discountPercent: 0,
532
554
  cannotIncreaseDiscountPercent: false,
555
+ cantBuyWithCredits: false,
533
556
  allowOwnerMint: false,
534
557
  useReserveBeneficiaryAsDefault: false,
535
558
  transfersPausable: false,
@@ -551,6 +574,7 @@ contract Drop1Script is Script, Sphinx {
551
574
  category: 11,
552
575
  discountPercent: 0,
553
576
  cannotIncreaseDiscountPercent: false,
577
+ cantBuyWithCredits: false,
554
578
  allowOwnerMint: false,
555
579
  useReserveBeneficiaryAsDefault: false,
556
580
  transfersPausable: false,
@@ -572,6 +596,7 @@ contract Drop1Script is Script, Sphinx {
572
596
  category: 11,
573
597
  discountPercent: 0,
574
598
  cannotIncreaseDiscountPercent: false,
599
+ cantBuyWithCredits: false,
575
600
  allowOwnerMint: false,
576
601
  useReserveBeneficiaryAsDefault: false,
577
602
  transfersPausable: false,
@@ -593,6 +618,7 @@ contract Drop1Script is Script, Sphinx {
593
618
  category: 11,
594
619
  discountPercent: 0,
595
620
  cannotIncreaseDiscountPercent: false,
621
+ cantBuyWithCredits: false,
596
622
  allowOwnerMint: false,
597
623
  useReserveBeneficiaryAsDefault: false,
598
624
  transfersPausable: false,
@@ -614,6 +640,7 @@ contract Drop1Script is Script, Sphinx {
614
640
  category: 11,
615
641
  discountPercent: 0,
616
642
  cannotIncreaseDiscountPercent: false,
643
+ cantBuyWithCredits: false,
617
644
  allowOwnerMint: false,
618
645
  useReserveBeneficiaryAsDefault: false,
619
646
  transfersPausable: false,
@@ -635,6 +662,7 @@ contract Drop1Script is Script, Sphinx {
635
662
  category: 11,
636
663
  discountPercent: 0,
637
664
  cannotIncreaseDiscountPercent: false,
665
+ cantBuyWithCredits: false,
638
666
  allowOwnerMint: false,
639
667
  useReserveBeneficiaryAsDefault: false,
640
668
  transfersPausable: false,
@@ -656,6 +684,7 @@ contract Drop1Script is Script, Sphinx {
656
684
  category: 12,
657
685
  discountPercent: 0,
658
686
  cannotIncreaseDiscountPercent: false,
687
+ cantBuyWithCredits: false,
659
688
  allowOwnerMint: false,
660
689
  useReserveBeneficiaryAsDefault: false,
661
690
  transfersPausable: false,
@@ -677,6 +706,7 @@ contract Drop1Script is Script, Sphinx {
677
706
  category: 12,
678
707
  discountPercent: 0,
679
708
  cannotIncreaseDiscountPercent: false,
709
+ cantBuyWithCredits: false,
680
710
  allowOwnerMint: false,
681
711
  useReserveBeneficiaryAsDefault: false,
682
712
  transfersPausable: false,
@@ -698,6 +728,7 @@ contract Drop1Script is Script, Sphinx {
698
728
  category: 12,
699
729
  discountPercent: 0,
700
730
  cannotIncreaseDiscountPercent: false,
731
+ cantBuyWithCredits: false,
701
732
  allowOwnerMint: false,
702
733
  useReserveBeneficiaryAsDefault: false,
703
734
  transfersPausable: false,
@@ -719,6 +750,7 @@ contract Drop1Script is Script, Sphinx {
719
750
  category: 12,
720
751
  discountPercent: 0,
721
752
  cannotIncreaseDiscountPercent: false,
753
+ cantBuyWithCredits: false,
722
754
  allowOwnerMint: false,
723
755
  useReserveBeneficiaryAsDefault: false,
724
756
  transfersPausable: false,
@@ -740,6 +772,7 @@ contract Drop1Script is Script, Sphinx {
740
772
  category: 12,
741
773
  discountPercent: 0,
742
774
  cannotIncreaseDiscountPercent: false,
775
+ cantBuyWithCredits: false,
743
776
  allowOwnerMint: false,
744
777
  useReserveBeneficiaryAsDefault: false,
745
778
  transfersPausable: false,
@@ -761,6 +794,7 @@ contract Drop1Script is Script, Sphinx {
761
794
  category: 12,
762
795
  discountPercent: 0,
763
796
  cannotIncreaseDiscountPercent: false,
797
+ cantBuyWithCredits: false,
764
798
  allowOwnerMint: false,
765
799
  useReserveBeneficiaryAsDefault: false,
766
800
  transfersPausable: false,
@@ -782,6 +816,7 @@ contract Drop1Script is Script, Sphinx {
782
816
  category: 12,
783
817
  discountPercent: 0,
784
818
  cannotIncreaseDiscountPercent: false,
819
+ cantBuyWithCredits: false,
785
820
  allowOwnerMint: false,
786
821
  useReserveBeneficiaryAsDefault: false,
787
822
  transfersPausable: false,
@@ -803,6 +838,7 @@ contract Drop1Script is Script, Sphinx {
803
838
  category: 12,
804
839
  discountPercent: 0,
805
840
  cannotIncreaseDiscountPercent: false,
841
+ cantBuyWithCredits: false,
806
842
  allowOwnerMint: false,
807
843
  useReserveBeneficiaryAsDefault: false,
808
844
  transfersPausable: false,
@@ -824,6 +860,7 @@ contract Drop1Script is Script, Sphinx {
824
860
  category: 13,
825
861
  discountPercent: 0,
826
862
  cannotIncreaseDiscountPercent: false,
863
+ cantBuyWithCredits: false,
827
864
  allowOwnerMint: false,
828
865
  useReserveBeneficiaryAsDefault: false,
829
866
  transfersPausable: false,
@@ -845,6 +882,7 @@ contract Drop1Script is Script, Sphinx {
845
882
  category: 13,
846
883
  discountPercent: 0,
847
884
  cannotIncreaseDiscountPercent: false,
885
+ cantBuyWithCredits: false,
848
886
  allowOwnerMint: false,
849
887
  useReserveBeneficiaryAsDefault: false,
850
888
  transfersPausable: false,
@@ -866,6 +904,7 @@ contract Drop1Script is Script, Sphinx {
866
904
  category: 13,
867
905
  discountPercent: 0,
868
906
  cannotIncreaseDiscountPercent: false,
907
+ cantBuyWithCredits: false,
869
908
  allowOwnerMint: false,
870
909
  useReserveBeneficiaryAsDefault: false,
871
910
  transfersPausable: false,
@@ -887,6 +926,7 @@ contract Drop1Script is Script, Sphinx {
887
926
  category: 13,
888
927
  discountPercent: 0,
889
928
  cannotIncreaseDiscountPercent: false,
929
+ cantBuyWithCredits: false,
890
930
  allowOwnerMint: false,
891
931
  useReserveBeneficiaryAsDefault: false,
892
932
  transfersPausable: false,
@@ -908,6 +948,7 @@ contract Drop1Script is Script, Sphinx {
908
948
  category: 13,
909
949
  discountPercent: 0,
910
950
  cannotIncreaseDiscountPercent: false,
951
+ cantBuyWithCredits: false,
911
952
  allowOwnerMint: false,
912
953
  useReserveBeneficiaryAsDefault: false,
913
954
  transfersPausable: false,
@@ -929,6 +970,7 @@ contract Drop1Script is Script, Sphinx {
929
970
  category: 13,
930
971
  discountPercent: 0,
931
972
  cannotIncreaseDiscountPercent: false,
973
+ cantBuyWithCredits: false,
932
974
  allowOwnerMint: false,
933
975
  useReserveBeneficiaryAsDefault: false,
934
976
  transfersPausable: false,
@@ -950,6 +992,7 @@ contract Drop1Script is Script, Sphinx {
950
992
  category: 13,
951
993
  discountPercent: 0,
952
994
  cannotIncreaseDiscountPercent: false,
995
+ cantBuyWithCredits: false,
953
996
  allowOwnerMint: false,
954
997
  useReserveBeneficiaryAsDefault: false,
955
998
  transfersPausable: false,
@@ -971,6 +1014,7 @@ contract Drop1Script is Script, Sphinx {
971
1014
  category: 13,
972
1015
  discountPercent: 0,
973
1016
  cannotIncreaseDiscountPercent: false,
1017
+ cantBuyWithCredits: false,
974
1018
  allowOwnerMint: false,
975
1019
  useReserveBeneficiaryAsDefault: false,
976
1020
  transfersPausable: false,
@@ -992,6 +1036,7 @@ contract Drop1Script is Script, Sphinx {
992
1036
  category: 13,
993
1037
  discountPercent: 0,
994
1038
  cannotIncreaseDiscountPercent: false,
1039
+ cantBuyWithCredits: false,
995
1040
  allowOwnerMint: false,
996
1041
  useReserveBeneficiaryAsDefault: false,
997
1042
  transfersPausable: false,
@@ -1013,6 +1058,7 @@ contract Drop1Script is Script, Sphinx {
1013
1058
  category: 14,
1014
1059
  discountPercent: 0,
1015
1060
  cannotIncreaseDiscountPercent: false,
1061
+ cantBuyWithCredits: false,
1016
1062
  allowOwnerMint: false,
1017
1063
  useReserveBeneficiaryAsDefault: false,
1018
1064
  transfersPausable: false,
@@ -1034,6 +1080,7 @@ contract Drop1Script is Script, Sphinx {
1034
1080
  category: 16,
1035
1081
  discountPercent: 0,
1036
1082
  cannotIncreaseDiscountPercent: false,
1083
+ cantBuyWithCredits: false,
1037
1084
  allowOwnerMint: false,
1038
1085
  useReserveBeneficiaryAsDefault: false,
1039
1086
  transfersPausable: false,
@@ -681,6 +681,7 @@ contract TestBanny721TokenUriResolver is Test {
681
681
  transfersPausable: false,
682
682
  cannotBeRemoved: false,
683
683
  cannotIncreaseDiscountPercent: false,
684
+ cantBuyWithCredits: false,
684
685
  splitPercent: 0,
685
686
  resolvedUri: ""
686
687
  });
@@ -165,6 +165,7 @@ contract BannyAttacks is Test {
165
165
  transfersPausable: false,
166
166
  cannotBeRemoved: false,
167
167
  cannotIncreaseDiscountPercent: false,
168
+ cantBuyWithCredits: false,
168
169
  splitPercent: 0,
169
170
  resolvedUri: ""
170
171
  })
@@ -1079,6 +1079,7 @@ contract DecorateFlowTests is Test {
1079
1079
  transfersPausable: false,
1080
1080
  cannotBeRemoved: false,
1081
1081
  cannotIncreaseDiscountPercent: false,
1082
+ cantBuyWithCredits: false,
1082
1083
  splitPercent: 0,
1083
1084
  resolvedUri: ""
1084
1085
  })
package/test/Fork.t.sol CHANGED
@@ -869,6 +869,7 @@ contract BannyForkTest is Test {
869
869
  transfersPausable: false,
870
870
  cannotBeRemoved: false,
871
871
  cannotIncreaseDiscountPercent: false,
872
+ cantBuyWithCredits: false,
872
873
  splitPercent: 0,
873
874
  resolvedUri: ""
874
875
  })
@@ -891,6 +892,7 @@ contract BannyForkTest is Test {
891
892
  transfersPausable: false,
892
893
  cannotBeRemoved: false,
893
894
  cannotIncreaseDiscountPercent: false,
895
+ cantBuyWithCredits: false,
894
896
  splitPercent: 0,
895
897
  resolvedUri: ""
896
898
  })
@@ -1940,6 +1942,7 @@ contract BannyForkTest is Test {
1940
1942
  useVotingUnits: false,
1941
1943
  cannotBeRemoved: false,
1942
1944
  cannotIncreaseDiscountPercent: false,
1945
+ cantBuyWithCredits: false,
1943
1946
  splitPercent: 0,
1944
1947
  splits: new JBSplit[](0)
1945
1948
  });
@@ -383,6 +383,7 @@ contract OutfitTransferLifecycleTest is Test {
383
383
  transfersPausable: false,
384
384
  cannotBeRemoved: false,
385
385
  cannotIncreaseDiscountPercent: false,
386
+ cantBuyWithCredits: false,
386
387
  splitPercent: 0,
387
388
  resolvedUri: ""
388
389
  })
@@ -161,6 +161,7 @@ contract TestAuditGaps is Test {
161
161
  transfersPausable: false,
162
162
  cannotBeRemoved: false,
163
163
  cannotIncreaseDiscountPercent: false,
164
+ cantBuyWithCredits: false,
164
165
  splitPercent: 0,
165
166
  resolvedUri: ""
166
167
  })
@@ -186,6 +186,7 @@ contract TestQALastMile is Test {
186
186
  transfersPausable: false,
187
187
  cannotBeRemoved: false,
188
188
  cannotIncreaseDiscountPercent: false,
189
+ cantBuyWithCredits: false,
189
190
  splitPercent: 0,
190
191
  resolvedUri: ""
191
192
  })
@@ -384,6 +384,7 @@ contract AntiStrandingRetentionTest is Test {
384
384
  transfersPausable: false,
385
385
  cannotBeRemoved: false,
386
386
  cannotIncreaseDiscountPercent: false,
387
+ cantBuyWithCredits: false,
387
388
  splitPercent: 0,
388
389
  resolvedUri: ""
389
390
  });
@@ -0,0 +1,159 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity 0.8.28;
3
+
4
+ import {Test} from "forge-std/Test.sol";
5
+ import {JB721Tier} from "@bananapus/721-hook-v6/src/structs/JB721Tier.sol";
6
+ import {IERC721Receiver} from "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol";
7
+
8
+ import {Banny721TokenUriResolver} from "../../src/Banny721TokenUriResolver.sol";
9
+
10
+ contract MockBurnableHook is Test {
11
+ mapping(uint256 => address) public owners;
12
+ mapping(address => mapping(address => bool)) public isApprovedForAll;
13
+ address public immutable MOCK_STORE;
14
+
15
+ constructor(address store) {
16
+ MOCK_STORE = store;
17
+ }
18
+
19
+ function STORE() external view returns (address) {
20
+ return MOCK_STORE;
21
+ }
22
+
23
+ function setOwner(uint256 tokenId, address owner) external {
24
+ owners[tokenId] = owner;
25
+ }
26
+
27
+ function ownerOf(uint256 tokenId) external view returns (address) {
28
+ address owner = owners[tokenId];
29
+ require(owner != address(0), "ERC721: token does not exist");
30
+ return owner;
31
+ }
32
+
33
+ function burn(uint256 tokenId) external {
34
+ owners[tokenId] = address(0);
35
+ }
36
+
37
+ function setApprovalForAll(address operator, bool approved) external {
38
+ isApprovedForAll[msg.sender][operator] = approved;
39
+ }
40
+
41
+ function safeTransferFrom(address from, address to, uint256 tokenId) external {
42
+ address owner = owners[tokenId];
43
+ require(owner != address(0), "ERC721: token does not exist");
44
+ require(
45
+ msg.sender == owner || msg.sender == from || isApprovedForAll[from][msg.sender], "MockHook: not authorized"
46
+ );
47
+ owners[tokenId] = to;
48
+ if (to.code.length > 0) {
49
+ bytes4 retval = IERC721Receiver(to).onERC721Received(msg.sender, from, tokenId, "");
50
+ require(retval == IERC721Receiver.onERC721Received.selector, "MockHook: receiver rejected");
51
+ }
52
+ }
53
+
54
+ function pricingContext() external pure returns (uint256, uint256) {
55
+ return (1, 18);
56
+ }
57
+
58
+ function baseURI() external pure returns (string memory) {
59
+ return "ipfs://";
60
+ }
61
+ }
62
+
63
+ contract MockBurnableStore {
64
+ mapping(address => mapping(uint256 => JB721Tier)) public tiers;
65
+
66
+ function setTier(address hook, uint256 tokenId, JB721Tier memory tier) external {
67
+ tiers[hook][tokenId] = tier;
68
+ }
69
+
70
+ function tierOfTokenId(address hook, uint256 tokenId, bool) external view returns (JB721Tier memory) {
71
+ return tiers[hook][tokenId];
72
+ }
73
+
74
+ function encodedIPFSUriOf(address, uint256) external pure returns (bytes32) {
75
+ return bytes32(0);
76
+ }
77
+ }
78
+
79
+ contract BurnedBodyStrandsAssetsTest is Test {
80
+ Banny721TokenUriResolver resolver;
81
+ MockBurnableHook hook;
82
+ MockBurnableStore store;
83
+
84
+ address deployer = makeAddr("deployer");
85
+ address alice = makeAddr("alice");
86
+
87
+ uint256 constant BODY1 = 1_000_000_001;
88
+ uint256 constant BODY2 = 1_000_000_002;
89
+ uint256 constant BACKGROUND = 2_000_000_001;
90
+ uint256 constant OUTFIT = 3_000_000_001;
91
+
92
+ function setUp() public {
93
+ store = new MockBurnableStore();
94
+ hook = new MockBurnableHook(address(store));
95
+
96
+ vm.prank(deployer);
97
+ resolver = new Banny721TokenUriResolver(
98
+ "<path/>", "<necklace/>", "<mouth/>", "<eyes/>", "<alieneyes/>", deployer, address(0)
99
+ );
100
+
101
+ _setupTier(BODY1, 1, 0);
102
+ _setupTier(BODY2, 1, 0);
103
+ _setupTier(BACKGROUND, 2, 1);
104
+ _setupTier(OUTFIT, 3, 2);
105
+
106
+ hook.setOwner(BODY1, alice);
107
+ hook.setOwner(BODY2, alice);
108
+ hook.setOwner(BACKGROUND, alice);
109
+ hook.setOwner(OUTFIT, alice);
110
+
111
+ vm.prank(alice);
112
+ hook.setApprovalForAll(address(resolver), true);
113
+ }
114
+
115
+ function test_burningDressedBodyPermanentlyStrandsAttachedAssets() public {
116
+ uint256[] memory outfitIds = new uint256[](1);
117
+ outfitIds[0] = OUTFIT;
118
+
119
+ vm.prank(alice);
120
+ resolver.decorateBannyWith(address(hook), BODY1, BACKGROUND, outfitIds);
121
+
122
+ assertEq(resolver.userOf(address(hook), BACKGROUND), BODY1);
123
+ assertEq(resolver.wearerOf(address(hook), OUTFIT), BODY1);
124
+
125
+ hook.burn(BODY1);
126
+
127
+ vm.expectRevert(bytes("ERC721: token does not exist"));
128
+ vm.prank(alice);
129
+ resolver.decorateBannyWith(address(hook), BODY2, BACKGROUND, outfitIds);
130
+
131
+ uint256[] memory emptyOutfits = new uint256[](0);
132
+ vm.expectRevert(bytes("ERC721: token does not exist"));
133
+ vm.prank(alice);
134
+ resolver.decorateBannyWith(address(hook), BODY1, 0, emptyOutfits);
135
+ }
136
+
137
+ function _setupTier(uint256 tokenId, uint32 tierId, uint24 category) internal {
138
+ JB721Tier memory tier = JB721Tier({
139
+ id: tierId,
140
+ price: 0.01 ether,
141
+ remainingSupply: 100,
142
+ initialSupply: 100,
143
+ votingUnits: 0,
144
+ reserveFrequency: 0,
145
+ reserveBeneficiary: address(0),
146
+ encodedIPFSUri: bytes32(0),
147
+ category: category,
148
+ discountPercent: 0,
149
+ allowOwnerMint: false,
150
+ transfersPausable: false,
151
+ cannotBeRemoved: false,
152
+ cannotIncreaseDiscountPercent: false,
153
+ cantBuyWithCredits: false,
154
+ splitPercent: 0,
155
+ resolvedUri: ""
156
+ });
157
+ store.setTier(address(hook), tokenId, tier);
158
+ }
159
+ }
@@ -214,6 +214,7 @@ contract MergedOutfitExclusivityTest is Test {
214
214
  transfersPausable: false,
215
215
  cannotBeRemoved: false,
216
216
  cannotIncreaseDiscountPercent: false,
217
+ cantBuyWithCredits: false,
217
218
  splitPercent: 0,
218
219
  resolvedUri: ""
219
220
  });
@@ -184,6 +184,7 @@ contract TryTransferFromStrandsAssetsTest is Test {
184
184
  transfersPausable: false,
185
185
  cannotBeRemoved: false,
186
186
  cannotIncreaseDiscountPercent: false,
187
+ cantBuyWithCredits: false,
187
188
  splitPercent: 0,
188
189
  resolvedUri: ""
189
190
  });
@@ -135,6 +135,7 @@ contract BodyCategoryValidationTest is Test {
135
135
  transfersPausable: false,
136
136
  cannotBeRemoved: false,
137
137
  cannotIncreaseDiscountPercent: false,
138
+ cantBuyWithCredits: false,
138
139
  splitPercent: 0,
139
140
  resolvedUri: ""
140
141
  });
@@ -174,6 +174,7 @@ contract BurnedTokenCheckTest is Test {
174
174
  transfersPausable: false,
175
175
  cannotBeRemoved: false,
176
176
  cannotIncreaseDiscountPercent: false,
177
+ cantBuyWithCredits: false,
177
178
  splitPercent: 0,
178
179
  resolvedUri: ""
179
180
  });
@@ -197,6 +197,7 @@ contract CEIReorderTest is Test {
197
197
  transfersPausable: false,
198
198
  cannotBeRemoved: false,
199
199
  cannotIncreaseDiscountPercent: false,
200
+ cantBuyWithCredits: false,
200
201
  splitPercent: 0,
201
202
  resolvedUri: ""
202
203
  });
@@ -334,6 +334,7 @@ contract RemovedTierDesyncTest is Test {
334
334
  transfersPausable: false,
335
335
  cannotBeRemoved: false,
336
336
  cannotIncreaseDiscountPercent: false,
337
+ cantBuyWithCredits: false,
337
338
  splitPercent: 0,
338
339
  resolvedUri: ""
339
340
  });