@croptop/core-v6 0.0.31 → 0.0.33

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/README.md CHANGED
@@ -90,4 +90,6 @@ script/
90
90
  - fee routing depends on the designated fee project remaining correctly configured; if its terminal rejects payments,
91
91
  Croptop refunds the fee to `_msgSender()` instead of trapping ETH in `CTPublisher`
92
92
  - burn-lock ownership is intentionally irreversible and should only be used when immutability is desired
93
+ - after burn-locking into `CTProjectOwner`, the previous owner no longer holds the project NFT directly; control is
94
+ intentionally mediated through Croptop's owner helper and hook-admin surface instead of remaining a plain owner EOA
93
95
  - duplicate-content and stale-tier edge cases are guarded by tests, but integrations should still treat metadata reuse carefully
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@croptop/core-v6",
3
- "version": "0.0.31",
3
+ "version": "0.0.33",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -16,18 +16,18 @@
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.32",
20
- "@bananapus/buyback-hook-v6": "^0.0.26",
21
- "@bananapus/core-v6": "^0.0.32",
19
+ "@bananapus/721-hook-v6": "^0.0.33",
20
+ "@bananapus/buyback-hook-v6": "^0.0.27",
21
+ "@bananapus/core-v6": "^0.0.34",
22
22
  "@bananapus/ownable-v6": "^0.0.17",
23
- "@bananapus/permission-ids-v6": "^0.0.15",
23
+ "@bananapus/permission-ids-v6": "^0.0.17",
24
24
  "@bananapus/router-terminal-v6": "^0.0.26",
25
- "@bananapus/suckers-v6": "^0.0.22",
25
+ "@bananapus/suckers-v6": "^0.0.25",
26
26
  "@openzeppelin/contracts": "^5.6.1"
27
27
  },
28
28
  "devDependencies": {
29
29
  "@bananapus/address-registry-v6": "^0.0.17",
30
- "@rev-net/core-v6": "^0.0.28",
30
+ "@rev-net/core-v6": "^0.0.31",
31
31
  "@sphinx-labs/plugins": "^0.33.3"
32
32
  }
33
- }
33
+ }
@@ -1,9 +1,6 @@
1
1
  // SPDX-License-Identifier: MIT
2
2
  pragma solidity 0.8.28;
3
3
 
4
- import {ERC2771Context} from "@openzeppelin/contracts/metatx/ERC2771Context.sol";
5
- import {IERC721Receiver} from "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol";
6
- import {Context} from "@openzeppelin/contracts/utils/Context.sol";
7
4
  import {IJB721TiersHook} from "@bananapus/721-hook-v6/src/interfaces/IJB721TiersHook.sol";
8
5
  import {IJB721TiersHookDeployer} from "@bananapus/721-hook-v6/src/interfaces/IJB721TiersHookDeployer.sol";
9
6
  import {IJB721TokenUriResolver} from "@bananapus/721-hook-v6/src/interfaces/IJB721TokenUriResolver.sol";
@@ -28,6 +25,9 @@ import {JBRulesetConfig} from "@bananapus/core-v6/src/structs/JBRulesetConfig.so
28
25
  import {JBOwnable} from "@bananapus/ownable-v6/src/JBOwnable.sol";
29
26
  import {JBPermissionIds} from "@bananapus/permission-ids-v6/src/JBPermissionIds.sol";
30
27
  import {IJBSuckerRegistry} from "@bananapus/suckers-v6/src/interfaces/IJBSuckerRegistry.sol";
28
+ import {ERC2771Context} from "@openzeppelin/contracts/metatx/ERC2771Context.sol";
29
+ import {IERC721Receiver} from "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol";
30
+ import {Context} from "@openzeppelin/contracts/utils/Context.sol";
31
31
 
32
32
  import {ICTDeployer} from "./interfaces/ICTDeployer.sol";
33
33
  import {ICTPublisher} from "./interfaces/ICTPublisher.sol";
@@ -117,109 +117,6 @@ contract CTDeployer is ERC2771Context, JBPermissioned, IJBRulesetDataHook, IERC7
117
117
  PERMISSIONS.setPermissionsFor({account: address(this), permissionsData: permissionData});
118
118
  }
119
119
 
120
- //*********************************************************************//
121
- // ------------------------- external views -------------------------- //
122
- //*********************************************************************//
123
-
124
- /// @notice Allow cash outs from suckers without a tax.
125
- /// @dev This function is part of `IJBRulesetDataHook`, and gets called before the revnet processes a cash out.
126
- /// @param context Standard Juicebox cash out context. See `JBBeforeCashOutRecordedContext`.
127
- /// @return cashOutTaxRate The cash out tax rate, which influences the amount of terminal tokens which get cashed
128
- /// out.
129
- /// @return cashOutCount The number of project tokens that are cashed out.
130
- /// @return totalSupply The total project token supply.
131
- /// @return hookSpecifications The amount of funds and the data to send to cash out hooks (this contract).
132
- function beforeCashOutRecordedWith(JBBeforeCashOutRecordedContext calldata context)
133
- external
134
- view
135
- override
136
- returns (
137
- uint256 cashOutTaxRate,
138
- uint256 cashOutCount,
139
- uint256 totalSupply,
140
- JBCashOutHookSpecification[] memory hookSpecifications
141
- )
142
- {
143
- // If the cash out is from a sucker, return the full cash out amount without taxes or fees.
144
- if (SUCKER_REGISTRY.isSuckerOf({projectId: context.projectId, addr: context.holder})) {
145
- return (0, context.cashOutCount, context.totalSupply, hookSpecifications);
146
- }
147
-
148
- // If the ruleset has a data hook, forward the call to the datahook.
149
- IJBRulesetDataHook hook = dataHookOf[context.projectId];
150
- if (address(hook) == address(0)) {
151
- return (context.cashOutTaxRate, context.cashOutCount, context.totalSupply, hookSpecifications);
152
- }
153
- // slither-disable-next-line unused-return
154
- return hook.beforeCashOutRecordedWith(context);
155
- }
156
-
157
- /// @notice Forward the call to the original data hook.
158
- /// @dev This function is part of `IJBRulesetDataHook`, and gets called before the revnet processes a payment.
159
- /// @param context Standard Juicebox payment context. See `JBBeforePayRecordedContext`.
160
- /// @return weight The weight which project tokens are minted relative to. This can be used to customize how many
161
- /// tokens get minted by a payment.
162
- /// @return hookSpecifications Amounts (out of what's being paid in) to be sent to pay hooks instead of being paid
163
- /// into the project. Useful for automatically routing funds from a treasury as payments come in.
164
- function beforePayRecordedWith(JBBeforePayRecordedContext calldata context)
165
- external
166
- view
167
- override
168
- returns (uint256 weight, JBPayHookSpecification[] memory hookSpecifications)
169
- {
170
- // Forward the call to the data hook.
171
- IJBRulesetDataHook hook = dataHookOf[context.projectId];
172
- if (address(hook) == address(0)) {
173
- return (context.weight, hookSpecifications);
174
- }
175
- // slither-disable-next-line unused-return
176
- return hook.beforePayRecordedWith(context);
177
- }
178
-
179
- /// @notice A flag indicating whether an address has permission to mint a project's tokens on-demand.
180
- /// @dev A project's data hook can allow any address to mint its tokens.
181
- /// @param projectId The ID of the project whose token can be minted.
182
- /// @param addr The address to check the token minting permission of.
183
- /// @return flag A flag indicating whether the address has permission to mint the project's tokens on-demand.
184
- function hasMintPermissionFor(uint256 projectId, JBRuleset memory, address addr) external view returns (bool flag) {
185
- // If the address is a sucker for this project.
186
- return SUCKER_REGISTRY.isSuckerOf({projectId: projectId, addr: addr});
187
- }
188
-
189
- /// @dev Make sure only mints can be received.
190
- function onERC721Received(
191
- address operator,
192
- address from,
193
- uint256 tokenId,
194
- bytes calldata data
195
- )
196
- external
197
- view
198
- returns (bytes4)
199
- {
200
- data;
201
- tokenId;
202
- operator;
203
-
204
- // Make sure the 721 received is the JBProjects contract.
205
- if (msg.sender != address(PROJECTS)) revert();
206
- // Make sure the 721 is being received as a mint.
207
- if (from != address(0)) revert();
208
- return IERC721Receiver.onERC721Received.selector;
209
- }
210
-
211
- //*********************************************************************//
212
- // -------------------------- public views --------------------------- //
213
- //*********************************************************************//
214
-
215
- /// @notice Indicates if this contract adheres to the specified interface.
216
- /// @dev See `IERC165.supportsInterface`.
217
- /// @return A flag indicating if the provided interface ID is supported.
218
- function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) {
219
- return interfaceId == type(ICTDeployer).interfaceId || interfaceId == type(IJBRulesetDataHook).interfaceId
220
- || interfaceId == type(IERC721Receiver).interfaceId;
221
- }
222
-
223
120
  //*********************************************************************//
