@croptop/core-v6 0.0.26 → 0.0.28
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 +28 -1
- package/ARCHITECTURE.md +48 -157
- package/AUDIT_INSTRUCTIONS.md +69 -485
- package/CHANGELOG.md +57 -0
- package/README.md +48 -137
- package/RISKS.md +18 -1
- package/SKILLS.md +28 -187
- package/STYLE_GUIDE.md +56 -17
- package/USER_JOURNEYS.md +37 -708
- package/package.json +3 -3
- package/references/operations.md +25 -0
- package/references/runtime.md +27 -0
- package/script/ConfigureFeeProject.s.sol +0 -1
- package/src/CTPublisher.sol +10 -7
- package/test/CTPublisher.t.sol +10 -7
- package/test/regression/FeeEvasion.t.sol +15 -10
- package/test/regression/StaleTierIdMapping.t.sol +8 -5
- package/CHANGE_LOG.md +0 -273
package/USER_JOURNEYS.md
CHANGED
|
@@ -1,729 +1,58 @@
|
|
|
1
|
-
#
|
|
1
|
+
# User Journeys
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
## Who This Repo Serves
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
- project owners turning a Juicebox 721 project into a publishing marketplace
|
|
6
|
+
- publishers creating or reusing NFT tiers as posts
|
|
7
|
+
- operators locking project administration into Croptop-specific ownership patterns
|
|
6
8
|
|
|
7
|
-
## Journey 1:
|
|
9
|
+
## Journey 1: Turn A Project Into A Croptop Publisher
|
|
8
10
|
|
|
9
|
-
**
|
|
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
|
|
11
|
+
**Starting state:** a project already exists or is about to launch, and the owner wants category-level posting rules.
|
|
13
12
|
|
|
14
|
-
|
|
13
|
+
**Success:** Croptop posting criteria are installed and future posts must satisfy them.
|
|
15
14
|
|
|
16
|
-
|
|
17
|
-
-
|
|
18
|
-
|
|
19
|
-
|
|
15
|
+
**Flow**
|
|
16
|
+
1. Configure category-level posting constraints such as price floor, supply bounds, split limits, and optional allowlists.
|
|
17
|
+
2. Install or verify the 721 hook shape the project expects.
|
|
18
|
+
3. Route the project through Croptop's publisher logic so future post creation is policy-checked instead of free-form tier editing.
|
|
20
19
|
|
|
21
|
-
|
|
20
|
+
## Journey 2: Publish Content Into An Existing Croptop Project
|
|
22
21
|
|
|
23
|
-
|
|
22
|
+
**Starting state:** a publisher has a post that satisfies the target project's posting rules.
|
|
24
23
|
|
|
25
|
-
|
|
26
|
-
- Weight: `1_000_000 * 10^18`
|
|
27
|
-
- Base currency: ETH
|
|
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).
|
|
24
|
+
**Success:** the post becomes a valid 721 tier and the first mint settles correctly.
|
|
29
25
|
|
|
30
|
-
|
|
26
|
+
**Flow**
|
|
27
|
+
1. The publisher calls `mintFrom(...)` or the equivalent publishing surface with the content URI and pricing data.
|
|
28
|
+
2. `CTPublisher` checks the post against category rules and fee policy.
|
|
29
|
+
3. It creates or reuses the underlying 721 tier, mints the first copy, and routes both project revenue and the Croptop fee.
|
|
31
30
|
|
|
32
|
-
|
|
33
|
-
```
|
|
34
|
-
DEPLOYER.deployHookFor(projectId, config, salt)
|
|
35
|
-
```
|
|
36
|
-
- Salt: `keccak256(abi.encode(projectConfig.salt, _msgSender()))`
|
|
37
|
-
- Deployed with empty tiers, ETH currency, 18 decimals
|
|
38
|
-
- All `false` flags: new tiers with reserves, votes, and owner minting are allowed; overspending is not prevented; tokens are not issued for splits
|
|
31
|
+
**Failure cases that matter:** duplicate URIs, split configurations that evade fees, stale tier mappings, and publisher inputs that satisfy the 721 hook but violate Croptop's stricter publishing rules.
|
|
39
32
|
|
|
40
|
-
|
|
41
|
-
- Cash-out tax rate: `MAX_CASH_OUT_TAX_RATE` (100%)
|
|
42
|
-
- Data hook: `address(this)` (CTDeployer)
|
|
43
|
-
- `useDataHookForPay = true`, `useDataHookForCashOut = true`
|
|
33
|
+
## Journey 3: Launch A New Croptop Project End To End
|
|
44
34
|
|
|
45
|
-
|
|
46
|
-
```
|
|
47
|
-
controller.launchProjectFor(owner: address(this), ...)
|
|
48
|
-
```
|
|
49
|
-
- CTDeployer receives the project NFT temporarily
|
|
50
|
-
- `assert(projectId == returned ID)` -- reverts on mismatch (front-running protection)
|
|
35
|
+
**Starting state:** the product wants a fresh project that already has Croptop deployment choices baked in.
|
|
51
36
|
|
|
52
|
-
|
|
53
|
-
```
|
|
54
|
-
dataHookOf[projectId] = IJBRulesetDataHook(hook)
|
|
55
|
-
```
|
|
56
|
-
This is write-once. No setter exists.
|
|
37
|
+
**Success:** one deployment flow launches the project, wires the 721 hook, and installs the initial posting rules.
|
|
57
38
|
|
|
58
|
-
|
|
39
|
+
**Flow**
|
|
40
|
+
1. Use `CTDeployer` with project config, posting rules, and any omnichain deployment config.
|
|
41
|
+
2. The deployer launches the Juicebox project, configures the Croptop-specific owner model, and wires in publisher behavior.
|
|
42
|
+
3. The project is ready for publishers without a manual post-launch setup phase.
|
|
59
43
|
|
|
60
|
-
|
|
61
|
-
```
|
|
62
|
-
SUCKER_REGISTRY.deploySuckersFor(projectId, salt, configurations)
|
|
63
|
-
```
|
|
44
|
+
## Journey 4: Lock Administration Into Croptop's Owner Surface
|
|
64
45
|
|
|
65
|
-
|
|
66
|
-
```
|
|
67
|
-
PROJECTS.transferFrom(address(this), owner, projectId)
|
|
68
|
-
```
|
|
46
|
+
**Starting state:** the project should continue operating through Croptop's policy surface instead of ordinary project-owner discretion.
|
|
69
47
|
|
|
70
|
-
|
|
71
|
-
- `ADJUST_721_TIERS`
|
|
72
|
-
- `SET_721_METADATA`
|
|
73
|
-
- `MINT_721`
|
|
74
|
-
- `SET_721_DISCOUNT_PERCENT`
|
|
48
|
+
**Success:** the project's admin path is burn-locked or otherwise routed through `CTProjectOwner`.
|
|
75
49
|
|
|
76
|
-
|
|
50
|
+
**Flow**
|
|
51
|
+
1. Transfer or configure ownership so Croptop's owner helper controls the relevant admin surface.
|
|
52
|
+
2. Restrict future edits to the paths Croptop intentionally exposes.
|
|
53
|
+
3. Accept that this is a product-shaping choice, not a cosmetic deployment detail.
|
|
77
54
|
|
|
78
|
-
|
|
55
|
+
## Hand-Offs
|
|
79
56
|
|
|
80
|
-
|
|
81
|
-
|
|
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.
|
|
84
|
-
|
|
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.
|
|
97
|
-
|
|
98
|
-
### Edge Cases
|
|
99
|
-
|
|
100
|
-
- **Front-running:** If another project is created between `PROJECTS.count()` and `launchProjectFor()`, the `assert` fails and the transaction reverts. No funds are lost.
|
|
101
|
-
- **`owner = address(0)`:** The project NFT transfer to `address(0)` would revert (ERC-721 constraint). The deployment fails.
|
|
102
|
-
- **`owner` is a contract without `onERC721Received`:** The `transferFrom` (not `safeTransferFrom`) succeeds even if the owner cannot handle ERC-721s. The project NFT could become stuck.
|
|
103
|
-
- **Sucker deployment failure:** If `deploySuckersFor` reverts, the entire deployment reverts. The sucker deployer uses a try-catch cascade internally, but registry-level reverts propagate.
|
|
104
|
-
- **Empty `terminalConfigurations`:** The project launches with no terminals. Payments and cash-outs are not possible until terminals are added separately.
|
|
105
|
-
- **Salt collision:** If `keccak256(abi.encode(salt, _msgSender()))` collides with a previously deployed hook, `DEPLOYER.deployHookFor()` reverts (Create2 collision).
|
|
106
|
-
|
|
107
|
-
---
|
|
108
|
-
|
|
109
|
-
## Journey 2: Post Content (Mint NFTs)
|
|
110
|
-
|
|
111
|
-
**Actor:** Content poster (any address, or allowlisted address)
|
|
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
|
|
115
|
-
**Value:** Must send `msg.value >= sum(tier prices) + 5% fee`
|
|
116
|
-
|
|
117
|
-
### Parameters
|
|
118
|
-
|
|
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.
|
|
125
|
-
|
|
126
|
-
### Execution Flow
|
|
127
|
-
|
|
128
|
-
**Phase 1: Validation and setup** (`_setupPosts`, lines 442-589)
|
|
129
|
-
|
|
130
|
-
For each post in the batch:
|
|
131
|
-
|
|
132
|
-
1. **URI check:** `encodedIPFSUri != bytes32("")` or revert `CTPublisher_EmptyEncodedIPFSUri`
|
|
133
|
-
2. **Duplicate check:** O(i) scan against all prior posts. Revert `CTPublisher_DuplicatePost` on match.
|
|
134
|
-
3. **Existing tier lookup:** Check `tierIdForEncodedIPFSUriOf[hook][encodedIPFSUri]`
|
|
135
|
-
- **Tier exists and live:** Reuse tier ID. Accumulate `store.tierOf().price`.
|
|
136
|
-
- **Tier exists but removed:** Delete mapping. Fall through to new tier.
|
|
137
|
-
- **No tier:** Validate against category allowance, create `JB721TierConfig`.
|
|
138
|
-
|
|
139
|
-
**Phase 2: Fee calculation** (lines 336-354)
|
|
140
|
-
|
|
141
|
-
```
|
|
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)
|
|
151
|
-
```
|
|
152
|
-
|
|
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)
|
|
156
|
-
|
|
157
|
-
```
|
|
158
|
-
hook.adjustTiers(tiersToAdd, [])
|
|
159
|
-
```
|
|
160
|
-
|
|
161
|
-
**Phase 4: Metadata construction** (lines 361-377)
|
|
162
|
-
|
|
163
|
-
Build JBMetadataResolver-compatible metadata with tier IDs and referral ID.
|
|
164
|
-
|
|
165
|
-
**Phase 5: Project payment** (lines 398-406)
|
|
166
|
-
|
|
167
|
-
```
|
|
168
|
-
projectTerminal.pay{value: payValue}(projectId, NATIVE_TOKEN, payValue, nftBeneficiary, 0, "Minted from Croptop", mintMetadata)
|
|
169
|
-
```
|
|
170
|
-
|
|
171
|
-
**Phase 6: Fee payment** (lines 409-438)
|
|
172
|
-
|
|
173
|
-
```
|
|
174
|
-
payValue = msg.value - payValue // reuse payValue for pre-computed fee amount
|
|
175
|
-
|
|
176
|
-
if (payValue != 0) {
|
|
177
|
-
feeTerminal = DIRECTORY.primaryTerminalOf(FEE_PROJECT_ID, NATIVE_TOKEN)
|
|
178
|
-
try feeTerminal.pay{value: payValue}(FEE_PROJECT_ID, ...) {}
|
|
179
|
-
catch {
|
|
180
|
-
// Fallback: send fee to feeBeneficiary, then msg.sender
|
|
181
|
-
feeBeneficiary.call{value: payValue}("")
|
|
182
|
-
// If that fails too, msg.sender.call{value: payValue}("")
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
```
|
|
186
|
-
|
|
187
|
-
### State Changes
|
|
188
|
-
|
|
189
|
-
1. `CTPublisher.tierIdForEncodedIPFSUriOf[hook][uri]` -- set for each new tier created.
|
|
190
|
-
2. `CTPublisher.tierIdForEncodedIPFSUriOf[hook][uri]` -- deleted if stale mapping detected (removed tier).
|
|
191
|
-
3. `JB721TiersHookStore` (external) -- new tiers added via `adjustTiers`.
|
|
192
|
-
4. Project terminal (external) -- balance increased by `payValue`.
|
|
193
|
-
5. Fee project terminal (external) -- balance increased by fee amount.
|
|
194
|
-
|
|
195
|
-
### Events
|
|
196
|
-
|
|
197
|
-
- `Mint(projectId, hook, nftBeneficiary, feeBeneficiary, posts, postValue, txValue, caller)` -- emitted at line 380 after setup is complete, before the project payment. Full signature:
|
|
198
|
-
```solidity
|
|
199
|
-
event Mint(
|
|
200
|
-
uint256 indexed projectId,
|
|
201
|
-
IJB721TiersHook indexed hook,
|
|
202
|
-
address indexed nftBeneficiary,
|
|
203
|
-
address feeBeneficiary,
|
|
204
|
-
CTPost[] posts,
|
|
205
|
-
uint256 postValue,
|
|
206
|
-
uint256 txValue,
|
|
207
|
-
address caller
|
|
208
|
-
);
|
|
209
|
-
```
|
|
210
|
-
|
|
211
|
-
### Edge Cases
|
|
212
|
-
|
|
213
|
-
- **Empty posts array:** `_setupPosts` returns with `totalPrice = 0`, `tiersToAdd` and `tierIdsToMint` both empty. `adjustTiers` is called with an empty array (no-op). The project terminal receives `msg.value` (no fee deducted since `totalPrice / 20 = 0`). The fee terminal receives nothing.
|
|
214
|
-
- **All posts reuse existing tiers:** No new tiers are created. `tiersToAdd` is resized to length 0 via assembly. `adjustTiers` is a no-op. Fees are calculated from on-chain tier prices.
|
|
215
|
-
- **Mixed new and existing tiers:** `tiersToAdd` is resized via assembly to contain only new tiers. `tierIdsToMint` contains a mix of new and existing IDs.
|
|
216
|
-
- **`msg.value` exceeds required amount:** Excess ETH goes to the fee project via the pre-computed fee (`msg.value - payValue`). The poster overpays the fee project.
|
|
217
|
-
- **`msg.value` is exactly right:** The pre-computed fee amount equals the expected fee. Fee project receives the correct amount.
|
|
218
|
-
- **`projectId == FEE_PROJECT_ID`:** No fee is deducted. Full `msg.value` goes to the project terminal. The pre-computed fee is 0 (no fee payment occurs).
|
|
219
|
-
- **Tier price is 0:** Posting criteria allow `minimumPrice = 0`. A post with `price = 0` creates a free tier. Fee on a free tier is 0. The poster sends 0 ETH (or only ETH for other posts in the batch).
|
|
220
|
-
- **`nftBeneficiary = address(0)`:** The terminal payment may succeed (depending on terminal implementation), but the NFTs would be minted to `address(0)`, effectively burning them.
|
|
221
|
-
- **`hook.adjustTiers()` reverts:** The entire transaction reverts. No state changes are committed. The poster's ETH is returned.
|
|
222
|
-
- **Terminal payment reverts:** The entire transaction reverts. The `adjustTiers` state change is also rolled back.
|
|
223
|
-
- **Fee terminal payment reverts:** This occurs after the project payment has already succeeded. If the fee terminal reverts, the entire transaction reverts, undoing the project payment too.
|
|
224
|
-
- **Batch with posts in different categories:** Each post is validated against its own category's allowance independently. A batch can contain posts in multiple categories.
|
|
225
|
-
- **Re-posting a removed tier's URI:** The stale mapping is cleared and a new tier is created. The new tier may have different price/supply/splits than the original.
|
|
226
|
-
|
|
227
|
-
---
|
|
228
|
-
|
|
229
|
-
## Journey 3: Configure Posting Criteria (Allowlist Setup)
|
|
230
|
-
|
|
231
|
-
**Actor:** Hook owner (or permissioned delegate)
|
|
232
|
-
**Entry point:** `CTPublisher.configurePostingCriteriaFor(CTAllowedPost[] memory allowedPosts) external`
|
|
233
|
-
**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)`.
|
|
234
|
-
**Source:** `src/CTPublisher.sol` lines 243-298
|
|
235
|
-
|
|
236
|
-
### Parameters
|
|
237
|
-
|
|
238
|
-
- `allowedPosts` (`CTAllowedPost[]`): Array of per-category posting rules.
|
|
239
|
-
|
|
240
|
-
Each `CTAllowedPost` contains:
|
|
241
|
-
|
|
242
|
-
| Field | Type | Constraints |
|
|
243
|
-
|-------|------|-------------|
|
|
244
|
-
| `hook` | `address` | Must be a JBOwnable + IJB721TiersHook |
|
|
245
|
-
| `category` | `uint24` | 0 to 16,777,215 |
|
|
246
|
-
| `minimumPrice` | `uint104` | Must fit in 104 bits |
|
|
247
|
-
| `minimumTotalSupply` | `uint32` | Must be > 0 |
|
|
248
|
-
| `maximumTotalSupply` | `uint32` | Must be >= `minimumTotalSupply` |
|
|
249
|
-
| `maximumSplitPercent` | `uint32` | 0 = splits disabled, up to `SPLITS_TOTAL_PERCENT` (1,000,000,000) |
|
|
250
|
-
| `allowedAddresses` | `address[]` | Empty = permissionless, non-empty = restricted |
|
|
251
|
-
|
|
252
|
-
### Execution Flow
|
|
253
|
-
|
|
254
|
-
For each `CTAllowedPost` in the array:
|
|
255
|
-
|
|
256
|
-
1. **Emit event** (line 252): `ConfigurePostingCriteria(hook, allowedPost, caller)`
|
|
257
|
-
|
|
258
|
-
2. **Permission check** (lines 256-260):
|
|
259
|
-
```
|
|
260
|
-
_requirePermissionFrom(
|
|
261
|
-
account: JBOwnable(hook).owner(),
|
|
262
|
-
projectId: IJB721TiersHook(hook).PROJECT_ID(),
|
|
263
|
-
permissionId: JBPermissionIds.ADJUST_721_TIERS
|
|
264
|
-
)
|
|
265
|
-
```
|
|
266
|
-
|
|
267
|
-
3. **Validation:**
|
|
268
|
-
- `minimumTotalSupply > 0` or revert `CTPublisher_ZeroTotalSupply` (line 263)
|
|
269
|
-
- `minimumTotalSupply <= maximumTotalSupply` or revert `CTPublisher_MaxTotalSupplyLessThanMin` (line 268)
|
|
270
|
-
|
|
271
|
-
4. **Pack and store** (lines 274-284):
|
|
272
|
-
```
|
|
273
|
-
packed = minimumPrice | (minimumTotalSupply << 104) | (maximumTotalSupply << 136) | (maximumSplitPercent << 168)
|
|
274
|
-
_packedAllowanceFor[hook][category] = packed
|
|
275
|
-
```
|
|
276
|
-
|
|
277
|
-
5. **Allowlist storage** (lines 287-296):
|
|
278
|
-
```
|
|
279
|
-
delete _allowedAddresses[hook][category]
|
|
280
|
-
for each address in allowedAddresses:
|
|
281
|
-
_allowedAddresses[hook][category].push(address)
|
|
282
|
-
```
|
|
283
|
-
|
|
284
|
-
### State Changes
|
|
285
|
-
|
|
286
|
-
1. `CTPublisher._packedAllowanceFor[hook][category]` -- overwritten with new packed values.
|
|
287
|
-
2. `CTPublisher._allowedAddresses[hook][category]` -- deleted and repopulated.
|
|
288
|
-
|
|
289
|
-
### Events
|
|
290
|
-
|
|
291
|
-
- `ConfigurePostingCriteria(address indexed hook, CTAllowedPost allowedPost, address caller)` -- emitted once per entry in the `allowedPosts` array (line 252), **before** the permission check. Full signature:
|
|
292
|
-
```solidity
|
|
293
|
-
event ConfigurePostingCriteria(address indexed hook, CTAllowedPost allowedPost, address caller);
|
|
294
|
-
```
|
|
295
|
-
Note: the event is emitted before `_requirePermissionFrom`, so an unauthorized call will emit the event then revert, rolling back the event emission.
|
|
296
|
-
|
|
297
|
-
### Edge Cases
|
|
298
|
-
|
|
299
|
-
- **Overwriting existing criteria:** The entire packed value and allowlist are replaced. There is no merge or append behavior.
|
|
300
|
-
- **Multiple categories in one call:** Each `CTAllowedPost` can target a different hook/category pair. A single call can configure multiple categories across multiple hooks (provided the caller has permission for each).
|
|
301
|
-
- **Same category twice in one call:** The second entry overwrites the first. No duplicate check on the input array.
|
|
302
|
-
- **`maximumSplitPercent = 0`:** Splits are disabled. Any post with `splitPercent > 0` will revert with `CTPublisher_SplitPercentExceedsMaximum`.
|
|
303
|
-
- **`maximumTotalSupply = type(uint32).max`:** Effectively unlimited supply (4,294,967,295).
|
|
304
|
-
- **Large allowlist:** Stored via a push loop. A 1,000-address allowlist costs approximately 20M gas for the SSTORE operations. A 10,000-address list is infeasible in a single transaction.
|
|
305
|
-
- **Empty allowlist after previously non-empty:** `delete _allowedAddresses[hook][category]` clears the array. Posting becomes permissionless for that category.
|
|
306
|
-
- **Cannot disable category:** There is no way to set `minimumTotalSupply = 0` (it reverts). To effectively disable a category, set `minimumPrice = type(uint104).max` and `minimumTotalSupply = maximumTotalSupply = 1`. This makes posting economically infeasible.
|
|
307
|
-
- **Permission check uses `hook.owner()`:** If hook ownership has been transferred (e.g., to the project via `claimCollectionOwnershipOf`), the new owner (or their delegate) must call this function.
|
|
308
|
-
- **ERC2771 context:** `_msgSender()` is used for the permission check. If a trusted forwarder relays the call, the original sender (appended to calldata) is checked against permissions.
|
|
309
|
-
|
|
310
|
-
---
|
|
311
|
-
|
|
312
|
-
## Journey 4: Collect Posting Fees
|
|
313
|
-
|
|
314
|
-
**Actor:** Passive (fee project). Fees are collected automatically during `mintFrom()`.
|
|
315
|
-
**Entry point:** Triggered within `CTPublisher.mintFrom()` at lines 413-429
|
|
316
|
-
**Who can call:** N/A -- this is an internal sub-flow of Journey 2, not independently callable.
|
|
317
|
-
**Beneficiary:** The project with ID `FEE_PROJECT_ID` (immutable, set at construction)
|
|
318
|
-
|
|
319
|
-
### Fee Calculation
|
|
320
|
-
|
|
321
|
-
```
|
|
322
|
-
totalPrice = sum of all post prices in the batch
|
|
323
|
-
(on-chain tier price for existing tiers, post.price for new tiers)
|
|
324
|
-
|
|
325
|
-
payValue = msg.value
|
|
326
|
-
|
|
327
|
-
if (projectId != FEE_PROJECT_ID) {
|
|
328
|
-
fee = totalPrice / FEE_DIVISOR (FEE_DIVISOR = 20, so fee = 5%)
|
|
329
|
-
require(payValue >= fee) (reverts if msg.value < fee)
|
|
330
|
-
payValue -= fee (fee held in contract balance for later routing)
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
require(totalPrice <= payValue) (reverts if insufficient ETH for the posts)
|
|
334
|
-
```
|
|
335
|
-
|
|
336
|
-
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.
|
|
337
|
-
|
|
338
|
-
### Fee Routing
|
|
339
|
-
|
|
340
|
-
After the project payment completes, the fee amount is pre-computed as `msg.value - payValue`:
|
|
341
|
-
|
|
342
|
-
```solidity
|
|
343
|
-
payValue = msg.value - payValue; // reuse payValue for the pre-computed fee amount
|
|
344
|
-
|
|
345
|
-
if (payValue != 0) {
|
|
346
|
-
IJBTerminal feeTerminal = DIRECTORY.primaryTerminalOf(FEE_PROJECT_ID, NATIVE_TOKEN);
|
|
347
|
-
try feeTerminal.pay{value: payValue}({
|
|
348
|
-
projectId: FEE_PROJECT_ID,
|
|
349
|
-
amount: payValue,
|
|
350
|
-
token: NATIVE_TOKEN,
|
|
351
|
-
beneficiary: feeBeneficiary,
|
|
352
|
-
minReturnedTokens: 0,
|
|
353
|
-
memo: "",
|
|
354
|
-
metadata: feeMetadata
|
|
355
|
-
}) {}
|
|
356
|
-
catch {
|
|
357
|
-
// Fallback: send fee to feeBeneficiary, then msg.sender if that fails too.
|
|
358
|
-
(bool success,) = feeBeneficiary.call{value: payValue}("");
|
|
359
|
-
if (!success) {
|
|
360
|
-
(success,) = msg.sender.call{value: payValue}("");
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
}
|
|
364
|
-
```
|
|
365
|
-
|
|
366
|
-
### Fee Project Token Distribution
|
|
367
|
-
|
|
368
|
-
The `feeBeneficiary` parameter in `mintFrom()` determines who receives the fee project's tokens minted from the fee payment. The `feeMetadata` parameter allows the caller to pass arbitrary metadata to the fee payment (e.g., for data hooks on the fee project).
|
|
369
|
-
|
|
370
|
-
### Fee Accounting Details
|
|
371
|
-
|
|
372
|
-
| Scenario | Fee Behavior |
|
|
373
|
-
|----------|-------------|
|
|
374
|
-
| Normal mint (1 post, 1 ETH price) | `fee = 1 ether / 20 = 0.05 ether`. Poster sends >= 1.05 ETH. |
|
|
375
|
-
| Batch mint (3 posts, 1 ETH each) | `fee = 3 ether / 20 = 0.15 ether`. Poster sends >= 3.15 ETH. |
|
|
376
|
-
| Existing tier reuse (1 ETH on-chain price) | Fee uses on-chain price, not `post.price`. `fee = 1 ether / 20`. |
|
|
377
|
-
| Free tier (price = 0) | `fee = 0 / 20 = 0`. No fee payment occurs. |
|
|
378
|
-
| `projectId == FEE_PROJECT_ID` | Fee deduction skipped entirely. Full `msg.value` goes to project. |
|
|
379
|
-
| `msg.value` exceeds requirement | Excess goes to fee project. Poster overpays. |
|
|
380
|
-
| Dust from integer division | Up to 19 wei lost per tx. Fee project receives slightly less. |
|
|
381
|
-
|
|
382
|
-
### Events
|
|
383
|
-
|
|
384
|
-
- 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.
|
|
385
|
-
|
|
386
|
-
### Edge Cases
|
|
387
|
-
|
|
388
|
-
- **Fee project has no primary terminal:** `DIRECTORY.primaryTerminalOf()` returns `address(0)`. The `pay()` call to address(0) reverts inside the try-catch. The fee falls back to `feeBeneficiary.call{value}`, then `msg.sender.call{value}`. The main project payment is not affected.
|
|
389
|
-
- **Fee terminal reverts:** The fee terminal payment is wrapped in try-catch. If the fee terminal reverts, the fee is sent to `feeBeneficiary` via low-level call. If that also fails, the fee is sent to `msg.sender`. The main project payment is never rolled back due to a fee terminal failure. The fee project loses revenue during the outage.
|
|
390
|
-
- **Pre-computed fee is 0:** This happens when `fee == 0` (e.g., `totalPrice < FEE_DIVISOR` or `projectId == FEE_PROJECT_ID`). The fee payment is skipped entirely.
|
|
391
|
-
- **Force-sent ETH (via `selfdestruct`):** The fee amount is now pre-computed as `msg.value - payValue` rather than using `address(this).balance`. Force-sent ETH does not affect fee calculation or routing. CTPublisher has no `receive()` or `fallback()`, so normal sends revert. Only `selfdestruct` (deprecated post-Dencun) can force-send ETH, and it remains in the contract.
|
|
392
|
-
|
|
393
|
-
---
|
|
394
|
-
|
|
395
|
-
## Journey 5: Deploy a Croptop Project via CTDeployer with Posting Criteria
|
|
396
|
-
|
|
397
|
-
**Actor:** Project creator
|
|
398
|
-
**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`
|
|
399
|
-
**Who can call:** Anyone. No access control on this function. Same as Journey 1.
|
|
400
|
-
**Source:** `src/CTDeployer.sol` lines 244-348, internal `_configurePostingCriteriaFor()` at lines 382-411
|
|
401
|
-
|
|
402
|
-
This is an extension of Journey 1 that details the posting criteria configuration during deployment.
|
|
403
|
-
|
|
404
|
-
### Posting Criteria Flow
|
|
405
|
-
|
|
406
|
-
1. CTDeployer receives `CTDeployerAllowedPost[]` (which omits the `hook` field, since the hook hasn't been deployed yet).
|
|
407
|
-
2. After the hook is deployed, `_configurePostingCriteriaFor()` converts each `CTDeployerAllowedPost` to a `CTAllowedPost` by injecting `hook: address(hook)` (lines 398-406).
|
|
408
|
-
3. Calls `PUBLISHER.configurePostingCriteriaFor(formattedAllowedPosts)` (line 410).
|
|
409
|
-
4. The publisher validates each entry (supply bounds, permissions) and stores the packed allowances and allowlists.
|
|
410
|
-
|
|
411
|
-
### Permission Flow
|
|
412
|
-
|
|
413
|
-
The `PUBLISHER.configurePostingCriteriaFor()` call checks `ADJUST_721_TIERS` permission from `JBOwnable(hook).owner()`. At this point in the deployment:
|
|
414
|
-
|
|
415
|
-
- The hook was deployed by `DEPLOYER.deployHookFor()` on behalf of CTDeployer
|
|
416
|
-
- Hook ownership is set to CTDeployer (the deployer is the effective owner after deployment)
|
|
417
|
-
- The permission check passes because CTDeployer is both the caller and the hook owner (or has wildcard permission)
|
|
418
|
-
|
|
419
|
-
After the deployment completes and ownership is transferred to `owner`, only the new owner (or their delegate) can reconfigure posting criteria.
|
|
420
|
-
|
|
421
|
-
### State Changes
|
|
422
|
-
|
|
423
|
-
1. `CTPublisher._packedAllowanceFor[hook][category]` -- set for each allowed post category.
|
|
424
|
-
2. `CTPublisher._allowedAddresses[hook][category]` -- set for each allowed post category with allowlists.
|
|
425
|
-
|
|
426
|
-
### Events
|
|
427
|
-
|
|
428
|
-
- `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.
|
|
429
|
-
|
|
430
|
-
### Edge Cases
|
|
431
|
-
|
|
432
|
-
- **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.
|
|
433
|
-
- **Invalid criteria in deployment:** If any `CTDeployerAllowedPost` has `minimumTotalSupply == 0` or `minimumTotalSupply > maximumTotalSupply`, the publisher reverts, and the entire deployment fails.
|
|
434
|
-
|
|
435
|
-
---
|
|
436
|
-
|
|
437
|
-
## Journey 6: Lock Project Ownership (Burn-Lock)
|
|
438
|
-
|
|
439
|
-
**Actor:** Project owner
|
|
440
|
-
**Entry point:** `IERC721(PROJECTS).safeTransferFrom(address from, address to, uint256 tokenId)` where `to = address(ctProjectOwner)`
|
|
441
|
-
**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)`.
|
|
442
|
-
**Source:** `src/CTProjectOwner.sol` lines 50-83
|
|
443
|
-
|
|
444
|
-
### Parameters
|
|
445
|
-
|
|
446
|
-
- `from` (`address`): Current holder of the project NFT (not checked by CTProjectOwner, unlike CTDeployer).
|
|
447
|
-
- `to` (`address`): Must be `address(ctProjectOwner)`.
|
|
448
|
-
- `tokenId` (`uint256`): The project ID to lock.
|
|
449
|
-
|
|
450
|
-
### Execution Flow
|
|
451
|
-
|
|
452
|
-
1. The project owner calls `safeTransferFrom` on the JBProjects ERC-721 contract, transferring their project NFT to the CTProjectOwner contract.
|
|
453
|
-
2. The ERC-721 contract calls `CTProjectOwner.onERC721Received()`.
|
|
454
|
-
3. **Validation** (line 65): `msg.sender == address(PROJECTS)` -- only accepts tokens from the JBProjects contract. Reverts with empty revert on failure.
|
|
455
|
-
4. **Permission grant** (lines 68-80):
|
|
456
|
-
```
|
|
457
|
-
PERMISSIONS.setPermissionsFor(
|
|
458
|
-
account: address(this),
|
|
459
|
-
permissionsData: JBPermissionsData({
|
|
460
|
-
operator: address(PUBLISHER),
|
|
461
|
-
projectId: uint64(tokenId),
|
|
462
|
-
permissionIds: [ADJUST_721_TIERS]
|
|
463
|
-
})
|
|
464
|
-
)
|
|
465
|
-
```
|
|
466
|
-
5. Returns `IERC721Receiver.onERC721Received.selector`.
|
|
467
|
-
|
|
468
|
-
### State Changes
|
|
469
|
-
|
|
470
|
-
1. `JBProjects` (ERC-721) -- token transferred from owner to CTProjectOwner.
|
|
471
|
-
2. `JBPermissions` -- CTPublisher granted `ADJUST_721_TIERS` for this project from CTProjectOwner's account.
|
|
472
|
-
|
|
473
|
-
### Events
|
|
474
|
-
|
|
475
|
-
- 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.
|
|
476
|
-
|
|
477
|
-
### Consequences
|
|
478
|
-
|
|
479
|
-
- The project NFT is now held by CTProjectOwner. Since CTProjectOwner has no transfer function, ownership is effectively burned.
|
|
480
|
-
- CTPublisher can still adjust tiers (post content) because it has `ADJUST_721_TIERS` permission.
|
|
481
|
-
- The project owner can no longer change rulesets, add terminals, or perform any owner-only operations.
|
|
482
|
-
- This is irreversible. There is no recovery mechanism.
|
|
483
|
-
|
|
484
|
-
### Edge Cases
|
|
485
|
-
|
|
486
|
-
- **`from != address(0)`:** Unlike CTDeployer, CTProjectOwner does NOT check `from != address(0)`. It accepts both mints and transfers. Any project holder can transfer their project to CTProjectOwner.
|
|
487
|
-
- **`tokenId` truncation:** The `uint64(tokenId)` cast truncates if `tokenId > type(uint64).max`. For realistic sequential project IDs, this is not a concern.
|
|
488
|
-
- **Transfer vs. `safeTransferFrom`:** Only `safeTransferFrom` triggers `onERC721Received`. A raw `transferFrom` would transfer the NFT without granting the publisher permission, leaving the project locked without posting capability.
|
|
489
|
-
- **Double transfer:** If two different project NFTs are transferred to CTProjectOwner, each gets its own scoped permission. The contract can hold multiple projects simultaneously.
|
|
490
|
-
- **Accidental transfer:** There is no confirmation, cooling period, or undo. A user who accidentally sends their project NFT to CTProjectOwner loses ownership permanently.
|
|
491
|
-
|
|
492
|
-
---
|
|
493
|
-
|
|
494
|
-
## Journey 7: Claim Hook Collection Ownership
|
|
495
|
-
|
|
496
|
-
**Actor:** Project owner
|
|
497
|
-
**Entry point:** `CTDeployer.claimCollectionOwnershipOf(IJB721TiersHook hook) external`
|
|
498
|
-
**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.
|
|
499
|
-
**Source:** `src/CTDeployer.sol` lines 224-235
|
|
500
|
-
|
|
501
|
-
### Parameters
|
|
502
|
-
|
|
503
|
-
- `hook` (`IJB721TiersHook`): The 721 hook to claim ownership of.
|
|
504
|
-
|
|
505
|
-
### Execution Flow
|
|
506
|
-
|
|
507
|
-
1. **Read project ID** (line 226): `projectId = hook.PROJECT_ID()`
|
|
508
|
-
2. **Owner check** (lines 229-231): `PROJECTS.ownerOf(projectId) == _msgSender()` or revert `CTDeployer_NotOwnerOfProject(projectId, address(hook), _msgSender())`
|
|
509
|
-
3. **Transfer ownership** (line 234):
|
|
510
|
-
```
|
|
511
|
-
JBOwnable(address(hook)).transferOwnershipToProject(projectId)
|
|
512
|
-
```
|
|
513
|
-
|
|
514
|
-
### State Changes
|
|
515
|
-
|
|
516
|
-
1. Hook's `JBOwnable` storage -- owner changed from CTDeployer to the project (ownership tied to project NFT holder via `PROJECTS.ownerOf(projectId)`).
|
|
517
|
-
|
|
518
|
-
### Events
|
|
519
|
-
|
|
520
|
-
- No events are emitted directly by CTDeployer. The `JBOwnable.transferOwnershipToProject()` call emits its own ownership transfer event from the hook contract.
|
|
521
|
-
|
|
522
|
-
### Consequences
|
|
523
|
-
|
|
524
|
-
After claiming, the hook's ownership follows the project NFT. Whoever owns the project NFT can call owner-only functions on the hook (tier adjustments, metadata changes, etc.) directly, without going through CTDeployer.
|
|
525
|
-
|
|
526
|
-
**Important:** After claiming, the project owner must grant CTPublisher the `ADJUST_721_TIERS` permission for the project so that `mintFrom()` continues to work. Without this permission grant, all subsequent posts will revert.
|
|
527
|
-
|
|
528
|
-
### Edge Cases
|
|
529
|
-
|
|
530
|
-
- **Already claimed:** If hook ownership has already been transferred, `transferOwnershipToProject` may revert (depending on JBOwnable implementation). CTDeployer is no longer the owner.
|
|
531
|
-
- **Project transferred after deployment:** If the project was sold or transferred, the new owner can claim the hook. The original deployer cannot.
|
|
532
|
-
- **Hook not deployed by CTDeployer:** If the `hook` was deployed independently, `JBOwnable(hook).transferOwnershipToProject()` will revert because CTDeployer is not the owner.
|
|
533
|
-
|
|
534
|
-
---
|
|
535
|
-
|
|
536
|
-
## Journey 8: Deploy Suckers for Existing Project
|
|
537
|
-
|
|
538
|
-
**Actor:** Project owner (or permissioned delegate)
|
|
539
|
-
**Entry point:** `CTDeployer.deploySuckersFor(uint256 projectId, CTSuckerDeploymentConfig calldata suckerDeploymentConfiguration) external returns (address[] memory suckers)`
|
|
540
|
-
**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)`.
|
|
541
|
-
**Source:** `src/CTDeployer.sol` lines 354-373
|
|
542
|
-
|
|
543
|
-
### Parameters
|
|
544
|
-
|
|
545
|
-
- `projectId` (`uint256`): The project to deploy suckers for.
|
|
546
|
-
- `suckerDeploymentConfiguration` (`CTSuckerDeploymentConfig`): Deployer configs + salt.
|
|
547
|
-
|
|
548
|
-
### Execution Flow
|
|
549
|
-
|
|
550
|
-
1. **Permission check** (lines 362-364):
|
|
551
|
-
```
|
|
552
|
-
_requirePermissionFrom(
|
|
553
|
-
account: PROJECTS.ownerOf(projectId),
|
|
554
|
-
projectId: projectId,
|
|
555
|
-
permissionId: JBPermissionIds.DEPLOY_SUCKERS
|
|
556
|
-
)
|
|
557
|
-
```
|
|
558
|
-
|
|
559
|
-
2. **Sucker deployment** (lines 368-372):
|
|
560
|
-
```
|
|
561
|
-
suckers = SUCKER_REGISTRY.deploySuckersFor(
|
|
562
|
-
projectId,
|
|
563
|
-
keccak256(abi.encode(suckerDeploymentConfiguration.salt, _msgSender())),
|
|
564
|
-
suckerDeploymentConfiguration.deployerConfigurations
|
|
565
|
-
)
|
|
566
|
-
```
|
|
567
|
-
|
|
568
|
-
### State Changes
|
|
569
|
-
|
|
570
|
-
1. Sucker Registry -- new suckers registered for the project.
|
|
571
|
-
2. Deployed sucker contracts -- new contracts deployed via Create2.
|
|
572
|
-
|
|
573
|
-
### Events
|
|
574
|
-
|
|
575
|
-
- No events are emitted directly by CTDeployer. The `SUCKER_REGISTRY.deploySuckersFor()` call emits its own events from the registry contract.
|
|
576
|
-
|
|
577
|
-
### Edge Cases
|
|
578
|
-
|
|
579
|
-
- **Permission not granted:** Reverts. The project owner must explicitly grant `DEPLOY_SUCKERS` to the caller, or the caller must be the project owner.
|
|
580
|
-
- **Salt collision:** If the computed salt matches a previously deployed sucker, the Create2 deployment reverts.
|
|
581
|
-
- **Empty `deployerConfigurations`:** The sucker registry call succeeds with zero suckers deployed.
|
|
582
|
-
|
|
583
|
-
---
|
|
584
|
-
|
|
585
|
-
## Journey 9: Data Hook Interception (Pay)
|
|
586
|
-
|
|
587
|
-
**Actor:** Anyone paying a Croptop-deployed project
|
|
588
|
-
**Entry point:** `CTDeployer.beforePayRecordedWith(JBBeforePayRecordedContext calldata context) external view returns (uint256 weight, JBPayHookSpecification[] memory hookSpecifications)`
|
|
589
|
-
**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.
|
|
590
|
-
**Source:** `src/CTDeployer.sol` lines 160-169
|
|
591
|
-
|
|
592
|
-
### Parameters
|
|
593
|
-
|
|
594
|
-
- `context` (`JBBeforePayRecordedContext`): Standard Juicebox payment context containing `projectId`, payer details, amount, and metadata.
|
|
595
|
-
|
|
596
|
-
### Execution Flow
|
|
597
|
-
|
|
598
|
-
1. JBMultiTerminal calls `CTDeployer.beforePayRecordedWith(context)` because CTDeployer is registered as the project's data hook.
|
|
599
|
-
2. CTDeployer forwards the call directly to `dataHookOf[context.projectId]` (line 168), which is the JB721TiersHook.
|
|
600
|
-
3. The hook returns `(weight, hookSpecifications)` which determine token issuance and pay hook routing.
|
|
601
|
-
|
|
602
|
-
### State Changes
|
|
603
|
-
|
|
604
|
-
- None. This is a `view` function.
|
|
605
|
-
|
|
606
|
-
### Events
|
|
607
|
-
|
|
608
|
-
- None. This is a `view` function.
|
|
609
|
-
|
|
610
|
-
### Edge Cases
|
|
611
|
-
|
|
612
|
-
- **`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.
|
|
613
|
-
- **Hook reverts:** The entire `pay()` call reverts. The payer's ETH is returned. This can cause permanent DoS for a project if the hook is in a broken state.
|
|
614
|
-
|
|
615
|
-
---
|
|
616
|
-
|
|
617
|
-
## Journey 10: Data Hook Interception (Cash Out)
|
|
618
|
-
|
|
619
|
-
**Actor:** Token holder cashing out from a Croptop-deployed project
|
|
620
|
-
**Entry point:** `CTDeployer.beforeCashOutRecordedWith(JBBeforeCashOutRecordedContext calldata context) external view returns (uint256 cashOutTaxRate, uint256 cashOutCount, uint256 totalSupply, JBCashOutHookSpecification[] memory hookSpecifications)`
|
|
621
|
-
**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.
|
|
622
|
-
**Source:** `src/CTDeployer.sol` lines 132-151
|
|
623
|
-
|
|
624
|
-
### Parameters
|
|
625
|
-
|
|
626
|
-
- `context` (`JBBeforeCashOutRecordedContext`): Standard Juicebox cash-out context containing `projectId`, `holder`, `cashOutCount`, `totalSupply`, and metadata.
|
|
627
|
-
|
|
628
|
-
### Execution Flow
|
|
629
|
-
|
|
630
|
-
1. JBMultiTerminal calls `CTDeployer.beforeCashOutRecordedWith(context)`.
|
|
631
|
-
|
|
632
|
-
2. **Sucker check** (line 144):
|
|
633
|
-
```
|
|
634
|
-
if (SUCKER_REGISTRY.isSuckerOf(context.projectId, context.holder))
|
|
635
|
-
return (0, context.cashOutCount, context.totalSupply, [])
|
|
636
|
-
```
|
|
637
|
-
If the holder is a registered sucker: return zero tax rate (fee-free cash out). Skip the hook entirely.
|
|
638
|
-
|
|
639
|
-
3. **Normal path** (line 150): Forward to `dataHookOf[context.projectId].beforeCashOutRecordedWith(context)`.
|
|
640
|
-
|
|
641
|
-
### State Changes
|
|
642
|
-
|
|
643
|
-
- None. This is a `view` function.
|
|
644
|
-
|
|
645
|
-
### Events
|
|
646
|
-
|
|
647
|
-
- None. This is a `view` function.
|
|
648
|
-
|
|
649
|
-
### Edge Cases
|
|
650
|
-
|
|
651
|
-
- **Sucker impersonation:** If an attacker can register as a sucker (via compromised registry), they get zero-tax cash outs from any Croptop project.
|
|
652
|
-
- **Sucker registry reverts:** If `isSuckerOf()` reverts, the entire cash-out reverts. This could DoS cash-outs.
|
|
653
|
-
- **Hook reverts (non-sucker path):** Same as Journey 9 -- permanent DoS for non-sucker cash-outs.
|
|
654
|
-
- **Sucker cashing out:** The sucker receives full treasury value without paying the project's configured cash-out tax. This is intentional (cross-chain bridging needs lossless value transfer).
|
|
655
|
-
|
|
656
|
-
---
|
|
657
|
-
|
|
658
|
-
## Journey 11: Read Posting Allowance
|
|
659
|
-
|
|
660
|
-
**Actor:** Anyone (view function)
|
|
661
|
-
**Entry point:** `CTPublisher.allowanceFor(address hook, uint256 category) public view returns (uint256 minimumPrice, uint256 minimumTotalSupply, uint256 maximumTotalSupply, uint256 maximumSplitPercent, address[] memory allowedAddresses)`
|
|
662
|
-
**Who can call:** Anyone. This is a public view function with no access control.
|
|
663
|
-
**Source:** `src/CTPublisher.sol` lines 161-193
|
|
664
|
-
|
|
665
|
-
### Parameters
|
|
666
|
-
|
|
667
|
-
- `hook` (`address`): The hook contract.
|
|
668
|
-
- `category` (`uint256`): The posting category.
|
|
669
|
-
|
|
670
|
-
### Returns
|
|
671
|
-
|
|
672
|
-
| Return | Type | Description |
|
|
673
|
-
|--------|------|-------------|
|
|
674
|
-
| `minimumPrice` | `uint256` | Extracted from bits 0-103 of packed storage |
|
|
675
|
-
| `minimumTotalSupply` | `uint256` | Extracted from bits 104-135 |
|
|
676
|
-
| `maximumTotalSupply` | `uint256` | Extracted from bits 136-167 |
|
|
677
|
-
| `maximumSplitPercent` | `uint256` | Extracted from bits 168-199 |
|
|
678
|
-
| `allowedAddresses` | `address[]` | Full copy of the allowlist array |
|
|
679
|
-
|
|
680
|
-
### State Changes
|
|
681
|
-
|
|
682
|
-
- None. This is a `view` function.
|
|
683
|
-
|
|
684
|
-
### Events
|
|
685
|
-
|
|
686
|
-
- None. This is a `view` function.
|
|
687
|
-
|
|
688
|
-
### Edge Cases
|
|
689
|
-
|
|
690
|
-
- **Unconfigured category:** Returns all zeros and empty array. A `minimumTotalSupply` of 0 means posting is not allowed.
|
|
691
|
-
- **Gas cost for large allowlists:** The function copies the entire `_allowedAddresses` array to memory. For a 10,000-address list, this is approximately 200,000 gas for the memory copy.
|
|
692
|
-
|
|
693
|
-
---
|
|
694
|
-
|
|
695
|
-
## Journey 12: Look Up Tiers by IPFS URI
|
|
696
|
-
|
|
697
|
-
**Actor:** Anyone (view function)
|
|
698
|
-
**Entry point:** `CTPublisher.tiersFor(address hook, bytes32[] memory encodedIPFSUris) external view returns (JB721Tier[] memory tiers)`
|
|
699
|
-
**Who can call:** Anyone. This is an external view function with no access control.
|
|
700
|
-
**Source:** `src/CTPublisher.sol` lines 118-145
|
|
701
|
-
|
|
702
|
-
### Parameters
|
|
703
|
-
|
|
704
|
-
- `hook` (`address`): The hook contract.
|
|
705
|
-
- `encodedIPFSUris` (`bytes32[]`): Array of encoded IPFS URIs to look up.
|
|
706
|
-
|
|
707
|
-
### Returns
|
|
708
|
-
|
|
709
|
-
`JB721Tier[]` -- one tier per URI. Empty tier (all zeros) if the URI has no associated tier.
|
|
710
|
-
|
|
711
|
-
### Execution Flow
|
|
712
|
-
|
|
713
|
-
For each URI:
|
|
714
|
-
1. Look up `tierIdForEncodedIPFSUriOf[hook][uri]`
|
|
715
|
-
2. If non-zero, call `hook.STORE().tierOf(hook, tierId, false)` (line 142)
|
|
716
|
-
3. If zero, return an empty `JB721Tier`
|
|
717
|
-
|
|
718
|
-
### State Changes
|
|
719
|
-
|
|
720
|
-
- None. This is a `view` function.
|
|
721
|
-
|
|
722
|
-
### Events
|
|
723
|
-
|
|
724
|
-
- None. This is a `view` function.
|
|
725
|
-
|
|
726
|
-
### Edge Cases
|
|
727
|
-
|
|
728
|
-
- **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.
|
|
729
|
-
- **Large array:** Each URI requires an external call to the store. Gas scales linearly with array length.
|
|
57
|
+
- Use [nana-721-hook-v6](../nana-721-hook-v6/USER_JOURNEYS.md) for the underlying tier issuance behavior Croptop wraps.
|
|
58
|
+
- Use [nana-core-v6](../nana-core-v6/USER_JOURNEYS.md) when the question is about base project accounting rather than post validation or fee routing.
|