@croptop/core-v6 0.0.20 → 0.0.22

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
@@ -1,85 +1,99 @@
1
1
  # croptop-core-v6 -- User Journeys
2
2
 
3
- Complete user path documentation for auditors. Each journey describes the entry point, parameters, state changes, external calls, and edge cases.
3
+ Complete user path documentation for auditors. Each journey describes the entry point, who can call, parameters, state changes, events, external calls, and edge cases.
4
4
 
5
5
  ---
6
6
 
7
7
  ## Journey 1: Deploy a Croptop Project
8
8
 
9
9
  **Actor:** Project creator
10
- **Entry point:** `CTDeployer.deployProjectFor(owner, projectConfig, suckerDeploymentConfiguration, controller)`
11
- **Source:** `src/CTDeployer.sol` lines 241-342
10
+ **Entry point:** `CTDeployer.deployProjectFor(address owner, CTProjectConfig calldata projectConfig, CTSuckerDeploymentConfig calldata suckerDeploymentConfiguration, IJBController controller) external returns (uint256 projectId, IJB721TiersHook hook)`
11
+ **Who can call:** Anyone. No access control on this function.
12
+ **Source:** `src/CTDeployer.sol` lines 244-348
12
13
 
13
14
  ### Parameters
14
15
 
15
- | Parameter | Type | Description |
16
- |-----------|------|-------------|
17
- | `owner` | `address` | Final owner of the project NFT after deployment |
18
- | `projectConfig` | `CTProjectConfig` | Name, symbol, URIs, terminal configs, allowed posts, salt |
19
- | `suckerDeploymentConfiguration` | `CTSuckerDeploymentConfig` | Cross-chain sucker deployer configs + salt (set salt to `bytes32(0)` to skip) |
20
- | `controller` | `IJBController` | The JB controller that will manage the project |
16
+ - `owner` (`address`): Final owner of the project NFT after deployment.
17
+ - `projectConfig` (`CTProjectConfig`): Name, symbol, URIs, terminal configs, allowed posts, salt.
18
+ - `suckerDeploymentConfiguration` (`CTSuckerDeploymentConfig`): Cross-chain sucker deployer configs + salt (set salt to `bytes32(0)` to skip).
19
+ - `controller` (`IJBController`): The JB controller that will manage the project.
21
20
 
22
21
  ### Execution Flow
23
22
 
24
- 1. **Controller validation** (line 251): Reverts if `controller.PROJECTS() != PROJECTS`.
23
+ 1. **Controller validation** (line 254): Reverts if `controller.PROJECTS() != PROJECTS`.
25
24
 
26
- 2. **Ruleset configuration** (lines 253-288):
25
+ 2. **Ruleset configuration -- phase 1** (lines 256-258):
27
26
  - Weight: `1_000_000 * 10^18`
28
27
  - Base currency: ETH
29
- - Cash-out tax rate: `MAX_CASH_OUT_TAX_RATE` (100%)
30
- - Data hook: `address(this)` (CTDeployer)
31
- - `useDataHookForPay = true`, `useDataHookForCashOut = true`
28
+ - The remaining ruleset metadata (data hook, cash-out tax rate) cannot be set yet because the hook address is not known until after deployment (step 4).
32
29
 
33
- 3. **Project ID prediction** (line 258): `projectId = PROJECTS.count() + 1`
30
+ 3. **Project ID prediction** (line 261): `projectId = PROJECTS.count() + 1`
34
31
 
35
- 4. **Hook deployment** (lines 262-283):
32
+ 4. **Hook deployment** (lines 265-286):
36
33
  ```
37
34
  DEPLOYER.deployHookFor(projectId, config, salt)
38
35
  ```
39
36
  - Salt: `keccak256(abi.encode(projectConfig.salt, _msgSender()))`
40
37
  - Deployed with empty tiers, ETH currency, 18 decimals
41
- - No reserves, no votes, no owner minting, no overspend prevention
38
+ - All `false` flags: new tiers with reserves, votes, and owner minting are allowed; overspending is not prevented; tokens are not issued for splits
42
39
 
43
- 5. **Project launch** (lines 291-300):
40
+ 5. **Ruleset configuration -- phase 2** (lines 288-291): Now that the hook is deployed, the remaining metadata fields are set:
41
+ - Cash-out tax rate: `MAX_CASH_OUT_TAX_RATE` (100%)
42
+ - Data hook: `address(this)` (CTDeployer)
43
+ - `useDataHookForPay = true`, `useDataHookForCashOut = true`
44
+
45
+ 6. **Project launch** (lines 294-303):
44
46
  ```
45
47
  controller.launchProjectFor(owner: address(this), ...)
46
48
  ```
47
49
  - CTDeployer receives the project NFT temporarily
48
50
  - `assert(projectId == returned ID)` -- reverts on mismatch (front-running protection)
49
51
 
50
- 6. **Data hook registration** (line 303):
52
+ 7. **Data hook registration** (line 306):
51
53
  ```
52
54
  dataHookOf[projectId] = IJBRulesetDataHook(hook)
53
55
  ```
54
56
  This is write-once. No setter exists.
55
57
 
56
- 7. **Posting criteria** (lines 306-308): If `projectConfig.allowedPosts.length > 0`, calls internal `_configurePostingCriteriaFor()` which formats `CTDeployerAllowedPost` into `CTAllowedPost` (adding the hook address) and delegates to `PUBLISHER.configurePostingCriteriaFor()`.
58
+ 8. **Posting criteria** (lines 309-311): If `projectConfig.allowedPosts.length > 0`, calls internal `_configurePostingCriteriaFor()` which formats `CTDeployerAllowedPost` into `CTAllowedPost` (adding the hook address) and delegates to `PUBLISHER.configurePostingCriteriaFor()`.
57
59
 