224
121
  // ---------------------- external transactions ---------------------- //
225
122
  //*********************************************************************//
@@ -389,6 +286,118 @@ contract CTDeployer is ERC2771Context, JBPermissioned, IJBRulesetDataHook, IERC7
389
286
  });
390
287
  }
391
288
 
289
+ //*********************************************************************//
290
+ // ------------------------- external views -------------------------- //
291
+ //*********************************************************************//
292
+
293
+ /// @notice Allow cash outs from suckers without a tax.
294
+ /// @dev This function is part of `IJBRulesetDataHook`, and gets called before the revnet processes a cash out.
295
+ /// @param context Standard Juicebox cash out context. See `JBBeforeCashOutRecordedContext`.
296
+ /// @return cashOutTaxRate The cash out tax rate, which influences the amount of terminal tokens which get cashed
297
+ /// out.
298
+ /// @return cashOutCount The number of project tokens that are cashed out.
299
+ /// @return totalSupply The total project token supply.
300
+ /// @return surplusValue The surplus value to use for the bonding curve calculation.
301
+ /// @return hookSpecifications The amount of funds and the data to send to cash out hooks (this contract).
302
+ function beforeCashOutRecordedWith(JBBeforeCashOutRecordedContext calldata context)
303
+ external
304
+ view
305
+ override
306
+ returns (
307
+ uint256 cashOutTaxRate,
308
+ uint256 cashOutCount,
309
+ uint256 totalSupply,
310
+ uint256 surplusValue,
311
+ JBCashOutHookSpecification[] memory hookSpecifications
312
+ )
313
+ {
314
+ // If the cash out is from a sucker, return the full cash out amount without taxes or fees.
315
+ if (SUCKER_REGISTRY.isSuckerOf({projectId: context.projectId, addr: context.holder})) {
316
+ return (0, context.cashOutCount, context.totalSupply, context.surplus.value, hookSpecifications);
317
+ }
318
+
319
+ // If the ruleset has a data hook, forward the call to the datahook.
320
+ IJBRulesetDataHook hook = dataHookOf[context.projectId];
321
+ if (address(hook) == address(0)) {
322
+ return (
323
+ context.cashOutTaxRate,
324
+ context.cashOutCount,
325
+ context.totalSupply,
326
+ context.surplus.value,
327
+ hookSpecifications
328
+ );
329
+ }
330
+ // slither-disable-next-line unused-return
331
+ return hook.beforeCashOutRecordedWith(context);
332
+ }
333
+
334
+ /// @notice Forward the call to the original data hook.
335
+ /// @dev This function is part of `IJBRulesetDataHook`, and gets called before the revnet processes a payment.
336
+ /// @param context Standard Juicebox payment context. See `JBBeforePayRecordedContext`.
337
+ /// @return weight The weight which project tokens are minted relative to. This can be used to customize how many
338
+ /// tokens get minted by a payment.
339
+ /// @return hookSpecifications Amounts (out of what's being paid in) to be sent to pay hooks instead of being paid
340
+ /// into the project. Useful for automatically routing funds from a treasury as payments come in.
341
+ function beforePayRecordedWith(JBBeforePayRecordedContext calldata context)
342
+ external
343
+ view
344
+ override
345
+ returns (uint256 weight, JBPayHookSpecification[] memory hookSpecifications)
346
+ {
347
+ // Forward the call to the data hook.
348
+ IJBRulesetDataHook hook = dataHookOf[context.projectId];
349
+ if (address(hook) == address(0)) {
350
+ return (context.weight, hookSpecifications);
351
+ }
352
+
353
+ // slither-disable-next-line unused-return
354
+ return hook.beforePayRecordedWith(context);
355
+ }
356
+
357
+ /// @notice A flag indicating whether an address has permission to mint a project's tokens on-demand.
358
+ /// @dev A project's data hook can allow any address to mint its tokens.
359
+ /// @param projectId The ID of the project whose token can be minted.
360
+ /// @param addr The address to check the token minting permission of.
361
+ /// @return flag A flag indicating whether the address has permission to mint the project's tokens on-demand.
362
+ function hasMintPermissionFor(uint256 projectId, JBRuleset memory, address addr) external view returns (bool flag) {
363
+ // If the address is a sucker for this project.
364
+ return SUCKER_REGISTRY.isSuckerOf({projectId: projectId, addr: addr});
365
+ }
366
+
367
+ /// @dev Make sure only mints can be received.
368
+ function onERC721Received(
369
+ address operator,
370
+ address from,
371
+ uint256 tokenId,
372
+ bytes calldata data
373
+ )
374
+ external
375
+ view
376
+ returns (bytes4)
377
+ {
378
+ data;
379
+ tokenId;
380
+ operator;
381
+
382
+ // Make sure the 721 received is the JBProjects contract.
383
+ if (msg.sender != address(PROJECTS)) revert();
384
+ // Make sure the 721 is being received as a mint.
385
+ if (from != address(0)) revert();
386
+ return IERC721Receiver.onERC721Received.selector;
387
+ }
388
+
389
+ //*********************************************************************//
390
+ // -------------------------- public views --------------------------- //
391
+ //*********************************************************************//
392
+
393
+ /// @notice Indicates if this contract adheres to the specified interface.
394
+ /// @dev See `IERC165.supportsInterface`.
395
+ /// @return A flag indicating if the provided interface ID is supported.
396
+ function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) {
397
+ return interfaceId == type(ICTDeployer).interfaceId || interfaceId == type(IJBRulesetDataHook).interfaceId
398
+ || interfaceId == type(IERC721Receiver).interfaceId;
399
+ }
400
+
392
401
  //*********************************************************************//
