@croptop/core-v6 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +45 -0
  3. package/SKILLS.md +88 -0
  4. package/deployments/croptop-core-v5/arbitrum/CTDeployer.json +1896 -0
  5. package/deployments/croptop-core-v5/arbitrum/CTProjectOwner.json +186 -0
  6. package/deployments/croptop-core-v5/arbitrum/CTPublisher.json +738 -0
  7. package/deployments/croptop-core-v5/arbitrum_sepolia/CTDeployer.json +1883 -0
  8. package/deployments/croptop-core-v5/arbitrum_sepolia/CTProjectOwner.json +186 -0
  9. package/deployments/croptop-core-v5/arbitrum_sepolia/CTPublisher.json +738 -0
  10. package/deployments/croptop-core-v5/base/CTDeployer.json +1908 -0
  11. package/deployments/croptop-core-v5/base/CTProjectOwner.json +190 -0
  12. package/deployments/croptop-core-v5/base/CTPublisher.json +741 -0
  13. package/deployments/croptop-core-v5/base_sepolia/CTDeployer.json +1894 -0
  14. package/deployments/croptop-core-v5/base_sepolia/CTProjectOwner.json +190 -0
  15. package/deployments/croptop-core-v5/base_sepolia/CTPublisher.json +741 -0
  16. package/deployments/croptop-core-v5/ethereum/CTDeployer.json +1894 -0
  17. package/deployments/croptop-core-v5/ethereum/CTProjectOwner.json +190 -0
  18. package/deployments/croptop-core-v5/ethereum/CTPublisher.json +741 -0
  19. package/deployments/croptop-core-v5/optimism/CTDeployer.json +1894 -0
  20. package/deployments/croptop-core-v5/optimism/CTProjectOwner.json +190 -0
  21. package/deployments/croptop-core-v5/optimism/CTPublisher.json +741 -0
  22. package/deployments/croptop-core-v5/optimism_sepolia/CTDeployer.json +1894 -0
  23. package/deployments/croptop-core-v5/optimism_sepolia/CTProjectOwner.json +190 -0
  24. package/deployments/croptop-core-v5/optimism_sepolia/CTPublisher.json +741 -0
  25. package/deployments/croptop-core-v5/sepolia/CTDeployer.json +1894 -0
  26. package/deployments/croptop-core-v5/sepolia/CTProjectOwner.json +190 -0
  27. package/deployments/croptop-core-v5/sepolia/CTPublisher.json +741 -0
  28. package/foundry.toml +25 -0
  29. package/package.json +31 -0
  30. package/remappings.txt +2 -0
  31. package/script/ConfigureFeeProject.s.sol +386 -0
  32. package/script/Deploy.s.sol +138 -0
  33. package/script/helpers/CroptopDeploymentLib.sol +75 -0
  34. package/slither-ci.config.json +10 -0
  35. package/sphinx.lock +507 -0
  36. package/src/CTDeployer.sol +425 -0
  37. package/src/CTProjectOwner.sol +78 -0
  38. package/src/CTPublisher.sol +540 -0
  39. package/src/interfaces/ICTDeployer.sol +56 -0
  40. package/src/interfaces/ICTProjectOwner.sol +24 -0
  41. package/src/interfaces/ICTPublisher.sol +91 -0
  42. package/src/structs/CTAllowedPost.sol +22 -0
  43. package/src/structs/CTDeployerAllowedPost.sol +20 -0
  44. package/src/structs/CTPost.sol +22 -0
  45. package/src/structs/CTProjectConfig.sol +22 -0
  46. package/src/structs/CTSuckerDeploymentConfig.sol +11 -0
  47. package/test/CTPublisher.t.sol +672 -0
  48. package/test/CroptopAttacks.t.sol +439 -0
  49. package/test/Fork.t.sol +114 -0
  50. package/test/Test_MetadataGeneration.t.sol +70 -0
