@croptop/core-v6 0.0.19 → 0.0.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@croptop/core-v6",
3
- "version": "0.0.19",
3
+ "version": "0.0.20",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -16,17 +16,17 @@
16
16
  "artifacts": "source ./.env && npx sphinx artifacts --org-id 'ea165b21-7cdc-4d7b-be59-ecdd4c26bee4' --project-name 'croptop-core-v5'"
17
17
  },
18
18
  "dependencies": {
19
- "@bananapus/721-hook-v6": "^0.0.17",
20
- "@bananapus/buyback-hook-v6": "^0.0.13",
21
- "@bananapus/core-v6": "^0.0.17",
22
- "@bananapus/ownable-v6": "^0.0.10",
19
+ "@bananapus/721-hook-v6": "^0.0.19",
20
+ "@bananapus/buyback-hook-v6": "^0.0.16",
21
+ "@bananapus/core-v6": "^0.0.24",
22
+ "@bananapus/ownable-v6": "^0.0.12",
23
23
  "@bananapus/permission-ids-v6": "^0.0.10",
24
- "@bananapus/router-terminal-v6": "^0.0.13",
25
- "@bananapus/suckers-v6": "^0.0.11",
24
+ "@bananapus/router-terminal-v6": "^0.0.16",
25
+ "@bananapus/suckers-v6": "^0.0.13",
26
26
  "@openzeppelin/contracts": "^5.6.1"
27
27
  },
28
28
  "devDependencies": {
29
- "@rev-net/core-v6": "^0.0.12",
29
+ "@rev-net/core-v6": "^0.0.14",
30
30
  "@sphinx-labs/plugins": "^0.33.1"
31
31
  }
32
32
  }
@@ -14,6 +14,8 @@ import {IJB721TiersHookStore} from "@bananapus/721-hook-v6/src/interfaces/IJB721
14
14
  import {JBConstants} from "@bananapus/core-v6/src/libraries/JBConstants.sol";
15
15
  import {JBSplit} from "@bananapus/core-v6/src/structs/JBSplit.sol";
16
16
 
17
+ import {JB721TierConfig} from "@bananapus/721-hook-v6/src/structs/JB721TierConfig.sol";
18
+
17
19
  import {CTPublisher} from "../src/CTPublisher.sol";
18
20
  import {CTAllowedPost} from "../src/structs/CTAllowedPost.sol";
19
21
  import {CTPost} from "../src/structs/CTPost.sol";