58
- 8. **Sucker deployment** (lines 314-321): If `suckerDeploymentConfiguration.salt != bytes32(0)`:
60
+ 9. **Sucker deployment** (lines 317-324): If `suckerDeploymentConfiguration.salt != bytes32(0)`:
59
61
  ```
60
62
  SUCKER_REGISTRY.deploySuckersFor(projectId, salt, configurations)
61
63
  ```
62
64
 
63
- 9. **Ownership transfer** (line 324):
64
- ```
65
- PROJECTS.transferFrom(address(this), owner, projectId)
66
- ```
65
+ 10. **Ownership transfer** (line 327):
66
+ ```
67
+ PROJECTS.transferFrom(address(this), owner, projectId)
68
+ ```
67
69
 
68
- 10. **Permission grants** (lines 327-341): Grants `owner` four permissions from CTDeployer's account:
70
+ 11. **Permission grants** (lines 329-347): Grants `owner` four permissions from CTDeployer's account:
69
71
  - `ADJUST_721_TIERS`
70
72
  - `SET_721_METADATA`
71
73
  - `MINT_721`
72
74
  - `SET_721_DISCOUNT_PERCENT`
73
75
 
74
- ### State Changes
76
+ ### Constructor (One-Time Setup)
77
+
78
+ The CTDeployer constructor (lines 82-118) grants two wildcard (`projectId = 0`) permissions from CTDeployer's account. These are set once at contract deployment, not on every `deployProjectFor` call:
79
+
80
+ 1. `JBPermissions` -- sucker registry granted `MAP_SUCKER_TOKEN` (wildcard, all projects).
81
+ 2. `JBPermissions` -- CTPublisher granted `ADJUST_721_TIERS` (wildcard, all projects).
82
+
83
+ These wildcard permissions allow the sucker registry and publisher to act on behalf of CTDeployer for any project it deploys, without needing per-project permission grants.
75
84
 
76
- | Storage | Change |
77
- |---------|--------|
78
- | `CTDeployer.dataHookOf[projectId]` | Set to the deployed hook address (permanent) |
79
- | `JBProjects` (ERC-721) | New token minted, transferred from CTDeployer to `owner` |
80
- | `JBPermissions` | 5 permission entries set (1 for sucker registry, 1 for publisher, 4 for owner) |
81
- | `CTPublisher._packedAllowanceFor` | Set for each allowed post category (if any) |
82
- | `CTPublisher._allowedAddresses` | Set for each allowed post category with allowlists (if any) |
85
+ ### State Changes (Per Call)
86
+
87
+ 1. `CTDeployer.dataHookOf[projectId]` -- set to the deployed hook address (permanent, write-once).
88
+ 2. `JBProjects` (ERC-721) -- new token minted, transferred from CTDeployer to `owner`.
89
+ 3. `JBPermissions` -- 4 permission entries set for `owner` from CTDeployer's account (`ADJUST_721_TIERS`, `SET_721_METADATA`, `MINT_721`, `SET_721_DISCOUNT_PERCENT`), scoped to the new `projectId`.
90
+ 4. `CTPublisher._packedAllowanceFor[hook][category]` -- set for each allowed post category (if any).
91
+ 5. `CTPublisher._allowedAddresses[hook][category]` -- set for each allowed post category with allowlists (if any).
92
+
93
+ ### Events
94
+
95
+ - `ConfigurePostingCriteria(hook, allowedPost, caller)` -- emitted by CTPublisher for each allowed post entry (only if `projectConfig.allowedPosts` is non-empty). See Journey 3 for the full event signature.
96
+ - No events are emitted directly by CTDeployer itself. External calls to `controller.launchProjectFor()`, `DEPLOYER.deployHookFor()`, `PROJECTS.transferFrom()`, and `PERMISSIONS.setPermissionsFor()` emit their own events from those contracts.
83
97
 
84
98
  ### Edge Cases
85
99
 
@@ -95,24 +109,23 @@ Complete user path documentation for auditors. Each journey describes the entry
95
109
  ## Journey 2: Post Content (Mint NFTs)
96
110
 
97
111
  **Actor:** Content poster (any address, or allowlisted address)
98
- **Entry point:** `CTPublisher.mintFrom(hook, posts, nftBeneficiary, feeBeneficiary, additionalPayMetadata, feeMetadata)`
99
- **Source:** `src/CTPublisher.sol` lines 307-420
112
+ **Entry point:** `CTPublisher.mintFrom(IJB721TiersHook hook, CTPost[] calldata posts, address nftBeneficiary, address feeBeneficiary, bytes calldata additionalPayMetadata, bytes calldata feeMetadata) external payable`
113
+ **Who can call:** Anyone, subject to per-category allowlist restrictions. If a category has a non-empty `allowedAddresses` list, only those addresses may post in that category. Checked via `_isAllowed(_msgSender(), addresses)`.
114
+ **Source:** `src/CTPublisher.sol` lines 310-430
100
115
  **Value:** Must send `msg.value >= sum(tier prices) + 5% fee`
101
116
 
102
117
  ### Parameters
103
118
 