393
402
  // --------------------- internal transactions ----------------------- //
394
403
  //*********************************************************************//
@@ -432,7 +441,7 @@ contract CTDeployer is ERC2771Context, JBPermissioned, IJBRulesetDataHook, IERC7
432
441
  }
433
442
 
434
443
  //*********************************************************************//
435
- // ------------------------ internal functions ----------------------- //
444
+ // -------------------------- internal views ------------------------- //
436
445
  //*********************************************************************//
437
446
 
438
447
  /// @dev ERC-2771 specifies the context as being a single address (20 bytes).
@@ -105,141 +105,6 @@ contract CTPublisher is JBPermissioned, ERC2771Context, ICTPublisher {
105
105
  FEE_PROJECT_ID = feeProjectId;
106
106
  }
107
107
 
108
- //*********************************************************************//
109
- // ------------------------- external views -------------------------- //
110
- //*********************************************************************//
111
-
112
- /// @notice Get the tiers for the provided encoded IPFS URIs.
113
- /// @dev The returned tier IDs may be stale if the corresponding tiers were removed externally via adjustTiers.
114
- /// In that case, the store's tierOf call will return a tier with default/empty values. Callers should check
115
- /// the returned tier's initialSupply or other fields to confirm the tier still exists.
116
- /// @param hook The hook from which to get tiers.
117
- /// @param encodedIPFSUris The URIs to get tiers of.
118
- /// @return tiers The tiers that correspond to the provided encoded IPFS URIs. If there's no tier yet, an empty tier
119
- /// is returned.
120
- function tiersFor(
121
- address hook,
122
- // forge-lint: disable-next-line(mixed-case-variable)
123
- bytes32[] memory encodedIPFSUris
124
- )
125
- external
126
- view
127
- override
128
- returns (JB721Tier[] memory tiers)
129
- {
130
- // forge-lint: disable-next-line(mixed-case-variable)
131
- uint256 numberOfEncodedIPFSUris = encodedIPFSUris.length;
132
-
133
- // Initialize the tier array being returned.
134
- tiers = new JB721Tier[](numberOfEncodedIPFSUris);
135
-
136
- // Get the tier for each provided encoded IPFS URI.
137
- for (uint256 i; i < numberOfEncodedIPFSUris;) {
138
- // Check if there's a tier ID stored for the encoded IPFS URI.
139
- uint256 tierId = tierIdForEncodedIPFSUriOf[hook][encodedIPFSUris[i]];
140
-
141
- // If there's a tier ID stored, resolve it.
142
- if (tierId != 0) {
143
- // slither-disable-next-line calls-loop
144
- tiers[i] = IJB721TiersHook(hook).STORE().tierOf({hook: hook, id: tierId, includeResolvedUri: false});
145
- }
146
-
147
- unchecked {
148
- ++i;
149
- }
150
- }
151
- }
152
-
153
- //*********************************************************************//
154
- // -------------------------- public views --------------------------- //
155
- //*********************************************************************//
156
-
157
- /// @notice Post allowances for a particular category on a particular hook.
158
- /// @param hook The hook contract for which this allowance applies.
159
- /// @param category The category for which this allowance applies.
160
- /// @return minimumPrice The minimum price that a poster must pay to record a new NFT.
161
- /// @return minimumTotalSupply The minimum total number of available tokens that a minter must set to record a new
162
- /// NFT.
163
- /// @return maximumTotalSupply The max total supply of NFTs that can be made available when minting. Must be >=
164
- /// minimumTotalSupply.
165
- /// @return maximumSplitPercent The maximum split percent that a poster can set. 0 means splits are not allowed.
166
- /// @return allowedAddresses The addresses allowed to post. Returns empty if all addresses are allowed.
167
- function allowanceFor(
168
- address hook,
169
- uint256 category
170
- )
171
- public
172
- view
173
- override
174
- returns (
175
- uint256 minimumPrice,
176
- uint256 minimumTotalSupply,
177
- uint256 maximumTotalSupply,
178
- uint256 maximumSplitPercent,
179
- address[] memory allowedAddresses
180
- )
181
- {
182
- // Get a reference to the packed values.
183
- uint256 packed = _packedAllowanceFor[hook][category];
184
-
185
- // minimum price in bits 0-103 (104 bits).
186
- // forge-lint: disable-next-line(unsafe-typecast)
187
- minimumPrice = uint256(uint104(packed));
188
- // minimum supply in bits 104-135 (32 bits).
189
- // forge-lint: disable-next-line(unsafe-typecast)
190
- minimumTotalSupply = uint256(uint32(packed >> 104));
191
- // maximum supply in bits 136-167 (32 bits).
192
- // forge-lint: disable-next-line(unsafe-typecast)
193
- maximumTotalSupply = uint256(uint32(packed >> 136));
194
- // maximum split percent in bits 168-199 (32 bits).
195
- // forge-lint: disable-next-line(unsafe-typecast)
196
- maximumSplitPercent = uint256(uint32(packed >> 168));
197
-
198
- allowedAddresses = _allowedAddresses[hook][category];
199
- }
200
-
201
- //*********************************************************************//
202
- // -------------------------- internal views ------------------------- //
203
- //*********************************************************************//
204
-
205
- /// @dev ERC-2771 specifies the context as being a single address (20 bytes).
206
- function _contextSuffixLength() internal view virtual override(ERC2771Context, Context) returns (uint256) {
207
- return super._contextSuffixLength();
208
- }
209
-
210
- /// @notice Check if an address is included in an allow list.
211
- /// @dev Uses an O(n) linear scan over the `addresses` array. This is acceptable for typical allow list sizes
212
- /// (fewer than ~100 addresses), where gas cost is negligible. For very large allow lists, a Merkle proof
213
- /// pattern would scale better, but the added complexity is not warranted for the expected use case.
214
- /// @param addrs The candidate address.
215
- /// @param addresses An array of allowed addresses.
216
- function _isAllowed(address addrs, address[] memory addresses) internal pure returns (bool) {
217
- // Keep a reference to the number of address to check against.
218
- uint256 numberOfAddresses = addresses.length;
219
-
220
- // Check if the address is included
221
- for (uint256 i; i < numberOfAddresses;) {
222
- if (addrs == addresses[i]) return true;
223
- unchecked {
224
- ++i;
225
- }
226
- }
227
-
228
- return false;
229
- }
230
-
231
- /// @notice Returns the calldata, prefered to use over `msg.data`
232
- /// @return calldata the `msg.data` of this call
233
- function _msgData() internal view override(ERC2771Context, Context) returns (bytes calldata) {
234
- return ERC2771Context._msgData();
235
- }
236
-
237
- /// @notice Returns the sender, prefered to use over `msg.sender`
238
- /// @return sender the sender address of this call.
239
- function _msgSender() internal view override(ERC2771Context, Context) returns (address sender) {
240
- return ERC2771Context._msgSender();
241
- }
242
-
243
108
  //*********************************************************************//
