@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 +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 +9 -9
- 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 +60 -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/fork/PublishFork.t.sol +437 -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/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()
|
|
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()`
|
|
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
|
|
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
|
|
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
|
|
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()`
|
|
56
|
-
| `claimCollectionOwnershipOf()`
|
|
57
|
-
| `deploySuckersFor()`
|
|
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()`
|
|
64
|
-
| `mintFrom()`
|
|
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()`
|
|
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 |
|
|
77
|
-
|
|
78
|
-
| `MAP_SUCKER_TOKEN` | `SUCKER_REGISTRY` |
|
|
79
|
-
| `ADJUST_721_TIERS` | `PUBLISHER` (CTPublisher) |
|
|
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
|
|
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
|
-
|
|
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()`
|
|
108
|
-
| Project weight | CTDeployer | `deployProjectFor()`
|
|
109
|
-
| Hook deploy salt | CTDeployer | `deployProjectFor()`
|
|
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
|
|
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
|
|
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
|
-
|
|
30
|
-
→
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
48
|
+
Publisher → CTPublisher.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
|
-
###
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
|
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
|
-
|
|
60
|
-
- `@bananapus/
|
|
61
|
-
- `@bananapus/
|
|
62
|
-
- `@bananapus/
|
|
63
|
-
- `@bananapus/
|
|
64
|
-
- `@bananapus/suckers-v6`
|
|
65
|
-
- `@
|
|
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
|