@@ -770,6 +772,63 @@ contract TestCTPublisher is Test {
770
772
  // --- Multiple Posts With Different Split Percents ------------------- //
771
773
  //*********************************************************************//
772
774
 
775
+ function test_mintFrom_nonzeroSplitPercent_passesSplitsToTier() public {
776
+ _configureCategoryWithSplits(5, 0.01 ether, 1, 100, 500_000_000);
777
+ _setupMintMocks();
778
+
779
+ address splitBeneficiary = makeAddr("splitBeneficiary");
780
+
781
+ JBSplit[] memory splits = new JBSplit[](1);
782
+ splits[0] = JBSplit({
783
+ percent: 500_000_000, // 50% of tier revenue to beneficiary
784
+ projectId: 0,
785
+ beneficiary: payable(splitBeneficiary),
786
+ preferAddToBalance: false,
787
+ lockedUntil: 0,
788
+ hook: IJBSplitHook(address(0))
789
+ });
790
+
791
+ CTPost[] memory posts = new CTPost[](1);
792
+ posts[0] = CTPost({
793
+ encodedIPFSUri: keccak256("split-beneficiary-test"),
794
+ totalSupply: 10,
795
+ price: 0.1 ether,
796
+ category: 5,
797
+ splitPercent: 250_000_000, // 25% split
798
+ splits: splits
799
+ });
800
+
801
+ // Build expected tier config to verify splits are passed through.
802
+ JB721TierConfig[] memory expectedTiers = new JB721TierConfig[](1);
803
+ expectedTiers[0] = JB721TierConfig({
804
+ price: 0.1 ether,
805
+ initialSupply: 10,
806
+ votingUnits: 0,
807
+ reserveFrequency: 0,
808
+ reserveBeneficiary: address(0),
809
+ encodedIPFSUri: keccak256("split-beneficiary-test"),
810
+ category: 5,
811
+ discountPercent: 0,
812
+ allowOwnerMint: false,
813
+ useReserveBeneficiaryAsDefault: false,
814
+ transfersPausable: false,
815
+ useVotingUnits: true,
816
+ cannotBeRemoved: false,
817
+ cannotIncreaseDiscountPercent: false,
818
+ splitPercent: 250_000_000,
819
+ splits: splits
820
+ });
821
+
822
+ // Verify adjustTiers receives the tier config with the correct split beneficiary and percent.
823
+ vm.expectCall(
824
+ hookAddr, abi.encodeWithSelector(IJB721TiersHook.adjustTiers.selector, expectedTiers, new uint256[](0))
825
+ );
826
+
827
+ uint256 fee = 0.1 ether / 20;
828
+ vm.prank(poster);
829
+ publisher.mintFrom{value: 0.1 ether + fee}(IJB721TiersHook(hookAddr), posts, poster, poster, "", "");
830
+ }
831
+
773
832
  function test_mintFrom_multiplePostsDifferentSplits() public {
774
833
  // Category 5 allows up to 50% splits.
775
834
  _configureCategoryWithSplits(5, 0, 1, 100, 500_000_000);
@@ -0,0 +1,437 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity ^0.8.17;
3
+
4
+ // forge-lint: disable-next-line(unaliased-plain-import)
5
+ import "forge-std/Test.sol";
6
+
7
+ // JB core — deploy fresh within fork.
8
+ import {JBPermissions} from "@bananapus/core-v6/src/JBPermissions.sol";
9
+ import {JBProjects} from "@bananapus/core-v6/src/JBProjects.sol";
10
+ import {JBDirectory} from "@bananapus/core-v6/src/JBDirectory.sol";
11
+ import {JBRulesets} from "@bananapus/core-v6/src/JBRulesets.sol";
12
+ import {JBTokens} from "@bananapus/core-v6/src/JBTokens.sol";
13
+ import {JBERC20} from "@bananapus/core-v6/src/JBERC20.sol";
14
+ import {JBSplits} from "@bananapus/core-v6/src/JBSplits.sol";
15
+ import {JBPrices} from "@bananapus/core-v6/src/JBPrices.sol";
16
+ import {JBController} from "@bananapus/core-v6/src/JBController.sol";
17
+ import {JBFundAccessLimits} from "@bananapus/core-v6/src/JBFundAccessLimits.sol";
18
+ import {JBMultiTerminal} from "@bananapus/core-v6/src/JBMultiTerminal.sol";
19
+ import {JBTerminalStore} from "@bananapus/core-v6/src/JBTerminalStore.sol";
20
+ import {JBFeelessAddresses} from "@bananapus/core-v6/src/JBFeelessAddresses.sol";
21
+ import {JBConstants} from "@bananapus/core-v6/src/libraries/JBConstants.sol";
22
+ import {JBCurrencyIds} from "@bananapus/core-v6/src/libraries/JBCurrencyIds.sol";
23
+ import {IJBTerminal} from "@bananapus/core-v6/src/interfaces/IJBTerminal.sol";
24
+
25
+ import {JBTerminalConfig} from "@bananapus/core-v6/src/structs/JBTerminalConfig.sol";
26
+ import {JBAccountingContext} from "@bananapus/core-v6/src/structs/JBAccountingContext.sol";
27
+ import {JBRulesetConfig} from "@bananapus/core-v6/src/structs/JBRulesetConfig.sol";
28
+ import {JBSplit} from "@bananapus/core-v6/src/structs/JBSplit.sol";
29
+ import {MockPriceFeed} from "@bananapus/core-v6/test/mock/MockPriceFeed.sol";
30
+
31
+ // 721 hook — deploy fresh within fork.
32
+ import {JB721TiersHookStore} from "@bananapus/721-hook-v6/src/JB721TiersHookStore.sol";
33
+ import {JB721TiersHook} from "@bananapus/721-hook-v6/src/JB721TiersHook.sol";
34
+ import {JB721TiersHookDeployer} from "@bananapus/721-hook-v6/src/JB721TiersHookDeployer.sol";
35
+ import {IJB721TiersHook} from "@bananapus/721-hook-v6/src/interfaces/IJB721TiersHook.sol";
36
+ import {JBAddressRegistry} from "@bananapus/address-registry-v6/src/JBAddressRegistry.sol";
37
+ import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol";
38
+
39
+ // Suckers — deploy fresh within fork.
40
+ import {JBSuckerRegistry} from "@bananapus/suckers-v6/src/JBSuckerRegistry.sol";
41
+ import {JBOptimismSuckerDeployer} from "@bananapus/suckers-v6/src/deployers/JBOptimismSuckerDeployer.sol";
42
+ import {JBOptimismSucker} from "@bananapus/suckers-v6/src/JBOptimismSucker.sol";
43
+ import {IOPMessenger} from "@bananapus/suckers-v6/src/interfaces/IOPMessenger.sol";
44
+ import {IOPStandardBridge} from "@bananapus/suckers-v6/src/interfaces/IOPStandardBridge.sol";
45
+ import {JBSuckerDeployerConfig} from "@bananapus/suckers-v6/src/structs/JBSuckerDeployerConfig.sol";
46
+
47
+ // Permit2
48
+ import {IPermit2} from "@uniswap/permit2/src/interfaces/IPermit2.sol";
49
+ import {DeployPermit2} from "@uniswap/permit2/test/utils/DeployPermit2.sol";
50
+
51
+ // Croptop
52
+ // forge-lint: disable-next-line(unaliased-plain-import)
53
+ import "./../../src/CTDeployer.sol";
54
+ import {CTPublisher} from "./../../src/CTPublisher.sol";
55
+ import {CTPost} from "./../../src/structs/CTPost.sol";
56
+
57
+ /// @notice Fork tests for CTPublisher.mintFrom(). Deploys all JB infrastructure fresh within a mainnet fork,
58
+ /// then exercises the publish-and-mint flow end-to-end.
59
+ contract PublishForkTest is Test, DeployPermit2 {
60
+ // ───────────────────────── Mainnet addresses
61
+ // ──────────────────────────
62
+
63
+ IOPMessenger constant OP_L1_MESSENGER = IOPMessenger(0x25ace71c97B33Cc4729CF772ae268934F7ab5fA1);
64
+ IOPStandardBridge constant OP_L1_BRIDGE = IOPStandardBridge(0x99C9fc46f92E8a1c0deC1b1747d010903E884bE1);
65
+
66
+ // ───────────────────────── JB core (deployed fresh)
67
+ // ───────────────────
68
+
69
+ address multisig = address(0xBEEF);
70
+ address trustedForwarder = address(0);
71
+
72
+ JBPermissions jbPermissions;
73
+ JBProjects jbProjects;
74
+ JBDirectory jbDirectory;
75
+ JBRulesets jbRulesets;
76
+ JBTokens jbTokens;
77
+ JBSplits jbSplits;
78
+ JBPrices jbPrices;
79
+ JBFundAccessLimits jbFundAccessLimits;
80
+ JBController jbController;
81
+
82
+ // Terminal infrastructure.
83
+ JBFeelessAddresses jbFeelessAddresses;
84
+ JBTerminalStore jbTerminalStore;
85
+ JBMultiTerminal jbMultiTerminal;
86
+
87
+ // ───────────────────────── 721 hook (deployed fresh)
88
+ // ──────────────────
89
+
90
+ JB721TiersHookDeployer hookDeployer;
91
+
92
+ // ───────────────────────── Suckers (deployed fresh)
93
+ // ───────────────────
94
+
95
+ JBSuckerRegistry suckerRegistry;
96
+ JBOptimismSuckerDeployer opSuckerDeployer;
97
+
98
+ // ───────────────────────── Croptop
99
+ // ────────────────────────────────────
100
+
101
+ CTPublisher publisher;
102
+ CTDeployer deployer;
103
+
104
+ // ───────────────────────── Test actors & state
105
+ // ────────────────────────
106
+
107
+ address projectOwner = address(0xA11CE);
108
+ address poster = address(0xB0B);
109
+ address nftBeneficiary = address(0xCAFE);
110
+ address feeBeneficiary = address(0xFEE);
111
+
112
+ uint256 feeProjectId; // project 1
113
+ uint256 testProjectId;
114
+ IJB721TiersHook testHook;
115
+
116
+ // ───────────────────────── Constants
117
+ // ──────────────────────────────────
118
+
119
+ uint104 constant POST_PRICE = 0.1 ether;
120
+ uint32 constant POST_SUPPLY = 100;
121
+ uint24 constant POST_CATEGORY = 1;
122
+ // forge-lint: disable-next-line(unsafe-typecast)
123
+ bytes32 constant TEST_URI = bytes32("test_ipfs_uri");
124
+ // forge-lint: disable-next-line(unsafe-typecast)
125
+ bytes32 constant TEST_URI_2 = bytes32("test_ipfs_uri_2");
126
+
127
+ // ───────────────────────── Setup
128
+ // ─────────────────────────────────────
129
+
130
+ function setUp() public {
131
+ // Fork ETH mainnet.
132
+ vm.createSelectFork("ethereum");
133
+
134
+ // Deploy all JB core contracts fresh within the fork.
135
+ _deployJBCore();
136
+
137
+ // CTDeployer hardcodes baseCurrency = JBCurrencyIds.ETH (1), but the accounting context
138
+ // uses currency = uint32(uint160(NATIVE_TOKEN)) = 61166. Add an identity price feed
139
+ // so JBPrices can convert between them.
140
+ MockPriceFeed identityFeed = new MockPriceFeed(1e18, 18);
141
+ vm.prank(multisig);
142
+ jbPrices.addPriceFeedFor({
143
+ projectId: 0,
144
+ pricingCurrency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
145
+ unitCurrency: JBCurrencyIds.ETH,
146
+ feed: identityFeed
147
+ });
148
+
149
+ // Deploy the terminal infrastructure.
150
+ _deployTerminal();
151
+
152
+ // Deploy the 721 hook infrastructure.
153
+ _deploy721Hook();
154
+
155
+ // Deploy the sucker infrastructure.
156
+ _deploySuckers();
157
+
158
+ // Deploy the croptop contracts.
159
+ publisher = new CTPublisher(jbDirectory, jbPermissions, 1, trustedForwarder);
160
+ deployer = new CTDeployer(jbPermissions, jbProjects, hookDeployer, publisher, suckerRegistry, trustedForwarder);
161
+
162
+ // Launch the fee project (project 1) with a terminal that accepts ETH.
163
+ feeProjectId = _launchFeeProject();
164
+
165
+ // Launch a test project via CTDeployer with a terminal + allowed posts.
166
+ (testProjectId, testHook) = _launchTestProject();
167
+
168
+ // Fund the poster.
169
+ vm.deal(poster, 10 ether);
170
+ }
171
+
172
+ // ───────────────────────── Tests
173
+ // ─────────────────────────────────────
174
+
175
+ /// @notice Verify that mintFrom() mints an NFT to the specified beneficiary.
176
+ function testFork_MintFromPublishesNFT() public {
177
+ // Build a valid post.
178
+ CTPost[] memory posts = _singlePost(TEST_URI, POST_PRICE, POST_SUPPLY, POST_CATEGORY);
179
+
180
+ // Calculate required msg.value: price + fee.
181
+ uint256 fee = uint256(POST_PRICE) / 20;
182
+ uint256 totalValue = uint256(POST_PRICE) + fee;
183
+
184
+ // Check NFT balance before.
185
+ uint256 balanceBefore = IERC721(address(testHook)).balanceOf(nftBeneficiary);
186
+
187
+ // Mint.
188
+ vm.prank(poster);
189
+ publisher.mintFrom{value: totalValue}(testHook, posts, nftBeneficiary, feeBeneficiary, "", "");
190
+
191
+ // Verify NFT was minted to the beneficiary.
192
+ uint256 balanceAfter = IERC721(address(testHook)).balanceOf(nftBeneficiary);
193
+ assertEq(balanceAfter, balanceBefore + 1, "NFT should be minted to beneficiary");
194
+ }
195
+
196
+ /// @notice Verify 5% fee is routed to fee project and the rest to the test project.
197
+ function testFork_MintFromFeeDistribution() public {
198
+ CTPost[] memory posts = _singlePost(TEST_URI, POST_PRICE, POST_SUPPLY, POST_CATEGORY);
199
+
200
+ uint256 fee = uint256(POST_PRICE) / 20;
201
+ uint256 totalValue = uint256(POST_PRICE) + fee;
202
+
203
+ // Record terminal balances before minting.
204
+ uint256 feeProjectBalanceBefore =
205
+ jbTerminalStore.balanceOf(address(jbMultiTerminal), feeProjectId, JBConstants.NATIVE_TOKEN);
206
+ uint256 testProjectBalanceBefore =
207
+ jbTerminalStore.balanceOf(address(jbMultiTerminal), testProjectId, JBConstants.NATIVE_TOKEN);
208
+
209
+ // Mint.
210
+ vm.prank(poster);
211
+ publisher.mintFrom{value: totalValue}(testHook, posts, nftBeneficiary, feeBeneficiary, "", "");
212
+
213
+ // Verify fee project terminal balance increased by the fee amount.
214
+ uint256 feeProjectBalanceAfter =
215
+ jbTerminalStore.balanceOf(address(jbMultiTerminal), feeProjectId, JBConstants.NATIVE_TOKEN);
216
+ assertEq(
217
+ feeProjectBalanceAfter - feeProjectBalanceBefore,
218
+ fee,
219
+ "Fee project balance should increase by totalPrice / 20"
220
+ );
221
+
222
+ // Verify test project terminal balance increased by the post price.
223
+ uint256 testProjectBalanceAfter =
224
+ jbTerminalStore.balanceOf(address(jbMultiTerminal), testProjectId, JBConstants.NATIVE_TOKEN);
225
+ assertEq(
226
+ testProjectBalanceAfter - testProjectBalanceBefore,
227
+ uint256(POST_PRICE),
228
+ "Test project balance should increase by post price"
229
+ );
230
+ }
231
+
232
+ /// @notice Verify that sending less ETH than required reverts.
233
+ function testFork_MintFromInsufficientFeeReverts() public {
234
+ CTPost[] memory posts = _singlePost(TEST_URI, POST_PRICE, POST_SUPPLY, POST_CATEGORY);
235
+
236
+ // Send only the post price, not the post price + fee.
237
+ uint256 insufficientValue = uint256(POST_PRICE);
238
+
239
+ vm.prank(poster);
240
+ vm.expectRevert();
241
+ publisher.mintFrom{value: insufficientValue}(testHook, posts, nftBeneficiary, feeBeneficiary, "", "");
242
+ }
243
+
244
+ /// @notice Verify that minting the same encodedIPFSUri twice reuses the existing tier ID.
245
+ function testFork_MintFromDuplicatePostReusesExistingTier() public {
246
+ CTPost[] memory posts = _singlePost(TEST_URI, POST_PRICE, POST_SUPPLY, POST_CATEGORY);
247
+
248
+ uint256 fee = uint256(POST_PRICE) / 20;
249
+ uint256 totalValue = uint256(POST_PRICE) + fee;
250
+
251
+ // First mint.
252
+ vm.prank(poster);
253
+ publisher.mintFrom{value: totalValue}(testHook, posts, nftBeneficiary, feeBeneficiary, "", "");
254
+
255
+ // Record the tier ID assigned to this URI after the first mint.
256
+ uint256 tierIdAfterFirst = publisher.tierIdForEncodedIPFSUriOf(address(testHook), TEST_URI);
257
+ assertGt(tierIdAfterFirst, 0, "Tier ID should be non-zero after first mint");
258
+
259
+ // Second mint with the same URI. The existing tier should be reused.
260
+ vm.prank(poster);
261
+ publisher.mintFrom{value: totalValue}(testHook, posts, nftBeneficiary, feeBeneficiary, "", "");
262
+
263
+ // Verify the tier ID is unchanged — no new tier was created.
264
+ uint256 tierIdAfterSecond = publisher.tierIdForEncodedIPFSUriOf(address(testHook), TEST_URI);
265
+ assertEq(tierIdAfterFirst, tierIdAfterSecond, "Tier ID should be reused for duplicate encodedIPFSUri");
266
+
267
+ // Verify two NFTs were minted total.
268
+ assertEq(IERC721(address(testHook)).balanceOf(nftBeneficiary), 2, "Two NFTs should be minted across both calls");
269
+ }
270
+
271
+ // ───────────────────────── Internal deployment helpers
272
+ // ────────────────
273
+
274
+ // forge-lint: disable-next-line(mixed-case-function)
275
+ function _deployJBCore() internal {
276
+ jbPermissions = new JBPermissions(trustedForwarder);
277
+ jbProjects = new JBProjects(multisig, address(0), trustedForwarder);
278
+ jbDirectory = new JBDirectory(jbPermissions, jbProjects, multisig);
279
+ JBERC20 jbErc20 = new JBERC20();
280
+ jbTokens = new JBTokens(jbDirectory, jbErc20);
281
+ jbRulesets = new JBRulesets(jbDirectory);
282
+ jbPrices = new JBPrices(jbDirectory, jbPermissions, jbProjects, multisig, trustedForwarder);
283
+ jbSplits = new JBSplits(jbDirectory);
284
+ jbFundAccessLimits = new JBFundAccessLimits(jbDirectory);
285
+
286
+ jbController = new JBController(
287
+ jbDirectory,
288
+ jbFundAccessLimits,
289
+ jbPermissions,
290
+ jbPrices,
291
+ jbProjects,
292
+ jbRulesets,
293
+ jbSplits,
294
+ jbTokens,
295
+ address(0), // omnichainRulesetOperator
296
+ trustedForwarder
297
+ );
298
+
299
+ vm.prank(multisig);
300
+ jbDirectory.setIsAllowedToSetFirstController(address(jbController), true);
301
+ }
302
+
303
+ function _deployTerminal() internal {
304
+ jbFeelessAddresses = new JBFeelessAddresses(multisig);
305
+ jbTerminalStore = new JBTerminalStore(jbDirectory, jbPrices, jbRulesets);
306
+
307
+ address permit2 = deployPermit2();
308
+
309
+ jbMultiTerminal = new JBMultiTerminal(
310
+ jbFeelessAddresses,
311
+ jbPermissions,
312
+ jbProjects,
313
+ jbSplits,
314
+ jbTerminalStore,
315
+ jbTokens,
316
+ IPermit2(permit2),
317
+ trustedForwarder
318
+ );
319
+ }
320
+
321
+ function _deploy721Hook() internal {
322
+ JB721TiersHookStore store = new JB721TiersHookStore();
323
+ JBAddressRegistry addressRegistry = new JBAddressRegistry();
324
+
325
+ JB721TiersHook hookImpl =
326
+ new JB721TiersHook(jbDirectory, jbPermissions, jbPrices, jbRulesets, store, jbSplits, trustedForwarder);
327
+
328
+ hookDeployer = new JB721TiersHookDeployer(hookImpl, store, addressRegistry, trustedForwarder);
329
+ }
330
+
331
+ function _deploySuckers() internal {
332
+ suckerRegistry = new JBSuckerRegistry(jbDirectory, jbPermissions, multisig, trustedForwarder);
333
+
334
+ opSuckerDeployer =
335
+ new JBOptimismSuckerDeployer(jbDirectory, jbPermissions, jbTokens, multisig, trustedForwarder);
336
+
337
+ vm.startPrank(multisig);
338
+ opSuckerDeployer.setChainSpecificConstants(OP_L1_MESSENGER, OP_L1_BRIDGE);
339
+
340
+ JBOptimismSucker singleton = new JBOptimismSucker(
341
+ opSuckerDeployer, jbDirectory, jbPermissions, jbTokens, 1, suckerRegistry, trustedForwarder
342
+ );
343
+ opSuckerDeployer.configureSingleton(singleton);
344
+
345
+ suckerRegistry.allowSuckerDeployer(address(opSuckerDeployer));
346
+ vm.stopPrank();
347
+ }
348
+
349
+ /// @notice Launch fee project (project 1) with ETH terminal so it can receive fees.
350
+ function _launchFeeProject() internal returns (uint256 projectId) {
351
+ // Build terminal config accepting native ETH.
352
+ JBTerminalConfig[] memory terminalConfigs = _ethTerminalConfig();
353
+
354
+ // A simple ruleset with no special rules.
355
+ JBRulesetConfig[] memory rulesetConfigs = new JBRulesetConfig[](1);
356
+ rulesetConfigs[0].weight = 1_000_000 * (10 ** 18);
357
+ rulesetConfigs[0].metadata.baseCurrency = JBCurrencyIds.ETH;
358
+
359
+ projectId = jbController.launchProjectFor({
360
+ owner: multisig,
361
+ projectUri: "Fee Project",
362
+ rulesetConfigurations: rulesetConfigs,
363
+ terminalConfigurations: terminalConfigs,
364
+ memo: "Fee project launch"
365
+ });
366
+
367
+ // Sanity check: fee project must be project 1.
368
+ assertEq(projectId, 1, "Fee project must be project ID 1");
369
+ }
370
+
371
+ /// @notice Launch a test project via CTDeployer with ETH terminal and allowed posts.
372
+ function _launchTestProject() internal returns (uint256 projectId, IJB721TiersHook hook) {
373
+ // Build terminal config accepting native ETH.
374
+ JBTerminalConfig[] memory terminalConfigs = _ethTerminalConfig();
375
+
376
+ // Build allowed posts for the deployer.
377
+ CTDeployerAllowedPost[] memory allowedPosts = new CTDeployerAllowedPost[](1);
378
+ allowedPosts[0] = CTDeployerAllowedPost({
379
+ category: POST_CATEGORY,
380
+ minimumPrice: 0,
381
+ minimumTotalSupply: 1,
382
+ maximumTotalSupply: 10_000,
383
+ maximumSplitPercent: 500_000_000, // 50%
384
+ allowedAddresses: new address[](0) // anyone can post
385
+ });
386
+
387
+ CTProjectConfig memory config = CTProjectConfig({
388
+ terminalConfigurations: terminalConfigs,
389
+ projectUri: "https://test.croptop.eth/",
390
+ allowedPosts: allowedPosts,
391
+ contractUri: "https://test.croptop.eth/contract",
392
+ name: "TestCrop",
393
+ symbol: "TCROP",
394
+ salt: bytes32(uint256(1))
395
+ });
396
+
397
+ CTSuckerDeploymentConfig memory suckerConfig =
398
+ CTSuckerDeploymentConfig({deployerConfigurations: new JBSuckerDeployerConfig[](0), salt: bytes32(0)});
399
+
400
+ (projectId, hook) = deployer.deployProjectFor(projectOwner, config, suckerConfig, jbController);
401
+ }
402
+
403
+ /// @notice Build a JBTerminalConfig[] with a single entry for native ETH.
404
+ function _ethTerminalConfig() internal view returns (JBTerminalConfig[] memory configs) {
405
+ JBAccountingContext[] memory contexts = new JBAccountingContext[](1);
406
+ contexts[0] = JBAccountingContext({
407
+ token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
408
+ });
409
+
410
+ configs = new JBTerminalConfig[](1);
411
+ configs[0] =
412
+ JBTerminalConfig({terminal: IJBTerminal(address(jbMultiTerminal)), accountingContextsToAccept: contexts});
413
+ }
414
+
415
+ /// @notice Build a single-element CTPost array.
416
+ function _singlePost(
417
+ // forge-lint: disable-next-line(mixed-case-variable)
418
+ bytes32 encodedIPFSUri,
419
+ uint104 price,
420
+ uint32 totalSupply,
421
+ uint24 category
422
+ )
423
+ internal
424
+ pure
425
+ returns (CTPost[] memory posts)
426
+ {
427
+ posts = new CTPost[](1);
428
+ posts[0] = CTPost({
429
+ encodedIPFSUri: encodedIPFSUri,
430
+ price: price,
431
+ totalSupply: totalSupply,
432
+ category: category,
433
+ splitPercent: 0,
434
+ splits: new JBSplit[](0)
435
+ });
436
+ }
437
+ }