244
109
  // ---------------------- external transactions ---------------------- //
245
110
  //*********************************************************************//
@@ -446,7 +311,100 @@ contract CTPublisher is JBPermissioned, ERC2771Context, ICTPublisher {
446
311
  }
447
312
 
448
313
  //*********************************************************************//
449
- // ------------------------ internal functions ----------------------- //
314
+ // ------------------------- external views -------------------------- //
315
+ //*********************************************************************//
316
+
317
+ /// @notice Get the tiers for the provided encoded IPFS URIs.
318
+ /// @dev The returned tier IDs may be stale if the corresponding tiers were removed externally via adjustTiers.
319
+ /// In that case, the store's tierOf call will return a tier with default/empty values. Callers should check
320
+ /// the returned tier's initialSupply or other fields to confirm the tier still exists.
321
+ /// @param hook The hook from which to get tiers.
322
+ /// @param encodedIPFSUris The URIs to get tiers of.
323
+ /// @return tiers The tiers that correspond to the provided encoded IPFS URIs. If there's no tier yet, an empty tier
324
+ /// is returned.
325
+ function tiersFor(
326
+ address hook,
327
+ // forge-lint: disable-next-line(mixed-case-variable)
328
+ bytes32[] memory encodedIPFSUris
329
+ )
330
+ external
331
+ view
332
+ override
333
+ returns (JB721Tier[] memory tiers)
334
+ {
335
+ // forge-lint: disable-next-line(mixed-case-variable)
336
+ uint256 numberOfEncodedIPFSUris = encodedIPFSUris.length;
337
+
338
+ // Initialize the tier array being returned.
339
+ tiers = new JB721Tier[](numberOfEncodedIPFSUris);
340
+
341
+ // Get the tier for each provided encoded IPFS URI.
342
+ for (uint256 i; i < numberOfEncodedIPFSUris;) {
343
+ // Check if there's a tier ID stored for the encoded IPFS URI.
344
+ uint256 tierId = tierIdForEncodedIPFSUriOf[hook][encodedIPFSUris[i]];
345
+
346
+ // If there's a tier ID stored, resolve it.
347
+ if (tierId != 0) {
348
+ // slither-disable-next-line calls-loop
349
+ tiers[i] = IJB721TiersHook(hook).STORE().tierOf({hook: hook, id: tierId, includeResolvedUri: false});
350
+ }
351
+
352
+ unchecked {
353
+ ++i;
354
+ }
355
+ }
356
+ }
357
+
358
+ //*********************************************************************//
359
+ // -------------------------- public views --------------------------- //
360
+ //*********************************************************************//
361
+
362
+ /// @notice Post allowances for a particular category on a particular hook.
363
+ /// @param hook The hook contract for which this allowance applies.
364
+ /// @param category The category for which this allowance applies.
365
+ /// @return minimumPrice The minimum price that a poster must pay to record a new NFT.
366
+ /// @return minimumTotalSupply The minimum total number of available tokens that a minter must set to record a new
367
+ /// NFT.
368
+ /// @return maximumTotalSupply The max total supply of NFTs that can be made available when minting. Must be >=
369
+ /// minimumTotalSupply.
370
+ /// @return maximumSplitPercent The maximum split percent that a poster can set. 0 means splits are not allowed.
371
+ /// @return allowedAddresses The addresses allowed to post. Returns empty if all addresses are allowed.
372
+ function allowanceFor(
373
+ address hook,
374
+ uint256 category
375
+ )
376
+ public
377
+ view
378
+ override
379
+ returns (
380
+ uint256 minimumPrice,
381
+ uint256 minimumTotalSupply,
382
+ uint256 maximumTotalSupply,
383
+ uint256 maximumSplitPercent,
384
+ address[] memory allowedAddresses
385
+ )
386
+ {
387
+ // Get a reference to the packed values.
388
+ uint256 packed = _packedAllowanceFor[hook][category];
389
+
390
+ // minimum price in bits 0-103 (104 bits).
391
+ // forge-lint: disable-next-line(unsafe-typecast)
392
+ minimumPrice = uint256(uint104(packed));
393
+ // minimum supply in bits 104-135 (32 bits).
394
+ // forge-lint: disable-next-line(unsafe-typecast)
395
+ minimumTotalSupply = uint256(uint32(packed >> 104));
396
+ // maximum supply in bits 136-167 (32 bits).
397
+ // forge-lint: disable-next-line(unsafe-typecast)
398
+ maximumTotalSupply = uint256(uint32(packed >> 136));
399
+ // maximum split percent in bits 168-199 (32 bits).
400
+ // forge-lint: disable-next-line(unsafe-typecast)
401
+ maximumSplitPercent = uint256(uint32(packed >> 168));
402
+
403
+ allowedAddresses = _allowedAddresses[hook][category];
404
+ }
405
+
406
+ //*********************************************************************//
407
+ // ------------------------ internal helpers ------------------------- //
450
408
  //*********************************************************************//
451
409
 
452
410
  /// @notice Setup the posts.
@@ -613,4 +571,46 @@ contract CTPublisher is JBPermissioned, ERC2771Context, ICTPublisher {
613
571
  }
614
572
  }
615
573
  }