@@ -0,0 +1,540 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity 0.8.23;
3
+
4
+ import {IJB721TiersHook} from "@bananapus/721-hook-v6/src/interfaces/IJB721TiersHook.sol";
5
+ import {JB721Tier} from "@bananapus/721-hook-v6/src/structs/JB721Tier.sol";
6
+ import {JB721TierConfig} from "@bananapus/721-hook-v6/src/structs/JB721TierConfig.sol";
7
+ import {JBSplit} from "@bananapus/core-v6/src/structs/JBSplit.sol";
8
+ import {JBPermissioned} from "@bananapus/core-v6/src/abstract/JBPermissioned.sol";
9
+ import {IJBDirectory} from "@bananapus/core-v6/src/interfaces/IJBDirectory.sol";
10
+ import {IJBPermissions} from "@bananapus/core-v6/src/interfaces/IJBPermissions.sol";
11
+ import {IJBTerminal} from "@bananapus/core-v6/src/interfaces/IJBTerminal.sol";
12
+ import {JBConstants} from "@bananapus/core-v6/src/libraries/JBConstants.sol";
13
+ import {JBMetadataResolver} from "@bananapus/core-v6/src/libraries/JBMetadataResolver.sol";
14
+ import {JBOwnable} from "@bananapus/ownable-v6/src/JBOwnable.sol";
15
+ import {JBPermissionIds} from "@bananapus/permission-ids-v6/src/JBPermissionIds.sol";
16
+ import {ERC2771Context} from "@openzeppelin/contracts/metatx/ERC2771Context.sol";
17
+ import {Context} from "@openzeppelin/contracts/utils/Context.sol";
18
+
19
+ import {ICTPublisher} from "./interfaces/ICTPublisher.sol";
20
+ import {CTAllowedPost} from "./structs/CTAllowedPost.sol";
21
+ import {CTPost} from "./structs/CTPost.sol";
22
+
23
+ /// @notice A contract that facilitates the permissioned publishing of NFT posts to a Juicebox project.
24
+ contract CTPublisher is JBPermissioned, ERC2771Context, ICTPublisher {
25
+ //*********************************************************************//
26
+ // --------------------------- custom errors ------------------------- //
27
+ //*********************************************************************//
28
+
29
+ error CTPublisher_EmptyEncodedIPFSUri();
30
+ error CTPublisher_InsufficientEthSent(uint256 expected, uint256 sent);
31
+ error CTPublisher_MaxTotalSupplyLessThanMin(uint256 min, uint256 max);
32
+ error CTPublisher_NotInAllowList(address addr, address[] allowedAddresses);
33
+ error CTPublisher_PriceTooSmall(uint256 price, uint256 minimumPrice);
34
+ error CTPublisher_SplitPercentExceedsMaximum(uint256 splitPercent, uint256 maximumSplitPercent);
35
+ error CTPublisher_TotalSupplyTooBig(uint256 totalSupply, uint256 maximumTotalSupply);
36
+ error CTPublisher_TotalSupplyTooSmall(uint256 totalSupply, uint256 minimumTotalSupply);
37
+ error CTPublisher_UnauthorizedToPostInCategory();
38
+ error CTPublisher_ZeroTotalSupply();
39
+
40
+ //*********************************************************************//
41
+ // ------------------------- public constants ------------------------ //
42
+ //*********************************************************************//
43
+
44
+ /// @notice The divisor that describes the fee that should be taken.
45
+ /// @dev This is equal to 100 divided by the fee percent.
46
+ uint256 public constant override FEE_DIVISOR = 20;
47
+
48
+ //*********************************************************************//
49
+ // ---------------- public immutable stored properties --------------- //
50
+ //*********************************************************************//
51
+
52
+ /// @notice The directory that contains the projects being posted to.
53
+ IJBDirectory public immutable override DIRECTORY;
54
+
55
+ /// @notice The ID of the project to which fees will be routed.
56
+ uint256 public immutable override FEE_PROJECT_ID;
57
+
58
+ //*********************************************************************//
59
+ // --------------------- public stored properties -------------------- //
60
+ //*********************************************************************//
61
+
62
+ /// @notice The ID of the tier that an IPFS metadata has been saved to.
63
+ /// @custom:param hook The hook for which the tier ID applies.
64
+ /// @custom:param encodedIPFSUri The IPFS URI.
65
+ mapping(address hook => mapping(bytes32 encodedIPFSUri => uint256)) public override tierIdForEncodedIPFSUriOf;
66
+
67
+ //*********************************************************************//
68
+ // --------------------- internal stored properties ------------------ //
69
+ //*********************************************************************//
70
+
71
+ /// @notice Stores addresses that are allowed to post onto a hook category.
72
+ /// @custom:param hook The hook for which this allowance applies.
73
+ /// @custom:param category The category for which the allowance applies.
74
+ /// @custom:param address The address to check an allowance for.
75
+ mapping(address hook => mapping(uint256 category => address[])) internal _allowedAddresses;
76
+
77
+ /// @notice Packed values that determine the allowance of posts.
78
+ /// @custom:param hook The hook for which this allowance applies.
79
+ /// @custom:param category The category for which the allowance applies
80
+ mapping(address hook => mapping(uint256 category => uint256)) internal _packedAllowanceFor;
81
+
82
+ //*********************************************************************//
83
+ // -------------------------- constructor ---------------------------- //
84
+ //*********************************************************************//
85
+
86
+ /// @param directory The directory that contains the projects being posted to.
87
+ /// @param permissions A contract storing permissions.
88
+ /// @param feeProjectId The ID of the project to which fees will be routed.
89
+ /// @param trustedForwarder The trusted forwarder for the ERC2771Context.
90
+ constructor(
91
+ IJBDirectory directory,
92
+ IJBPermissions permissions,
93
+ uint256 feeProjectId,
94
+ address trustedForwarder
95
+ )
96
+ JBPermissioned(permissions)
97
+ ERC2771Context(trustedForwarder)
98
+ {
99
+ DIRECTORY = directory;
100
+ FEE_PROJECT_ID = feeProjectId;
101
+ }
102
+
103
+ //*********************************************************************//
104
+ // ------------------------- external views -------------------------- //
105
+ //*********************************************************************//
106
+
107
+ /// @notice Get the tiers for the provided encoded IPFS URIs.
108
+ /// @param hook The hook from which to get tiers.
109
+ /// @param encodedIPFSUris The URIs to get tiers of.
110
+ /// @return tiers The tiers that correspond to the provided encoded IPFS URIs. If there's no tier yet, an empty tier
111
+ /// is returned.
112
+ function tiersFor(
113
+ address hook,
114
+ bytes32[] memory encodedIPFSUris
115
+ )
116
+ external
117
+ view
118
+ override
119
+ returns (JB721Tier[] memory tiers)
120
+ {
121
+ uint256 numberOfEncodedIPFSUris = encodedIPFSUris.length;
122
+
123
+ // Initialize the tier array being returned.
124
+ tiers = new JB721Tier[](numberOfEncodedIPFSUris);
125
+
126
+ // Get the tier for each provided encoded IPFS URI.
127
+ for (uint256 i; i < numberOfEncodedIPFSUris; i++) {
128
+ // Check if there's a tier ID stored for the encoded IPFS URI.
129
+ uint256 tierId = tierIdForEncodedIPFSUriOf[hook][encodedIPFSUris[i]];
130
+
131
+ // If there's a tier ID stored, resolve it.
132
+ if (tierId != 0) {
133
+ // slither-disable-next-line calls-loop
134
+ tiers[i] = IJB721TiersHook(hook).STORE().tierOf({hook: hook, id: tierId, includeResolvedUri: false});
135
+ }
136
+ }
137
+ }
138
+
139
+ //*********************************************************************//
140
+ // -------------------------- public views --------------------------- //
141
+ //*********************************************************************//
142
+
143
+ /// @notice Post allowances for a particular category on a particular hook.
144
+ /// @param hook The hook contract for which this allowance applies.
145
+ /// @param category The category for which this allowance applies.
146
+ /// @return minimumPrice The minimum price that a poster must pay to record a new NFT.
147
+ /// @return minimumTotalSupply The minimum total number of available tokens that a minter must set to record a new
148
+ /// NFT.
149
+ /// @return maximumTotalSupply The max total supply of NFTs that can be made available when minting. Leave as 0 for
150
+ /// max.
151
+ /// @return maximumSplitPercent The maximum split percent that a poster can set. 0 means splits are not allowed.
152
+ /// @return allowedAddresses The addresses allowed to post. Returns empty if all addresses are allowed.
153
+ function allowanceFor(
154
+ address hook,
155
+ uint256 category
156
+ )
157
+ public
158
+ view
159
+ override
160
+ returns (
161
+ uint256 minimumPrice,
162
+ uint256 minimumTotalSupply,
163
+ uint256 maximumTotalSupply,
164
+ uint256 maximumSplitPercent,
165
+ address[] memory allowedAddresses
166
+ )
167
+ {
168
+ // Get a reference to the packed values.
169
+ uint256 packed = _packedAllowanceFor[hook][category];
170
+
171
+ // minimum price in bits 0-103 (104 bits).
172
+ minimumPrice = uint256(uint104(packed));
173
+ // minimum supply in bits 104-135 (32 bits).
174
+ minimumTotalSupply = uint256(uint32(packed >> 104));
175
+ // maximum supply in bits 136-167 (32 bits).
176
+ maximumTotalSupply = uint256(uint32(packed >> 136));
177
+ // maximum split percent in bits 168-199 (32 bits).
178
+ maximumSplitPercent = uint256(uint32(packed >> 168));
179
+
180
+ allowedAddresses = _allowedAddresses[hook][category];
181
+ }
182
+
183
+ //*********************************************************************//
184
+ // -------------------------- internal views ------------------------- //
185
+ //*********************************************************************//
186
+
187
+ /// @dev ERC-2771 specifies the context as being a single address (20 bytes).
188
+ function _contextSuffixLength() internal view virtual override(ERC2771Context, Context) returns (uint256) {
189
+ return super._contextSuffixLength();
190
+ }
191
+
192
+ /// @notice Check if an address is included in an allow list.
193
+ /// @dev Uses an O(n) linear scan over the `addresses` array. This is acceptable for typical allow list sizes
194
+ /// (fewer than ~100 addresses), where gas cost is negligible. For very large allow lists, a Merkle proof
195
+ /// pattern would scale better, but the added complexity is not warranted for the expected use case.
196
+ /// @param addrs The candidate address.
197
+ /// @param addresses An array of allowed addresses.
198
+ function _isAllowed(address addrs, address[] memory addresses) internal pure returns (bool) {
199
+ // Keep a reference to the number of address to check against.
200
+ uint256 numberOfAddresses = addresses.length;
201
+
202
+ // Check if the address is included
203
+ for (uint256 i; i < numberOfAddresses; i++) {
204
+ if (addrs == addresses[i]) return true;
205
+ }
206
+
207
+ return false;
208
+ }
209
+
210
+ /// @notice Returns the calldata, prefered to use over `msg.data`
211
+ /// @return calldata the `msg.data` of this call
212
+ function _msgData() internal view override(ERC2771Context, Context) returns (bytes calldata) {
213
+ return ERC2771Context._msgData();
214
+ }
215
+
216
+ /// @notice Returns the sender, prefered to use over `msg.sender`
217
+ /// @return sender the sender address of this call.
218
+ function _msgSender() internal view override(ERC2771Context, Context) returns (address sender) {
219
+ return ERC2771Context._msgSender();
220
+ }
221
+
222
+ //*********************************************************************//
223
+ // ---------------------- external transactions ---------------------- //
224
+ //*********************************************************************//
225
+
226
+ /// @notice Collection owners can set the allowed criteria for publishing a new NFT to their project.
227
+ /// @param allowedPosts An array of criteria for allowed posts.
228
+ function configurePostingCriteriaFor(CTAllowedPost[] memory allowedPosts) external override {
229
+ // Keep a reference to the number of post criteria.
230
+ uint256 numberOfAllowedPosts = allowedPosts.length;
231
+
232
+ // For each post criteria, save the specifications.
233
+ for (uint256 i; i < numberOfAllowedPosts; i++) {
234
+ // Set the post criteria being iterated on.
235
+ CTAllowedPost memory allowedPost = allowedPosts[i];
236
+
237
+ emit ConfigurePostingCriteria({hook: allowedPost.hook, allowedPost: allowedPost, caller: _msgSender()});
238
+
239
+ // Enforce permissions.
240
+ // slither-disable-next-line reentrancy-events,calls-loop
241
+ _requirePermissionFrom({
242
+ account: JBOwnable(allowedPost.hook).owner(),
243
+ projectId: IJB721TiersHook(allowedPost.hook).PROJECT_ID(),
244
+ permissionId: JBPermissionIds.ADJUST_721_TIERS
245
+ });
246
+
247
+ // Make sure there is a minimum supply.
248
+ if (allowedPost.minimumTotalSupply == 0) {
249
+ revert CTPublisher_ZeroTotalSupply();
250
+ }
251
+
252
+ // Make sure the minimum supply does not surpass the maximum supply.
253
+ if (allowedPost.minimumTotalSupply > allowedPost.maximumTotalSupply) {
254
+ revert CTPublisher_MaxTotalSupplyLessThanMin(
255
+ allowedPost.minimumTotalSupply, allowedPost.maximumTotalSupply
256
+ );
257
+ }
258
+
259
+ uint256 packed;
260
+ // minimum price in bits 0-103 (104 bits).
261
+ packed |= uint256(allowedPost.minimumPrice);
262
+ // minimum total supply in bits 104-135 (32 bits).
263
+ packed |= uint256(allowedPost.minimumTotalSupply) << 104;
264
+ // maximum total supply in bits 136-167 (32 bits).
265
+ packed |= uint256(allowedPost.maximumTotalSupply) << 136;
266
+ // maximum split percent in bits 168-199 (32 bits).
267
+ packed |= uint256(allowedPost.maximumSplitPercent) << 168;
268
+ // Store the packed value.
269
+ _packedAllowanceFor[allowedPost.hook][allowedPost.category] = packed;
270
+
271
+ // Store the allow list.
272
+ uint256 numberOfAddresses = allowedPost.allowedAddresses.length;
273
+ // Reset the addresses.
274
+ delete _allowedAddresses[allowedPost.hook][allowedPost.category];
275
+ // Add the number allowed addresses.
276
+ if (numberOfAddresses != 0) {
277
+ // Keep a reference to the storage of the allowed addresses.
278
+ for (uint256 j = 0; j < numberOfAddresses; j++) {
279
+ _allowedAddresses[allowedPost.hook][allowedPost.category].push(allowedPost.allowedAddresses[j]);
280
+ }
281
+ }
282
+ }
283
+ }
284
+
285
+ /// @notice Publish an NFT to become mintable, and mint a first copy.
286
+ /// @dev A fee is taken into the appropriate treasury.
287
+ /// @param hook The hook to mint from.
288
+ /// @param posts An array of posts that should be published as NFTs to the specified project.
289
+ /// @param nftBeneficiary The beneficiary of the NFT mints.
290
+ /// @param feeBeneficiary The beneficiary of the fee project's token.
291
+ /// @param additionalPayMetadata Metadata bytes that should be included in the pay function's metadata. This
292
+ /// prepends the
293
+ /// payload needed for NFT creation.
294
+ /// @param feeMetadata The metadata to send alongside the fee payment.
295
+ function mintFrom(
296
+ IJB721TiersHook hook,
297
+ CTPost[] calldata posts,
298
+ address nftBeneficiary,
299
+ address feeBeneficiary,
300
+ bytes calldata additionalPayMetadata,
301
+ bytes calldata feeMetadata
302
+ )
303
+ external
304
+ payable
305
+ override
306
+ {
307
+ // Keep a reference to the amount being paid, which is msg.value minus the fee.
308
+ uint256 payValue = msg.value;
309
+
310
+ // Keep a reference to the mint metadata.
311
+ bytes memory mintMetadata;
312
+
313
+ // Keep a reference to the project's ID.
314
+ uint256 projectId = hook.PROJECT_ID();
315
+
316
+ {
317
+ // Setup the posts.
318
+ (JB721TierConfig[] memory tiersToAdd, uint256[] memory tierIdsToMint, uint256 totalPrice) =
319
+ _setupPosts(hook, posts);
320
+
321
+ if (projectId != FEE_PROJECT_ID) {
322
+ // Keep a reference to the fee that will be paid.
323
+ // Note: integer division truncates, so the fee loses up to (FEE_DIVISOR - 1) wei of dust.
324
+ // For example, a totalPrice of 39 wei with FEE_DIVISOR=20 yields a fee of 1 wei instead of 1.95.
325
+ // This rounding is in the payer's favor and the loss is negligible for practical amounts.
326
+ payValue -= totalPrice / FEE_DIVISOR;
327
+ }
328
+
329
+ // Make sure the amount sent to this function is at least the specified price of the tier plus the fee.
330
+ if (totalPrice > payValue) {
331
+ revert CTPublisher_InsufficientEthSent(totalPrice, msg.value);
332
+ }
333
+
334
+ // Add the new tiers.
335
+ // slither-disable-next-line reentrancy-events
336
+ hook.adjustTiers({tierDataToAdd: tiersToAdd, tierIdsToRemove: new uint256[](0)});
337
+
338
+ // Keep a reference to the metadata ID target.
339
+ address metadataIdTarget = hook.METADATA_ID_TARGET();
340
+
341
+ // Create the metadata for the payment to specify the tier IDs that should be minted. We create manually the
342
+ // original metadata, following
343
+ // the specifications from the JBMetadataResolver library.
344
+ mintMetadata = JBMetadataResolver.addToMetadata({
345
+ originalMetadata: additionalPayMetadata,
346
+ idToAdd: JBMetadataResolver.getId({purpose: "pay", target: metadataIdTarget}),
347
+ dataToAdd: abi.encode(true, tierIdsToMint)
348
+ });
349
+
350
+ // Store the referal id in the first 32 bytes of the metadata (push to stack for immutable in assembly)
351
+ uint256 feeProjectId = FEE_PROJECT_ID;
352
+
353
+ assembly {
354
+ mstore(add(mintMetadata, 32), feeProjectId)
355
+ }
356
+ }
357
+
358
+ emit Mint({
359
+ projectId: projectId,
360
+ hook: hook,
361
+ nftBeneficiary: nftBeneficiary,
362
+ feeBeneficiary: feeBeneficiary,
363
+ posts: posts,
364
+ postValue: payValue,
365
+ txValue: msg.value,
366
+ caller: _msgSender()
367
+ });
368
+
369
+ {
370
+ // Get a reference to the project's current ETH payment terminal.
371
+ IJBTerminal projectTerminal =
372
+ DIRECTORY.primaryTerminalOf({projectId: projectId, token: JBConstants.NATIVE_TOKEN});
373
+
374
+ // Make the payment.
375
+ // slither-disable-next-line unused-return
376
+ projectTerminal.pay{value: payValue}({
377
+ projectId: projectId,
378
+ token: JBConstants.NATIVE_TOKEN,
379
+ amount: payValue,
380
+ beneficiary: nftBeneficiary,
381
+ minReturnedTokens: 0,
382
+ memo: "Minted from Croptop",
383
+ metadata: mintMetadata
384
+ });
385
+ }
386
+
387
+ // Pay a fee if there are funds left.
388
+ if (address(this).balance != 0) {
389
+ // Get a reference to the fee project's current ETH payment terminal.
390
+ IJBTerminal feeTerminal =
391
+ DIRECTORY.primaryTerminalOf({projectId: FEE_PROJECT_ID, token: JBConstants.NATIVE_TOKEN});
392
+
393
+ // Make the fee payment.
394
+ // slither-disable-next-line unused-return
395
+ feeTerminal.pay{value: address(this).balance}({
396
+ projectId: FEE_PROJECT_ID,
397
+ amount: address(this).balance,
398
+ token: JBConstants.NATIVE_TOKEN,
399
+ beneficiary: feeBeneficiary,
400
+ minReturnedTokens: 0,
401
+ memo: "",
402
+ metadata: feeMetadata
403
+ });
404
+ }
405
+ }
406
+
407
+ //*********************************************************************//
408
+ // ------------------------ internal functions ----------------------- //
409
+ //*********************************************************************//
410
+
411
+ /// @notice Setup the posts.
412
+ /// @param hook The NFT hook on which the posts will apply.
413
+ /// @param posts An array of posts that should be published as NFTs to the specified project.
414
+ /// @return tiersToAdd The tiers that will be created to represent the posts.
415
+ /// @return tierIdsToMint The tier IDs of the posts that should be minted once published.
416
+ /// @return totalPrice The total price being paid.
417
+ function _setupPosts(
418
+ IJB721TiersHook hook,
419
+ CTPost[] memory posts
420
+ )
421
+ internal
422
+ returns (JB721TierConfig[] memory tiersToAdd, uint256[] memory tierIdsToMint, uint256 totalPrice)
423
+ {
424
+ // Set the max size of the tier data that will be added.
425
+ tiersToAdd = new JB721TierConfig[](posts.length);
426
+
427
+ // Set the size of the tier IDs of the posts that should be minted once published.
428
+ tierIdsToMint = new uint256[](posts.length);
429
+
430
+ // The tier ID that will be created, and the first one that should be minted from, is one more than the current
431
+ // max.
432
+ uint256 startingTierId = hook.STORE().maxTierIdOf(address(hook)) + 1;
433
+
434
+ // Keep a reference to the total number of tiers being added.
435
+ uint256 numberOfTiersBeingAdded;
436
+
437
+ // For each post, create tiers after validating to make sure they fulfill the allowance specified by the
438
+ // project's owner.
439
+ for (uint256 i; i < posts.length; i++) {
440
+ // Get the current post being iterated on.
441
+ CTPost memory post = posts[i];
442
+
443
+ // Make sure the post includes an encodedIPFSUri.
444
+ if (post.encodedIPFSUri == bytes32("")) {
445
+ revert CTPublisher_EmptyEncodedIPFSUri();
446
+ }
447
+
448
+ // Scoped section to prevent stack too deep.
449
+ {
450
+ // Check if there's an ID of a tier already minted for this encodedIPFSUri.
451
+ uint256 tierId = tierIdForEncodedIPFSUriOf[address(hook)][post.encodedIPFSUri];
452
+
453
+ if (tierId != 0) tierIdsToMint[i] = tierId;
454
+ }
455
+
456
+ // If no tier already exists, post the tier.
457
+ if (tierIdsToMint[i] == 0) {
458
+ // Scoped error handling section to prevent Stack Too Deep.
459
+ {
460
+ // Get references to the allowance.
461
+ (
462
+ uint256 minimumPrice,
463
+ uint256 minimumTotalSupply,
464
+ uint256 maximumTotalSupply,
465
+ uint256 maximumSplitPercent,
466
+ address[] memory addresses
467
+ ) = allowanceFor({hook: address(hook), category: post.category});
468
+
469
+ // Make sure the category being posted to allows publishing.
470
+ if (minimumTotalSupply == 0) {
471
+ revert CTPublisher_UnauthorizedToPostInCategory();
472
+ }
473
+
474
+ // Make sure the price being paid for the post is at least the allowed minimum price.
475
+ if (post.price < minimumPrice) {
476
+ revert CTPublisher_PriceTooSmall(post.price, minimumPrice);
477
+ }
478
+
479
+ // Make sure the total supply being made available for the post is at least the allowed minimum
480
+ // total supply.
481
+ if (post.totalSupply < minimumTotalSupply) {
482
+ revert CTPublisher_TotalSupplyTooSmall(post.totalSupply, minimumTotalSupply);
483
+ }
484
+
485
+ // Make sure the total supply being made available for the post is at most the allowed maximum total
486
+ // supply.
487
+ if (post.totalSupply > maximumTotalSupply) {
488
+ revert CTPublisher_TotalSupplyTooBig(post.totalSupply, maximumTotalSupply);
489
+ }
490
+
491
+ // Make sure the split percent is within the allowed maximum.
492
+ if (post.splitPercent > maximumSplitPercent) {
493
+ revert CTPublisher_SplitPercentExceedsMaximum(post.splitPercent, maximumSplitPercent);
494
+ }
495
+
496
+ // Make sure the address is allowed to post.
497
+ if (addresses.length != 0 && !_isAllowed(_msgSender(), addresses)) {
498
+ revert CTPublisher_NotInAllowList(_msgSender(), addresses);
499
+ }
500
+ }
501
+
502
+ // Set the tier.
503
+ tiersToAdd[numberOfTiersBeingAdded] = JB721TierConfig({
504
+ price: post.price,
505
+ initialSupply: post.totalSupply,
506
+ votingUnits: 0,
507
+ reserveFrequency: 0,
508
+ reserveBeneficiary: address(0),
509
+ encodedIPFSUri: post.encodedIPFSUri,
510
+ category: post.category,
511
+ discountPercent: 0,
512
+ allowOwnerMint: false,
513
+ useReserveBeneficiaryAsDefault: false,
514
+ transfersPausable: false,
515
+ useVotingUnits: true,
516
+ cannotBeRemoved: false,
517
+ cannotIncreaseDiscountPercent: false,
518
+ splitPercent: post.splitPercent,
519
+ splits: post.splits
520
+ });
521
+
522
+ // Set the ID of the tier to mint.
523
+ tierIdsToMint[i] = startingTierId + numberOfTiersBeingAdded++;
524
+
525
+ // Save the encodedIPFSUri as minted.
526
+ tierIdForEncodedIPFSUriOf[address(hook)][post.encodedIPFSUri] = tierIdsToMint[i];
527
+ }
528
+
529
+ // Increment the total price.
530
+ totalPrice += post.price;
531
+ }
532
+
533
+ // Resize the array if there's a mismatch in length.
534
+ if (numberOfTiersBeingAdded != posts.length) {
535
+ assembly ("memory-safe") {
536
+ mstore(tiersToAdd, numberOfTiersBeingAdded)
537
+ }
538
+ }
539
+ }
540
+ }
@@ -0,0 +1,56 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity ^0.8.0;
3
+
4
+ import {IJB721TiersHook} from "@bananapus/721-hook-v6/src/interfaces/IJB721TiersHook.sol";
5
+ import {IJB721TiersHookDeployer} from "@bananapus/721-hook-v6/src/interfaces/IJB721TiersHookDeployer.sol";
6
+ import {IJBController} from "@bananapus/core-v6/src/interfaces/IJBController.sol";
7
+ import {IJBProjects} from "@bananapus/core-v6/src/interfaces/IJBProjects.sol";
8
+
9
+ import {ICTPublisher} from "./ICTPublisher.sol";
10
+ import {CTSuckerDeploymentConfig} from "../structs/CTSuckerDeploymentConfig.sol";
11
+ import {CTProjectConfig} from "../structs/CTProjectConfig.sol";
12
+
13
+ interface ICTDeployer {
14
+ /// @notice The contract that mints ERC-721s representing Juicebox project ownership.
15
+ /// @return The projects contract.
16
+ function PROJECTS() external view returns (IJBProjects);
17
+
18
+ /// @notice The deployer used to launch tiered ERC-721 hook collections.
19
+ /// @return The hook deployer contract.
20
+ function DEPLOYER() external view returns (IJB721TiersHookDeployer);
21
+
22
+ /// @notice The Croptop publisher that manages posting criteria and minting.
23
+ /// @return The publisher contract.
24
+ function PUBLISHER() external view returns (ICTPublisher);
25
+
26
+ /// @notice Deploy a simple Juicebox project configured to receive posts from Croptop templates.
27
+ /// @param owner The address that will own the project after deployment.
28
+ /// @param projectConfig The configuration for the project, including name, symbol, and allowed posts.
29
+ /// @param suckerDeploymentConfiguration The configuration for cross-chain suckers to deploy.
30
+ /// @param controller The controller that will manage the project.
31
+ /// @return projectId The ID of the newly created project.
32
+ /// @return hook The tiered ERC-721 hook that was deployed for the project.
33
+ function deployProjectFor(
34
+ address owner,
35
+ CTProjectConfig calldata projectConfig,
36
+ CTSuckerDeploymentConfig calldata suckerDeploymentConfiguration,
37
+ IJBController controller
38
+ )
39
+ external
40
+ returns (uint256 projectId, IJB721TiersHook hook);
41
+
42
+ /// @notice Claim ownership of a tiered ERC-721 hook collection by transferring it to the project.
43
+ /// @param hook The hook to claim ownership of. The caller must own the project the hook belongs to.
44
+ function claimCollectionOwnershipOf(IJB721TiersHook hook) external;
45
+
46
+ /// @notice Deploy new suckers for an existing project.
47
+ /// @param projectId The ID of the project to deploy suckers for.
48
+ /// @param suckerDeploymentConfiguration The suckers to set up for the project.
49
+ /// @return suckers The addresses of the deployed suckers.
50
+ function deploySuckersFor(
51
+ uint256 projectId,
52
+ CTSuckerDeploymentConfig calldata suckerDeploymentConfiguration
53
+ )
54
+ external
55
+ returns (address[] memory suckers);
56
+ }
@@ -0,0 +1,24 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity ^0.8.0;
3
+
4
+ import {IJBPermissions} from "@bananapus/core-v6/src/interfaces/IJBPermissions.sol";
5
+ import {IJBProjects} from "@bananapus/core-v6/src/interfaces/IJBProjects.sol";
6
+
7
+ import {ICTPublisher} from "./ICTPublisher.sol";
8
+
9
+ /// @notice A contract that can receive a Juicebox project NFT (via `safeTransferFrom`) and automatically grants the
10
+ /// Croptop publisher permission to manage the project's 721 tiers. Once the project is transferred to this contract,
11
+ /// its ownership is effectively burned while still allowing croptop posts.
12
+ interface ICTProjectOwner {
13
+ /// @notice The contract where operator permissions are stored.
14
+ /// @return The permissions contract.
15
+ function PERMISSIONS() external view returns (IJBPermissions);
16
+
17
+ /// @notice The contract from which Juicebox projects are minted as ERC-721 tokens.
18
+ /// @return The projects contract.
19
+ function PROJECTS() external view returns (IJBProjects);
20
+
21
+ /// @notice The Croptop publisher that manages posting criteria and minting.
22
+ /// @return The publisher contract.
23
+ function PUBLISHER() external view returns (ICTPublisher);
24
+ }