@croptop/core-v6 0.0.20 → 0.0.21
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/ADMINISTRATION.md +41 -27
- package/ARCHITECTURE.md +141 -28
- package/AUDIT_INSTRUCTIONS.md +118 -70
- package/CHANGE_LOG.md +14 -2
- package/README.md +45 -6
- package/RISKS.md +21 -4
- package/SKILLS.md +23 -11
- package/STYLE_GUIDE.md +1 -1
- package/USER_JOURNEYS.md +246 -132
- package/package.json +8 -8
- package/script/ConfigureFeeProject.s.sol +1 -1
- package/script/Deploy.s.sol +1 -1
- package/script/helpers/CroptopDeploymentLib.sol +1 -1
- package/src/CTDeployer.sol +14 -4
- package/src/CTProjectOwner.sol +1 -1
- package/src/CTPublisher.sol +11 -10
- package/src/interfaces/ICTDeployer.sol +2 -2
- package/src/interfaces/ICTPublisher.sol +1 -1
- package/test/CTDeployer.t.sol +15 -8
- package/test/CTProjectOwner.t.sol +1 -1
- package/test/CTPublisher.t.sol +1 -1
- package/test/ClaimCollectionOwnership.t.sol +1 -1
- package/test/CroptopAttacks.t.sol +1 -1
- package/test/TestAuditGaps.sol +16 -10
- package/test/audit/CodexFeeBeneficiaryReentrancy.t.sol +243 -0
- package/test/regression/DuplicateUriFeeEvasion.t.sol +1 -1
- package/test/regression/FeeEvasion.t.sol +1 -1
- package/test/regression/StaleTierIdMapping.t.sol +1 -1
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
|
-
**
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
|
23
|
+
1. **Controller validation** (line 254): Reverts if `controller.PROJECTS() != PROJECTS`.
|
|
25
24
|
|
|
26
|
-
2. **Ruleset configuration** (lines
|
|
25
|
+
2. **Ruleset configuration -- phase 1** (lines 256-258):
|
|
27
26
|
- Weight: `1_000_000 * 10^18`
|
|
28
27
|
- Base currency: ETH
|
|
29
|
-
-
|
|
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
|
|
30
|
+
3. **Project ID prediction** (line 261): `projectId = PROJECTS.count() + 1`
|
|
34
31
|
|
|
35
|
-
4. **Hook deployment** (lines
|
|
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
|
-
-
|
|
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. **
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
65
|
+
10. **Ownership transfer** (line 327):
|
|
66
|
+
```
|
|
67
|
+
PROJECTS.transferFrom(address(this), owner, projectId)
|
|
68
|
+
```
|
|
67
69
|
|
|
68
|
-
|
|
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
|
-
###
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
**
|
|
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
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
|
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
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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
|
-
**
|
|
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
|
-
|
|
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
|
|
248
|
+
1. **Emit event** (line 252): `ConfigurePostingCriteria(hook, allowedPost, caller)`
|
|
216
249
|
|
|
217
|
-
2. **Permission check** (lines
|
|
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
|
|
228
|
-
- `minimumTotalSupply <= maximumTotalSupply` or revert `CTPublisher_MaxTotalSupplyLessThanMin` (line
|
|
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
|
|
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
|
|
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
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
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
|
|
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
|
-
|
|
278
|
-
|
|
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
|
-
**
|
|
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
|
|
337
|
-
3. Calls `PUBLISHER.configurePostingCriteriaFor(formattedAllowedPosts)` (line
|
|
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
|
|
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(
|
|
361
|
-
**
|
|
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
|
|
368
|
-
4. **Permission grant** (lines
|
|
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
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
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
|
-
**
|
|
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
|
-
|
|
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
|
|
420
|
-
2. **Owner check** (lines
|
|
421
|
-
3. **Transfer ownership** (line
|
|
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
|
-
|
|
429
|
-
|
|
430
|
-
|
|
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
|
-
**
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
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:**
|
|
498
|
-
**
|
|
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:**
|
|
517
|
-
**
|
|
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
|
-
**
|
|
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
|
-
|
|
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
|
-
**
|
|
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
|
-
|
|
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
|
|
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.
|