@croptop/core-v6 0.0.32 → 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/USER_JOURNEYS.md CHANGED
@@ -52,17 +52,6 @@
52
52
  2. Restrict future edits to the paths Croptop intentionally exposes.
53
53
  3. Accept that this is a product-shaping choice, not a cosmetic deployment detail.
54
54
 
55
- ## Journey 5: Support Cross-Chain Payments Through Data Hooks
56
-
57
- **Starting state:** a sucker pays the Croptop project on behalf of a remote user via `payRemote`, and `CTDeployer.beforePayRecordedWith` needs to forward the correct beneficiary to downstream hooks.
58
-
59
- **Success:** downstream data hooks see the real remote user so any hook-specific accounting accrues to the right person.
60
-
61
- **Flow**
62
- 1. The sucker calls `terminal.pay()` with relay-beneficiary metadata.
63
- 2. `CTDeployer.beforePayRecordedWith()` resolves the relay beneficiary when the payer is a registered sucker.
64
- 3. The swapped beneficiary is forwarded to the downstream data hook.
65
-
66
55
  ## Hand-Offs
67
56
 
68
57
  - Use [nana-721-hook-v6](../nana-721-hook-v6/USER_JOURNEYS.md) for the underlying tier issuance behavior Croptop wraps.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@croptop/core-v6",
3
- "version": "0.0.32",
3
+ "version": "0.0.33",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -27,7 +27,7 @@
27
27
  },
28
28
  "devDependencies": {
29
29
  "@bananapus/address-registry-v6": "^0.0.17",
30
- "@rev-net/core-v6": "^0.0.30",
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,7 +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";
31
- import {JBRelayBeneficiary} from "@bananapus/suckers-v6/src/libraries/JBRelayBeneficiary.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";
32
31
 
33
32
  import {ICTDeployer} from "./interfaces/ICTDeployer.sol";
34
33
  import {ICTPublisher} from "./interfaces/ICTPublisher.sol";
@@ -118,127 +117,6 @@ contract CTDeployer is ERC2771Context, JBPermissioned, IJBRulesetDataHook, IERC7
118
117
  PERMISSIONS.setPermissionsFor({account: address(this), permissionsData: permissionData});
119
118
  }
120
119
 
