@bananapus/721-hook-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.
Files changed (100) hide show
  1. package/.gas-snapshot +152 -0
  2. package/LICENSE +21 -0
  3. package/README.md +253 -0
  4. package/SKILLS.md +140 -0
  5. package/docs/book.css +13 -0
  6. package/docs/book.toml +12 -0
  7. package/docs/solidity.min.js +74 -0
  8. package/docs/src/README.md +253 -0
  9. package/docs/src/SUMMARY.md +38 -0
  10. package/docs/src/src/JB721TiersHook.sol/contract.JB721TiersHook.md +645 -0
  11. package/docs/src/src/JB721TiersHookDeployer.sol/contract.JB721TiersHookDeployer.md +99 -0
  12. package/docs/src/src/JB721TiersHookProjectDeployer.sol/contract.JB721TiersHookProjectDeployer.md +288 -0
  13. package/docs/src/src/JB721TiersHookStore.sol/contract.JB721TiersHookStore.md +1096 -0
  14. package/docs/src/src/README.md +11 -0
  15. package/docs/src/src/abstract/ERC721.sol/abstract.ERC721.md +430 -0
  16. package/docs/src/src/abstract/JB721Hook.sol/abstract.JB721Hook.md +309 -0
  17. package/docs/src/src/abstract/README.md +5 -0
  18. package/docs/src/src/interfaces/IJB721Hook.sol/interface.IJB721Hook.md +29 -0
  19. package/docs/src/src/interfaces/IJB721TiersHook.sol/interface.IJB721TiersHook.md +203 -0
  20. package/docs/src/src/interfaces/IJB721TiersHookDeployer.sol/interface.IJB721TiersHookDeployer.md +25 -0
  21. package/docs/src/src/interfaces/IJB721TiersHookProjectDeployer.sol/interface.IJB721TiersHookProjectDeployer.md +64 -0
  22. package/docs/src/src/interfaces/IJB721TiersHookStore.sol/interface.IJB721TiersHookStore.md +265 -0
  23. package/docs/src/src/interfaces/IJB721TokenUriResolver.sol/interface.IJB721TokenUriResolver.md +12 -0
  24. package/docs/src/src/interfaces/README.md +9 -0
  25. package/docs/src/src/libraries/JB721Constants.sol/library.JB721Constants.md +14 -0
  26. package/docs/src/src/libraries/JB721TiersRulesetMetadataResolver.sol/library.JB721TiersRulesetMetadataResolver.md +68 -0
  27. package/docs/src/src/libraries/JBBitmap.sol/library.JBBitmap.md +82 -0
  28. package/docs/src/src/libraries/JBIpfsDecoder.sol/library.JBIpfsDecoder.md +61 -0
  29. package/docs/src/src/libraries/README.md +7 -0
  30. package/docs/src/src/structs/JB721InitTiersConfig.sol/struct.JB721InitTiersConfig.md +27 -0
  31. package/docs/src/src/structs/JB721Tier.sol/struct.JB721Tier.md +59 -0
  32. package/docs/src/src/structs/JB721TierConfig.sol/struct.JB721TierConfig.md +60 -0
  33. package/docs/src/src/structs/JB721TiersHookFlags.sol/struct.JB721TiersHookFlags.md +26 -0
  34. package/docs/src/src/structs/JB721TiersMintReservesConfig.sol/struct.JB721TiersMintReservesConfig.md +16 -0
  35. package/docs/src/src/structs/JB721TiersRulesetMetadata.sol/struct.JB721TiersRulesetMetadata.md +20 -0
  36. package/docs/src/src/structs/JB721TiersSetDiscountPercentConfig.sol/struct.JB721TiersSetDiscountPercentConfig.md +16 -0
  37. package/docs/src/src/structs/JBBitmapWord.sol/struct.JBBitmapWord.md +19 -0
  38. package/docs/src/src/structs/JBDeploy721TiersHookConfig.sol/struct.JBDeploy721TiersHookConfig.md +34 -0
  39. package/docs/src/src/structs/JBLaunchProjectConfig.sol/struct.JBLaunchProjectConfig.md +23 -0
  40. package/docs/src/src/structs/JBLaunchRulesetsConfig.sol/struct.JBLaunchRulesetsConfig.md +22 -0
  41. package/docs/src/src/structs/JBPayDataHookRulesetConfig.sol/struct.JBPayDataHookRulesetConfig.md +51 -0
  42. package/docs/src/src/structs/JBPayDataHookRulesetMetadata.sol/struct.JBPayDataHookRulesetMetadata.md +66 -0
  43. package/docs/src/src/structs/JBQueueRulesetsConfig.sol/struct.JBQueueRulesetsConfig.md +21 -0
  44. package/docs/src/src/structs/JBStored721Tier.sol/struct.JBStored721Tier.md +42 -0
  45. package/docs/src/src/structs/README.md +18 -0
  46. package/foundry.lock +11 -0
  47. package/foundry.toml +22 -0
  48. package/package.json +31 -0
  49. package/remappings.txt +1 -0
  50. package/script/Deploy.s.sol +140 -0
  51. package/script/helpers/Hook721DeploymentLib.sol +81 -0
  52. package/slither-ci.config.json +10 -0
  53. package/sphinx.lock +476 -0
  54. package/src/JB721TiersHook.sol +765 -0
  55. package/src/JB721TiersHookDeployer.sol +114 -0
  56. package/src/JB721TiersHookProjectDeployer.sol +413 -0
  57. package/src/JB721TiersHookStore.sol +1195 -0
  58. package/src/abstract/ERC721.sol +484 -0
  59. package/src/abstract/JB721Hook.sol +279 -0
  60. package/src/interfaces/IJB721Hook.sol +21 -0
  61. package/src/interfaces/IJB721TiersHook.sol +135 -0
  62. package/src/interfaces/IJB721TiersHookDeployer.sol +22 -0
  63. package/src/interfaces/IJB721TiersHookProjectDeployer.sol +76 -0
  64. package/src/interfaces/IJB721TiersHookStore.sol +220 -0
  65. package/src/interfaces/IJB721TokenUriResolver.sol +10 -0
  66. package/src/libraries/JB721Constants.sol +7 -0
  67. package/src/libraries/JB721TiersRulesetMetadataResolver.sol +44 -0
  68. package/src/libraries/JBBitmap.sol +57 -0
  69. package/src/libraries/JBIpfsDecoder.sol +95 -0
  70. package/src/structs/JB721InitTiersConfig.sol +20 -0
  71. package/src/structs/JB721Tier.sol +39 -0
  72. package/src/structs/JB721TierConfig.sol +40 -0
  73. package/src/structs/JB721TiersHookFlags.sol +17 -0
  74. package/src/structs/JB721TiersMintReservesConfig.sol +9 -0
  75. package/src/structs/JB721TiersRulesetMetadata.sol +12 -0
  76. package/src/structs/JB721TiersSetDiscountPercentConfig.sol +9 -0
  77. package/src/structs/JBBitmapWord.sol +11 -0
  78. package/src/structs/JBDeploy721TiersHookConfig.sol +25 -0
  79. package/src/structs/JBLaunchProjectConfig.sol +18 -0
  80. package/src/structs/JBLaunchRulesetsConfig.sol +17 -0
  81. package/src/structs/JBPayDataHookRulesetConfig.sol +44 -0
  82. package/src/structs/JBPayDataHookRulesetMetadata.sol +46 -0
  83. package/src/structs/JBQueueRulesetsConfig.sol +13 -0
  84. package/src/structs/JBStored721Tier.sol +24 -0
  85. package/test/721HookAttacks.t.sol +396 -0
  86. package/test/E2E/Pay_Mint_Redeem_E2E.t.sol +944 -0
  87. package/test/invariants/TierLifecycleInvariant.t.sol +187 -0
  88. package/test/invariants/TieredHookStoreInvariant.t.sol +81 -0
  89. package/test/invariants/handlers/TierLifecycleHandler.sol +262 -0
  90. package/test/invariants/handlers/TierStoreHandler.sol +155 -0
  91. package/test/unit/JB721TiersRulesetMetadataResolver.t.sol +141 -0
  92. package/test/unit/JBBitmap.t.sol +169 -0
  93. package/test/unit/JBIpfsDecoder.t.sol +131 -0
  94. package/test/unit/M6_TierSupplyCheck.t.sol +220 -0
  95. package/test/unit/adjustTier_Unit.t.sol +1740 -0
  96. package/test/unit/deployer_Unit.t.sol +103 -0
  97. package/test/unit/getters_constructor_Unit.t.sol +548 -0
  98. package/test/unit/mintFor_mintReservesFor_Unit.t.sol +443 -0
  99. package/test/unit/pay_Unit.t.sol +1537 -0
  100. package/test/unit/redeem_Unit.t.sol +459 -0
