@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,944 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity 0.8.23;
3
+
4
+ import "@openzeppelin/contracts/token/ERC721/IERC721.sol";
5
+ import "@bananapus/address-registry-v6/src/JBAddressRegistry.sol";
6
+
7
+ import "../../src/JB721TiersHook.sol";
8
+ import "../../src/JB721TiersHookProjectDeployer.sol";
9
+ import "../../src/JB721TiersHookDeployer.sol";
10
+ import "../../src/JB721TiersHookStore.sol";
11
+
12
+ import "../utils/TestBaseWorkflow.sol";
13
+ import "../../src/interfaces/IJB721TiersHook.sol";
14
+ import {MetadataResolverHelper} from "@bananapus/core-v6/test/helpers/MetadataResolverHelper.sol";
15
+
16
+ contract Test_TiersHook_E2E is TestBaseWorkflow {
17
+ using JBRulesetMetadataResolver for JBRuleset;
18
+
19
+ uint256 totalSupplyAfterPay;
20
+
21
+ address reserveBeneficiary = address(bytes20(keccak256("reserveBeneficiary")));
22
+ address trustedForwarder = address(123_456);
23
+
24
+ JB721TiersHook hook;
25
+
26
+ MetadataResolverHelper metadataHelper;
27
+
28
+ event Mint(
29
+ uint256 indexed tokenId,
30
+ uint256 indexed tierId,
31
+ address indexed beneficiary,
32
+ uint256 totalAmountPaid,
33
+ address caller
34
+ );
35
+ event Burn(uint256 indexed tokenId, address owner, address caller);
36
+
37
+ string name = "NAME";
38
+ string symbol = "SYM";
39
+ string baseUri = "http://www.null.com/";
40
+ string contractUri = "ipfs://null";
41
+ //QmWmyoMoctfbAaiEs2G46gpeUmhqFRDW6KWo64y5r581Vz
42
+ bytes32[] tokenUris = [
43
+ bytes32(0x7D5A99F603F231D53A4F39D1521F98D2E8BB279CF29BEBFD0687DC98458E7F89),
44
+ bytes32(0x7D5A99F603F231D53A4F39D1521F98D2E8BB279CF29BEBFD0687DC98458E7F89),
45
+ bytes32(0x7D5A99F603F231D53A4F39D1521F98D2E8BB279CF29BEBFD0687DC98458E7F89),
46
+ bytes32(0x7D5A99F603F231D53A4F39D1521F98D2E8BB279CF29BEBFD0687DC98458E7F89),
47
+ bytes32(0x7D5A99F603F231D53A4F39D1521F98D2E8BB279CF29BEBFD0687DC98458E7F89),
48
+ bytes32(0x7D5A99F603F231D53A4F39D1521F98D2E8BB279CF29BEBFD0687DC98458E7F89),
49
+ bytes32(0x7D5A99F603F231D53A4F39D1521F98D2E8BB279CF29BEBFD0687DC98458E7F89),
50
+ bytes32(0x7D5A99F603F231D53A4F39D1521F98D2E8BB279CF29BEBFD0687DC98458E7F89),
51
+ bytes32(0x7D5A99F603F231D53A4F39D1521F98D2E8BB279CF29BEBFD0687DC98458E7F89),
52
+ bytes32(0x7D5A99F603F231D53A4F39D1521F98D2E8BB279CF29BEBFD0687DC98458E7F89)
53
+ ];
54
+
55
+ JB721TiersHookProjectDeployer deployer;
56
+ JB721TiersHookStore store;
57
+ JBAddressRegistry addressRegistry;
58
+
59
+ function setUp() public override {
60
+ super.setUp();
61
+ store = new JB721TiersHookStore();
62
+ hook = new JB721TiersHook(jbDirectory, jbPermissions, jbRulesets, store, trustedForwarder);
63
+ addressRegistry = new JBAddressRegistry();
64
+ JB721TiersHookDeployer hookDeployer = new JB721TiersHookDeployer(hook, store, addressRegistry, trustedForwarder);
65
+ deployer = new JB721TiersHookProjectDeployer(
66
+ IJBDirectory(jbDirectory), IJBPermissions(jbPermissions), hookDeployer, address(0)
67
+ );
68
+
69
+ metadataHelper = new MetadataResolverHelper();
70
+ }
71
+
72
+ function testLaunchProjectAndAddHookToRegistry(bytes32 salt) external {
73
+ (JBDeploy721TiersHookConfig memory tiersHookConfig, JBLaunchProjectConfig memory launchProjectConfig) =
74
+ createData();
75
+ (uint256 projectId, IJB721TiersHook _hook) =
76
+ deployer.launchProjectFor(projectOwner, tiersHookConfig, launchProjectConfig, jbController, bytes32(0));
77
+ // Check: is the first project's ID 1?
78
+ assertEq(projectId, 1);
79
+ // Check: was the hook added to the address registry?
80
+ address dataHook = jbRulesets.currentOf(projectId).dataHook();
81
+ assertEq(address(_hook), dataHook);
82
+ assertEq(addressRegistry.deployerOf(dataHook), address(deployer.HOOK_DEPLOYER()));
83
+
84
+ // Laucnh another project with a salt
85
+ (projectId, _hook) =
86
+ deployer.launchProjectFor(projectOwner, tiersHookConfig, launchProjectConfig, jbController, salt);
87
+ // Check: is the second project's ID 2?
88
+ assertEq(projectId, 2);
89
+ // Check: was the hook added to the address registry?
90
+ dataHook = jbRulesets.currentOf(projectId).dataHook();
91
+ assertEq(address(_hook), dataHook);
92
+ assertEq(addressRegistry.deployerOf(dataHook), address(deployer.HOOK_DEPLOYER()));
93
+
94
+ // Laucnh another project with no salt
95
+ (projectId, _hook) =
96
+ deployer.launchProjectFor(projectOwner, tiersHookConfig, launchProjectConfig, jbController, bytes32(0));
97
+
98
+ // Check: is the third project's ID 3?
99
+ assertEq(projectId, 3);
100
+
101
+ // Check: was the hook added to the address registry?
102
+ dataHook = jbRulesets.currentOf(projectId).dataHook();
103
+ assertEq(address(_hook), dataHook);
104
+ assertEq(addressRegistry.deployerOf(dataHook), address(deployer.HOOK_DEPLOYER()));
105
+ }
106
+
107
+ function testMintOnPayIfOneTierIsPassed(uint256 valueSent, bytes32 salt) external {
108
+ valueSent = bound(valueSent, 10, 2000);
109
+ // Cap the highest tier ID possible to 10.
110
+ uint256 highestTier = valueSent <= 100 ? (valueSent / 10) : 10;
111
+ (JBDeploy721TiersHookConfig memory tiersHookConfig, JBLaunchProjectConfig memory launchProjectConfig) =
112
+ createData();
113
+ (uint256 projectId, IJB721TiersHook _hook) =
114
+ deployer.launchProjectFor(projectOwner, tiersHookConfig, launchProjectConfig, jbController, salt);
115
+
116
+ // Crafting the payment metadata: add the highest tier ID.
117
+ uint16[] memory rawMetadata = new uint16[](1);
118
+ rawMetadata[0] = uint16(highestTier);
119
+
120
+ // Build the metadata using the tiers to mint and the overspending flag.
121
+ bytes[] memory data = new bytes[](1);
122
+ data[0] = abi.encode(true, rawMetadata);
123
+
124
+ address dataHook = jbRulesets.currentOf(projectId).dataHook();
125
+ assertEq(address(_hook), dataHook);
126
+ // Pass the hook ID.
127
+ bytes4[] memory ids = new bytes4[](1);
128
+ ids[0] = JBMetadataResolver.getId("pay", address(hook));
129
+
130
+ // Generate the metadata.
131
+ bytes memory hookMetadata = metadataHelper.createMetadata(ids, data);
132
+
133
+ // Check: was an NFT with the correct tier ID and token ID minted?
134
+ vm.expectEmit(true, true, true, true);
135
+ emit Mint(
136
+ _generateTokenId(highestTier, 1),
137
+ highestTier,
138
+ beneficiary,
139
+ valueSent,
140
+ address(jbMultiTerminal) // msg.sender
141
+ );
142
+
143
+ // Pay the terminal to mint the NFTs.
144
+ vm.prank(caller);
145
+ jbMultiTerminal.pay{value: valueSent}({
146
+ projectId: projectId,
147
+ amount: 100,
148
+ token: JBConstants.NATIVE_TOKEN,
149
+ beneficiary: beneficiary,
150
+ minReturnedTokens: 0,
151
+ memo: "Take my money!",
152
+ metadata: hookMetadata
153
+ });
154
+ uint256 tokenId = _generateTokenId(highestTier, 1);
155
+ // Check: did the beneficiary receive the NFT?
156
+ if (valueSent < 10) {
157
+ assertEq(IERC721(dataHook).balanceOf(beneficiary), 0);
158
+ } else {
159
+ assertEq(IERC721(dataHook).balanceOf(beneficiary), 1);
160
+ }
161
+
162
+ // Check: is the beneficiary the first owner of the NFT?
163
+ assertEq(IERC721(dataHook).ownerOf(tokenId), beneficiary);
164
+ assertEq(IJB721TiersHook(dataHook).firstOwnerOf(tokenId), beneficiary);
165
+
166
+ // Check: after a transfer, are the `firstOwnerOf` and `ownerOf` still correct?
167
+ vm.prank(beneficiary);
168
+ IERC721(dataHook).transferFrom(beneficiary, address(696_969_420), tokenId);
169
+ assertEq(IERC721(dataHook).ownerOf(tokenId), address(696_969_420));
170
+ assertEq(IJB721TiersHook(dataHook).firstOwnerOf(tokenId), beneficiary);
171
+
172
+ // Check: is the same true after a second transfer?
173
+ vm.prank(address(696_969_420));
174
+ IERC721(dataHook).transferFrom(address(696_969_420), address(123_456_789), tokenId);
175
+ assertEq(IERC721(dataHook).ownerOf(tokenId), address(123_456_789));
176
+ assertEq(IJB721TiersHook(dataHook).firstOwnerOf(tokenId), beneficiary);
177
+ }
178
+
179
+ function testFuzzMintWithDiscountOnPayIfOneTierIsPassed(uint256 tierStartPrice, uint256 discountPercent) external {
180
+ // Cap our fuzzed params
181
+ tierStartPrice = bound(tierStartPrice, 1, type(uint208).max - 1);
182
+ discountPercent = bound(discountPercent, 1, 200);
183
+
184
+ {
185
+ uint256 amountMinted = (tierStartPrice * 1000) / 2;
186
+ totalSupplyAfterPay += amountMinted;
187
+ }
188
+
189
+ // Cap the highest tier ID.
190
+ uint256 highestTier = 1;
191
+
192
+ (JBDeploy721TiersHookConfig memory tiersHookConfig, JBLaunchProjectConfig memory launchProjectConfig) =
193
+ createDiscountedData(tierStartPrice, uint8(discountPercent));
194
+ (uint256 projectId, IJB721TiersHook _hook) =
195
+ deployer.launchProjectFor(projectOwner, tiersHookConfig, launchProjectConfig, jbController, bytes32(0));
196
+
197
+ // Crafting the payment metadata: add the highest tier ID.
198
+ uint16[] memory rawMetadata = new uint16[](1);
199
+ rawMetadata[0] = uint16(highestTier);
200
+
201
+ // Build the metadata using the tiers to mint and the overspending flag.
202
+ bytes[] memory data = new bytes[](1);
203
+ data[0] = abi.encode(true, rawMetadata);
204
+
205
+ address dataHook = jbRulesets.currentOf(projectId).dataHook();
206
+ assertEq(address(_hook), dataHook);
207
+ bytes memory hookMetadata;
208
+ {
209
+ // Pass the hook ID.
210
+ bytes4[] memory ids = new bytes4[](1);
211
+ ids[0] = JBMetadataResolver.getId("pay", address(hook));
212
+
213
+ // Generate the metadata.
214
+ hookMetadata = metadataHelper.createMetadata(ids, data);
215
+ }
216
+
217
+ /* // Check: was an NFT with the correct tier ID and token ID minted?
218
+ vm.expectEmit(true, true, true, true);
219
+ emit Mint(
220
+ _generateTokenId(highestTier, 1),
221
+ highestTier,
222
+ beneficiary,
223
+ tierStartPrice,
224
+ address(jbMultiTerminal) // msg.sender
225
+ ); */
226
+
227
+ if (totalSupplyAfterPay > type(uint208).max) {
228
+ vm.expectRevert(
229
+ abi.encodeWithSelector(JBTokens.JBTokens_OverflowAlert.selector, totalSupplyAfterPay, type(uint208).max)
230
+ );
231
+ }
232
+
233
+ // Pay the terminal to mint the NFTs.
234
+ vm.deal(caller, type(uint256).max);
235
+ vm.prank(caller);
236
+ jbMultiTerminal.pay{value: tierStartPrice}({
237
+ projectId: projectId,
238
+ amount: tierStartPrice,
239
+ token: JBConstants.NATIVE_TOKEN,
240
+ beneficiary: beneficiary,
241
+ minReturnedTokens: 0,
242
+ memo: "Take my money!",
243
+ metadata: hookMetadata
244
+ });
245
+
246
+ if (totalSupplyAfterPay < type(uint208).max) {
247
+ if (tierStartPrice > type(uint104).max) {
248
+ uint256 expectedDiscount =
249
+ mulDiv(uint104(tierStartPrice), discountPercent, JB721Constants.DISCOUNT_DENOMINATOR);
250
+ uint256 paidForNft = uint104(tierStartPrice) - expectedDiscount;
251
+
252
+ // Check: should be credited tierStartPrice minus what you paid for the NFT plus the discount
253
+ assertEq(IJB721TiersHook(dataHook).payCreditsOf(beneficiary), tierStartPrice - paidForNft);
254
+ } else {
255
+ uint256 expectedCredits = mulDiv(tierStartPrice, discountPercent, JB721Constants.DISCOUNT_DENOMINATOR);
256
+ assertEq(IJB721TiersHook(dataHook).payCreditsOf(beneficiary), expectedCredits);
257
+ }
258
+
259
+ {
260
+ // Check: did the beneficiary receive the NFT?
261
+ assertEq(IERC721(dataHook).balanceOf(beneficiary), 1);
262
+
263
+ uint256 tokenId = _generateTokenId(highestTier, 1);
264
+
265
+ // Check: is the beneficiary the first owner of the NFT?
266
+ assertEq(IERC721(dataHook).ownerOf(tokenId), beneficiary);
267
+ assertEq(IJB721TiersHook(dataHook).firstOwnerOf(tokenId), beneficiary);
268
+
269
+ // Check: after a transfer, are the `firstOwnerOf` and `ownerOf` still correct?
270
+ vm.prank(beneficiary);
271
+ IERC721(dataHook).transferFrom(beneficiary, address(696_969_420), tokenId);
272
+ assertEq(IERC721(dataHook).ownerOf(tokenId), address(696_969_420));
273
+ assertEq(IJB721TiersHook(dataHook).firstOwnerOf(tokenId), beneficiary);
274
+
275
+ // Check: is the same true after a second transfer?
276
+ vm.prank(address(696_969_420));
277
+ IERC721(dataHook).transferFrom(address(696_969_420), address(123_456_789), tokenId);
278
+ assertEq(IERC721(dataHook).ownerOf(tokenId), address(123_456_789));
279
+ assertEq(IJB721TiersHook(dataHook).firstOwnerOf(tokenId), beneficiary);
280
+ }
281
+ }
282
+ }
283
+
284
+ function testMintOnPayIfMultipleTiersArePassed(bytes32 salt) external {
285
+ (JBDeploy721TiersHookConfig memory tiersHookConfig, JBLaunchProjectConfig memory launchProjectConfig) =
286
+ createData();
287
+ (uint256 projectId, IJB721TiersHook _hook) =
288
+ deployer.launchProjectFor(projectOwner, tiersHookConfig, launchProjectConfig, jbController, salt);
289
+
290
+ // Prices of the first 5 tiers (10 * `tierId`)
291
+ uint256 amountNeeded = 50 + 40 + 30 + 20 + 10;
292
+ uint16[] memory rawMetadata = new uint16[](5);
293
+
294
+ // Mint one NFT per tier from the first 5 tiers.
295
+ for (uint256 i = 0; i < 5; i++) {
296
+ rawMetadata[i] = uint16(i + 1); // Start at `tierId` 1.
297
+ // Check: correct tier IDs and token IDs?
298
+ vm.expectEmit(true, true, true, true);
299
+ emit Mint(
300
+ _generateTokenId(i + 1, 1),
301
+ i + 1,
302
+ beneficiary,
303
+ amountNeeded,
304
+ address(jbMultiTerminal) // `msg.sender`
305
+ );
306
+ }
307
+
308
+ // Build the metadata using the tiers to mint and the overspending flag.
309
+ bytes[] memory data = new bytes[](1);
310
+ data[0] = abi.encode(true, rawMetadata);
311
+
312
+ address dataHook = jbRulesets.currentOf(projectId).dataHook();
313
+ assertEq(address(_hook), dataHook);
314
+
315
+ // Pass the hook ID.
316
+ bytes4[] memory ids = new bytes4[](1);
317
+ ids[0] = JBMetadataResolver.getId("pay", address(hook));
318
+
319
+ // Generate the metadata.
320
+ bytes memory hookMetadata = metadataHelper.createMetadata(ids, data);
321
+
322
+ // Pay the terminal to mint the NFTs.
323
+ vm.prank(caller);
324
+ jbMultiTerminal.pay{value: amountNeeded}({
325
+ projectId: projectId,
326
+ amount: amountNeeded,
327
+ token: JBConstants.NATIVE_TOKEN,
328
+ beneficiary: beneficiary,
329
+ minReturnedTokens: 0,
330
+ memo: "Take my money!",
331
+ metadata: hookMetadata
332
+ });
333
+
334
+ // Check: were the NFTs actually received?
335
+ assertEq(IERC721(dataHook).balanceOf(beneficiary), 5);
336
+ for (uint256 i = 1; i <= 5; i++) {
337
+ uint256 tokenId = _generateTokenId(i, 1);
338
+ assertEq(IJB721TiersHook(dataHook).firstOwnerOf(tokenId), beneficiary);
339
+ // Check: are `firstOwnerOf` and `ownerOf` correct after a transfer?
340
+ vm.prank(beneficiary);
341
+ IERC721(dataHook).transferFrom(beneficiary, address(696_969_420), tokenId);
342
+ assertEq(IERC721(dataHook).ownerOf(tokenId), address(696_969_420));
343
+ assertEq(IJB721TiersHook(dataHook).firstOwnerOf(tokenId), beneficiary);
344
+ }
345
+ }
346
+
347
+ function testNoMintOnPayWhenNotIncludingTierIds(uint256 valueSent, bytes32 salt) external {
348
+ valueSent = bound(valueSent, 10, 2000);
349
+ (JBDeploy721TiersHookConfig memory tiersHookConfig, JBLaunchProjectConfig memory launchProjectConfig) =
350
+ createData();
351
+ (uint256 projectId, IJB721TiersHook _hook) =
352
+ deployer.launchProjectFor(projectOwner, tiersHookConfig, launchProjectConfig, jbController, salt);
353
+
354
+ address dataHook = jbRulesets.currentOf(projectId).dataHook();
355
+ assertEq(address(_hook), dataHook);
356
+
357
+ // Build the metadata with no tiers specified and the overspending flag.
358
+ bool allowOverspending = true;
359
+ uint16[] memory rawMetadata = new uint16[](0);
360
+ bytes memory metadata =
361
+ abi.encode(bytes32(0), bytes32(0), type(IJB721TiersHook).interfaceId, allowOverspending, rawMetadata);
362
+
363
+ // Pay the terminal and pass the metadata.
364
+ vm.prank(caller);
365
+ jbMultiTerminal.pay{value: valueSent}({
366
+ projectId: projectId,
367
+ amount: 100,
368
+ token: JBConstants.NATIVE_TOKEN,
369
+ beneficiary: beneficiary,
370
+ minReturnedTokens: 0,
371
+ memo: "Take my money!",
372
+ metadata: metadata
373
+ });
374
+
375
+ // Ensure that no NFT was minted.
376
+ assertEq(IERC721(dataHook).balanceOf(beneficiary), 0);
377
+
378
+ // Ensure the beneficiary received pay credits (since no NFTs were minted).
379
+ assertEq(IJB721TiersHook(dataHook).payCreditsOf(beneficiary), valueSent);
380
+ }
381
+
382
+ function testNoMintOnPayWhenNotIncludingMetadata(uint256 valueSent, bytes32 salt) external {
383
+ valueSent = bound(valueSent, 10, 2000);
384
+ (JBDeploy721TiersHookConfig memory tiersHookConfig, JBLaunchProjectConfig memory launchProjectConfig) =
385
+ createData();
386
+ (uint256 projectId, IJB721TiersHook _hook) =
387
+ deployer.launchProjectFor(projectOwner, tiersHookConfig, launchProjectConfig, jbController, salt);
388
+
389
+ address dataHook = jbRulesets.currentOf(projectId).dataHook();
390
+ assertEq(address(_hook), dataHook);
391
+
392
+ // Pay the terminal with empty metadata (`bytes(0)`).
393
+ vm.prank(caller);
394
+ jbMultiTerminal.pay{value: valueSent}({
395
+ projectId: projectId,
396
+ amount: 100,
397
+ token: JBConstants.NATIVE_TOKEN,
398
+ beneficiary: beneficiary,
399
+ minReturnedTokens: 0,
400
+ memo: "Take my money!",
401
+ metadata: new bytes(0)
402
+ });
403
+
404
+ // Ensure that no NFTs were minted.
405
+ assertEq(IERC721(dataHook).balanceOf(beneficiary), 0);
406
+
407
+ // Ensure that the beneficiary received pay credits (since no NFTs were minted).
408
+ assertEq(IJB721TiersHook(dataHook).payCreditsOf(beneficiary), valueSent);
409
+ }
410
+
411
+ function testMintReservedNft(uint256 valueSent, bytes32 salt) external {
412
+ // cheapest tier is worth 10
413
+ valueSent = bound(valueSent, 10, 20 ether);
414
+
415
+ // Cap the highest tier ID possible to 10.
416
+ uint256 highestTier = valueSent <= 100 ? valueSent / 10 : 10;
417
+
418
+ (JBDeploy721TiersHookConfig memory tiersHookConfig, JBLaunchProjectConfig memory launchProjectConfig) =
419
+ createData();
420
+ (uint256 projectId, IJB721TiersHook _hook) =
421
+ deployer.launchProjectFor(projectOwner, tiersHookConfig, launchProjectConfig, jbController, salt);
422
+ address dataHook = jbRulesets.currentOf(projectId).dataHook();
423
+ assertEq(address(_hook), dataHook);
424
+
425
+ // Check: Ensure no pending reserves at start (since no minting has happened).
426
+ assertEq(IJB721TiersHook(dataHook).STORE().numberOfPendingReservesFor(dataHook, highestTier), 0);
427
+
428
+ // Check: cannot mint pending reserves (since none should be pending)?
429
+ vm.expectRevert(
430
+ abi.encodeWithSelector(JB721TiersHookStore.JB721TiersHookStore_InsufficientPendingReserves.selector, 1, 0)
431
+ );
432
+ vm.prank(projectOwner);
433
+ IJB721TiersHook(dataHook).mintPendingReservesFor(highestTier, 1);
434
+
435
+ // Crafting the payment metadata: add the highest tier ID.
436
+ uint16[] memory rawMetadata = new uint16[](1);
437
+ rawMetadata[0] = uint16(highestTier);
438
+
439
+ // Build the metadata using the tiers to mint and the overspending flag.
440
+ bytes[] memory data = new bytes[](1);
441
+ data[0] = abi.encode(true, rawMetadata);
442
+
443
+ // Pass the hook ID.
444
+ bytes4[] memory ids = new bytes4[](1);
445
+ ids[0] = JBMetadataResolver.getId("pay", address(hook));
446
+
447
+ // Generate the metadata.
448
+ bytes memory hookMetadata = metadataHelper.createMetadata(ids, data);
449
+
450
+ // Check: were an NFT with the correct tier ID and token ID minted?
451
+ vm.expectEmit(true, true, true, true);
452
+ emit Mint(
453
+ _generateTokenId(highestTier, 1), // First one
454
+ highestTier,
455
+ beneficiary,
456
+ valueSent,
457
+ address(jbMultiTerminal) // msg.sender
458
+ );
459
+
460
+ // Pay the terminal to mint the NFTs.
461
+ vm.prank(caller);
462
+ jbMultiTerminal.pay{value: valueSent}({
463
+ projectId: projectId,
464
+ amount: 100,
465
+ token: JBConstants.NATIVE_TOKEN,
466
+ beneficiary: beneficiary,
467
+ minReturnedTokens: 0,
468
+ memo: "Take my money!",
469
+ metadata: hookMetadata
470
+ });
471
+
472
+ // Check: is there now 1 pending reserve? 1 mint should yield 1 pending reserve, due to rounding up.
473
+ assertEq(IJB721TiersHook(dataHook).STORE().numberOfPendingReservesFor(dataHook, highestTier), 1);
474
+
475
+ JB721Tier memory tierBeforeMintingReserves =
476
+ JB721TiersHook(dataHook).STORE().tierOf(dataHook, highestTier, false);
477
+
478
+ // Mint the pending reserve NFT.
479
+ vm.prank(projectOwner);
480
+ IJB721TiersHook(dataHook).mintPendingReservesFor(highestTier, 1);
481
+ // Check: did the reserve beneficiary receive the NFT?
482
+ assertEq(IERC721(dataHook).balanceOf(reserveBeneficiary), 1);
483
+
484
+ JB721Tier memory tierAfterMintingReserves =
485
+ JB721TiersHook(dataHook).STORE().tierOf(dataHook, highestTier, false);
486
+ // The tier's remaining supply should have decreased by 1.
487
+ assertLt(tierAfterMintingReserves.remainingSupply, tierBeforeMintingReserves.remainingSupply);
488
+
489
+ // Check: there should now be 0 pending reserves.
490
+ assertEq(IJB721TiersHook(dataHook).STORE().numberOfPendingReservesFor(dataHook, highestTier), 0);
491
+ // Check: it should not be possible to mint pending reserves now (since there are none left).
492
+ vm.expectRevert(
493
+ abi.encodeWithSelector(JB721TiersHookStore.JB721TiersHookStore_InsufficientPendingReserves.selector, 1, 0)
494
+ );
495
+ vm.prank(projectOwner);
496
+ IJB721TiersHook(dataHook).mintPendingReservesFor(highestTier, 1);
497
+ }
498
+
499
+ // - Mint an NFT.
500
+ // - Check the number of pending reserve mints available within that NFT's tier, which should be non-zero due to
501
+ // rounding up.
502
+ // - Burn an NFT from that tier.
503
+ // - Check the number of pending reserve mints available within the NFT's tier again.
504
+ // This number should be back to 0, since the NFT was burned.
505
+ function testCashOutToken(uint256 valueSent, bytes32 salt) external {
506
+ valueSent = bound(valueSent, 10, 2000);
507
+
508
+ // Cap the highest tier ID possible to 10.
509
+ uint256 highestTier = valueSent <= 100 ? (valueSent / 10) : 10;
510
+ (JBDeploy721TiersHookConfig memory tiersHookConfig, JBLaunchProjectConfig memory launchProjectConfig) =
511
+ createData();
512
+
513
+ // rr of 1.
514
+ tiersHookConfig.tiersConfig.tiers[highestTier - 1].reserveFrequency = 1;
515
+
516
+ (uint256 projectId, IJB721TiersHook _hook) =
517
+ deployer.launchProjectFor(projectOwner, tiersHookConfig, launchProjectConfig, jbController, salt);
518
+
519
+ // Craft the metadata: buy 1 NFT from the highest tier.
520
+ bytes memory hookMetadata;
521
+ bytes[] memory data;
522
+ bytes4[] memory ids;
523
+ address dataHook = jbRulesets.currentOf(projectId).dataHook();
524
+ assertEq(address(_hook), dataHook);
525
+ {
526
+ uint16[] memory rawMetadata = new uint16[](1);
527
+ rawMetadata[0] = uint16(highestTier);
528
+
529
+ // Build the metadata using the tiers to mint and the overspending flag.
530
+ data = new bytes[](1);
531
+ data[0] = abi.encode(true, rawMetadata);
532
+
533
+ // Pass the hook ID.
534
+ ids = new bytes4[](1);
535
+ ids[0] = metadataHelper.getId("pay", address(hook));
536
+
537
+ // Generate the metadata.
538
+ hookMetadata = metadataHelper.createMetadata(ids, data);
539
+ }
540
+
541
+ // Pay the terminal to mint the NFTs.
542
+ vm.prank(caller);
543
+ jbMultiTerminal.pay{value: valueSent}({
544
+ projectId: projectId,
545
+ amount: 100,
546
+ token: JBConstants.NATIVE_TOKEN,
547
+ beneficiary: beneficiary,
548
+ minReturnedTokens: 0,
549
+ memo: "Take my money!",
550
+ metadata: hookMetadata
551
+ });
552
+
553
+ {
554
+ // Get the token ID of the NFT that was minted.
555
+ uint256 tokenId = _generateTokenId(highestTier, 1);
556
+
557
+ // Craft the metadata: cash out the `tokenId` which was minted.
558
+ uint256[] memory cashOutId = new uint256[](1);
559
+ cashOutId[0] = tokenId;
560
+
561
+ // Build the metadata with the tiers to cash out.
562
+ data[0] = abi.encode(cashOutId);
563
+
564
+ // Pass the hook ID.
565
+ ids[0] = metadataHelper.getId("cashOut", address(hook));
566
+
567
+ // Generate the metadata.
568
+ hookMetadata = metadataHelper.createMetadata(ids, data);
569
+ }
570
+
571
+ // Check: was the beneficiary's NFT balance decreased by 1?
572
+ assertEq(IERC721(dataHook).balanceOf(beneficiary), 1);
573
+
574
+ // Cash out the NFT.
575
+ vm.prank(beneficiary);
576
+ jbMultiTerminal.cashOutTokensOf({
577
+ holder: beneficiary,
578
+ projectId: projectId,
579
+ tokenToReclaim: JBConstants.NATIVE_TOKEN,
580
+ cashOutCount: 0,
581
+ minTokensReclaimed: 0,
582
+ beneficiary: payable(beneficiary),
583
+ metadata: hookMetadata
584
+ });
585
+
586
+ // Check: was the beneficiary's NFT balance decreased by 1?
587
+ assertEq(IERC721(dataHook).balanceOf(beneficiary), 0);
588
+
589
+ // Check: was the burn accounted for in the store?
590
+ assertEq(IJB721TiersHook(dataHook).STORE().numberOfBurnedFor(dataHook, highestTier), 1);
591
+
592
+ // Check: the number of pending reserves should be equal to the calculated figure which accounts for rounding.
593
+ assertEq(IJB721TiersHook(dataHook).STORE().numberOfPendingReservesFor(dataHook, highestTier), 1);
594
+
595
+ {
596
+ uint16[] memory rawMetadata = new uint16[](1);
597
+ rawMetadata[0] = uint16(highestTier);
598
+
599
+ // Build the metadata using the tiers to mint and the overspending flag.
600
+ data = new bytes[](1);
601
+ data[0] = abi.encode(true, rawMetadata);
602
+
603
+ // Pass the hook ID.
604
+ ids = new bytes4[](1);
605
+ ids[0] = metadataHelper.getId("pay", address(hook));
606
+
607
+ // Generate the metadata.
608
+ hookMetadata = metadataHelper.createMetadata(ids, data);
609
+ }
610
+
611
+ // Pay the terminal to mint one more NFT.
612
+ vm.prank(caller);
613
+ jbMultiTerminal.pay{value: valueSent}({
614
+ projectId: projectId,
615
+ amount: 100,
616
+ token: JBConstants.NATIVE_TOKEN,
617
+ beneficiary: beneficiary,
618
+ minReturnedTokens: 0,
619
+ memo: "Take my money!",
620
+ metadata: hookMetadata
621
+ });
622
+
623
+ // Check: was the beneficiary's NFT balance is 1.
624
+ assertEq(IERC721(dataHook).balanceOf(beneficiary), 1);
625
+
626
+ // Check: the number of pending reserves shouldn't have changed.
627
+ assertEq(IJB721TiersHook(dataHook).STORE().numberOfPendingReservesFor(dataHook, highestTier), 2);
628
+ }
629
+
630
+ // - Mint 5 NFTs from a tier.
631
+ // - Check the remaining supply within that NFT's tier. (highest tier == 10, reserved percent is maximum -> 5)
632
+ // - Cash out all of the corresponding token from that tier
633
+ function testCashOutAll(bytes32 salt) external {
634
+ (JBDeploy721TiersHookConfig memory tiersHookConfig, JBLaunchProjectConfig memory launchProjectConfig) =
635
+ createData();
636
+ uint256 tier = 10;
637
+ uint256 tierPrice = tiersHookConfig.tiersConfig.tiers[tier - 1].price;
638
+ (uint256 projectId, IJB721TiersHook _hook) =
639
+ deployer.launchProjectFor(projectOwner, tiersHookConfig, launchProjectConfig, jbController, salt);
640
+
641
+ // Craft the metadata: buy 5 NFTs from tier 10.
642
+ uint16[] memory rawMetadata = new uint16[](5);
643
+ for (uint256 i; i < rawMetadata.length; i++) {
644
+ rawMetadata[i] = uint16(tier);
645
+ }
646
+
647
+ // Build the metadata using the tiers to mint and the overspending flag.
648
+ bytes[] memory data = new bytes[](1);
649
+ data[0] = abi.encode(true, rawMetadata);
650
+
651
+ address dataHook = jbRulesets.currentOf(projectId).dataHook();
652
+ assertEq(address(_hook), dataHook);
653
+
654
+ // Pass the hook ID.
655
+ bytes4[] memory ids = new bytes4[](1);
656
+ ids[0] = metadataHelper.getId("pay", address(hook));
657
+
658
+ // Generate the metadata.
659
+ bytes memory hookMetadata = metadataHelper.createMetadata(ids, data);
660
+
661
+ // Pay the terminal to mint the NFTs.
662
+ vm.prank(caller);
663
+ jbMultiTerminal.pay{value: tierPrice * rawMetadata.length}({
664
+ projectId: projectId,
665
+ amount: 100,
666
+ token: JBConstants.NATIVE_TOKEN,
667
+ beneficiary: beneficiary,
668
+ minReturnedTokens: 0,
669
+ memo: "Take my money!",
670
+ metadata: hookMetadata
671
+ });
672
+
673
+ // Get the beneficiary's new NFT balance.
674
+ uint256 nftBalance = IERC721(dataHook).balanceOf(beneficiary);
675
+ // Check: are the NFT balance and pending reserves correct?
676
+ assertEq(rawMetadata.length, nftBalance);
677
+ // Add 1 to the pending reserves check, as we round up for non-null values.
678
+ assertEq(
679
+ IJB721TiersHook(dataHook).STORE().numberOfPendingReservesFor(dataHook, tier),
680
+ (nftBalance / tiersHookConfig.tiersConfig.tiers[tier - 1].reserveFrequency) + 1
681
+ );
682
+ // Craft the metadata to cash out the `tokenId`s.
683
+ uint256[] memory cashOutId = new uint256[](5);
684
+ for (uint256 i; i < rawMetadata.length; i++) {
685
+ uint256 tokenId = _generateTokenId(tier, i + 1);
686
+ cashOutId[i] = tokenId;
687
+ }
688
+
689
+ // Build the metadata with the tiers to cash out.
690
+ data[0] = abi.encode(cashOutId);
691
+
692
+ // Pass the hook ID.
693
+ ids[0] = metadataHelper.getId("cashOut", address(hook));
694
+
695
+ // Generate the metadata.
696
+ hookMetadata = metadataHelper.createMetadata(ids, data);
697
+
698
+ // Cash out the NFTs.
699
+ vm.prank(beneficiary);
700
+ jbMultiTerminal.cashOutTokensOf({
701
+ holder: beneficiary,
702
+ projectId: projectId,
703
+ tokenToReclaim: JBConstants.NATIVE_TOKEN,
704
+ cashOutCount: 0,
705
+ minTokensReclaimed: 0,
706
+ beneficiary: payable(beneficiary),
707
+ metadata: hookMetadata
708
+ });
709
+
710
+ // Check: did the beneficiary's NFT balance decrease by 5 (to 0)?
711
+ assertEq(IERC721(dataHook).balanceOf(beneficiary), 0);
712
+ // Check: were the NFT burns accounted for in the store?
713
+ assertEq(IJB721TiersHook(dataHook).STORE().numberOfBurnedFor(dataHook, tier), 5);
714
+ // Check: did the number of pending reserves didnt change due to the burn.
715
+ assertEq(
716
+ IJB721TiersHook(dataHook).STORE().numberOfPendingReservesFor(dataHook, tier),
717
+ (nftBalance / tiersHookConfig.tiersConfig.tiers[tier - 1].reserveFrequency) + 1
718
+ );
719
+
720
+ // Craft the metadata: buy *1* NFT from tier 10.
721
+ uint16[] memory rawMetadata2 = new uint16[](1);
722
+ for (uint256 i; i < rawMetadata2.length; i++) {
723
+ rawMetadata2[i] = uint16(tier);
724
+ }
725
+
726
+ // Build the metadata using the tiers to mint and the overspending flag.
727
+ data[0] = abi.encode(true, rawMetadata2);
728
+
729
+ // Pass the hook ID.
730
+ ids[0] = metadataHelper.getId("pay", address(hook));
731
+
732
+ // Generate the metadata.
733
+ hookMetadata = metadataHelper.createMetadata(ids, data);
734
+
735
+ // Check: can more NFTs be minted (now that the previous ones were burned)?
736
+ vm.prank(caller);
737
+ jbMultiTerminal.pay{value: tierPrice * rawMetadata2.length}({
738
+ projectId: projectId,
739
+ amount: 100,
740
+ token: JBConstants.NATIVE_TOKEN,
741
+ beneficiary: beneficiary,
742
+ minReturnedTokens: 0,
743
+ memo: "Take my money!",
744
+ metadata: hookMetadata
745
+ });
746
+
747
+ // Get the new NFT balance.
748
+ nftBalance = IERC721(dataHook).balanceOf(beneficiary);
749
+ // Check: are the NFT balance and pending reserves correct?
750
+ assertEq(rawMetadata2.length, nftBalance);
751
+ // Add 1 to the pending reserves check, as we round up for non-null values.
752
+ assertEq(
753
+ IJB721TiersHook(dataHook).STORE().numberOfPendingReservesFor(dataHook, tier),
754
+ (nftBalance / tiersHookConfig.tiersConfig.tiers[tier - 1].reserveFrequency) + 1
755
+ );
756
+ }
757
+
758
+ // ----- internal helpers ------
759
+ // Creates a `launchProjectFor(...)` payload.
760
+ function createData()
761
+ internal
762
+ view
763
+ returns (JBDeploy721TiersHookConfig memory tiersHookConfig, JBLaunchProjectConfig memory launchProjectConfig)
764
+ {
765
+ JB721TierConfig[] memory tierConfigs = new JB721TierConfig[](10);
766
+ for (uint256 i; i < 10; i++) {
767
+ tierConfigs[i] = JB721TierConfig({
768
+ price: uint104((i + 1) * 10),
769
+ initialSupply: uint32(10),
770
+ votingUnits: uint32((i + 1) * 10),
771
+ reserveFrequency: 10,
772
+ reserveBeneficiary: reserveBeneficiary,
773
+ encodedIPFSUri: tokenUris[i],
774
+ category: uint24(100),
775
+ discountPercent: uint8(0),
776
+ allowOwnerMint: false,
777
+ useReserveBeneficiaryAsDefault: false,
778
+ transfersPausable: false,
779
+ useVotingUnits: false,
780
+ cannotBeRemoved: false,
781
+ cannotIncreaseDiscountPercent: false
782
+ });
783
+ }
784
+ tiersHookConfig = JBDeploy721TiersHookConfig({
785
+ name: name,
786
+ symbol: symbol,
787
+ baseUri: baseUri,
788
+ tokenUriResolver: IJB721TokenUriResolver(address(0)),
789
+ contractUri: contractUri,
790
+ tiersConfig: JB721InitTiersConfig({
791
+ tiers: tierConfigs,
792
+ currency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
793
+ decimals: 18,
794
+ prices: IJBPrices(address(0))
795
+ }),
796
+ reserveBeneficiary: reserveBeneficiary,
797
+ flags: JB721TiersHookFlags({
798
+ preventOverspending: false,
799
+ noNewTiersWithReserves: false,
800
+ noNewTiersWithVotes: false,
801
+ noNewTiersWithOwnerMinting: true
802
+ })
803
+ });
804
+
805
+ JBPayDataHookRulesetMetadata memory metadata = JBPayDataHookRulesetMetadata({
806
+ reservedPercent: 5000, //50%
807
+ cashOutTaxRate: 5000, //50%
808
+ baseCurrency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
809
+ pausePay: false,
810
+ pauseCreditTransfers: false,
811
+ allowOwnerMinting: true,
812
+ allowTerminalMigration: false,
813
+ allowSetTerminals: false,
814
+ allowSetController: false,
815
+ ownerMustSendPayouts: false,
816
+ allowAddAccountingContext: false,
817
+ allowAddPriceFeed: false,
818
+ holdFees: false,
819
+ useTotalSurplusForCashOuts: false,
820
+ useDataHookForCashOut: true,
821
+ metadata: 0x00
822
+ });
823
+
824
+ JBPayDataHookRulesetConfig[] memory rulesetConfigurations = new JBPayDataHookRulesetConfig[](1);
825
+ // Package up the ruleset configuration.
826
+ rulesetConfigurations[0].mustStartAtOrAfter = 0;
827
+ rulesetConfigurations[0].duration = 14;
828
+ rulesetConfigurations[0].weight = 1000 * 10 ** 18;
829
+ rulesetConfigurations[0].weightCutPercent = 450_000_000;
830
+ rulesetConfigurations[0].approvalHook = IJBRulesetApprovalHook(address(0));
831
+ rulesetConfigurations[0].metadata = metadata;
832
+
833
+ JBTerminalConfig[] memory terminalConfigurations = new JBTerminalConfig[](1);
834
+ JBAccountingContext[] memory accountingContextsToAccept = new JBAccountingContext[](1);
835
+ accountingContextsToAccept[0] = JBAccountingContext({
836
+ token: JBConstants.NATIVE_TOKEN, currency: uint32(uint160(JBConstants.NATIVE_TOKEN)), decimals: 18
837
+ });
838
+ terminalConfigurations[0] =
839
+ JBTerminalConfig({terminal: jbMultiTerminal, accountingContextsToAccept: accountingContextsToAccept});
840
+
841
+ launchProjectConfig = JBLaunchProjectConfig({
842
+ projectUri: projectUri,
843
+ rulesetConfigurations: rulesetConfigurations,
844
+ terminalConfigurations: terminalConfigurations,
845
+ memo: ""
846
+ });
847
+ }
848
+
849
+ function createDiscountedData(
850
+ uint256 _price,
851
+ uint8 _discountPercent
852
+ )
853
+ internal
854
+ view
855
+ returns (JBDeploy721TiersHookConfig memory tiersHookConfig, JBLaunchProjectConfig memory launchProjectConfig)
856
+ {
857
+ JB721TierConfig[] memory tierConfigs = new JB721TierConfig[](1);
858
+ tierConfigs[0] = JB721TierConfig({
859
+ price: uint104(_price),
860
+ initialSupply: uint32(10),
861
+ votingUnits: uint32(10),
862
+ reserveFrequency: 10,
863
+ reserveBeneficiary: reserveBeneficiary,
864
+ encodedIPFSUri: tokenUris[0],
865
+ category: uint24(100),
866
+ discountPercent: _discountPercent,
867
+ allowOwnerMint: false,
868
+ useReserveBeneficiaryAsDefault: false,
869
+ transfersPausable: false,
870
+ useVotingUnits: false,
871
+ cannotBeRemoved: false,
872
+ cannotIncreaseDiscountPercent: false
873
+ });
874
+
875
+ tiersHookConfig = JBDeploy721TiersHookConfig({
876
+ name: name,
877
+ symbol: symbol,
878
+ baseUri: baseUri,
879
+ tokenUriResolver: IJB721TokenUriResolver(address(0)),
880
+ contractUri: contractUri,
881
+ tiersConfig: JB721InitTiersConfig({
882
+ tiers: tierConfigs,
883
+ currency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
884
+ decimals: 18,
885
+ prices: IJBPrices(address(0))
886
+ }),
887
+ reserveBeneficiary: reserveBeneficiary,
888
+ flags: JB721TiersHookFlags({
889
+ preventOverspending: false,
890
+ noNewTiersWithReserves: false,
891
+ noNewTiersWithVotes: false,
892
+ noNewTiersWithOwnerMinting: true
893
+ })
894
+ });
895
+
896
+ JBPayDataHookRulesetMetadata memory metadata = JBPayDataHookRulesetMetadata({
897
+ reservedPercent: 5000, //50%
898
+ cashOutTaxRate: 5000, //50%
899
+ baseCurrency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
900
+ pausePay: false,
901
+ pauseCreditTransfers: false,
902
+ allowOwnerMinting: true,
903
+ allowTerminalMigration: false,
904
+ allowSetTerminals: false,
905
+ allowSetController: false,
906
+ ownerMustSendPayouts: false,
907
+ allowAddAccountingContext: false,
908
+ allowAddPriceFeed: false,
909
+ holdFees: false,
910
+ useTotalSurplusForCashOuts: false,
911
+ useDataHookForCashOut: true,
912
+ metadata: 0x00
913
+ });
914
+
915
+ JBPayDataHookRulesetConfig[] memory rulesetConfigurations = new JBPayDataHookRulesetConfig[](1);
916
+ // Package up the ruleset configuration.
917
+ rulesetConfigurations[0].mustStartAtOrAfter = 0;
918
+ rulesetConfigurations[0].duration = 14;
919
+ rulesetConfigurations[0].weight = 1000 * 10 ** 18;
920
+ rulesetConfigurations[0].weightCutPercent = 450_000_000;
921
+ rulesetConfigurations[0].approvalHook = IJBRulesetApprovalHook(address(0));
922
+ rulesetConfigurations[0].metadata = metadata;
923
+
924
+ JBTerminalConfig[] memory terminalConfigurations = new JBTerminalConfig[](1);
925
+ JBAccountingContext[] memory accountingContextsToAccept = new JBAccountingContext[](1);
926
+ accountingContextsToAccept[0] = JBAccountingContext({
927
+ token: JBConstants.NATIVE_TOKEN, currency: uint32(uint160(JBConstants.NATIVE_TOKEN)), decimals: 18
928
+ });
929
+ terminalConfigurations[0] =
930
+ JBTerminalConfig({terminal: jbMultiTerminal, accountingContextsToAccept: accountingContextsToAccept});
931
+
932
+ launchProjectConfig = JBLaunchProjectConfig({
933
+ projectUri: projectUri,
934
+ rulesetConfigurations: rulesetConfigurations,
935
+ terminalConfigurations: terminalConfigurations,
936
+ memo: ""
937
+ });
938
+ }
939
+
940
+ // Generate `tokenId`s based on the tier ID and token number provided.
941
+ function _generateTokenId(uint256 tierId, uint256 tokenNumber) internal pure returns (uint256) {
942
+ return (tierId * 1_000_000_000) + tokenNumber;
943
+ }
944
+ }