@croptop/core-v6 0.0.19 → 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 CHANGED
@@ -6,25 +6,25 @@ Admin privileges and their scope in croptop-core-v6.
6
6
 
7
7
  ### 1. Project Owner
8
8
 
9
- **How assigned:** Receives the JBProjects ERC-721 NFT for the project. Initially set by the `owner` parameter in `CTDeployer.deployProjectFor()` (line 325, `CTDeployer.sol`). Can be transferred via standard ERC-721 transfer.
9
+ **How assigned:** Receives the JBProjects ERC-721 NFT for the project. Initially set by the `owner` parameter in `CTDeployer.deployProjectFor()`. Can be transferred via standard ERC-721 transfer.
10
10
 
11
11
  **Scope:** Per-project. Controls posting rules and hook ownership for a single project.
12
12
 
13
13
  ### 2. Hook Owner
14
14
 
15
- **How assigned:** Determined by `JBOwnable(hook).owner()`. For CTDeployer-launched projects, the deployer initially owns the hook (via `DEPLOYER.deployHookFor()` at line 264). The project owner can later claim hook ownership via `claimCollectionOwnershipOf()` (line 223).
15
+ **How assigned:** Determined by `JBOwnable(hook).owner()`. For CTDeployer-launched projects, the deployer initially owns the hook (via `DEPLOYER.deployHookFor()`). The project owner can later claim hook ownership via `claimCollectionOwnershipOf()`.
16
16
 