121
- //*********************************************************************//
122
- // ------------------------- external views -------------------------- //
123
- //*********************************************************************//
124
-
125
- /// @notice Allow cash outs from suckers without a tax.
126
- /// @dev This function is part of `IJBRulesetDataHook`, and gets called before the revnet processes a cash out.
127
- /// @param context Standard Juicebox cash out context. See `JBBeforeCashOutRecordedContext`.
128
- /// @return cashOutTaxRate The cash out tax rate, which influences the amount of terminal tokens which get cashed
129
- /// out.
130
- /// @return cashOutCount The number of project tokens that are cashed out.
131
- /// @return totalSupply The total project token supply.
132
- /// @return hookSpecifications The amount of funds and the data to send to cash out hooks (this contract).
133
- function beforeCashOutRecordedWith(JBBeforeCashOutRecordedContext calldata context)
134
- external
135
- view
136
- override
137
- returns (
138
- uint256 cashOutTaxRate,
139
- uint256 cashOutCount,
140
- uint256 totalSupply,
141
- JBCashOutHookSpecification[] memory hookSpecifications
142
- )
143
- {
144
- // If the cash out is from a sucker, return the full cash out amount without taxes or fees.
145
- if (SUCKER_REGISTRY.isSuckerOf({projectId: context.projectId, addr: context.holder})) {
146
- return (0, context.cashOutCount, context.totalSupply, hookSpecifications);
147
- }
148
-
149
- // If the ruleset has a data hook, forward the call to the datahook.
150
- IJBRulesetDataHook hook = dataHookOf[context.projectId];
151
- if (address(hook) == address(0)) {
152
- return (context.cashOutTaxRate, context.cashOutCount, context.totalSupply, hookSpecifications);
153
- }
154
- // slither-disable-next-line unused-return
155
- return hook.beforeCashOutRecordedWith(context);
156
- }
157
-
158
- /// @notice Forward the call to the original data hook.
159
- /// @dev This function is part of `IJBRulesetDataHook`, and gets called before the revnet processes a payment.
160
- /// @param context Standard Juicebox payment context. See `JBBeforePayRecordedContext`.
161
- /// @return weight The weight which project tokens are minted relative to. This can be used to customize how many
162
- /// tokens get minted by a payment.
163
- /// @return hookSpecifications Amounts (out of what's being paid in) to be sent to pay hooks instead of being paid
164
- /// into the project. Useful for automatically routing funds from a treasury as payments come in.
165
- function beforePayRecordedWith(JBBeforePayRecordedContext calldata context)
166
- external
167
- view
168
- override
169
- returns (uint256 weight, JBPayHookSpecification[] memory hookSpecifications)
170
- {
171
- // Forward the call to the data hook.
172
- IJBRulesetDataHook hook = dataHookOf[context.projectId];
173
- if (address(hook) == address(0)) {
174
- return (context.weight, hookSpecifications);
175
- }
176
-
177
- // Resolve the relay beneficiary — if the payer is a sucker with relay metadata,
178
- // swap the beneficiary so downstream hooks see the real user.
179
- address effectiveBeneficiary = JBRelayBeneficiary.resolve({
180
- payer: context.payer,
181
- beneficiary: context.beneficiary,
182
- projectId: context.projectId,
183
- metadata: context.metadata,
184
- registry: SUCKER_REGISTRY
185
- });
186
-
187
- // If the beneficiary was swapped, create a memory copy with the new beneficiary.
188
- if (effectiveBeneficiary != context.beneficiary) {
189
- JBBeforePayRecordedContext memory hookContext = context;
190
- hookContext.beneficiary = effectiveBeneficiary;
191
- return hook.beforePayRecordedWith(hookContext);
192
- }
193
-
194
- // slither-disable-next-line unused-return
195
- return hook.beforePayRecordedWith(context);
196
- }
197
-
198
- /// @notice A flag indicating whether an address has permission to mint a project's tokens on-demand.
199
- /// @dev A project's data hook can allow any address to mint its tokens.
200
- /// @param projectId The ID of the project whose token can be minted.
201
- /// @param addr The address to check the token minting permission of.
202
- /// @return flag A flag indicating whether the address has permission to mint the project's tokens on-demand.
203
- function hasMintPermissionFor(uint256 projectId, JBRuleset memory, address addr) external view returns (bool flag) {
204
- // If the address is a sucker for this project.
205
- return SUCKER_REGISTRY.isSuckerOf({projectId: projectId, addr: addr});
206
- }
207
-
208
- /// @dev Make sure only mints can be received.
209
- function onERC721Received(
210
- address operator,
211
- address from,
212
- uint256 tokenId,
213
- bytes calldata data
214
- )
215
- external
216
- view
217
- returns (bytes4)
218
- {
219
- data;
220
- tokenId;
221
- operator;
222
-
223
- // Make sure the 721 received is the JBProjects contract.
224
- if (msg.sender != address(PROJECTS)) revert();
225
- // Make sure the 721 is being received as a mint.
226
- if (from != address(0)) revert();
227
- return IERC721Receiver.onERC721Received.selector;
228
- }
229
-
230
- //*********************************************************************//
231
- // -------------------------- public views --------------------------- //
232
- //*********************************************************************//
233
-
234
- /// @notice Indicates if this contract adheres to the specified interface.
235
- /// @dev See `IERC165.supportsInterface`.
236
- /// @return A flag indicating if the provided interface ID is supported.
237
- function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) {
238
- return interfaceId == type(ICTDeployer).interfaceId || interfaceId == type(IJBRulesetDataHook).interfaceId
239
- || interfaceId == type(IERC721Receiver).interfaceId;
240
- }
241
-
242
120
  //*********************************************************************//
243
121
  // ---------------------- external transactions ---------------------- //
244
122
  //*********************************************************************//
@@ -408,6 +286,118 @@ contract CTDeployer is ERC2771Context, JBPermissioned, IJBRulesetDataHook, IERC7
408
286
  });
409
287
  }
410
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
+
411
401
  //*********************************************************************//
412
402
  // --------------------- internal transactions ----------------------- //
413
403
  //*********************************************************************//
@@ -451,7 +441,7 @@ contract CTDeployer is ERC2771Context, JBPermissioned, IJBRulesetDataHook, IERC7
451
441
  }
452
442
 
453
443
  //*********************************************************************//
454
- // ------------------------ internal functions ----------------------- //
444
+ // -------------------------- internal views ------------------------- //
455
445
  //*********************************************************************//
456
446
 
457
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
  }