@@ -0,0 +1,141 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity 0.8.23;
3
+
4
+ import "forge-std/Test.sol";
5
+
6
+ import {JB721TiersRulesetMetadataResolver} from "../../src/libraries/JB721TiersRulesetMetadataResolver.sol";
7
+ import {JB721TiersRulesetMetadata} from "../../src/structs/JB721TiersRulesetMetadata.sol";
8
+
9
+ /// @notice Unit + fuzz tests for `JB721TiersRulesetMetadataResolver`.
10
+ contract TestJB721TiersRulesetMetadataResolver is Test {
11
+ //*********************************************************************//
12
+ // --- pack: individual flags ---------------------------------------- //
13
+ //*********************************************************************//
14
+
15
+ function test_pack_allFalse() public {
16
+ JB721TiersRulesetMetadata memory meta =
17
+ JB721TiersRulesetMetadata({pauseTransfers: false, pauseMintPendingReserves: false});
18
+ uint256 packed = JB721TiersRulesetMetadataResolver.pack721TiersRulesetMetadata(meta);
19
+ assertEq(packed, 0, "both false should pack to 0");
20
+ }
21
+
22
+ function test_pack_pauseTransfersOnly() public {
23
+ JB721TiersRulesetMetadata memory meta =
24
+ JB721TiersRulesetMetadata({pauseTransfers: true, pauseMintPendingReserves: false});
25
+ uint256 packed = JB721TiersRulesetMetadataResolver.pack721TiersRulesetMetadata(meta);
26
+ assertEq(packed, 1, "pauseTransfers only should pack to 1");
27
+ }
28
+
29
+ function test_pack_pauseMintPendingReservesOnly() public {
30
+ JB721TiersRulesetMetadata memory meta =
31
+ JB721TiersRulesetMetadata({pauseTransfers: false, pauseMintPendingReserves: true});
32
+ uint256 packed = JB721TiersRulesetMetadataResolver.pack721TiersRulesetMetadata(meta);
33
+ assertEq(packed, 2, "pauseMintPendingReserves only should pack to 2");
34
+ }
35
+
36
+ function test_pack_bothTrue() public {
37
+ JB721TiersRulesetMetadata memory meta =
38
+ JB721TiersRulesetMetadata({pauseTransfers: true, pauseMintPendingReserves: true});
39
+ uint256 packed = JB721TiersRulesetMetadataResolver.pack721TiersRulesetMetadata(meta);
40
+ assertEq(packed, 3, "both true should pack to 3");
41
+ }
42
+
43
+ //*********************************************************************//
44
+ // --- transfersPaused / mintPendingReservesPaused -------------------- //
45
+ //*********************************************************************//
46
+
47
+ function test_transfersPaused() public {
48
+ assertFalse(JB721TiersRulesetMetadataResolver.transfersPaused(0));
49
+ assertTrue(JB721TiersRulesetMetadataResolver.transfersPaused(1));
50
+ assertFalse(JB721TiersRulesetMetadataResolver.transfersPaused(2));
51
+ assertTrue(JB721TiersRulesetMetadataResolver.transfersPaused(3));
52
+ }
53
+
54
+ function test_mintPendingReservesPaused() public {
55
+ assertFalse(JB721TiersRulesetMetadataResolver.mintPendingReservesPaused(0));
56
+ assertFalse(JB721TiersRulesetMetadataResolver.mintPendingReservesPaused(1));
57
+ assertTrue(JB721TiersRulesetMetadataResolver.mintPendingReservesPaused(2));
58
+ assertTrue(JB721TiersRulesetMetadataResolver.mintPendingReservesPaused(3));
59
+ }
60
+
61
+ //*********************************************************************//
62
+ // --- expandMetadata ------------------------------------------------ //
63
+ //*********************************************************************//
64
+
65
+ function test_expandMetadata_zero() public {
66
+ JB721TiersRulesetMetadata memory meta = JB721TiersRulesetMetadataResolver.expandMetadata(0);
67
+ assertFalse(meta.pauseTransfers);
68
+ assertFalse(meta.pauseMintPendingReserves);
69
+ }
70
+
71
+ function test_expandMetadata_one() public {
72
+ JB721TiersRulesetMetadata memory meta = JB721TiersRulesetMetadataResolver.expandMetadata(1);
73
+ assertTrue(meta.pauseTransfers);
74
+ assertFalse(meta.pauseMintPendingReserves);
75
+ }
76
+
77
+ function test_expandMetadata_two() public {
78
+ JB721TiersRulesetMetadata memory meta = JB721TiersRulesetMetadataResolver.expandMetadata(2);
79
+ assertFalse(meta.pauseTransfers);
80
+ assertTrue(meta.pauseMintPendingReserves);
81
+ }
82
+
83
+ function test_expandMetadata_three() public {
84
+ JB721TiersRulesetMetadata memory meta = JB721TiersRulesetMetadataResolver.expandMetadata(3);
85
+ assertTrue(meta.pauseTransfers);
86
+ assertTrue(meta.pauseMintPendingReserves);
87
+ }
88
+
89
+ //*********************************************************************//
90
+ // --- Round-Trip ----------------------------------------------------- //
91
+ //*********************************************************************//
92
+
93
+ function test_packExpandRoundTrip_allCombinations() public {
94
+ for (uint256 i; i < 4; i++) {
95
+ bool transfers = (i & 1) == 1;
96
+ bool reserves = (i & 2) == 2;
97
+
98
+ JB721TiersRulesetMetadata memory meta =
99
+ JB721TiersRulesetMetadata({pauseTransfers: transfers, pauseMintPendingReserves: reserves});
100
+
101
+ uint256 packed = JB721TiersRulesetMetadataResolver.pack721TiersRulesetMetadata(meta);
102
+ JB721TiersRulesetMetadata memory expanded = JB721TiersRulesetMetadataResolver.expandMetadata(uint16(packed));
103
+
104
+ assertEq(expanded.pauseTransfers, transfers, "transfers round-trip");
105
+ assertEq(expanded.pauseMintPendingReserves, reserves, "reserves round-trip");
106
+ }
107
+ }
108
+
109
+ //*********************************************************************//
110
+ // --- Fuzz ---------------------------------------------------------- //
111
+ //*********************************************************************//
112
+
113
+ function testFuzz_packExpandRoundTrip(bool pauseTransfers, bool pauseMintPendingReserves) public {
114
+ JB721TiersRulesetMetadata memory meta = JB721TiersRulesetMetadata({
115
+ pauseTransfers: pauseTransfers, pauseMintPendingReserves: pauseMintPendingReserves
116
+ });
117
+
118
+ uint256 packed = JB721TiersRulesetMetadataResolver.pack721TiersRulesetMetadata(meta);
119
+ JB721TiersRulesetMetadata memory expanded = JB721TiersRulesetMetadataResolver.expandMetadata(uint16(packed));
120
+
121
+ assertEq(expanded.pauseTransfers, pauseTransfers, "fuzz transfers round-trip");
122
+ assertEq(expanded.pauseMintPendingReserves, pauseMintPendingReserves, "fuzz reserves round-trip");
123
+ }
124
+
125
+ function testFuzz_transfersPaused_bitIsolation(uint256 data) public {
126
+ bool result = JB721TiersRulesetMetadataResolver.transfersPaused(data);
127
+ assertEq(result, (data & 1) == 1, "transfersPaused should check bit 0");
128
+ }
129
+
130
+ function testFuzz_mintPendingReservesPaused_bitIsolation(uint256 data) public {
131
+ bool result = JB721TiersRulesetMetadataResolver.mintPendingReservesPaused(data);
132
+ assertEq(result, ((data >> 1) & 1) == 1, "mintPendingReservesPaused should check bit 1");
133
+ }
134
+
135
+ function testFuzz_pack_onlyUsesLow2Bits(bool a, bool b) public {
136
+ JB721TiersRulesetMetadata memory meta =
137
+ JB721TiersRulesetMetadata({pauseTransfers: a, pauseMintPendingReserves: b});
138
+ uint256 packed = JB721TiersRulesetMetadataResolver.pack721TiersRulesetMetadata(meta);
139
+ assertEq(packed & ~uint256(3), 0, "packed value should only use bits 0 and 1");
140
+ }
141
+ }
@@ -0,0 +1,169 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity 0.8.23;
3
+
4
+ import "forge-std/Test.sol";
5
+
6
+ import {JBBitmap} from "../../src/libraries/JBBitmap.sol";
7
+ import {JBBitmapWord} from "../../src/structs/JBBitmapWord.sol";
8
+
9
+ /// @notice Unit + fuzz tests for `JBBitmap`.
10
+ contract TestJBBitmap is Test {
11
+ using JBBitmap for mapping(uint256 => uint256);
12
+ using JBBitmap for JBBitmapWord;
13
+
14
+ mapping(uint256 => uint256) internal bitmap;
15
+
16
+ //*********************************************************************//
17
+ // --- readId -------------------------------------------------------- //
18
+ //*********************************************************************//
19
+
20
+ function test_readId_initiallyZero() public {
21
+ JBBitmapWord memory word = bitmap.readId(0);
22
+ assertEq(word.currentWord, 0, "initial word should be 0");
23
+ assertEq(word.currentDepth, 0, "depth for index 0 should be 0");
24
+ }
25
+
26
+ function test_readId_depthCalculation() public {
27
+ // Index 255 is in depth 0, index 256 is in depth 1.
28
+ JBBitmapWord memory word0 = bitmap.readId(255);
29
+ assertEq(word0.currentDepth, 0, "index 255 should be depth 0");
30
+
31
+ JBBitmapWord memory word1 = bitmap.readId(256);
32
+ assertEq(word1.currentDepth, 1, "index 256 should be depth 1");
33
+
34
+ JBBitmapWord memory word2 = bitmap.readId(512);
35
+ assertEq(word2.currentDepth, 2, "index 512 should be depth 2");
36
+ }
37
+
38
+ //*********************************************************************//
39
+ // --- removeTier / isTierIdRemoved ---------------------------------- //
40
+ //*********************************************************************//
41
+
42
+ function test_removeTier_setsbit() public {
43
+ assertFalse(bitmap.isTierIdRemoved(5), "should not be removed initially");
44
+
45
+ bitmap.removeTier(5);
46
+
47
+ assertTrue(bitmap.isTierIdRemoved(5), "should be removed after removeTier");
48
+ }
49
+
50
+ function test_removeTier_doesNotAffectOtherBits() public {
51
+ bitmap.removeTier(5);
52
+
53
+ assertFalse(bitmap.isTierIdRemoved(4), "adjacent bit should not be affected");
54
+ assertFalse(bitmap.isTierIdRemoved(6), "adjacent bit should not be affected");
55
+ assertFalse(bitmap.isTierIdRemoved(0), "index 0 should not be affected");
56
+ }
57
+
58
+ function test_removeTier_multipleBitsInSameWord() public {
59
+ bitmap.removeTier(0);
60
+ bitmap.removeTier(1);
61
+ bitmap.removeTier(255);
62
+
63
+ assertTrue(bitmap.isTierIdRemoved(0));
64
+ assertTrue(bitmap.isTierIdRemoved(1));
65
+ assertTrue(bitmap.isTierIdRemoved(255));
66
+ assertFalse(bitmap.isTierIdRemoved(2));
67
+ }
68
+
69
+ function test_removeTier_acrossWords() public {
70
+ bitmap.removeTier(0); // depth 0
71
+ bitmap.removeTier(256); // depth 1
72
+ bitmap.removeTier(512); // depth 2
73
+
74
+ assertTrue(bitmap.isTierIdRemoved(0));
75
+ assertTrue(bitmap.isTierIdRemoved(256));
76
+ assertTrue(bitmap.isTierIdRemoved(512));
77
+
78
+ assertFalse(bitmap.isTierIdRemoved(1));
79
+ assertFalse(bitmap.isTierIdRemoved(257));
80
+ assertFalse(bitmap.isTierIdRemoved(513));
81
+ }
82
+
83
+ function test_removeTier_idempotent() public {
84
+ bitmap.removeTier(10);
85
+ bitmap.removeTier(10); // Remove again.
86
+ assertTrue(bitmap.isTierIdRemoved(10), "should still be removed");
87
+ }
88
+
89
+ //*********************************************************************//
90
+ // --- isTierIdRemoved (memory struct variant) ----------------------- //
91
+ //*********************************************************************//
92
+
93
+ function test_isTierIdRemoved_memoryStruct() public {
94
+ bitmap.removeTier(3);
95
+
96
+ JBBitmapWord memory word = bitmap.readId(3);
97
+ assertTrue(word.isTierIdRemoved(3), "memory struct should read removed bit");
98
+ assertFalse(word.isTierIdRemoved(4), "memory struct should read non-removed bit");
99
+ }
100
+
101
+ function test_isTierIdRemoved_wrongDepthReturnsWrong() public {
102
+ bitmap.removeTier(3); // depth 0
103
+
104
+ // Read a word from depth 1 — should not see index 3's removal.
105
+ JBBitmapWord memory word = bitmap.readId(256);
106
+ assertFalse(word.isTierIdRemoved(3), "wrong depth should not see bit");
107
+ }
108
+
109
+ //*********************************************************************//
110
+ // --- refreshBitmapNeeded ------------------------------------------- //
111
+ //*********************************************************************//
112
+
113
+ function test_refreshBitmapNeeded_sameDepth() public {
114
+ JBBitmapWord memory word = bitmap.readId(0);
115
+ assertFalse(word.refreshBitmapNeeded(100), "same depth should not need refresh");
116
+ assertFalse(word.refreshBitmapNeeded(255), "still depth 0, no refresh needed");
117
+ }
118
+
119
+ function test_refreshBitmapNeeded_differentDepth() public {
120
+ JBBitmapWord memory word = bitmap.readId(0);
121
+ assertTrue(word.refreshBitmapNeeded(256), "depth 1 should need refresh from depth 0");
122
+ assertTrue(word.refreshBitmapNeeded(512), "depth 2 should need refresh from depth 0");
123
+ }
124
+
125
+ //*********************************************************************//
126
+ // --- Fuzz Tests ---------------------------------------------------- //
127
+ //*********************************************************************//
128
+
129
+ function testFuzz_removeTier_roundTrip(uint16 index) public {
130
+ assertFalse(bitmap.isTierIdRemoved(index), "should start unremoved");
131
+
132
+ bitmap.removeTier(index);
133
+
134
+ assertTrue(bitmap.isTierIdRemoved(index), "should be removed after removeTier");
135
+ }
136
+
137
+ function testFuzz_removeTier_isolatedBit(uint16 indexA, uint16 indexB) public {
138
+ vm.assume(indexA != indexB);
139
+
140
+ bitmap.removeTier(indexA);
141
+
142
+ assertTrue(bitmap.isTierIdRemoved(indexA), "A should be removed");
143
+ assertFalse(bitmap.isTierIdRemoved(indexB), "B should not be removed");
144
+ }
145
+
146
+ function testFuzz_readId_depthMatchesIndex(uint16 index) public {
147
+ JBBitmapWord memory word = bitmap.readId(index);
148
+ assertEq(word.currentDepth, uint256(index) >> 8, "depth should be index / 256");
149
+ }
150
+
151
+ function testFuzz_refreshBitmapNeeded_consistency(uint16 indexA, uint16 indexB) public {
152
+ JBBitmapWord memory word = bitmap.readId(indexA);
153
+ bool needed = word.refreshBitmapNeeded(indexB);
154
+ // Refresh is needed iff depths differ.
155
+ assertEq(needed, (uint256(indexA) >> 8) != (uint256(indexB) >> 8), "refresh iff different depth");
156
+ }
157
+
158
+ function testFuzz_removeTier_multipleBits(uint8 a, uint8 b, uint8 c) public {
159
+ vm.assume(a != b && b != c && a != c);
160
+
161
+ bitmap.removeTier(a);
162
+ bitmap.removeTier(b);
163
+ bitmap.removeTier(c);
164
+
165
+ assertTrue(bitmap.isTierIdRemoved(a));
166
+ assertTrue(bitmap.isTierIdRemoved(b));
167
+ assertTrue(bitmap.isTierIdRemoved(c));
168
+ }
169
+ }
@@ -0,0 +1,131 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity 0.8.23;
3
+
4
+ import "forge-std/Test.sol";
5
+
6
+ import {JBIpfsDecoder} from "../../src/libraries/JBIpfsDecoder.sol";
7
+
8
+ /// @notice Unit tests for `JBIpfsDecoder`.
9
+ contract TestJBIpfsDecoder is Test {
10
+ /// @notice Known IPFS CID v0 hash for testing.
11
+ /// @dev Obtained by hashing a known payload: the CID for the hex 0x1220... prefix + this hash should decode to a
12
+ /// valid base58 string starting with "Qm".
13
+ bytes32 constant TEST_HASH = 0x7465737468617368000000000000000000000000000000000000000000000000;
14
+
15
+ //*********************************************************************//
16
+ // --- decode: basic output ------------------------------------------ //
17
+ //*********************************************************************//
18
+
19
+ function test_decode_prependsBaseUri() public {
20
+ string memory result = JBIpfsDecoder.decode("ipfs://", TEST_HASH);
21
+ // Result must start with the base URI.
22
+ bytes memory resultBytes = bytes(result);
23
+ bytes memory prefix = bytes("ipfs://");
24
+ for (uint256 i; i < prefix.length; i++) {
25
+ assertEq(resultBytes[i], prefix[i], "prefix mismatch");
26
+ }
27
+ }
28
+
29
+ function test_decode_emptyBaseUri() public {
30
+ string memory result = JBIpfsDecoder.decode("", TEST_HASH);
31
+ // Should still produce a non-empty base58 hash.
32
+ assertTrue(bytes(result).length > 0, "should produce output with empty base URI");
33
+ }
34
+
35
+ function test_decode_outputStartsWithQm() public {
36
+ // All CIDv0 hashes start with "Qm" because the 0x1220 prefix encodes to "Qm" in base58.
37
+ string memory result = JBIpfsDecoder.decode("", TEST_HASH);
38
+ bytes memory resultBytes = bytes(result);
39
+ assertEq(resultBytes[0], bytes1("Q"), "first char should be Q");
40
+ assertEq(resultBytes[1], bytes1("m"), "second char should be m");
41
+ }
42
+
43
+ function test_decode_outputLength() public {
44
+ // CIDv0 hashes are always 46 characters in base58.
45
+ string memory result = JBIpfsDecoder.decode("", TEST_HASH);
46
+ assertEq(bytes(result).length, 46, "CIDv0 base58 hash should be 46 characters");
47
+ }
48
+
49
+ //*********************************************************************//
50
+ // --- decode: determinism ------------------------------------------- //
51
+ //*********************************************************************//
52
+
53
+ function test_decode_deterministic() public {
54
+ string memory a = JBIpfsDecoder.decode("ipfs://", TEST_HASH);
55
+ string memory b = JBIpfsDecoder.decode("ipfs://", TEST_HASH);
56
+ assertEq(keccak256(bytes(a)), keccak256(bytes(b)), "same input should produce same output");
57
+ }
58
+
59
+ function test_decode_differentHashesDifferentOutput() public {
60
+ bytes32 hashA = bytes32(uint256(1));
61
+ bytes32 hashB = bytes32(uint256(2));
62
+ string memory a = JBIpfsDecoder.decode("", hashA);
63
+ string memory b = JBIpfsDecoder.decode("", hashB);
64
+ assertTrue(keccak256(bytes(a)) != keccak256(bytes(b)), "different hashes should produce different output");
65
+ }
66
+
67
+ //*********************************************************************//
68
+ // --- decode: base58 alphabet --------------------------------------- //
69
+ //*********************************************************************//
70
+
71
+ function test_decode_onlyBase58Chars() public {
72
+ string memory result = JBIpfsDecoder.decode("", TEST_HASH);
73
+ bytes memory resultBytes = bytes(result);
74
+ bytes memory alphabet = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
75
+
76
+ for (uint256 i; i < resultBytes.length; i++) {
77
+ bool found;
78
+ for (uint256 j; j < alphabet.length; j++) {
79
+ if (resultBytes[i] == alphabet[j]) {
80
+ found = true;
81
+ break;
82
+ }
83
+ }
84
+ assertTrue(found, "output should only contain base58 characters");
85
+ }
86
+ }
87
+
88
+ //*********************************************************************//
89
+ // --- decode: fuzz -------------------------------------------------- //
90
+ //*********************************************************************//
91
+
92
+ function testFuzz_decode_alwaysProduces46Chars(bytes32 hash) public {
93
+ string memory result = JBIpfsDecoder.decode("", hash);
94
+ assertEq(bytes(result).length, 46, "any hash should produce 46-char CIDv0");
95
+ }
96
+
97
+ function testFuzz_decode_alwaysStartsWithQm(bytes32 hash) public {
98
+ string memory result = JBIpfsDecoder.decode("", hash);
99
+ bytes memory resultBytes = bytes(result);
100
+ assertEq(resultBytes[0], bytes1("Q"), "first char should be Q");
101
+ assertEq(resultBytes[1], bytes1("m"), "second char should be m");
102
+ }
103
+
104
+ function testFuzz_decode_prependsBaseUri(bytes32 hash, uint8 baseLen) public {
105
+ // Create a base URI of varying length (0-255 chars).
106
+ bytes memory base = new bytes(baseLen);
107
+ for (uint256 i; i < baseLen; i++) {
108
+ base[i] = "x";
109
+ }
110
+ string memory baseUri = string(base);
111
+ string memory result = JBIpfsDecoder.decode(baseUri, hash);
112
+ assertEq(bytes(result).length, uint256(baseLen) + 46, "output length = base + 46");
113
+ }
114
+
115
+ function testFuzz_decode_onlyBase58Chars(bytes32 hash) public {
116
+ string memory result = JBIpfsDecoder.decode("", hash);
117
+ bytes memory resultBytes = bytes(result);
118
+ bytes memory alphabet = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
119
+
120
+ for (uint256 i; i < resultBytes.length; i++) {
121
+ bool found;
122
+ for (uint256 j; j < alphabet.length; j++) {
123
+ if (resultBytes[i] == alphabet[j]) {
124
+ found = true;
125
+ break;
126
+ }
127
+ }
128
+ assertTrue(found, "all chars should be in base58 alphabet");
129
+ }
130
+ }
131
+ }
@@ -0,0 +1,220 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity 0.8.23;
3
+
4
+ import "../utils/UnitTestSetup.sol";
5
+
6
+ /// @title M6_TierSupplyCheck
7
+ /// @notice Tests proving the M-6 fix: the supply check must account for pending reserves when minting paid NFTs.
8
+ /// Without the `1 +` in the supply check, the last available slot can be consumed by a paid mint, making
9
+ /// pending reserves unmintable (recordMintReservesFor reverts decrementing remainingSupply past zero).
10
+ contract M6_TierSupplyCheck is UnitTestSetup {
11
+ using stdStorage for StdStorage;
12
+
13
+ /// @dev Mock the directory to accept `mockTerminalAddress` as a terminal for `projectId`.
14
+ function _mockTerminalAuth() internal {
15
+ mockAndExpect(
16
+ mockJBDirectory,
17
+ abi.encodeWithSelector(IJBDirectory.isTerminalOf.selector, projectId, mockTerminalAddress),
18
+ abi.encode(true)
19
+ );
20
+ }
21
+
22
+ /// @dev Create a pay context that requests minting specific tier IDs.
23
+ function _buildPayContext(
24
+ address targetHook,
25
+ uint256 value,
26
+ uint16[] memory tierIds
27
+ )
28
+ internal
29
+ view
30
+ returns (JBAfterPayRecordedContext memory)
31
+ {
32
+ bytes[] memory data = new bytes[](1);
33
+ data[0] = abi.encode(false, tierIds);
34
+ bytes4[] memory ids = new bytes4[](1);
35
+ ids[0] = metadataHelper.getId("pay", targetHook);
36
+ bytes memory hookMetadata = metadataHelper.createMetadata(ids, data);
37
+
38
+ return JBAfterPayRecordedContext({
39
+ payer: beneficiary,
40
+ projectId: projectId,
41
+ rulesetId: 0,
42
+ amount: JBTokenAmount({
43
+ token: JBConstants.NATIVE_TOKEN,
44
+ value: value,
45
+ decimals: 18,
46
+ currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
47
+ }),
48
+ forwardedAmount: JBTokenAmount({
49
+ token: JBConstants.NATIVE_TOKEN,
50
+ value: 0,
51
+ decimals: 18,
52
+ currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
53
+ }),
54
+ weight: 10 ** 18,
55
+ newlyIssuedTokenCount: 0,
56
+ beneficiary: beneficiary,
57
+ hookMetadata: bytes(""),
58
+ payerMetadata: hookMetadata
59
+ });
60
+ }
61
+
62
+ /// @dev Helper: mint `count` NFTs from tier 1 via pay.
63
+ function _mintPaid(ForTest_JB721TiersHook targetHook, uint256 count) internal {
64
+ uint16[] memory tierIds = new uint16[](count);
65
+ for (uint256 i; i < count; i++) {
66
+ tierIds[i] = 1;
67
+ }
68
+ JBAfterPayRecordedContext memory ctx = _buildPayContext(address(targetHook), count * 10, tierIds); // price=10
69
+ // per NFT
70
+ vm.prank(mockTerminalAddress);
71
+ targetHook.afterPayRecordedWith(ctx);
72
+ }
73
+
74
+ // =========================================================================
75
+ // Test 1: Prove the edge case — paid mint would steal reserves' last slot
76
+ // =========================================================================
77
+ /// @notice With reserveFrequency=2 and initialSupply=10:
78
+ /// After 6 paid mints → 4 remaining, 3 pending reserves.
79
+ /// Without the fix, a 7th paid mint would pass (4 > 3) leaving only 3 remaining for 4 pending reserves.
80
+ /// With the fix, the 7th mint decrements first (remaining→3), then checks 3 < ceil(7/2)=4 → reverts.
81
+ function test_M6_paidMintCannotStealReserveSlot() public {
82
+ // Configure: small supply, reserve every 2 mints.
83
+ defaultTierConfig.price = uint104(10);
84
+ defaultTierConfig.initialSupply = uint32(10);
85
+ defaultTierConfig.reserveFrequency = uint16(2);
86
+ defaultTierConfig.reserveBeneficiary = reserveBeneficiary;
87
+
88
+ ForTest_JB721TiersHook targetHook = _initializeForTestHook(1);
89
+ _mockTerminalAuth();
90
+
91
+ // Mint 6 paid NFTs. State: remaining=4, nonReserveMints=6, pending=ceil(6/2)=3.
92
+ _mintPaid(targetHook, 6);
93
+
94
+ JB721Tier memory tier = targetHook.STORE().tierOf(address(targetHook), 1, false);
95
+ assertEq(tier.remainingSupply, 4, "Should have 4 remaining after 6 mints");
96
+
97
+ uint256 pending = targetHook.STORE().numberOfPendingReservesFor(address(targetHook), 1);
98
+ assertEq(pending, 3, "Should have 3 pending reserves (ceil(6/2)=3)");
99
+
100
+ // The 7th paid mint should revert: remaining(4) <= 1 + pending(3) = 4.
101
+ // Without the fix (just `<=` pending), this would pass since 4 > 3.
102
+ uint16[] memory oneMore = new uint16[](1);
103
+ oneMore[0] = 1;
104
+ JBAfterPayRecordedContext memory ctx = _buildPayContext(address(targetHook), 10, oneMore);
105
+
106
+ vm.prank(mockTerminalAddress);
107
+ vm.expectRevert(
108
+ abi.encodeWithSelector(JB721TiersHookStore.JB721TiersHookStore_InsufficientSupplyRemaining.selector, 1)
109
+ );
110
+ targetHook.afterPayRecordedWith(ctx);
111
+ }
112
+
113
+ // =========================================================================
114
+ // Test 2: Reserves remain fully mintable after paid mints
115
+ // =========================================================================
116
+ /// @notice After minting paid NFTs up to the allowed limit, all pending reserves should be mintable.
117
+ function test_M6_reservesFullyMintableAfterPaidMints() public {
118
+ defaultTierConfig.price = uint104(10);
119
+ defaultTierConfig.initialSupply = uint32(10);
120
+ defaultTierConfig.reserveFrequency = uint16(2);
121
+ defaultTierConfig.reserveBeneficiary = reserveBeneficiary;
122
+
123
+ ForTest_JB721TiersHook targetHook = _initializeForTestHook(1);
124
+ _mockTerminalAuth();
125
+
126
+ // Mint 6 paid NFTs (the maximum allowed given the fix).
127
+ _mintPaid(targetHook, 6);
128
+
129
+ // Mint all pending reserves — this must succeed.
130
+ uint256 pending = targetHook.STORE().numberOfPendingReservesFor(address(targetHook), 1);
131
+ assertEq(pending, 3, "3 pending reserves");
132
+
133
+ vm.prank(owner);
134
+ targetHook.mintPendingReservesFor(1, pending);
135
+
136
+ // Verify: reserve beneficiary got the reserves.
137
+ assertEq(targetHook.balanceOf(reserveBeneficiary), pending, "Reserve beneficiary should have all reserves");
138
+
139
+ // Verify: remaining supply is 1 (10 - 6 paid - 3 reserves = 1).
140
+ JB721Tier memory tier = targetHook.STORE().tierOf(address(targetHook), 1, false);
141
+ assertEq(tier.remainingSupply, 1, "Should have 1 remaining (10 - 6 - 3)");
142
+ }
143
+
144
+ // =========================================================================
145
+ // Test 3: Boundary — reserves exactly fill remaining supply after max paid mints
146
+ // =========================================================================
147
+ /// @notice With reserveFrequency=5, after 16 paid mints of 20 supply:
148
+ /// remaining=4, pending=ceil(16/5)=4. The 17th mint reverts (4 <= 1+4=5).
149
+ /// All 4 pending reserves are still fully mintable.
150
+ function test_M6_noMintWhenRemainingEqualsReserves() public {
151
+ defaultTierConfig.price = uint104(10);
152
+ defaultTierConfig.initialSupply = uint32(20);
153
+ defaultTierConfig.reserveFrequency = uint16(5);
154
+ defaultTierConfig.reserveBeneficiary = reserveBeneficiary;
155
+
156
+ ForTest_JB721TiersHook targetHook = _initializeForTestHook(1);
157
+ _mockTerminalAuth();
158
+
159
+ // Mint 16 paid NFTs in two batches to stay under gas limits.
160
+ _mintPaid(targetHook, 10);
161
+ _mintPaid(targetHook, 6);
162
+
163
+ // State: remaining=4, nonReserveMints=16, pending=ceil(16/5)=4.
164
+ uint256 pending = targetHook.STORE().numberOfPendingReservesFor(address(targetHook), 1);
165
+ assertEq(pending, 4, "Should have 4 pending reserves (ceil(16/5)=4)");
166
+
167
+ JB721Tier memory tier = targetHook.STORE().tierOf(address(targetHook), 1, false);
168
+ assertEq(tier.remainingSupply, 4, "Should have 4 remaining");
169
+
170
+ // 17th mint: after decrement remaining would be 3, but pending would be ceil(17/5)=4. 3 < 4 → reverts.
171
+ uint16[] memory oneMore = new uint16[](1);
172
+ oneMore[0] = 1;
173
+ JBAfterPayRecordedContext memory ctx = _buildPayContext(address(targetHook), 10, oneMore);
174
+
175
+ vm.prank(mockTerminalAddress);
176
+ vm.expectRevert(
177
+ abi.encodeWithSelector(JB721TiersHookStore.JB721TiersHookStore_InsufficientSupplyRemaining.selector, 1)
178
+ );
179
+ targetHook.afterPayRecordedWith(ctx);
180
+
181
+ // But reserves should still be fully mintable — remaining(4) covers all pending(4).
182
+ vm.prank(owner);
183
+ targetHook.mintPendingReservesFor(1, pending);
184
+ assertEq(targetHook.balanceOf(reserveBeneficiary), 4, "All reserves fully minted");
185
+
186
+ // Final state: 0 remaining.
187
+ tier = targetHook.STORE().tierOf(address(targetHook), 1, false);
188
+ assertEq(tier.remainingSupply, 0, "Fully exhausted");
189
+ }
190
+
191
+ // =========================================================================
192
+ // Test 4: No reserves — full supply mintable
193
+ // =========================================================================
194
+ /// @notice Without reserves, all NFTs in a tier should be mintable (no off-by-one).
195
+ function test_M6_noReserves_fullSupplyMintable() public {
196
+ defaultTierConfig.price = uint104(10);
197
+ defaultTierConfig.initialSupply = uint32(5);
198
+ defaultTierConfig.reserveFrequency = uint16(0);
199
+ defaultTierConfig.reserveBeneficiary = address(0);
200
+
201
+ ForTest_JB721TiersHook targetHook = _initializeForTestHook(1);
202
+ _mockTerminalAuth();
203
+
204
+ // Mint all 5 — should succeed since no reserves to protect.
205
+ _mintPaid(targetHook, 5);
206
+
207
+ JB721Tier memory tier = targetHook.STORE().tierOf(address(targetHook), 1, false);
208
+ assertEq(tier.remainingSupply, 0, "Fully minted");
209
+ assertEq(targetHook.balanceOf(beneficiary), 5, "Beneficiary has all 5");
210
+
211
+ // One more should revert (supply exhausted).
212
+ uint16[] memory oneMore = new uint16[](1);
213
+ oneMore[0] = 1;
214
+ JBAfterPayRecordedContext memory ctx = _buildPayContext(address(targetHook), 10, oneMore);
215
+
216
+ vm.prank(mockTerminalAddress);
217
+ vm.expectRevert();
218
+ targetHook.afterPayRecordedWith(ctx);
219
+ }
220
+ }