104
- | Parameter | Type | Description |
105
- |-----------|------|-------------|
106
- | `hook` | `IJB721TiersHook` | The 721 hook to post to |
107
- | `posts` | `CTPost[]` | Array of posts (URI, supply, price, category, splits) |
108
- | `nftBeneficiary` | `address` | Receives the minted NFTs |
109
- | `feeBeneficiary` | `address` | Receives fee project tokens |
110
- | `additionalPayMetadata` | `bytes` | Extra metadata appended to the payment |
111
- | `feeMetadata` | `bytes` | Metadata sent with the fee payment |
119
+ - `hook` (`IJB721TiersHook`): The 721 hook to post to.
120
+ - `posts` (`CTPost[]`): Array of posts (URI, supply, price, category, splits).
121
+ - `nftBeneficiary` (`address`): Receives the minted NFTs.
122
+ - `feeBeneficiary` (`address`): Receives fee project tokens.
123
+ - `additionalPayMetadata` (`bytes`): Extra metadata appended to the payment.
124
+ - `feeMetadata` (`bytes`): Metadata sent with the fee payment.
112
125
 
113
126
  ### Execution Flow
114
127
 
115
- **Phase 1: Validation and setup** (`_setupPosts`, lines 432-579)
128
+ **Phase 1: Validation and setup** (`_setupPosts`, lines 442-589)
116
129
 
117
130
  For each post in the batch:
118
131
 
@@ -126,29 +139,36 @@ For each post in the batch:
126
139
  **Phase 2: Fee calculation** (lines 336-354)
127
140
 
128
141
  ```
129
- fee = totalPrice / FEE_DIVISOR (integer division)
130
- require(payValue >= fee) (reverts CTPublisher_InsufficientEthSent if not)
131
- payValue = msg.value - fee (if projectId != FEE_PROJECT_ID)
132
- require(totalPrice <= payValue) (reverts CTPublisher_InsufficientEthSent if not)
142
+ payValue = msg.value
143
+
144
+ if (projectId != FEE_PROJECT_ID) {
145
+ fee = totalPrice / FEE_DIVISOR (integer division, 5%)
146
+ require(payValue >= fee) (reverts CTPublisher_InsufficientEthSent if not)
147
+ payValue -= fee (fee portion held in contract balance)
148
+ }
149
+
150
+ require(totalPrice <= payValue) (reverts CTPublisher_InsufficientEthSent if not)
133
151
  ```
134
152
 
135
- **Phase 3: Tier creation** (line 348)
153
+ When `projectId == FEE_PROJECT_ID`, no fee is deducted; `payValue` remains `msg.value` and the full amount goes to the project payment. Any remaining `address(this).balance` after the project payment (which would be 0 in this case) is skipped in Phase 6.
154
+
155
+ **Phase 3: Tier creation** (line 358)
136
156
 
137
157
  ```
138
158
  hook.adjustTiers(tiersToAdd, [])
139
159
  ```
140
160
 
141
- **Phase 4: Metadata construction** (lines 356-367)
161
+ **Phase 4: Metadata construction** (lines 361-377)
142
162
 
143
163
  Build JBMetadataResolver-compatible metadata with tier IDs and referral ID.
144
164
 
145
- **Phase 5: Project payment** (lines 388-396)
165
+ **Phase 5: Project payment** (lines 398-406)
146
166
 
147
167
  ```
148
168
  projectTerminal.pay{value: payValue}(projectId, NATIVE_TOKEN, payValue, nftBeneficiary, 0, "Minted from Croptop", mintMetadata)
149
169
  ```
150
170
 
151
- **Phase 6: Fee payment** (lines 403-418)
171
+ **Phase 6: Fee payment** (lines 413-429)
152
172
 
153
173
  ```
154
174
  if (address(this).balance != 0) {
@@ -158,13 +178,27 @@ if (address(this).balance != 0) {
158
178
 
159
179
  ### State Changes
160
180
 
161
- | Storage | Change |
162
- |---------|--------|
163
- | `CTPublisher.tierIdForEncodedIPFSUriOf[hook][uri]` | Set for each new tier created |
164
- | `CTPublisher.tierIdForEncodedIPFSUriOf[hook][uri]` | Deleted if stale mapping detected (removed tier) |
165
- | `JB721TiersHookStore` (external) | New tiers added via `adjustTiers` |
166
- | Project terminal (external) | Balance increased by `payValue` |
167
- | Fee project terminal (external) | Balance increased by fee amount |
181
+ 1. `CTPublisher.tierIdForEncodedIPFSUriOf[hook][uri]` -- set for each new tier created.
182
+ 2. `CTPublisher.tierIdForEncodedIPFSUriOf[hook][uri]` -- deleted if stale mapping detected (removed tier).
183
+ 3. `JB721TiersHookStore` (external) -- new tiers added via `adjustTiers`.
184
+ 4. Project terminal (external) -- balance increased by `payValue`.
185
+ 5. Fee project terminal (external) -- balance increased by fee amount.
186
+
187
+ ### Events
188
+
189
+ - `Mint(projectId, hook, nftBeneficiary, feeBeneficiary, posts, postValue, txValue, caller)` -- emitted at line 380 after setup is complete, before the project payment. Full signature:
190
+ ```solidity
191
+ event Mint(
192
+ uint256 indexed projectId,
193
+ IJB721TiersHook indexed hook,
194
+ address indexed nftBeneficiary,
195
+ address feeBeneficiary,
196
+ CTPost[] posts,
197
+ uint256 postValue,
198
+ uint256 txValue,
199
+ address caller
200
+ );
201
+ ```
168
202
 
169
203
  ### Edge Cases
170
204
 
@@ -187,14 +221,13 @@ if (address(this).balance != 0) {
187
221
  ## Journey 3: Configure Posting Criteria (Allowlist Setup)
188
222
 
189
223
  **Actor:** Hook owner (or permissioned delegate)
190
- **Entry point:** `CTPublisher.configurePostingCriteriaFor(allowedPosts)`
191
- **Source:** `src/CTPublisher.sol` lines 240-295
224
+ **Entry point:** `CTPublisher.configurePostingCriteriaFor(CTAllowedPost[] memory allowedPosts) external`
225
+ **Who can call:** The hook's owner (as returned by `JBOwnable(hook).owner()`) or any address that has been granted the `ADJUST_721_TIERS` permission for the hook's `PROJECT_ID()` from that owner. Checked per-entry via `_requirePermissionFrom(account: JBOwnable(hook).owner(), projectId: hook.PROJECT_ID(), permissionId: JBPermissionIds.ADJUST_721_TIERS)`.
226
+ **Source:** `src/CTPublisher.sol` lines 243-298
192
227
 
193
228
  ### Parameters
194
229
 
195
- | Parameter | Type | Description |
196
- |-----------|------|-------------|
197
- | `allowedPosts` | `CTAllowedPost[]` | Array of per-category posting rules |
230
+ - `allowedPosts` (`CTAllowedPost[]`): Array of per-category posting rules.
198
231
 
199
232
  Each `CTAllowedPost` contains:
200
233
 
@@ -212,9 +245,9 @@ Each `CTAllowedPost` contains:
212
245
 
213
246
  For each `CTAllowedPost` in the array:
214
247
 
215
- 1. **Emit event** (line 249): `ConfigurePostingCriteria(hook, allowedPost, caller)`
248
+ 1. **Emit event** (line 252): `ConfigurePostingCriteria(hook, allowedPost, caller)`
216
249
 
217
- 2. **Permission check** (lines 253-257):
250
+ 2. **Permission check** (lines 256-260):
218
251
  ```