17
17
  **Scope:** Per-hook. The hook owner (or anyone with `ADJUST_721_TIERS` permission for that hook's project) can configure posting criteria.
18
18
 
19
19
  ### 3. CTDeployer (Contract)
20
20
 
21
- **How assigned:** Immutable singleton deployed at construction. Acts as the `IJBRulesetDataHook` for all CTDeployer-launched projects (set at line 290 of `CTDeployer.sol`).
21
+ **How assigned:** Immutable singleton deployed at construction. Acts as the `IJBRulesetDataHook` for all CTDeployer-launched projects (set in `deployProjectFor()`).
22
22
 
23
23
  **Scope:** All Croptop-deployed projects. Proxies pay/cashout data hook calls, grants fee-free cashouts to suckers, and holds broad permissions on behalf of launched projects.
24
24
 
25
25
  ### 4. CTPublisher (Contract)
26
26
 
27
- **How assigned:** Immutable singleton deployed at construction. Receives `ADJUST_721_TIERS` permission from CTDeployer at construction (line 113-119, `CTDeployer.sol`).
27
+ **How assigned:** Immutable singleton deployed at construction. Receives `ADJUST_721_TIERS` permission from CTDeployer at construction.
28
28
 
29
29
  **Scope:** All hooks for which it has `ADJUST_721_TIERS` permission. Creates NFT tiers and mints first copies.
30
30
 
@@ -34,9 +34,11 @@ Admin privileges and their scope in croptop-core-v6.
34
34
 
35
35
  **Scope:** Per-project. Grants `CTPublisher` permanent `ADJUST_721_TIERS` permission for the received project. Once the project is transferred here, human ownership is effectively burned.
36
36
 
37
+ - **Important:** `onERC721Received()` accepts project NFTs from any transfer, not only mints. If a project owner accidentally transfers their project NFT to `CTProjectOwner`, it is permanently locked -- there is no recovery function. The only check is that `msg.sender` is the `PROJECTS` contract (ensuring it is a JBProjects NFT, not an arbitrary ERC-721).
38
+
37
39
  ### 6. Sucker Registry
38
40
 
39
- **How assigned:** Immutable dependency set at CTDeployer construction (line 98). Receives `MAP_SUCKER_TOKEN` permission at construction (line 101-110, `CTDeployer.sol`).
41
+ **How assigned:** Immutable dependency set at CTDeployer construction. Receives `MAP_SUCKER_TOKEN` permission at construction.
40
42
 
41
43
  **Scope:** All projects deployed via CTDeployer. Can map tokens for cross-chain bridging. Determines which addresses get fee-free cashouts.
42
44
 
@@ -52,42 +54,54 @@ Admin privileges and their scope in croptop-core-v6.
52
54
 
53
55
  | Function | Required Role | Permission ID | Scope | What It Does |
54
56
  |----------|--------------|---------------|-------|-------------|
55
- | `deployProjectFor()` (line 243) | Anyone | None | Global | Deploys a new Juicebox project with 721 hook, configures posting rules, optionally deploys suckers, transfers ownership to `owner`. No access restriction -- anyone can deploy a project. |
56
- | `claimCollectionOwnershipOf()` (line 223) | Project owner | None (direct `ownerOf` check) | Per-project | Transfers hook ownership to the project via `JBOwnable.transferOwnershipToProject()`. Caller must be `PROJECTS.ownerOf(projectId)`. |
57
- | `deploySuckersFor()` (line 346) | Project owner or delegate | `JBPermissionIds.DEPLOY_SUCKERS` | Per-project | Deploys new cross-chain suckers for an existing project. Uses `_requirePermissionFrom()` against the project owner. |
57
+ | `deployProjectFor()` | Anyone | None | Global | Deploys a new Juicebox project with 721 hook, configures posting rules, optionally deploys suckers, transfers ownership to `owner`. No access restriction -- anyone can deploy a project. |
58
+ | `claimCollectionOwnershipOf()` | Project owner | None (direct `ownerOf` check) | Per-project | Transfers hook ownership to the project via `JBOwnable.transferOwnershipToProject()`. Caller must be `PROJECTS.ownerOf(projectId)`. |
59
+ | `deploySuckersFor()` | Project owner or delegate | `JBPermissionIds.DEPLOY_SUCKERS` | Per-project | Deploys new cross-chain suckers for an existing project. Uses `_requirePermissionFrom()` against the project owner. |
58
60
 
59
61
  ### CTPublisher
60
62
 
61
63
  | Function | Required Role | Permission ID | Scope | What It Does |
62
64
  |----------|--------------|---------------|-------|-------------|
63
- | `configurePostingCriteriaFor()` (line 230) | Hook owner or delegate | `JBPermissionIds.ADJUST_721_TIERS` | Per-hook, per-category | Sets posting rules: minimum price, min/max supply, max split percent, address allowlist. Uses `_requirePermissionFrom()` against `JBOwnable(hook).owner()`. |
64
- | `mintFrom()` (line 297) | Anyone (subject to allowlist) | None (enforced by allowlist in `_setupPosts`) | Per-hook | Publishes posts as 721 tiers, mints first copies, routes 5% fee to `FEE_PROJECT_ID`. Validates all posts against configured criteria. |
65
+ | `configurePostingCriteriaFor()` | Hook owner or delegate | `JBPermissionIds.ADJUST_721_TIERS` | Per-hook, per-category | Sets posting rules: minimum price, min/max supply, max split percent, address allowlist. Uses `_requirePermissionFrom()` against `JBOwnable(hook).owner()`. |
66
+ | `mintFrom()` | Anyone (subject to allowlist) | None (enforced by allowlist in `_setupPosts`) | Per-hook | Publishes posts as 721 tiers, mints first copies, routes 5% fee to `FEE_PROJECT_ID`. Validates all posts against configured criteria. |
65
67
 
66
68
  ### CTProjectOwner
67
69
 
68
70
  | Function | Required Role | Permission ID | Scope | What It Does |
69
71
  |----------|--------------|---------------|-------|-------------|
70
- | `onERC721Received()` (line 47) | Anyone who transfers a JBProjects NFT | None | Per-project | On receiving a project NFT from `PROJECTS` (mint only, `from == address(0)` is NOT enforced here), grants `CTPublisher` the `ADJUST_721_TIERS` permission for that project. |
72
+ | `onERC721Received()` | Anyone who transfers a JBProjects NFT | None | Per-project | On receiving a project NFT from `PROJECTS` (mint only, `from == address(0)` is NOT enforced here), grants `CTPublisher` the `ADJUST_721_TIERS` permission for that project. |
71
73
 
72
74
  ### Permissions Granted at CTDeployer Construction
73
75
 
74
76
  These permissions are set in the CTDeployer constructor and apply to all projects it will ever deploy (wildcard `projectId: 0`):
75
77
 
76
- | Permission | Granted To | Line | Purpose |
77
- |-----------|-----------|------|---------|
78
- | `MAP_SUCKER_TOKEN` | `SUCKER_REGISTRY` | 101-110 | Allows the sucker registry to map tokens for cross-chain bridging on any project owned by CTDeployer. |
79
- | `ADJUST_721_TIERS` | `PUBLISHER` (CTPublisher) | 113-119 | Allows CTPublisher to add tiers to any hook on any project owned by CTDeployer. |
78
+ | Permission | Granted To | Purpose |
79
+ |-----------|-----------|---------|
80
+ | `MAP_SUCKER_TOKEN` | `SUCKER_REGISTRY` | Allows the sucker registry to map tokens for cross-chain bridging on any project owned by CTDeployer. |
81
+ | `ADJUST_721_TIERS` | `PUBLISHER` (CTPublisher) | Allows CTPublisher to add tiers to any hook on any project owned by CTDeployer. |
80
82
 
81
83
  ### Permissions Granted During `deployProjectFor()`
82
84
 
83
- These permissions are set per-project during deployment (line 328-339, `CTDeployer.sol`):
85
+ These permissions are set per-project during deployment:
86
+
87
+ | Permission | Granted To | Purpose |
88
+ |-----------|-----------|---------|
89
+ | `ADJUST_721_TIERS` | `owner` | Allows the project owner to adjust 721 tiers. |
90
+ | `SET_721_METADATA` | `owner` | Allows the project owner to update 721 metadata. |
91
+ | `MINT_721` | `owner` | Allows the project owner to mint 721 tokens directly. |
92
+ | `SET_721_DISCOUNT_PERCENT` | `owner` | Allows the project owner to set tier discount percentages. |
93
+
94
+ ## Data Hook Proxy
95
+
96
+ When deploying a project, `CTDeployer` sets itself as the project's `dataHook` in the ruleset metadata. It then proxies data hook calls to the project's actual 721 tiers hook:
97
+
98
+ - **`beforePayRecordedWith`**: Calls `IJBRulesetDataHook(hook).beforePayRecordedWith(context)` where `hook = dataHookOf[context.projectId]`, then returns the 721 hook's specifications.
99
+ - **`beforeCashOutRecordedWith`**: Checks if the caller is a registered sucker via `SUCKER_REGISTRY.isSuckerOf()`. If so, returns 0% cash-out tax (fee-free bridging). Otherwise, delegates to the 721 hook.
100
+ - **`hasMintPermissionFor`**: Returns `true` for registered suckers, `false` for all other addresses. Does not delegate to the 721 hook.
101
+
102
+ This proxy pattern exists so that CTDeployer can intercept cash-out calls to grant fee-free bridging to suckers while still supporting the 721 hook's NFT minting logic.
84
103
 
85
- | Permission | Granted To | Line | Purpose |
86
- |-----------|-----------|------|---------|
87
- | `ADJUST_721_TIERS` | `owner` | 329 | Allows the project owner to adjust 721 tiers. |
88
- | `SET_721_METADATA` | `owner` | 330 | Allows the project owner to update 721 metadata. |
89
- | `MINT_721` | `owner` | 331 | Allows the project owner to mint 721 tokens directly. |
90
- | `SET_721_DISCOUNT_PERCENT` | `owner` | 332 | Allows the project owner to set tier discount percentages. |
104
+ The `dataHookOf[projectId]` mapping is write-once (set during `deployProjectFor`, no setter function). The proxy target cannot be changed after deployment.
91
105
 
92
106
  ## Immutable Configuration
93
107
 
@@ -104,9 +118,9 @@ These values are set at deploy time and cannot be changed after deployment:
104
118
  | `SUCKER_REGISTRY` | CTDeployer | Constructor (immutable) | Sucker registry for cross-chain bridging. |
105
119
  | `PERMISSIONS` | CTDeployer, CTProjectOwner | Constructor (immutable) | JBPermissions contract for access control. |
106
120
  | `trustedForwarder` | CTDeployer, CTPublisher | Constructor (immutable via ERC2771Context) | Meta-transaction trusted forwarder address. |
107
- | `dataHookOf[projectId]` | CTDeployer | `deployProjectFor()` (line 307) | Once set during deployment, the data hook for a project can never be changed. Write-once storage. |
108
- | Project weight | CTDeployer | `deployProjectFor()` (line 256) | Hardcoded at `1_000_000 * 10^18` with ETH base currency and max cashout tax rate. |
109
- | Hook deploy salt | CTDeployer | `deployProjectFor()` (line 286) | `keccak256(abi.encode(salt, msg.sender))` -- deterministic but caller-specific. |
121
+ | `dataHookOf[projectId]` | CTDeployer | `deployProjectFor()` | Once set during deployment, the data hook for a project can never be changed. Write-once storage. |
122
+ | Project weight | CTDeployer | `deployProjectFor()` | Hardcoded at `1_000_000 * 10^18` with ETH base currency and max cashout tax rate. |
123
+ | Hook deploy salt | CTDeployer | `deployProjectFor()` | `keccak256(abi.encode(salt, msg.sender))` -- deterministic but caller-specific. |
110
124
 
111
125
  ## Admin Boundaries
112
126
 
@@ -118,11 +132,11 @@ What admins CANNOT do:
118
132
 
119
133
  3. **Project owners cannot change the data hook.** `dataHookOf[projectId]` is write-once (set during `deployProjectFor`, no setter function). The data hook proxy pattern is permanent.
120
134
 
121
- 4. **Project owners cannot disable Croptop posting entirely for a category.** `configurePostingCriteriaFor()` requires `minimumTotalSupply > 0` (line 250-252, `CTPublisher.sol`). The workaround is to set an astronomically high `minimumPrice` with `minimumTotalSupply = maximumTotalSupply = 1`. See finding NM-006.
135
+ 4. **Project owners cannot disable Croptop posting entirely for a category.** `configurePostingCriteriaFor()` requires `minimumTotalSupply > 0`. The workaround is to set an astronomically high `minimumPrice` with `minimumTotalSupply = maximumTotalSupply = 1`. See finding NM-006.
122
136
 
123
137
  5. **Project owners cannot bypass posting criteria to mint directly through CTPublisher.** They must use `mintFrom()` like anyone else, which enforces all configured rules. However, owners can adjust tiers directly on the hook (bypassing CTPublisher) if they have `ADJUST_721_TIERS` permission.
124
138
 
125
- 6. **CTPublisher cannot mint without paying.** `mintFrom()` requires `msg.value >= totalPrice + fee` (line 332-334). There is no free-mint path through CTPublisher.
139
+ 6. **CTPublisher cannot mint without paying.** `mintFrom()` requires `msg.value >= totalPrice + fee`. There is no free-mint path through CTPublisher.
126
140
 
127
141
  7. **CTProjectOwner cannot return project ownership.** Once a project NFT is transferred to CTProjectOwner, there is no function to transfer it back. Ownership is effectively burned.
128
142
 
package/ARCHITECTURE.md CHANGED
@@ -13,10 +13,11 @@ src/
13
13
  ├── CTProjectOwner.sol — Proxy owner for Croptop-deployed projects
14
14
  ├── interfaces/
15
15
  │ ├── ICTDeployer.sol
16
+ │ ├── ICTProjectOwner.sol
16
17
  │ └── ICTPublisher.sol
17
18
  └── structs/
18
19
  ├── CTAllowedPost.sol — Rules for what can be posted
19
- ├── CTDeployerAllowedPost.sol — Deployer-level post rules
20
+ ├── CTDeployerAllowedPost.sol — Deployer-level post rules (no hook field)
20
21
  ├── CTPost.sol — A post submission
21
22
  ├── CTProjectConfig.sol — Project configuration
22
23
  └── CTSuckerDeploymentConfig.sol — Cross-chain config
@@ -25,42 +26,154 @@ src/
25
26
  ## Key Data Flows
26
27
 
27
28
  ### Project Deployment
29
+
30
+ ```
31
+ Creator → CTDeployer.deployProjectFor(owner, projectConfig, suckerConfig, controller)
32
+ 1. Deploy 721 hook via IJB721TiersHookDeployer (empty tiers, ETH currency)
33
+ 2. Launch JB project with:
34
+ → weight = 1,000,000 * 10^18
35
+ → cashOutTaxRate = MAX (100%)
36
+ → dataHook = CTDeployer itself (see "Data Hook Behavior" below)
37
+ → useDataHookForPay = true, useDataHookForCashOut = true
38
+ 3. Store dataHookOf[projectId] = the 721 hook (for pay forwarding)
39
+ 4. Configure allowed post rules on CTPublisher
40
+ 5. Deploy suckers for cross-chain support (if configured)
41
+ 6. Transfer project NFT to the specified owner
42
+ 7. Grant owner permissions: ADJUST_721_TIERS, SET_721_METADATA, MINT_721, SET_721_DISCOUNT_PERCENT
43
+ ```
44
+
45
+ ### Content Publishing (mintFrom)
46
+
28
47
  ```
29
- CreatorCTDeployer.deployProjectFor()
30
- Launch JB project via JB721TiersHookProjectDeployer
31
- Configure CTDeployer as data hook
32
- Set allowed post rules (price floors, supply limits, categories)
33
- Transfer project ownership to CTProjectOwner proxy
48
+ PublisherCTPublisher.mintFrom(hook, posts[], nftBeneficiary, feeBeneficiary, ...)
49
+ _setupPosts: for each post:
50
+ 1. Reject empty encodedIPFSUri
51
+ 2. Reject duplicate encodedIPFSUri within the batch
52
+ 3. If tier already exists for this encodedIPFSUri:
53
+ → Use existing tier ID, add tier's price to totalPrice
54
+ → (Prevents fee evasion by using actual on-chain price, not user-supplied)
55
+ 4. If tier is new:
56
+ → Load allowance for (hook, category) — see "Allowed Post Rules" below
57
+ → Validate: category enabled, price >= minimum, supply in range,
58
+ splitPercent <= maximum, caller in allowlist (if restricted)
59
+ → Create JB721TierConfig with the post's price, supply, category,
60
+ splitPercent, and splits array
61
+ → Record tierIdForEncodedIPFSUriOf mapping
62
+ → Add post.price to totalPrice
63
+ → Calculate fee: totalPrice / FEE_DIVISOR (5% fee, FEE_DIVISOR = 20)
64
+ Fee is skipped when projectId == FEE_PROJECT_ID
65
+ → adjustTiers on the 721 hook to add new tiers
66
+ → Pay the project terminal: payValue = msg.value - fee
67
+ Metadata encodes tier IDs to mint, so the 721 hook mints one NFT per post
68
+ → Pay fee to FEE_PROJECT_ID terminal with remaining balance
34
69
  ```
35
70
 
36
- ### Content Publishing
71
+ ### Allowed Post Rules
72
+
73
+ Each category on each 721 hook has an allowance stored as bit-packed values in `_packedAllowanceFor[hook][category]`. The project owner configures these via `configurePostingCriteriaFor`.
74
+
75
+ | Field | Type | Bits | Purpose |
76
+ |-------|------|------|---------|
77
+ | `minimumPrice` | `uint104` | 0-103 | Floor price per NFT. Posts below this revert. |
78
+ | `minimumTotalSupply` | `uint32` | 104-135 | Minimum editions. Must be >= 1; a zero value means the category is disabled. |
79
+ | `maximumTotalSupply` | `uint32` | 136-167 | Maximum editions. Must be >= minimumTotalSupply. |
80
+ | `maximumSplitPercent` | `uint32` | 168-199 | Cap on the publisher's split (out of `SPLITS_TOTAL_PERCENT = 1,000,000,000`). 0 means no splits allowed. |
81
+ | `allowedAddresses` | `address[]` | separate storage | If non-empty, only these addresses may post in this category. Empty means anyone can post. |
82
+
83
+ Validation order in `_setupPosts`: category enabled (minimumTotalSupply != 0) -> price check -> supply range check -> split percent cap -> allowlist check.
84
+
85
+ Categories cannot be fully removed after creation. This is by design -- once a category exists, removing posting would break expectations for existing posters. Projects can set restrictive allowance configurations to effectively disable new posts.
86
+
87
+ ## Data Hook Behavior
88
+
89
+ CTDeployer registers itself as the ruleset's `dataHook` so it can intercept both payments and cash-outs. It acts as a transparent proxy that adds sucker-awareness:
90
+
91
+ **`beforePayRecordedWith`**: Passthrough with null check. If `dataHookOf[projectId]` is `address(0)`, returns the context weight and empty hook specifications (defaults). Otherwise, forwards the call to the stored data hook (the project's 721 hook). The 721 hook returns the weight and pay hook specifications that handle NFT minting. CTDeployer does not modify pay behavior.
92
+
93
+ **`beforeCashOutRecordedWith`**: Checks if the `holder` is a sucker for the project (via `SUCKER_REGISTRY.isSuckerOf`). If yes, returns `cashOutTaxRate = 0` with no hook specifications -- suckers cash out without any tax. If no, forwards to `dataHookOf[projectId]` for standard cash-out behavior.
94
+
95
+ **`hasMintPermissionFor`**: Returns `true` only for addresses that are suckers for the project. This allows suckers to mint tokens on-demand during cross-chain bridging.
96
+
37
97
  ```
38
- PublisherCTPublisher.postFor(projectId, posts[])
39
- For each post:
40
- Validate against allowed post rules
41
- Check category permissions
42
- → Check price >= minimum
43
- → Check supply within bounds
44
- → Check publisher in allowlist (if restricted)
45
- → Add NFT tier to project's 721 hook
46
- → Configure split for publisher (fee share)
47
- → Pay project to mint NFTs
98
+ Payment path: CTDeployer.beforePayRecordedWith 721 hook (passthrough)
99
+ Cash-out (sucker): CTDeployer.beforeCashOutRecordedWith return 0% tax
100
+ Cash-out (normal): CTDeployer.beforeCashOutRecordedWith 721 hook (forward)
101
+ Mint permission: Only suckers get on-demand mint permission
48
102
  ```
49
103
 
104
+ ## Ownership Model
105
+
106
+ ### Why a Proxy Owner?
107
+
108
+ When CTDeployer creates a project, it initially owns the project NFT (because `launchProjectFor` mints to `msg.sender`). CTDeployer needs to be the initial owner so it can:
109
+ 1. Configure posting criteria on CTPublisher (requires `ADJUST_721_TIERS` permission from the hook's owner, which is CTDeployer).
110
+ 2. Deploy suckers (requires project ownership).
111
+ 3. Grant the final owner NFT management permissions.
112
+
113
+ After setup, CTDeployer transfers the project NFT to the specified `owner`. The 721 hook's ownership stays with CTDeployer, which has granted `ADJUST_721_TIERS` permission to CTPublisher with `projectId = 0` (wildcard). This means CTPublisher can add tiers to any Croptop-deployed project without further permission grants.
114
+
115
+ ### CTProjectOwner: The Immutable-Rules Pattern
116
+
117
+ `CTProjectOwner` is a separate contract that serves as a "lockbox" for projects that want immutable posting rules. When the project owner transfers their project NFT to a CTProjectOwner instance:
118
+
119
+ 1. `onERC721Received` fires and grants CTPublisher the `ADJUST_721_TIERS` permission for that project.
120
+ 2. CTProjectOwner exposes no function to reconfigure posting criteria, queue new rulesets, or transfer the project further.
121
+ 3. The posting rules become permanently frozen -- content can still be published under the existing rules, but the rules themselves cannot change.
122
+
123
+ This enables a trust model: creators can prove to publishers that the rules (price floors, supply caps, split percentages) will never change, providing a credible commitment that the economic terms are permanent.
124
+
125
+ ### Claiming Collection Ownership
126
+
127
+ `CTDeployer.claimCollectionOwnershipOf(hook)` allows the project NFT holder to take direct ownership of the 721 hook by calling `JBOwnable.transferOwnershipToProject(projectId)`. After this, the hook's owner resolves to whoever holds the project NFT. The caller must then independently grant CTPublisher the `ADJUST_721_TIERS` permission, or subsequent posts will revert.
128
+
129
+ ## Publisher Fee and Split Mechanics
130
+
131
+ ### Fee Structure
132
+
133
+ CTPublisher charges a 5% fee (`FEE_DIVISOR = 20`) on the total price of all posts in a `mintFrom` call. The fee is skipped when the target project is the fee project itself (`FEE_PROJECT_ID`).
134
+
135
+ ```
136
+ msg.value breakdown:
137
+ totalPrice → paid to the project's terminal (mints NFTs + project tokens)
138
+ totalPrice/20 → paid to FEE_PROJECT_ID's terminal (Croptop platform fee)
139
+ remainder → reverts if insufficient, excess stays as overpayment
140
+ ```
141
+
142
+ ### Publisher Split Mechanism
143
+
144
+ When a publisher creates a new post, they can set a `splitPercent` and a `splits` array on their `CTPost`. These are stored directly on the 721 tier via `JB721TierConfig.splitPercent` and `JB721TierConfig.splits`.
145
+
146
+ The split mechanics work at the 721 hook level: whenever someone mints (buys) an NFT from that tier, the 721 hook routes `splitPercent` of the tier's price to the addresses in the `splits` array. The publisher typically sets themselves as a split recipient so they earn a share of every future mint from the tier they created.
147
+
148
+ The project owner controls the maximum split a publisher can claim via `maximumSplitPercent` in the allowed post rules. If `maximumSplitPercent` is 0, publishers cannot set any splits. This gives project owners control over how much revenue publishers can capture versus how much flows to the project treasury.
149
+
150
+ ## Design Decisions
151
+
152
+ **Permissioned publishing over open posting.** Posts are validated against per-category allowance rules rather than allowing anyone to post anything. This prevents spam, ensures minimum economic commitment (price floors), and lets project owners curate their collection's quality by restricting who can post and at what terms.
153
+
154
+ **Publisher gets a split, not a direct payment.** Rather than paying publishers upfront, Croptop uses the 721 hook's split mechanism. The publisher earns a percentage of every future mint from their tier. This aligns incentives: publishers profit when their content is popular enough that others want to mint copies, not just from the act of posting.
155
+
156
+ **CTDeployer as data hook proxy.** Instead of requiring projects to use a custom data hook, CTDeployer inserts itself as a transparent proxy. This lets it add sucker-awareness (tax-free cross-chain cash-outs) without requiring the underlying 721 hook to know about suckers. The 721 hook handles NFT minting logic; CTDeployer handles cross-chain policy.
157
+
158
+ **Immutable rules via CTProjectOwner.** Rather than building immutability into CTPublisher (which would add complexity for all projects), immutability is opt-in: transfer the project to CTProjectOwner and the rules freeze. Projects that want governance flexibility simply keep the project NFT in a wallet or multisig.
159
+
160
+ **Category-based organization.** Posts are organized by `category` (uint24), with each category having independent allowance rules. This lets a single project support multiple content types (e.g., category 1 for images at 0.01 ETH, category 2 for music at 0.1 ETH) with different price floors, supply limits, and access controls.
161
+
162
+ **Duplicate prevention via encodedIPFSUri mapping.** `tierIdForEncodedIPFSUriOf[hook][encodedIPFSUri]` ensures each piece of content can only create one tier. If a tier already exists, the mint uses the existing tier (at its on-chain price, not the caller-supplied price). This prevents duplicate tiers and fee evasion.
163
+
50
164
  ## Extension Points
51
165
 
52
166
  | Point | Interface | Purpose |
53
167
  |-------|-----------|---------|
54
- | Data hook | `IJBRulesetDataHook` | CTDeployer acts as data hook |
55
- | 721 hook | `IJB721TiersHook` | NFT tier management |
56
- | Publisher | `ICTPublisher` | Content posting workflow |
168
+ | Data hook | `IJBRulesetDataHook` | CTDeployer proxies pay/cash-out hooks, adds sucker-awareness |
169
+ | 721 hook | `IJB721TiersHook` | NFT tier management, split routing on mints |
170
+ | Publisher | `ICTPublisher` | Content posting workflow and allowance configuration |
57
171
 
58
172
  ## Dependencies
59
- - `@bananapus/core-v6` — Core protocol
60
- - `@bananapus/721-hook-v6` NFT tier system
61
- - `@bananapus/ownable-v6` JB-aware ownership
62
- - `@bananapus/permission-ids-v6` Permission constants
63
- - `@bananapus/buyback-hook-v6` Buyback integration
64
- - `@bananapus/suckers-v6` Cross-chain support
65
- - `@bananapus/router-terminal-v6` Payment routing
66
- - `@openzeppelin/contracts` — ERC2771, ERC721Receiver
173
+
174
+ - `@bananapus/core-v6` -- Core protocol (terminals, rulesets, permissions, directory)
175
+ - `@bananapus/721-hook-v6` -- NFT tier system (tiers, minting, splits)
176
+ - `@bananapus/ownable-v6` -- JB-aware ownership (ownership-to-project transfer)
177
+ - `@bananapus/permission-ids-v6` -- Permission constants
178
+ - `@bananapus/suckers-v6` -- Cross-chain support (sucker registry)
179
+ - `@openzeppelin/contracts` -- ERC2771 (meta-transactions), ERC721Receiver