574
+
575
+ /// @notice Check if an address is included in an allow list.
576
+ /// @dev Uses an O(n) linear scan over the `addresses` array. This is acceptable for typical allow list sizes
577
+ /// (fewer than ~100 addresses), where gas cost is negligible. For very large allow lists, a Merkle proof
578
+ /// pattern would scale better, but the added complexity is not warranted for the expected use case.
579
+ /// @param addrs The candidate address.
580
+ /// @param addresses An array of allowed addresses.
581
+ function _isAllowed(address addrs, address[] memory addresses) internal pure returns (bool) {
582
+ // Keep a reference to the number of address to check against.
583
+ uint256 numberOfAddresses = addresses.length;
584
+
585
+ // Check if the address is included
586
+ for (uint256 i; i < numberOfAddresses;) {
587
+ if (addrs == addresses[i]) return true;
588
+ unchecked {
589
+ ++i;
590
+ }
591
+ }
592
+
593
+ return false;
594
+ }
595
+
596
+ //*********************************************************************//
597
+ // -------------------------- internal views ------------------------- //
598
+ //*********************************************************************//
599
+
600
+ /// @dev ERC-2771 specifies the context as being a single address (20 bytes).
601
+ function _contextSuffixLength() internal view virtual override(ERC2771Context, Context) returns (uint256) {
602
+ return super._contextSuffixLength();
603
+ }
604
+
605
+ /// @notice Returns the calldata, prefered to use over `msg.data`
606
+ /// @return calldata the `msg.data` of this call
607
+ function _msgData() internal view override(ERC2771Context, Context) returns (bytes calldata) {
608
+ return ERC2771Context._msgData();
609
+ }
610
+
611
+ /// @notice Returns the sender, prefered to use over `msg.sender`
612
+ /// @return sender the sender address of this call.
613
+ function _msgSender() internal view override(ERC2771Context, Context) returns (address sender) {
614
+ return ERC2771Context._msgSender();
615
+ }
616
616
  }
@@ -56,10 +56,11 @@ contract MockDataHook is IJBRulesetDataHook {
56
56
  uint256 cashOutTaxRate,
57
57
  uint256 cashOutCount,
58
58
  uint256 totalSupply,
59
+ uint256 surplusValue,
59
60
  JBCashOutHookSpecification[] memory hookSpecifications
60
61
  )
61
62
  {
62
- return (TAX_RATE, context.cashOutCount, context.totalSupply, hookSpecifications);
63
+ return (TAX_RATE, context.cashOutCount, context.totalSupply, context.surplus.value, hookSpecifications);
63
64
  }
64
65
 
65
66
  function beforePayRecordedWith(JBBeforePayRecordedContext calldata)