219
252
  _requirePermissionFrom(
220
253
  account: JBOwnable(hook).owner(),
@@ -224,16 +257,16 @@ For each `CTAllowedPost` in the array:
224
257
  ```
225
258
 
226
259
  3. **Validation:**
227
- - `minimumTotalSupply > 0` or revert `CTPublisher_ZeroTotalSupply` (line 260)
228
- - `minimumTotalSupply <= maximumTotalSupply` or revert `CTPublisher_MaxTotalSupplyLessThanMin` (line 265)
260
+ - `minimumTotalSupply > 0` or revert `CTPublisher_ZeroTotalSupply` (line 263)
261
+ - `minimumTotalSupply <= maximumTotalSupply` or revert `CTPublisher_MaxTotalSupplyLessThanMin` (line 268)
229
262
 
230
- 4. **Pack and store** (lines 271-281):
263
+ 4. **Pack and store** (lines 274-284):
231
264
  ```
232
265
  packed = minimumPrice | (minimumTotalSupply << 104) | (maximumTotalSupply << 136) | (maximumSplitPercent << 168)
233
266
  _packedAllowanceFor[hook][category] = packed
234
267
  ```
235
268
 
236
- 5. **Allowlist storage** (lines 284-293):
269
+ 5. **Allowlist storage** (lines 287-296):
237
270
  ```
238
271
  delete _allowedAddresses[hook][category]
239
272
  for each address in allowedAddresses:
@@ -242,10 +275,16 @@ For each `CTAllowedPost` in the array:
242
275
 
243
276
  ### State Changes
244
277
 
245
- | Storage | Change |
246
- |---------|--------|
247
- | `_packedAllowanceFor[hook][category]` | Overwritten with new packed values |
248
- | `_allowedAddresses[hook][category]` | Deleted and repopulated |
278
+ 1. `CTPublisher._packedAllowanceFor[hook][category]` -- overwritten with new packed values.
279
+ 2. `CTPublisher._allowedAddresses[hook][category]` -- deleted and repopulated.
280
+
281
+ ### Events
282
+
283
+ - `ConfigurePostingCriteria(address indexed hook, CTAllowedPost allowedPost, address caller)` -- emitted once per entry in the `allowedPosts` array (line 252), **before** the permission check. Full signature:
284
+ ```solidity
285
+ event ConfigurePostingCriteria(address indexed hook, CTAllowedPost allowedPost, address caller);
286
+ ```
287
+ Note: the event is emitted before `_requirePermissionFrom`, so an unauthorized call will emit the event then revert, rolling back the event emission.
249
288
 
250
289
  ### Edge Cases
251
290
 
@@ -265,7 +304,8 @@ For each `CTAllowedPost` in the array:
265
304
  ## Journey 4: Collect Posting Fees
266
305
 
267
306
  **Actor:** Passive (fee project). Fees are collected automatically during `mintFrom()`.
268
- **Entry point:** Triggered within `CTPublisher.mintFrom()` at lines 403-418
307
+ **Entry point:** Triggered within `CTPublisher.mintFrom()` at lines 413-429
308
+ **Who can call:** N/A -- this is an internal sub-flow of Journey 2, not independently callable.
269
309
  **Beneficiary:** The project with ID `FEE_PROJECT_ID` (immutable, set at construction)
270
310
 
271
311
  ### Fee Calculation
@@ -274,10 +314,19 @@ For each `CTAllowedPost` in the array:
274
314
  totalPrice = sum of all post prices in the batch
275
315
  (on-chain tier price for existing tiers, post.price for new tiers)
276
316
 
277
- fee = totalPrice / FEE_DIVISOR (FEE_DIVISOR = 20, so fee = 5%)
278
- payValue = msg.value - fee (deducted before project payment)
317
+ payValue = msg.value
318
+
319
+ if (projectId != FEE_PROJECT_ID) {
320
+ fee = totalPrice / FEE_DIVISOR (FEE_DIVISOR = 20, so fee = 5%)
321
+ require(payValue >= fee) (reverts if msg.value < fee)
322
+ payValue -= fee (fee held in contract balance for later routing)
323
+ }
324
+
325
+ require(totalPrice <= payValue) (reverts if insufficient ETH for the posts)
279
326
  ```
280
327
 
328
+ When `projectId == FEE_PROJECT_ID`, the fee calculation is skipped entirely. The full `msg.value` is sent as `payValue` to the project terminal. After that payment, `address(this).balance` is 0, so the fee routing step (below) is a no-op.
329
+
281
330
  ### Fee Routing
282
331
 
283
332
  After the project payment completes:
@@ -313,6 +362,10 @@ The `feeBeneficiary` parameter in `mintFrom()` determines who receives the fee p
313
362
  | `msg.value` exceeds requirement | Excess goes to fee project. Poster overpays. |
314
363
  | Dust from integer division | Up to 19 wei lost per tx. Fee project receives slightly less. |
315
364
 
365
+ ### Events
366
+
367
+ - No events are emitted by the fee sub-flow itself. The `Mint` event (see Journey 2) is emitted before the fee payment. The fee terminal's `pay()` call emits its own events from the terminal contract.
368
+
316
369
  ### Edge Cases
317
370
 
318
371
  - **Fee project has no primary terminal:** `DIRECTORY.primaryTerminalOf()` returns `address(0)`. The `pay()` call to address(0) reverts. The entire `mintFrom()` transaction reverts (including the project payment).
@@ -325,16 +378,17 @@ The `feeBeneficiary` parameter in `mintFrom()` determines who receives the fee p
325
378
  ## Journey 5: Deploy a Croptop Project via CTDeployer with Posting Criteria
326
379
 
327
380
  **Actor:** Project creator
328
- **Entry point:** `CTDeployer.deployProjectFor()` with non-empty `projectConfig.allowedPosts`
329
- **Source:** `src/CTDeployer.sol` lines 241-342, internal `_configurePostingCriteriaFor()` at lines 376-405
381
+ **Entry point:** `CTDeployer.deployProjectFor(address owner, CTProjectConfig calldata projectConfig, CTSuckerDeploymentConfig calldata suckerDeploymentConfiguration, IJBController controller) external returns (uint256 projectId, IJB721TiersHook hook)` with non-empty `projectConfig.allowedPosts`
382
+ **Who can call:** Anyone. No access control on this function. Same as Journey 1.
383
+ **Source:** `src/CTDeployer.sol` lines 244-348, internal `_configurePostingCriteriaFor()` at lines 382-411
330
384
 
331
385
  This is an extension of Journey 1 that details the posting criteria configuration during deployment.
332
386
 
333
387
  ### Posting Criteria Flow
334
388
 
335
389
  1. CTDeployer receives `CTDeployerAllowedPost[]` (which omits the `hook` field, since the hook hasn't been deployed yet).
336
- 2. After the hook is deployed, `_configurePostingCriteriaFor()` converts each `CTDeployerAllowedPost` to a `CTAllowedPost` by injecting `hook: address(hook)` (lines 392-400).
337
- 3. Calls `PUBLISHER.configurePostingCriteriaFor(formattedAllowedPosts)` (line 404).
390
+ 2. After the hook is deployed, `_configurePostingCriteriaFor()` converts each `CTDeployerAllowedPost` to a `CTAllowedPost` by injecting `hook: address(hook)` (lines 398-406).
391
+ 3. Calls `PUBLISHER.configurePostingCriteriaFor(formattedAllowedPosts)` (line 410).
338
392
  4. The publisher validates each entry (supply bounds, permissions) and stores the packed allowances and allowlists.
339
393
 
340
394
  ### Permission Flow
@@ -347,9 +401,18 @@ The `PUBLISHER.configurePostingCriteriaFor()` call checks `ADJUST_721_TIERS` per
347
401
 
348
402
  After the deployment completes and ownership is transferred to `owner`, only the new owner (or their delegate) can reconfigure posting criteria.
349
403
 
404
+ ### State Changes
405
+
406
+ 1. `CTPublisher._packedAllowanceFor[hook][category]` -- set for each allowed post category.
407
+ 2. `CTPublisher._allowedAddresses[hook][category]` -- set for each allowed post category with allowlists.
408
+
409
+ ### Events
410
+
411
+ - `ConfigurePostingCriteria(address indexed hook, CTAllowedPost allowedPost, address caller)` -- emitted by CTPublisher once per `allowedPosts` entry. The `caller` is CTDeployer's address (since CTDeployer calls the publisher). The `hook` is the newly deployed hook address.
412
+
350
413
  ### Edge Cases
351
414
 
352
- - **Empty `allowedPosts`:** The `_configurePostingCriteriaFor()` call is skipped (line 306 condition). The project has no posting categories configured. Content cannot be posted until the owner configures criteria manually.
415
+ - **Empty `allowedPosts`:** The `_configurePostingCriteriaFor()` call is skipped (line 309 condition). The project has no posting categories configured. Content cannot be posted until the owner configures criteria manually.
353
416
  - **Invalid criteria in deployment:** If any `CTDeployerAllowedPost` has `minimumTotalSupply == 0` or `minimumTotalSupply > maximumTotalSupply`, the publisher reverts, and the entire deployment fails.
354
417
 
355
418
  ---
@@ -357,15 +420,22 @@ After the deployment completes and ownership is transferred to `owner`, only the
357
420
  ## Journey 6: Lock Project Ownership (Burn-Lock)
358
421
 
359
422
  **Actor:** Project owner
360
- **Entry point:** `IERC721(PROJECTS).safeTransferFrom(owner, address(ctProjectOwner), projectId)`
361
- **Source:** `src/CTProjectOwner.sol` lines 47-80
423
+ **Entry point:** `IERC721(PROJECTS).safeTransferFrom(address from, address to, uint256 tokenId)` where `to = address(ctProjectOwner)`
424
+ **Who can call:** The current owner of the project NFT, or an approved operator. The `safeTransferFrom` is an ERC-721 function with standard ownership/approval checks. CTProjectOwner itself has no caller restrictions in `onERC721Received` beyond requiring `msg.sender == address(PROJECTS)`.
425
+ **Source:** `src/CTProjectOwner.sol` lines 50-83
426
+
427
+ ### Parameters
428
+
429
+ - `from` (`address`): Current holder of the project NFT (not checked by CTProjectOwner, unlike CTDeployer).
430
+ - `to` (`address`): Must be `address(ctProjectOwner)`.
431
+ - `tokenId` (`uint256`): The project ID to lock.
362
432
 
363
433
  ### Execution Flow
364
434
 
365
435
  1. The project owner calls `safeTransferFrom` on the JBProjects ERC-721 contract, transferring their project NFT to the CTProjectOwner contract.
366
436
  2. The ERC-721 contract calls `CTProjectOwner.onERC721Received()`.
367
- 3. **Validation** (line 62): `msg.sender == address(PROJECTS)` -- only accepts tokens from the JBProjects contract.
368
- 4. **Permission grant** (lines 65-77):
437
+ 3. **Validation** (line 65): `msg.sender == address(PROJECTS)` -- only accepts tokens from the JBProjects contract. Reverts with empty revert on failure.
438
+ 4. **Permission grant** (lines 68-80):
369
439
  ```
370
440
  PERMISSIONS.setPermissionsFor(
371
441
  account: address(this),
@@ -380,10 +450,12 @@ After the deployment completes and ownership is transferred to `owner`, only the
380
450
 
381
451
  ### State Changes
382
452
 
383
- | Storage | Change |
384
- |---------|--------|
385
- | `JBProjects` (ERC-721) | Token transferred from owner to CTProjectOwner |
386
- | `JBPermissions` | CTPublisher granted `ADJUST_721_TIERS` for this project from CTProjectOwner |
453
+ 1. `JBProjects` (ERC-721) -- token transferred from owner to CTProjectOwner.
454
+ 2. `JBPermissions` -- CTPublisher granted `ADJUST_721_TIERS` for this project from CTProjectOwner's account.
455
+
456
+ ### Events
457
+
458
+ - No events are emitted directly by CTProjectOwner. The ERC-721 `Transfer(from, to, tokenId)` event is emitted by JBProjects. The `JBPermissions.setPermissionsFor()` call emits its own event from the permissions contract.
387
459
 
388
460
  ### Consequences
389
461
 
@@ -405,29 +477,30 @@ After the deployment completes and ownership is transferred to `owner`, only the
405
477
  ## Journey 7: Claim Hook Collection Ownership
406
478
 
407
479
  **Actor:** Project owner
408
- **Entry point:** `CTDeployer.claimCollectionOwnershipOf(hook)`
409
- **Source:** `src/CTDeployer.sol` lines 221-232
480
+ **Entry point:** `CTDeployer.claimCollectionOwnershipOf(IJB721TiersHook hook) external`
481
+ **Who can call:** Only the current owner of the project NFT (`PROJECTS.ownerOf(hook.PROJECT_ID())`). Checked via `PROJECTS.ownerOf(projectId) != _msgSender()` -- reverts with `CTDeployer_NotOwnerOfProject(projectId, address(hook), _msgSender())` on failure.
482
+ **Source:** `src/CTDeployer.sol` lines 224-235
410
483
 
411
484
  ### Parameters
412
485
 
413
- | Parameter | Type | Description |
414
- |-----------|------|-------------|
415
- | `hook` | `IJB721TiersHook` | The 721 hook to claim ownership of |
486
+ - `hook` (`IJB721TiersHook`): The 721 hook to claim ownership of.
416
487
 
417
488
  ### Execution Flow
418
489
 
419
- 1. **Read project ID** (line 223): `projectId = hook.PROJECT_ID()`
420
- 2. **Owner check** (lines 226-228): `PROJECTS.ownerOf(projectId) == _msgSender()` or revert `CTDeployer_NotOwnerOfProject`
421
- 3. **Transfer ownership** (line 231):
490
+ 1. **Read project ID** (line 226): `projectId = hook.PROJECT_ID()`
491
+ 2. **Owner check** (lines 229-231): `PROJECTS.ownerOf(projectId) == _msgSender()` or revert `CTDeployer_NotOwnerOfProject(projectId, address(hook), _msgSender())`
492
+ 3. **Transfer ownership** (line 234):
422
493
  ```
423
494
  JBOwnable(address(hook)).transferOwnershipToProject(projectId)
424
495
  ```
425
496
 
426
497
  ### State Changes
427
498
 
428
- | Storage | Change |
429
- |---------|--------|
430
- | Hook's JBOwnable storage | Owner changed from CTDeployer to the project (ownership tied to project NFT) |
499
+ 1. Hook's `JBOwnable` storage -- owner changed from CTDeployer to the project (ownership tied to project NFT holder via `PROJECTS.ownerOf(projectId)`).
500
+
501
+ ### Events
502
+
503
+ - No events are emitted directly by CTDeployer. The `JBOwnable.transferOwnershipToProject()` call emits its own ownership transfer event from the hook contract.
431
504
 
432
505
  ### Consequences
433
506
 
@@ -446,19 +519,18 @@ After claiming, the hook's ownership follows the project NFT. Whoever owns the p
446
519
  ## Journey 8: Deploy Suckers for Existing Project
447
520
 
448
521
  **Actor:** Project owner (or permissioned delegate)
449
- **Entry point:** `CTDeployer.deploySuckersFor(projectId, suckerDeploymentConfiguration)`
450
- **Source:** `src/CTDeployer.sol` lines 348-367
522
+ **Entry point:** `CTDeployer.deploySuckersFor(uint256 projectId, CTSuckerDeploymentConfig calldata suckerDeploymentConfiguration) external returns (address[] memory suckers)`
523
+ **Who can call:** The project owner (`PROJECTS.ownerOf(projectId)`) or any address that has been granted the `DEPLOY_SUCKERS` permission for that project. Checked via `_requirePermissionFrom(account: PROJECTS.ownerOf(projectId), projectId: projectId, permissionId: JBPermissionIds.DEPLOY_SUCKERS)`.
524
+ **Source:** `src/CTDeployer.sol` lines 354-373
451
525
 
452
526
  ### Parameters
453
527
 
454
- | Parameter | Type | Description |
455
- |-----------|------|-------------|
456
- | `projectId` | `uint256` | The project to deploy suckers for |
457
- | `suckerDeploymentConfiguration` | `CTSuckerDeploymentConfig` | Deployer configs + salt |
528
+ - `projectId` (`uint256`): The project to deploy suckers for.
529
+ - `suckerDeploymentConfiguration` (`CTSuckerDeploymentConfig`): Deployer configs + salt.
458
530
 
459
531
  ### Execution Flow
460
532
 
461
- 1. **Permission check** (lines 356-358):
533
+ 1. **Permission check** (lines 362-364):
462
534
  ```
463
535
  _requirePermissionFrom(
464
536
  account: PROJECTS.ownerOf(projectId),
@@ -467,7 +539,7 @@ After claiming, the hook's ownership follows the project NFT. Whoever owns the p
467
539
  )
468
540
  ```
469
541
 
470
- 2. **Sucker deployment** (lines 362-366):
542
+ 2. **Sucker deployment** (lines 368-372):
471
543
  ```
472
544
  suckers = SUCKER_REGISTRY.deploySuckersFor(
473
545
  projectId,
@@ -478,10 +550,12 @@ After claiming, the hook's ownership follows the project NFT. Whoever owns the p
478
550
 
479
551
  ### State Changes
480
552
 
481
- | Storage | Change |
482
- |---------|--------|
483
- | Sucker Registry | New suckers registered for the project |
484
- | Deployed sucker contracts | New contracts deployed via Create2 |
553
+ 1. Sucker Registry -- new suckers registered for the project.
554
+ 2. Deployed sucker contracts -- new contracts deployed via Create2.
555
+
556
+ ### Events
557
+
558
+ - No events are emitted directly by CTDeployer. The `SUCKER_REGISTRY.deploySuckersFor()` call emits its own events from the registry contract.
485
559
 
486
560
  ### Edge Cases
487
561
 
@@ -494,8 +568,13 @@ After claiming, the hook's ownership follows the project NFT. Whoever owns the p
494
568
  ## Journey 9: Data Hook Interception (Pay)
495
569
 
496
570
  **Actor:** Anyone paying a Croptop-deployed project
497
- **Entry point:** Called by JBMultiTerminal during `pay()` flow
498
- **Source:** `CTDeployer.beforePayRecordedWith(context)` at lines 160-169
571
+ **Entry point:** `CTDeployer.beforePayRecordedWith(JBBeforePayRecordedContext calldata context) external view returns (uint256 weight, JBPayHookSpecification[] memory hookSpecifications)`
572
+ **Who can call:** Intended to be called by JBMultiTerminal during the `pay()` flow. No explicit access control -- any address can call this function, but it is only meaningful when called by the terminal as part of a payment.
573
+ **Source:** `src/CTDeployer.sol` lines 160-169
574
+
575
+ ### Parameters
576
+
577
+ - `context` (`JBBeforePayRecordedContext`): Standard Juicebox payment context containing `projectId`, payer details, amount, and metadata.
499
578
 
500
579
  ### Execution Flow
501
580
 
@@ -503,6 +582,14 @@ After claiming, the hook's ownership follows the project NFT. Whoever owns the p
503
582
  2. CTDeployer forwards the call directly to `dataHookOf[context.projectId]` (line 168), which is the JB721TiersHook.
504
583
  3. The hook returns `(weight, hookSpecifications)` which determine token issuance and pay hook routing.
505
584
 
585
+ ### State Changes
586
+
587
+ - None. This is a `view` function.
588
+
589
+ ### Events
590
+
591
+ - None. This is a `view` function.
592
+
506
593
  ### Edge Cases
507
594
 
508
595
  - **`dataHookOf[projectId]` is `address(0)`:** If a project was somehow created without setting the data hook (not possible via normal deployment flow), the forwarding call reverts on the zero address.
@@ -513,8 +600,13 @@ After claiming, the hook's ownership follows the project NFT. Whoever owns the p
513
600
  ## Journey 10: Data Hook Interception (Cash Out)
514
601
 
515
602
  **Actor:** Token holder cashing out from a Croptop-deployed project
516
- **Entry point:** Called by JBMultiTerminal during `cashOut()` flow
517
- **Source:** `CTDeployer.beforeCashOutRecordedWith(context)` at lines 132-151
603
+ **Entry point:** `CTDeployer.beforeCashOutRecordedWith(JBBeforeCashOutRecordedContext calldata context) external view returns (uint256 cashOutTaxRate, uint256 cashOutCount, uint256 totalSupply, JBCashOutHookSpecification[] memory hookSpecifications)`
604
+ **Who can call:** Intended to be called by JBMultiTerminal during the `cashOut()` flow. No explicit access control -- any address can call this function, but it is only meaningful when called by the terminal as part of a cash-out.
605
+ **Source:** `src/CTDeployer.sol` lines 132-151
606
+
607
+ ### Parameters
608
+
609
+ - `context` (`JBBeforeCashOutRecordedContext`): Standard Juicebox cash-out context containing `projectId`, `holder`, `cashOutCount`, `totalSupply`, and metadata.
518
610
 
519
611
  ### Execution Flow
520
612
 
@@ -522,13 +614,21 @@ After claiming, the hook's ownership follows the project NFT. Whoever owns the p
522
614
 
523
615
  2. **Sucker check** (line 144):
524
616
  ```
525
- if (SUCKER_REGISTRY.isSuckerOf(projectId, context.holder))
617
+ if (SUCKER_REGISTRY.isSuckerOf(context.projectId, context.holder))
526
618
  return (0, context.cashOutCount, context.totalSupply, [])
527
619
  ```
528
620
  If the holder is a registered sucker: return zero tax rate (fee-free cash out). Skip the hook entirely.
529
621
 
530
622
  3. **Normal path** (line 150): Forward to `dataHookOf[context.projectId].beforeCashOutRecordedWith(context)`.
531
623
 
624
+ ### State Changes
625
+
626
+ - None. This is a `view` function.
627
+
628
+ ### Events
629
+
630
+ - None. This is a `view` function.
631
+
532
632
  ### Edge Cases
533
633
 
534
634
  - **Sucker impersonation:** If an attacker can register as a sucker (via compromised registry), they get zero-tax cash outs from any Croptop project.
@@ -541,15 +641,14 @@ After claiming, the hook's ownership follows the project NFT. Whoever owns the p
541
641
  ## Journey 11: Read Posting Allowance
542
642
 
543
643
  **Actor:** Anyone (view function)
544
- **Entry point:** `CTPublisher.allowanceFor(hook, category)`
545
- **Source:** `src/CTPublisher.sol` lines 158-190
644
+ **Entry point:** `CTPublisher.allowanceFor(address hook, uint256 category) public view returns (uint256 minimumPrice, uint256 minimumTotalSupply, uint256 maximumTotalSupply, uint256 maximumSplitPercent, address[] memory allowedAddresses)`
645
+ **Who can call:** Anyone. This is a public view function with no access control.
646
+ **Source:** `src/CTPublisher.sol` lines 161-193
546
647
 
547
648
  ### Parameters
548
649
 
549
- | Parameter | Type | Description |
550
- |-----------|------|-------------|
551
- | `hook` | `address` | The hook contract |
552
- | `category` | `uint256` | The posting category |
650
+ - `hook` (`address`): The hook contract.
651
+ - `category` (`uint256`): The posting category.
553
652
 
554
653
  ### Returns
555
654
 
@@ -561,6 +660,14 @@ After claiming, the hook's ownership follows the project NFT. Whoever owns the p
561
660
  | `maximumSplitPercent` | `uint256` | Extracted from bits 168-199 |
562
661
  | `allowedAddresses` | `address[]` | Full copy of the allowlist array |
563
662
 
663
+ ### State Changes
664
+
665
+ - None. This is a `view` function.
666
+
667
+ ### Events
668
+
669
+ - None. This is a `view` function.
670
+
564
671
  ### Edge Cases
565
672
 
566
673
  - **Unconfigured category:** Returns all zeros and empty array. A `minimumTotalSupply` of 0 means posting is not allowed.
@@ -571,15 +678,14 @@ After claiming, the hook's ownership follows the project NFT. Whoever owns the p
571
678
  ## Journey 12: Look Up Tiers by IPFS URI
572
679
 
573
680
  **Actor:** Anyone (view function)
574
- **Entry point:** `CTPublisher.tiersFor(hook, encodedIPFSUris)`
575
- **Source:** `src/CTPublisher.sol` lines 115-142
681
+ **Entry point:** `CTPublisher.tiersFor(address hook, bytes32[] memory encodedIPFSUris) external view returns (JB721Tier[] memory tiers)`
682
+ **Who can call:** Anyone. This is an external view function with no access control.
683
+ **Source:** `src/CTPublisher.sol` lines 118-145
576
684
 
577
685
  ### Parameters
578
686
 
579
- | Parameter | Type | Description |
580
- |-----------|------|-------------|
581
- | `hook` | `address` | The hook contract |
582
- | `encodedIPFSUris` | `bytes32[]` | Array of encoded IPFS URIs to look up |
687
+ - `hook` (`address`): The hook contract.
688
+ - `encodedIPFSUris` (`bytes32[]`): Array of encoded IPFS URIs to look up.
583
689
 
584
690
  ### Returns
585
691
 
@@ -589,9 +695,17 @@ After claiming, the hook's ownership follows the project NFT. Whoever owns the p
589
695
 
590
696
  For each URI:
591
697
  1. Look up `tierIdForEncodedIPFSUriOf[hook][uri]`
592
- 2. If non-zero, call `hook.STORE().tierOf(hook, tierId, false)` (line 139)
698
+ 2. If non-zero, call `hook.STORE().tierOf(hook, tierId, false)` (line 142)
593
699
  3. If zero, return an empty `JB721Tier`
594
700
 
701
+ ### State Changes
702
+
703
+ - None. This is a `view` function.
704
+
705
+ ### Events
706
+
707
+ - None. This is a `view` function.
708
+
595
709
  ### Edge Cases
596
710
 
597
711
  - **Stale mapping:** If a tier was removed but the mapping was not yet cleared (only cleared on re-post), `tierOf()` may return a tier with `remainingSupply = 0` or the store may revert.