@@ -434,7 +435,7 @@ contract TestCTDeployer is Test {
434
435
  JBBeforeCashOutRecordedContext memory context =
435
436
  _buildCashOutContext(deployedProjectId, unauthorized, 100e18, 1000e18);
436
437
 
437
- (uint256 taxRate, uint256 cashOutCount, uint256 totalSupply,) = deployer.beforeCashOutRecordedWith(context);
438
+ (uint256 taxRate, uint256 cashOutCount, uint256 totalSupply,,) = deployer.beforeCashOutRecordedWith(context);
438
439
 
439
440
  assertEq(taxRate, 5000, "tax rate should come from data hook");
440
441
  assertEq(cashOutCount, 100e18, "cashOutCount should be forwarded");
@@ -455,7 +456,7 @@ contract TestCTDeployer is Test {
455
456
  JBBeforeCashOutRecordedContext memory context =
456
457
  _buildCashOutContext(deployedProjectId, suckerAddr, 100e18, 1000e18);
457
458
 
458
- (uint256 taxRate, uint256 cashOutCount, uint256 totalSupply,) = deployer.beforeCashOutRecordedWith(context);
459
+ (uint256 taxRate, uint256 cashOutCount, uint256 totalSupply,,) = deployer.beforeCashOutRecordedWith(context);
459
460
 
460
461
  assertEq(taxRate, 0, "sucker should get 0% tax rate");
461
462
  assertEq(cashOutCount, 100e18, "cashOutCount should pass through");
@@ -466,7 +467,7 @@ contract TestCTDeployer is Test {
466
467
  function test_beforeCashOutRecordedWith_returnsDefaultsWhenNoDataHook() public {
467
468
  JBBeforeCashOutRecordedContext memory context = _buildCashOutContext(999, unauthorized, 100e18, 1000e18);
468
469
 
469
- (uint256 taxRate, uint256 cashOutCount, uint256 totalSupply, JBCashOutHookSpecification[] memory specs) =
470
+ (uint256 taxRate, uint256 cashOutCount, uint256 totalSupply,, JBCashOutHookSpecification[] memory specs) =
470
471
  deployer.beforeCashOutRecordedWith(context);
471
472
 
472
473
  assertEq(taxRate, context.cashOutTaxRate, "cashOutTaxRate should be returned as-is from context");
package/test/Fork.t.sol CHANGED
@@ -21,6 +21,7 @@ import {JBConstants} from "@bananapus/core-v6/src/libraries/JBConstants.sol";
21
21
  import {JB721TiersHookStore} from "@bananapus/721-hook-v6/src/JB721TiersHookStore.sol";
22
22
  import {JB721TiersHook} from "@bananapus/721-hook-v6/src/JB721TiersHook.sol";
23
23
  import {JB721TiersHookDeployer} from "@bananapus/721-hook-v6/src/JB721TiersHookDeployer.sol";
24
+ import {JB721CheckpointsDeployer} from "@bananapus/721-hook-v6/src/JB721CheckpointsDeployer.sol";
24
25
  import {JBAddressRegistry} from "@bananapus/address-registry-v6/src/JBAddressRegistry.sol";
25
26
 
26
27
  // Suckers — deploy fresh within fork.
@@ -166,7 +167,7 @@ contract ForkTest is Test {
166
167
  jbPermissions = new JBPermissions(trustedForwarder);
167
168
  jbProjects = new JBProjects(multisig, address(0), trustedForwarder);
168
169
  jbDirectory = new JBDirectory(jbPermissions, jbProjects, multisig);
169
- JBERC20 jbErc20 = new JBERC20();
170
+ JBERC20 jbErc20 = new JBERC20(jbPermissions, jbProjects);
170
171
  jbTokens = new JBTokens(jbDirectory, jbErc20);
171
172
  jbRulesets = new JBRulesets(jbDirectory);
172
173
  jbPrices = new JBPrices(jbDirectory, jbPermissions, jbProjects, multisig, trustedForwarder);
@@ -193,9 +194,11 @@ contract ForkTest is Test {
193
194
  function _deploy721Hook() internal {
194
195
  JB721TiersHookStore store = new JB721TiersHookStore();
195
196
  JBAddressRegistry addressRegistry = new JBAddressRegistry();
197
+ JB721CheckpointsDeployer checkpointsDeployer = new JB721CheckpointsDeployer();
196
198
 
197
- JB721TiersHook hookImpl =
198
- new JB721TiersHook(jbDirectory, jbPermissions, jbPrices, jbRulesets, store, jbSplits, trustedForwarder);
199
+ JB721TiersHook hookImpl = new JB721TiersHook(
200
+ jbDirectory, jbPermissions, jbPrices, jbRulesets, store, jbSplits, checkpointsDeployer, trustedForwarder
201
+ );
199
202
 
200
203
  hookDeployer = new JB721TiersHookDeployer(hookImpl, store, addressRegistry, trustedForwarder);
201
204
  }
@@ -37,7 +37,7 @@ contract RevertingDataHook is IJBRulesetDataHook {
37
37
  external
38
38
  pure
39
39
  override
40
- returns (uint256, uint256, uint256, JBCashOutHookSpecification[] memory)
40
+ returns (uint256, uint256, uint256, uint256, JBCashOutHookSpecification[] memory)
41
41
  {
42
42
  revert("DATA_HOOK_REVERTED");
43
43
  }
@@ -81,10 +81,11 @@ contract SuccessDataHook is IJBRulesetDataHook {
81
81
  uint256 cashOutTaxRate,
82
82
  uint256 cashOutCount,
83
83
  uint256 totalSupply,
84
+ uint256 surplusValue,
84
85
  JBCashOutHookSpecification[] memory hookSpecifications
85
86
  )
86
87
  {
87
- return (TAX_RATE, context.cashOutCount, context.totalSupply, hookSpecifications);
88
+ return (TAX_RATE, context.cashOutCount, context.totalSupply, context.surplus.value, hookSpecifications);
88
89
  }
89
90
 
90
91
  function beforePayRecordedWith(JBBeforePayRecordedContext calldata context)
@@ -243,7 +244,7 @@ contract TestAuditGaps is Test {
243
244
  // dataHookOf[999] is address(0) by default (never set).
244
245
  JBBeforeCashOutRecordedContext memory context = _buildCashOutContext(999, unauthorized, 100e18, 1000e18);
245
246
 
246
- (uint256 taxRate, uint256 cashOutCount, uint256 totalSupply, JBCashOutHookSpecification[] memory specs) =
247
+ (uint256 taxRate, uint256 cashOutCount, uint256 totalSupply,, JBCashOutHookSpecification[] memory specs) =
247
248
  deployer.beforeCashOutRecordedWith(context);
248
249
 
249
250
  assertEq(taxRate, context.cashOutTaxRate, "cashOutTaxRate should be returned as-is from context");
@@ -269,7 +270,7 @@ contract TestAuditGaps is Test {
269
270
  JBBeforeCashOutRecordedContext memory context =
270
271
  _buildCashOutContext(hookProjectId, unauthorized, 100e18, 1000e18);
271
272
 
272
- (uint256 taxRate, uint256 cashOutCount, uint256 totalSupply,) = deployer.beforeCashOutRecordedWith(context);
273
+ (uint256 taxRate, uint256 cashOutCount, uint256 totalSupply,,) = deployer.beforeCashOutRecordedWith(context);
273
274
 
274
275
  assertEq(taxRate, 5000, "tax rate should be forwarded from data hook");
275
276
  assertEq(cashOutCount, 100e18, "cashOutCount should be forwarded");
@@ -302,7 +303,7 @@ contract TestAuditGaps is Test {
302
303
  JBBeforeCashOutRecordedContext memory context = _buildCashOutContext(hookProjectId, realSucker, 100e18, 1000e18);
303
304
 
304
305
  // Should NOT revert because suckers bypass the data hook entirely.
305
- (uint256 taxRate, uint256 cashOutCount, uint256 totalSupply,) = deployer.beforeCashOutRecordedWith(context);
306
+ (uint256 taxRate, uint256 cashOutCount, uint256 totalSupply,,) = deployer.beforeCashOutRecordedWith(context);
306
307
 
307
308
  assertEq(taxRate, 0, "sucker should get 0% tax rate");
308
309
  assertEq(cashOutCount, 100e18, "cashOutCount should pass through");
@@ -320,7 +321,7 @@ contract TestAuditGaps is Test {
320
321
  // fakeSucker is NOT registered as a sucker (default mock returns false).
321
322
  JBBeforeCashOutRecordedContext memory context = _buildCashOutContext(hookProjectId, fakeSucker, 100e18, 1000e18);
322
323
 
323
- (uint256 taxRate,,,) = deployer.beforeCashOutRecordedWith(context);
324
+ (uint256 taxRate,,,,) = deployer.beforeCashOutRecordedWith(context);
324
325
 
325
326
  // Should get the data hook's tax rate (5000), not 0.
326
327
  assertEq(taxRate, 5000, "non-sucker should not bypass tax");
@@ -340,7 +341,7 @@ contract TestAuditGaps is Test {
340
341
 
341
342
  JBBeforeCashOutRecordedContext memory context = _buildCashOutContext(hookProjectId, realSucker, 100e18, 1000e18);
342
343
 
343
- (uint256 taxRate,,,) = deployer.beforeCashOutRecordedWith(context);
344
+ (uint256 taxRate,,,,) = deployer.beforeCashOutRecordedWith(context);
344
345
 
345
346
  // Should get the data hook's tax rate, not 0.
346
347
  assertEq(taxRate, 5000, "sucker from wrong project should not bypass tax");
@@ -359,7 +360,7 @@ contract TestAuditGaps is Test {
359
360
 
360
361
  JBBeforeCashOutRecordedContext memory context = _buildCashOutContext(hookProjectId, realSucker, 100e18, 1000e18);
361
362
 
362
- (uint256 taxRate,,,) = deployer.beforeCashOutRecordedWith(context);
363
+ (uint256 taxRate,,,,) = deployer.beforeCashOutRecordedWith(context);
363
364
 
364
365
  assertEq(taxRate, 0, "valid sucker should get 0% tax");
365
366
  }
@@ -0,0 +1,263 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity 0.8.28;
3
+
4
+ // forge-lint: disable-next-line(unaliased-plain-import)
5
+ import "forge-std/Test.sol";
6
+
7
+ import {IJB721TiersHook} from "@bananapus/721-hook-v6/src/interfaces/IJB721TiersHook.sol";
8
+ import {IJB721TiersHookDeployer} from "@bananapus/721-hook-v6/src/interfaces/IJB721TiersHookDeployer.sol";
9
+ import {JBDeploy721TiersHookConfig} from "@bananapus/721-hook-v6/src/structs/JBDeploy721TiersHookConfig.sol";
10
+ import {JB721TierConfig} from "@bananapus/721-hook-v6/src/structs/JB721TierConfig.sol";
11
+ import {JBPermissioned} from "@bananapus/core-v6/src/abstract/JBPermissioned.sol";
12
+ import {IJBController} from "@bananapus/core-v6/src/interfaces/IJBController.sol";
13
+ import {IJBDirectory} from "@bananapus/core-v6/src/interfaces/IJBDirectory.sol";
14
+ import {IJBPermissions} from "@bananapus/core-v6/src/interfaces/IJBPermissions.sol";
15
+ import {IJBProjects} from "@bananapus/core-v6/src/interfaces/IJBProjects.sol";
16
+ import {JBPermissions} from "@bananapus/core-v6/src/JBPermissions.sol";
17
+ import {JBRulesetConfig} from "@bananapus/core-v6/src/structs/JBRulesetConfig.sol";
18
+ import {JBTerminalConfig} from "@bananapus/core-v6/src/structs/JBTerminalConfig.sol";
19
+ import {JBPermissionIds} from "@bananapus/permission-ids-v6/src/JBPermissionIds.sol";
20
+ import {IJBSuckerRegistry} from "@bananapus/suckers-v6/src/interfaces/IJBSuckerRegistry.sol";
21
+ import {JBSuckerRegistry} from "@bananapus/suckers-v6/src/JBSuckerRegistry.sol";
22
+ import {JBSuckerDeployerConfig} from "@bananapus/suckers-v6/src/structs/JBSuckerDeployerConfig.sol";
23
+
24
+ import {CTDeployer} from "../../src/CTDeployer.sol";
25
+ import {CTPublisher} from "../../src/CTPublisher.sol";
26
+ import {ICTPublisher} from "../../src/interfaces/ICTPublisher.sol";
27
+ import {CTDeployerAllowedPost} from "../../src/structs/CTDeployerAllowedPost.sol";
28
+ import {CTProjectConfig} from "../../src/structs/CTProjectConfig.sol";
29
+ import {CTSuckerDeploymentConfig} from "../../src/structs/CTSuckerDeploymentConfig.sol";
30
+
31
+ contract NemesisMockProjects {
32
+ uint256 public countValue;
33
+ mapping(uint256 => address) internal _ownerOf;
34
+
35
+ function setCount(uint256 count_) external {
36
+ countValue = count_;
37
+ }
38
+
39
+ function setOwner(uint256 projectId, address owner_) external {
40
+ _ownerOf[projectId] = owner_;
41
+ }
42
+
43
+ function count() external view returns (uint256) {
44
+ return countValue;
45
+ }
46
+
47
+ function ownerOf(uint256 projectId) external view returns (address) {
48
+ return _ownerOf[projectId];
49
+ }
50
+
51
+ function transferFrom(address from, address to, uint256 tokenId) external {
52
+ require(_ownerOf[tokenId] == from, "wrong from");
53
+ _ownerOf[tokenId] = to;
54
+ }
55
+ }
56
+
57
+ contract NemesisMockController {
58
+ NemesisMockProjects public immutable PROJECTS;
59
+ uint256 public immutable nextProjectId;
60
+
61
+ constructor(NemesisMockProjects projects_, uint256 nextProjectId_) {
62
+ PROJECTS = projects_;
63
+ nextProjectId = nextProjectId_;
64
+ }
65
+
66
+ function launchProjectFor(
67
+ address owner,
68
+ string calldata,
69
+ JBRulesetConfig[] calldata,
70
+ JBTerminalConfig[] calldata,
71
+ string calldata
72
+ )
73
+ external
74
+ returns (uint256)
75
+ {
76
+ PROJECTS.setOwner(nextProjectId, owner);
77
+ return nextProjectId;
78
+ }
79
+ }
80
+
81
+ contract NemesisPermissionedHook is JBPermissioned {
82
+ address public immutable ownerAccount;
83
+ uint256 public immutable projectId;
84
+ bool public adjusted;
85
+
86
+ constructor(IJBPermissions permissions, address ownerAccount_, uint256 projectId_) JBPermissioned(permissions) {
87
+ ownerAccount = ownerAccount_;
88
+ projectId = projectId_;
89
+ }
90
+
91
+ // forge-lint: disable-next-line(mixed-case-function)
92
+ function PROJECT_ID() external view returns (uint256) {
93
+ return projectId;
94
+ }
95
+
96
+ function owner() external view returns (address) {
97
+ return ownerAccount;
98
+ }
99
+
100
+ function adjustTiers(JB721TierConfig[] calldata, uint256[] calldata) external {
101
+ _requirePermissionFrom(ownerAccount, projectId, JBPermissionIds.ADJUST_721_TIERS);
102
+ adjusted = true;
103
+ }
104
+ }
105
+
106
+ contract NemesisMockHookDeployer {
107
+ IJB721TiersHook public hook;
108
+
109
+ function setHook(IJB721TiersHook hook_) external {
110
+ hook = hook_;
111
+ }
112
+
113
+ function deployHookFor(
114
+ uint256,
115
+ JBDeploy721TiersHookConfig calldata,
116
+ bytes32
117
+ )
118
+ external
119
+ view
120
+ returns (IJB721TiersHook)
121
+ {
122
+ return hook;
123
+ }
124
+ }
125
+
126
+ contract NemesisNoopSuckerRegistry {
127
+ function isSuckerOf(uint256, address) external pure returns (bool) {
128
+ return false;
129
+ }
130
+
131
+ function deploySuckersFor(
132
+ uint256,
133
+ bytes32,
134
+ JBSuckerDeployerConfig[] calldata
135
+ )
136
+ external
137
+ pure
138
+ returns (address[] memory suckers)
139
+ {
140
+ return suckers;
141
+ }
142
+ }
143
+
144
+ contract NemesisMockDirectory {
145
+ IJBProjects public immutable PROJECTS;
146
+
147
+ constructor(IJBProjects projects_) {
148
+ PROJECTS = projects_;
149
+ }
150
+ }
151
+
152
+ contract CodexNemesisPoCs is Test {
153
+ JBPermissions permissions;
154
+ NemesisMockProjects projects;
155
+ NemesisMockHookDeployer hookDeployer;
156
+ NemesisMockController controller;
157
+ CTPublisher publisher;
158
+ CTDeployer deployer;
159
+ NemesisPermissionedHook hook;
160
+
161
+ address alice = makeAddr("alice");
162
+ address bob = makeAddr("bob");
163
+
164
+ function setUp() public {
165
+ permissions = new JBPermissions(address(0));
166
+ projects = new NemesisMockProjects();
167
+ projects.setCount(5);
168
+
169
+ hookDeployer = new NemesisMockHookDeployer();
170
+ publisher = new CTPublisher(IJBDirectory(makeAddr("directory")), permissions, 1, address(0));
171
+ deployer = new CTDeployer(
172
+ permissions,
173
+ IJBProjects(address(projects)),
174
+ IJB721TiersHookDeployer(address(hookDeployer)),
175
+ ICTPublisher(address(publisher)),
176
+ IJBSuckerRegistry(address(new NemesisNoopSuckerRegistry())),
177
+ address(0)
178
+ );
179
+
180
+ hook = new NemesisPermissionedHook(permissions, address(deployer), 6);
181
+ hookDeployer.setHook(IJB721TiersHook(address(hook)));
182
+ controller = new NemesisMockController(projects, 6);
183
+ }
184
+
185
+ function test_oldProjectOwnerRetainsHookControlAfterProjectNftTransferUntilClaim() public {
186
+ CTProjectConfig memory config = CTProjectConfig({
187
+ terminalConfigurations: new JBTerminalConfig[](0),
188
+ projectUri: "ipfs://project",
189
+ allowedPosts: new CTDeployerAllowedPost[](0),
190
+ contractUri: "ipfs://contract",
191
+ name: "Croptop",
192
+ symbol: "CT",
193
+ salt: bytes32(0)
194
+ });
195
+
196
+ CTSuckerDeploymentConfig memory suckerConfig =
197
+ CTSuckerDeploymentConfig({deployerConfigurations: new JBSuckerDeployerConfig[](0), salt: bytes32(0)});
198
+
199
+ deployer.deployProjectFor(alice, config, suckerConfig, IJBController(address(controller)));
200
+ assertEq(projects.ownerOf(6), alice, "alice should receive the project NFT");
201
+
202
+ projects.transferFrom(alice, bob, 6);
203
+ assertEq(projects.ownerOf(6), bob, "bob should own the project NFT after transfer");
204
+
205
+ JB721TierConfig[] memory arbitraryTiers = new JB721TierConfig[](0);
206
+ uint256[] memory removals = new uint256[](0);
207
+
208
+ vm.prank(alice);
209
+ NemesisPermissionedHook(address(hook)).adjustTiers(arbitraryTiers, removals);
210
+
211
+ assertTrue(
212
+ hook.adjusted(),
213
+ "the previous owner should still be able to mutate hook state until the new NFT owner explicitly claims"
214
+ );
215
+ }
216
+
217
+ function test_deploySuckersHelperBreaksAfterOwnershipTransferBecauseRegistrySeesCtDeployerAsCaller() public {
218
+ NemesisMockDirectory directory = new NemesisMockDirectory(IJBProjects(address(projects)));
219
+ JBSuckerRegistry registry =
220
+ new JBSuckerRegistry(IJBDirectory(address(directory)), permissions, address(this), address(0));
221
+
222
+ deployer = new CTDeployer(
223
+ permissions,
224
+ IJBProjects(address(projects)),
225
+ IJB721TiersHookDeployer(address(hookDeployer)),
226
+ ICTPublisher(address(publisher)),
227
+ IJBSuckerRegistry(address(registry)),
228
+ address(0)
229
+ );
230
+
231
+ hook = new NemesisPermissionedHook(permissions, address(deployer), 6);
232
+ hookDeployer.setHook(IJB721TiersHook(address(hook)));
233
+
234
+ CTProjectConfig memory config = CTProjectConfig({
235
+ terminalConfigurations: new JBTerminalConfig[](0),
236
+ projectUri: "ipfs://project",
237
+ allowedPosts: new CTDeployerAllowedPost[](0),
238
+ contractUri: "ipfs://contract",
239
+ name: "Croptop",
240
+ symbol: "CT",
241
+ salt: bytes32(0)
242
+ });
243
+
244
+ CTSuckerDeploymentConfig memory emptySuckerConfig =
245
+ CTSuckerDeploymentConfig({deployerConfigurations: new JBSuckerDeployerConfig[](0), salt: bytes32(0)});
246
+ deployer.deployProjectFor(alice, config, emptySuckerConfig, IJBController(address(controller)));
247
+
248
+ CTSuckerDeploymentConfig memory laterSuckerConfig =
249
+ CTSuckerDeploymentConfig({deployerConfigurations: new JBSuckerDeployerConfig[](0), salt: bytes32("later")});
250
+
251
+ vm.prank(alice);
252
+ vm.expectRevert(
253
+ abi.encodeWithSelector(
254
+ JBPermissioned.JBPermissioned_Unauthorized.selector,
255
+ alice,
256
+ address(deployer),
257
+ 6,
258
+ JBPermissionIds.DEPLOY_SUCKERS
259
+ )
260
+ );
261
+ deployer.deploySuckersFor(6, laterSuckerConfig);
262
+ }
263
+ }
@@ -32,6 +32,7 @@ import {MockPriceFeed} from "@bananapus/core-v6/test/mock/MockPriceFeed.sol";
32
32
  import {JB721TiersHookStore} from "@bananapus/721-hook-v6/src/JB721TiersHookStore.sol";
33
33
  import {JB721TiersHook} from "@bananapus/721-hook-v6/src/JB721TiersHook.sol";
34
34
  import {JB721TiersHookDeployer} from "@bananapus/721-hook-v6/src/JB721TiersHookDeployer.sol";
35
+ import {JB721CheckpointsDeployer} from "@bananapus/721-hook-v6/src/JB721CheckpointsDeployer.sol";
35
36
  import {IJB721TiersHook} from "@bananapus/721-hook-v6/src/interfaces/IJB721TiersHook.sol";
36
37
  import {JBAddressRegistry} from "@bananapus/address-registry-v6/src/JBAddressRegistry.sol";
37
38
  import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol";
@@ -276,7 +277,7 @@ contract PublishForkTest is Test, DeployPermit2 {
276
277
  jbPermissions = new JBPermissions(trustedForwarder);
277
278
  jbProjects = new JBProjects(multisig, address(0), trustedForwarder);
278
279
  jbDirectory = new JBDirectory(jbPermissions, jbProjects, multisig);
279
- JBERC20 jbErc20 = new JBERC20();
280
+ JBERC20 jbErc20 = new JBERC20(jbPermissions, jbProjects);
280
281
  jbTokens = new JBTokens(jbDirectory, jbErc20);
281
282
  jbRulesets = new JBRulesets(jbDirectory);
282
283
  jbPrices = new JBPrices(jbDirectory, jbPermissions, jbProjects, multisig, trustedForwarder);
@@ -321,9 +322,11 @@ contract PublishForkTest is Test, DeployPermit2 {
321
322
  function _deploy721Hook() internal {
322
323
  JB721TiersHookStore store = new JB721TiersHookStore();
323
324
  JBAddressRegistry addressRegistry = new JBAddressRegistry();
325
+ JB721CheckpointsDeployer checkpointsDeployer = new JB721CheckpointsDeployer();
324
326
 
325
- JB721TiersHook hookImpl =
326
- new JB721TiersHook(jbDirectory, jbPermissions, jbPrices, jbRulesets, store, jbSplits, trustedForwarder);
327
+ JB721TiersHook hookImpl = new JB721TiersHook(
328
+ jbDirectory, jbPermissions, jbPrices, jbRulesets, store, jbSplits, checkpointsDeployer, trustedForwarder
329
+ );
327
330
 
328
331
  hookDeployer = new JB721TiersHookDeployer(hookImpl, store, addressRegistry, trustedForwarder);
329
332
  }