@croptop/core-v6 0.0.27 → 0.0.29
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 +30 -3
- package/ARCHITECTURE.md +49 -157
- package/AUDIT_INSTRUCTIONS.md +69 -485
- package/CHANGELOG.md +57 -0
- package/README.md +54 -137
- package/RISKS.md +27 -3
- package/SKILLS.md +28 -187
- package/STYLE_GUIDE.md +56 -17
- package/USER_JOURNEYS.md +37 -708
- package/package.json +5 -6
- package/references/operations.md +25 -0
- package/references/runtime.md +27 -0
- package/script/Deploy.s.sol +4 -23
- package/src/CTDeployer.sol +5 -1
- package/src/CTPublisher.sol +16 -15
- package/test/CTPublisher.t.sol +10 -7
- package/test/audit/DeployerPermissionBypass.t.sol +213 -0
- package/test/audit/FeeFallbackBlackhole.t.sol +263 -0
- package/test/regression/FeeEvasion.t.sol +15 -10
- package/test/regression/StaleTierIdMapping.t.sol +8 -5
- package/CHANGE_LOG.md +0 -273
package/README.md
CHANGED
|
@@ -1,99 +1,43 @@
|
|
|
1
1
|
# Croptop Core
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Croptop turns a Juicebox project with a 721 hook into a permissioned publishing marketplace. Project owners define posting criteria, then anyone who meets those rules can publish new NFT tiers and mint the first copy of each post.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Docs: <https://docs.juicebox.money>
|
|
6
|
+
Site: <https://croptop.eth.limo>
|
|
7
|
+
Architecture: [ARCHITECTURE.md](./ARCHITECTURE.md)
|
|
6
8
|
|
|
7
|
-
|
|
9
|
+
## Overview
|
|
8
10
|
|
|
9
|
-
|
|
11
|
+
Croptop is built around three ideas:
|
|
10
12
|
|
|
11
|
-
|
|
13
|
+
- project owners set category-level posting criteria such as price floors, supply bounds, split limits, and optional allowlists
|
|
14
|
+
- publishers call `mintFrom` to create or reuse 721 tiers that represent their post
|
|
15
|
+
- a one-click deployer can create a full Juicebox project, its 721 hook configuration, and its posting rules in a single transaction
|
|
12
16
|
|
|
13
|
-
|
|
17
|
+
Every mint collects a 5% Croptop fee unless the target project is itself the fee project. If the configured fee
|
|
18
|
+
terminal rejects that fee payment, Croptop refunds the fee portion to `_msgSender()` and still lets the publish
|
|
19
|
+
continue. If `_msgSender()` cannot receive ETH, the mint reverts.
|
|
14
20
|
|
|
15
|
-
|
|
16
|
-
1. Project owner configures posting criteria per category
|
|
17
|
-
→ configurePostingCriteriaFor(allowedPosts)
|
|
18
|
-
→ Sets min price, supply bounds, max split %, address allowlist
|
|
19
|
-
|
|
|
20
|
-
2. Anyone posts content that meets the criteria
|
|
21
|
-
→ mintFrom(hook, posts, nftBeneficiary, feeBeneficiary, ...)
|
|
22
|
-
→ Validates each post against category rules
|
|
23
|
-
→ Creates new 721 tiers on the hook (or reuses existing ones)
|
|
24
|
-
→ Mints first copy of each tier to the poster
|
|
25
|
-
|
|
|
26
|
-
3. Payment routing
|
|
27
|
-
→ 5% fee (totalPrice / FEE_DIVISOR) sent to fee project
|
|
28
|
-
→ Remainder paid into the project's primary terminal
|
|
29
|
-
|
|
|
30
|
-
4. Anyone can mint additional copies
|
|
31
|
-
→ Standard 721 tier minting via the project's hook
|
|
32
|
-
```
|
|
33
|
-
|
|
34
|
-
```mermaid
|
|
35
|
-
sequenceDiagram
|
|
36
|
-
participant Poster
|
|
37
|
-
participant CTPublisher
|
|
38
|
-
participant Hook as 721 Tiers Hook
|
|
39
|
-
participant Terminal as Project Terminal
|
|
40
|
-
participant FeeTerminal as Fee Project Terminal
|
|
41
|
-
|
|
42
|
-
Poster->>CTPublisher: mintFrom(hook, posts, ...)
|
|
43
|
-
CTPublisher->>CTPublisher: Validate each post against category criteria
|
|
44
|
-
loop For each post
|
|
45
|
-
CTPublisher->>Hook: adjustTiersOf() -- create new tier (or reuse existing)
|
|
46
|
-
end
|
|
47
|
-
CTPublisher->>Terminal: pay() -- total price minus fee
|
|
48
|
-
Note over Terminal: Mints first copy of each tier to poster
|
|
49
|
-
CTPublisher->>FeeTerminal: pay() -- 5% fee (totalPrice / 20)
|
|
50
|
-
Note over FeeTerminal: Routes fee to designated fee project
|
|
51
|
-
```
|
|
52
|
-
|
|
53
|
-
### Fee Structure
|
|
54
|
-
|
|
55
|
-
Every `mintFrom` call collects a 5% fee on the total tier price. The fee is calculated as `totalPrice / FEE_DIVISOR` where `FEE_DIVISOR = 20`. The fee is paid to the primary ETH terminal of a designated fee project (`FEE_PROJECT_ID`, set at deployment). The remainder goes to the target project's primary terminal as a normal payment.
|
|
56
|
-
|
|
57
|
-
- If the project being posted to **is** the fee project, no fee is collected (avoids circular payments).
|
|
58
|
-
- Integer division truncates, so the fee loses up to 19 wei of dust per mint.
|
|
59
|
-
- The fee amount is pre-computed as `msg.value - payValue` (not derived from `address(this).balance`), so force-sent ETH does not affect fee routing. The fee terminal payment is wrapped in try-catch with a fallback to `feeBeneficiary` then `msg.sender`.
|
|
60
|
-
|
|
61
|
-
### One-Click Deployment
|
|
62
|
-
|
|
63
|
-
`CTDeployer` creates a complete Juicebox project + 721 hook + posting criteria in a single transaction. It also:
|
|
64
|
-
- Acts as a data hook proxy, forwarding pay/cash-out calls to the underlying 721 hook
|
|
65
|
-
- Grants fee-free cash outs to cross-chain suckers
|
|
66
|
-
- Optionally deploys suckers for omnichain support
|
|
21
|
+
Use this repo when the product is "permissioned publishing on a Juicebox project." Do not use it when you only need plain 721 tier sales; that belongs in `nana-721-hook-v6`.
|
|
67
22
|
|
|
68
|
-
|
|
23
|
+
If a bug looks like ordinary tier issuance or terminal accounting, start in the 721 hook or core repo first. Croptop is where posting policy, fee routing, and publishing-specific project wiring begin.
|
|
69
24
|
|
|
70
|
-
|
|
25
|
+
## Key Contracts
|
|
71
26
|
|
|
72
|
-
|
|
27
|
+
| Contract | Role |
|
|
28
|
+
| --- | --- |
|
|
29
|
+
| `CTPublisher` | Validates posts, adjusts 721 tiers, mints the first copy, and routes protocol and project payments. |
|
|
30
|
+
| `CTDeployer` | Launches a project, configures Croptop posting rules, and can wire in omnichain sucker deployments. |
|
|
31
|
+
| `CTProjectOwner` | Burn-lock ownership helper that can permanently route project administration through Croptop's publishing surface. |
|
|
73
32
|
|
|
74
|
-
|
|
75
|
-
|----------|-------------|
|
|
76
|
-
| `CTPublisher` | Core publishing engine. Validates posts against owner-configured allowances (min price, supply bounds, address allowlists, max split percent), creates new 721 tiers on the hook, mints the first copy to the poster, and routes a 5% fee to a designated fee project. Inherits `JBPermissioned` and `ERC2771Context`. |
|
|
77
|
-
| `CTDeployer` | One-click project factory. Deploys a Juicebox project with a 721 tiers hook pre-wired, configures posting criteria via `CTPublisher`, optionally deploys cross-chain suckers, and acts as an `IJBRulesetDataHook` proxy that forwards pay/cash-out calls to the underlying hook while granting fee-free cash outs to suckers. |
|
|
78
|
-
| `CTProjectOwner` | Burn-lock ownership helper. Receives a project's ERC-721 ownership token and automatically grants `CTPublisher` the `ADJUST_721_TIERS` permission, effectively making the project's tier configuration immutable except through Croptop posts. |
|
|
33
|
+
## Mental Model
|
|
79
34
|
|
|
80
|
-
|
|
35
|
+
There are two separate concerns here:
|
|
81
36
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
| `CTAllowedPost` | Full posting criteria: hook address, category, price/supply bounds, max split percent, and address allowlist. |
|
|
85
|
-
| `CTDeployerAllowedPost` | Same as `CTAllowedPost` but without the hook address (inferred during deployment). |
|
|
86
|
-
| `CTPost` | A post to publish: encoded IPFS URI, total supply, price, category, split percent, and splits. |
|
|
87
|
-
| `CTProjectConfig` | Project deployment configuration: terminals, metadata URIs, allowed posts, collection name/symbol, and deterministic salt. |
|
|
88
|
-
| `CTSuckerDeploymentConfig` | Cross-chain sucker deployment: deployer configurations and deterministic salt. |
|
|
37
|
+
1. `CTPublisher` governs whether a post is allowed and how it becomes a tier
|
|
38
|
+
2. `CTDeployer` governs how a Croptop-flavored project is packaged and launched
|
|
89
39
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
| Interface | Description |
|
|
93
|
-
|-----------|-------------|
|
|
94
|
-
| `ICTPublisher` | Publishing engine: `mintFrom`, `configurePostingCriteriaFor`, `allowanceFor`, `tiersFor`, plus events. |
|
|
95
|
-
| `ICTDeployer` | Factory: `deployProjectFor`, `claimCollectionOwnershipOf`, `deploySuckersFor`. |
|
|
96
|
-
| `ICTProjectOwner` | Burn-lock: `onERC721Received` (IERC721Receiver). |
|
|
40
|
+
That distinction matters because many "Croptop bugs" are deployment-shape bugs rather than publishing-rule bugs.
|
|
97
41
|
|
|
98
42
|
## Install
|
|
99
43
|
|
|
@@ -101,76 +45,49 @@ Every `mintFrom` call collects a 5% fee on the total tier price. The fee is calc
|
|
|
101
45
|
npm install @croptop/core-v6
|
|
102
46
|
```
|
|
103
47
|
|
|
104
|
-
|
|
48
|
+
## Development
|
|
105
49
|
|
|
106
50
|
```bash
|
|
107
|
-
|
|
51
|
+
npm install
|
|
52
|
+
forge build
|
|
53
|
+
forge test
|
|
108
54
|
```
|
|
109
55
|
|
|
110
|
-
|
|
56
|
+
Useful scripts:
|
|
57
|
+
|
|
58
|
+
- `npm run deploy:mainnets`
|
|
59
|
+
- `npm run deploy:testnets`
|
|
60
|
+
- `npm run deploy:mainnets:project`
|
|
61
|
+
- `npm run deploy:testnets:project`
|
|
62
|
+
|
|
63
|
+
## Deployment Notes
|
|
64
|
+
|
|
65
|
+
Deployments are handled through Sphinx using the environments configured in the repo scripts. `CTDeployer` can also compose cross-chain sucker deployments when the target publishing project needs omnichain support.
|
|
111
66
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
| `forge build` | Compile contracts |
|
|
115
|
-
| `forge test` | Run all tests (12 test files covering publishing, deployer, attacks, fork integration, metadata, and regressions) |
|
|
116
|
-
| `forge test -vvv` | Run tests with full trace |
|
|
67
|
+
The deploy script now expects an explicit nonzero `FEE_PROJECT_ID` for canonical deployments. It does not safely
|
|
68
|
+
autodiscover a fee project by scanning existing project IDs.
|
|
117
69
|
|
|
118
70
|
## Repository Layout
|
|
119
71
|
|
|
120
|
-
```
|
|
72
|
+
```text
|
|
121
73
|
src/
|
|
122
|
-
CTPublisher.sol
|
|
123
|
-
CTDeployer.sol
|
|
124
|
-
CTProjectOwner.sol
|
|
74
|
+
CTPublisher.sol
|
|
75
|
+
CTDeployer.sol
|
|
76
|
+
CTProjectOwner.sol
|
|
125
77
|
interfaces/
|
|
126
|
-
ICTPublisher.sol # Publisher interface + events
|
|
127
|
-
ICTDeployer.sol # Factory interface
|
|
128
|
-
ICTProjectOwner.sol # Burn-lock interface
|
|
129
78
|
structs/
|
|
130
|
-
CTAllowedPost.sol # Posting criteria with hook address
|
|
131
|
-
CTDeployerAllowedPost.sol # Posting criteria without hook (for deployment)
|
|
132
|
-
CTPost.sol # Post data: IPFS URI, supply, price, category
|
|
133
|
-
CTProjectConfig.sol # Full project deployment config
|
|
134
|
-
CTSuckerDeploymentConfig.sol # Cross-chain sucker config
|
|
135
79
|
test/
|
|
136
|
-
|
|
137
|
-
CTDeployer.t.sol # Deployer tests (~608 lines)
|
|
138
|
-
CTProjectOwner.t.sol # Project owner tests (~185 lines)
|
|
139
|
-
ClaimCollectionOwnership.t.sol # Collection ownership claim tests (~315 lines)
|
|
140
|
-
CroptopAttacks.t.sol # Security/adversarial tests (~437 lines, ~12 cases)
|
|
141
|
-
Fork.t.sol # Mainnet fork integration tests
|
|
142
|
-
TestAuditGaps.sol # Audit gap coverage tests (~689 lines)
|
|
143
|
-
Test_MetadataGeneration.t.sol # JBMetadataResolver roundtrip tests
|
|
144
|
-
fork/
|
|
145
|
-
PublishFork.t.sol # Fork-based publish tests (~437 lines)
|
|
146
|
-
regression/
|
|
147
|
-
DuplicateUriFeeEvasion.t.sol # Duplicate URI fee evasion regression (~312 lines)
|
|
148
|
-
FeeEvasion.t.sol # Fee evasion regression (~279 lines)
|
|
149
|
-
StaleTierIdMapping.t.sol # Stale tier ID mapping regression (~214 lines)
|
|
80
|
+
publisher, deployer, fork, attack, audit, metadata, and regression coverage
|
|
150
81
|
script/
|
|
151
|
-
Deploy.s.sol
|
|
152
|
-
ConfigureFeeProject.s.sol
|
|
82
|
+
Deploy.s.sol
|
|
83
|
+
ConfigureFeeProject.s.sol
|
|
153
84
|
helpers/
|
|
154
|
-
CroptopDeploymentLib.sol # Deployment artifact reader
|
|
155
85
|
```
|
|
156
86
|
|
|
157
|
-
##
|
|
158
|
-
|
|
159
|
-
| Permission | Required For |
|
|
160
|
-
|------------|-------------|
|
|
161
|
-
| `ADJUST_721_TIERS` | `configurePostingCriteriaFor` -- set posting criteria on a hook (from hook owner) |
|
|
162
|
-
| `DEPLOY_SUCKERS` | `deploySuckersFor` -- deploy cross-chain suckers for an existing project |
|
|
163
|
-
|
|
164
|
-
`CTDeployer` grants the following to the project owner during deployment:
|
|
165
|
-
- `ADJUST_721_TIERS` -- modify tiers directly
|
|
166
|
-
- `SET_721_METADATA` -- update collection metadata
|
|
167
|
-
- `MINT_721` -- mint NFTs directly
|
|
168
|
-
- `SET_721_DISCOUNT_PERCENT` -- adjust discount rates
|
|
169
|
-
|
|
170
|
-
## Risks
|
|
87
|
+
## Risks And Notes
|
|
171
88
|
|
|
172
|
-
-
|
|
173
|
-
-
|
|
174
|
-
|
|
175
|
-
-
|
|
176
|
-
-
|
|
89
|
+
- posting criteria are only as safe as the project owner configures them
|
|
90
|
+
- fee routing depends on the designated fee project remaining correctly configured; if its terminal rejects payments,
|
|
91
|
+
Croptop refunds the fee to `_msgSender()` instead of trapping ETH in `CTPublisher`
|
|
92
|
+
- burn-lock ownership is intentionally irreversible and should only be used when immutability is desired
|
|
93
|
+
- duplicate-content and stale-tier edge cases are guarded by tests, but integrations should still treat metadata reuse carefully
|
package/RISKS.md
CHANGED
|
@@ -1,4 +1,21 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Croptop Core Risk Register
|
|
2
|
+
|
|
3
|
+
This file focuses on the publishing, fee-routing, and hook-composition risks that matter once third parties can create NFT tiers on someone else's Juicebox project.
|
|
4
|
+
|
|
5
|
+
## How to use this file
|
|
6
|
+
|
|
7
|
+
- Read `Priority risks` first to understand the failure modes with the highest user or treasury impact.
|
|
8
|
+
- Use the detailed sections for contract-level reasoning about posting criteria, fee routing, and deployer composition.
|
|
9
|
+
- Treat `Accepted Behaviors` and `Invariants to Verify` as the line between intentional tradeoffs and defects.
|
|
10
|
+
|
|
11
|
+
## Priority risks
|
|
12
|
+
|
|
13
|
+
| Priority | Risk | Why it matters | Primary controls |
|
|
14
|
+
|----------|------|----------------|------------------|
|
|
15
|
+
| P0 | Hook/store and terminal trust | `mintFrom` depends on hook storage and directory terminal resolution; a bad integration can misprice posts or redirect value. | Audit integration assumptions, verify hook/store pairings, and monitor terminal configuration. |
|
|
16
|
+
| P1 | Tier ID race during concurrent posting | `_setupPosts` predicts future tier IDs before `adjustTiers`; concurrent writes can shift those IDs and break the batch. | Application-layer ordering, atomic reverts on mismatch, and operator awareness of concurrent posting. |
|
|
17
|
+
| P1 | Fee-path degradation without mint failure | The fee terminal is fail-open via try/catch, so posting continues even if the fee project temporarily stops receiving revenue. | Terminal health monitoring, fallback beneficiary handling, and explicit operational checks around fee routing. |
|
|
18
|
+
|
|
2
19
|
|
|
3
20
|
## 1. Trust Assumptions
|
|
4
21
|
|
|
@@ -14,7 +31,7 @@
|
|
|
14
31
|
- **Fee evasion via duplicate posts across hooks.** `tierIdForEncodedIPFSUriOf` is keyed per hook. The same `encodedIPFSUri` can be posted to different hooks without duplicate detection, potentially creating fee-arbitrage opportunities.
|
|
15
32
|
- **Fee calculation rounding.** Fee is `totalPrice / FEE_DIVISOR` (FEE_DIVISOR=20, so 5% fee). Integer division truncates, losing up to 19 wei per post. Negligible individually but could compound across many micro-priced posts. Explicit validation: reverts `CTPublisher_InsufficientEthSent` if `msg.value < fee` (before subtraction) or if `msg.value - fee < totalPrice` (after subtraction).
|
|
16
33
|
- **Pre-computed fee routing.** `CTPublisher.mintFrom` computes the fee as `msg.value - payValue` before the external payment call, so the fee amount is determined from `msg.value` alone. Force-sent ETH (via selfdestruct) does not affect fee calculation.
|
|
17
|
-
- **
|
|
34
|
+
- **Fee terminal fallback refunds the caller.** If the configured fee terminal cannot accept the fee payment, `mintFrom` refunds the fee portion to `_msgSender()`. This preserves mint liveness for normal callers, but relayers or contracts that cannot receive ETH will still cause the mint to revert.
|
|
18
35
|
- **Split percent manipulation.** Posters can set `splitPercent` up to `maximumSplitPercent`. Splits route funds away from the project treasury to poster-specified addresses. If `maximumSplitPercent` is set high, posters can redirect most of the tier revenue.
|
|
19
36
|
|
|
20
37
|
## 3. Access Control
|
|
@@ -44,7 +61,7 @@
|
|
|
44
61
|
- **No mechanism for hook migration.** `dataHookOf` is written once in `deployProjectFor` and never updated. If the data hook becomes compromised, there is no governance path to replace it without deploying a new project.
|
|
45
62
|
- **Tier ID prediction.** `_setupPosts` predicts new tier IDs as `maxTierIdOf(hook) + 1 + i`. If another transaction adds tiers between `maxTierIdOf` read and `adjustTiers` execution, tier IDs shift and the wrong tiers are minted. This is a race condition in concurrent posting.
|
|
46
63
|
- **CTProjectOwner accepts any project NFT.** `onERC721Received` grants `ADJUST_721_TIERS` to `PUBLISHER` for whatever tokenId is received. If a non-Croptop project is accidentally transferred to `CTProjectOwner`, the publisher gains tier adjustment permission for it.
|
|
47
|
-
- **Fee payment destination.** Fees are routed to `FEE_PROJECT_ID` via its primary terminal. If the fee project changes its terminal or token acceptance
|
|
64
|
+
- **Fee payment destination.** Fees are routed to `FEE_PROJECT_ID` via its primary terminal. If the fee project changes its terminal or token acceptance incompatibly, `mintFrom` attempts to refund the fee to `_msgSender()`. If the caller cannot receive ETH, the mint reverts.
|
|
48
65
|
|
|
49
66
|
## 7. Accepted Behaviors
|
|
50
67
|
|
|
@@ -56,6 +73,13 @@
|
|
|
56
73
|
|
|
57
74
|
`_setupPosts` predicts new tier IDs as `maxTierIdOf(hook) + 1 + i`. A concurrent `adjustTiers` call between the `maxTierIdOf` read and the `adjustTiers` execution shifts all predicted IDs, causing the wrong tiers to be minted. This is a known race condition. Mitigation is at the application layer: frontends should use nonce-based transaction ordering or warn users about concurrent posting. The hook-level `adjustTiers` is atomic (all-or-nothing), so a failed prediction reverts the entire batch cleanly.
|
|
58
75
|
|
|
76
|
+
### 7.3 Project owners can bypass the publisher surface while they retain direct hook permissions
|
|
77
|
+
|
|
78
|
+
`CTDeployer.deployProjectFor` intentionally grants the initial owner/operator enough hook permissions to manage the
|
|
79
|
+
collection directly. That means the owner can bypass `CTPublisher`'s policy and fee path until ownership is moved into
|
|
80
|
+
another authority surface or those permissions are narrowed. This is an accepted product tradeoff and should be treated
|
|
81
|
+
as part of the trust model, not as a hidden invariant enforced by `CTPublisher`.
|
|
82
|
+
|
|
59
83
|
## 8. Invariants to Verify
|
|
60
84
|
|
|
61
85
|
- `tierIdForEncodedIPFSUriOf[hook][encodedIPFSUri]` is set exactly once per (hook, encodedIPFSUri) pair and points to a valid, non-removed tier.
|
package/SKILLS.md
CHANGED
|
@@ -1,200 +1,41 @@
|
|
|
1
1
|
# Croptop Core
|
|
2
2
|
|
|
3
|
-
##
|
|
4
|
-
|
|
5
|
-
Permissioned NFT publishing system that lets anyone post content as 721 tiers to a Juicebox project, subject to owner-defined criteria for price, supply, split percentages, and poster identity. Routes a 5% fee on each mint to a designated fee project.
|
|
6
|
-
|
|
7
|
-
## Contracts
|
|
8
|
-
|
|
9
|
-
| Contract | Role |
|
|
10
|
-
|----------|------|
|
|
11
|
-
| `CTPublisher` | Core publishing engine. Validates posts against bit-packed allowances, creates 721 tiers on hooks, mints first copies to posters, and routes fees. Inherits `JBPermissioned`, `ERC2771Context`. |
|
|
12
|
-
| `CTDeployer` | Factory that deploys a Juicebox project + 721 hook + posting criteria in one transaction. Also acts as `IJBRulesetDataHook` proxy that forwards pay/cash-out calls to the underlying hook while granting fee-free cash outs to suckers. |
|
|
13
|
-
| `CTProjectOwner` | Burn-lock contract: receives the project ownership NFT and grants `CTPublisher` the `ADJUST_721_TIERS` permission permanently. Since `CTProjectOwner` has no `transferFrom`, `reconfigure`, or `withdraw` functions, ownership is effectively burned -- no one can reclaim the project NFT, change rulesets, or modify posting criteria after transfer. Use this when you want a fully autonomous project where only community posting (within pre-set criteria) is possible. If you need to retain the ability to reconfigure the project, adjust tiers, or withdraw funds, keep ownership yourself (or use a multisig) instead. Configure all desired posting criteria **before** transferring the project NFT here, as they become immutable once ownership moves. Accepts both mints and `safeTransferFrom` calls originating from the `PROJECTS` contract. |
|
|
14
|
-
|
|
15
|
-
## Key Functions
|
|
16
|
-
|
|
17
|
-
### Publishing
|
|
18
|
-
|
|
19
|
-
| Function | What it does |
|
|
20
|
-
|----------|-------------|
|
|
21
|
-
| `CTPublisher.mintFrom(hook, posts, nftBeneficiary, feeBeneficiary, additionalPayMetadata, feeMetadata)` | Publishes posts as new 721 tiers, mints first copies to `nftBeneficiary`, deducts a 5% fee (`totalPrice / FEE_DIVISOR`) routed to `FEE_PROJECT_ID`, and pays the remainder into the project's primary terminal. Reuses existing tiers if the IPFS URI was already minted. |
|
|
22
|
-
| `CTPublisher.configurePostingCriteriaFor(allowedPosts)` | Sets per-category posting rules (min price, min/max supply, max split percent, address allowlist) for a given hook. Requires `ADJUST_721_TIERS` permission from the hook owner. |
|
|
23
|
-
|
|
24
|
-
### Views
|
|
25
|
-
|
|
26
|
-
| Function | What it does |
|
|
27
|
-
|----------|-------------|
|
|
28
|
-
| `CTPublisher.allowanceFor(hook, category)` | Returns the posting criteria for a hook/category: minimum price (uint104), min supply (uint32), max supply (uint32), max split percent (uint32), plus the address allowlist. Reads from bit-packed storage. |
|
|
29
|
-
| `CTPublisher.tiersFor(hook, encodedIPFSUris)` | Resolves an array of encoded IPFS URIs to their corresponding `JB721Tier` structs via the stored `tierIdForEncodedIPFSUriOf` mapping. |
|
|
30
|
-
|
|
31
|
-
### Project Deployment
|
|
32
|
-
|
|
33
|
-
| Function | What it does |
|
|
34
|
-
|----------|-------------|
|
|
35
|
-
| `CTDeployer.deployProjectFor(owner, projectConfig, suckerDeploymentConfiguration, controller)` | Deploys a new Juicebox project with a 721 tiers hook, configures posting criteria, optionally deploys suckers, and transfers project ownership to the specified owner. Uses `CTDeployer` as data hook proxy. Returns `(projectId, hook)`. |
|
|
36
|
-
| `CTDeployer.claimCollectionOwnershipOf(hook)` | Transfers hook ownership to the project via `JBOwnable.transferOwnershipToProject`. Only callable by the project owner. After claiming, the project owner must grant CTPublisher `ADJUST_721_TIERS` permission for the project so that `mintFrom()` continues to work. |
|
|
37
|
-
| `CTDeployer.deploySuckersFor(projectId, suckerDeploymentConfiguration)` | Deploys new cross-chain suckers for an existing project. Requires `DEPLOY_SUCKERS` permission. |
|
|
38
|
-
|
|
39
|
-
### Data Hook Proxy
|
|
40
|
-
|
|
41
|
-
| Function | What it does |
|
|
42
|
-
|----------|-------------|
|
|
43
|
-
| `CTDeployer.beforePayRecordedWith(context)` | Forwards pay context to the stored `dataHookOf[projectId]` (typically the 721 tiers hook). Hook specifications returned include a `noop` field — the 721 hook always returns `noop: false`. |
|
|
44
|
-
| `CTDeployer.beforeCashOutRecordedWith(context)` | Returns zero tax rate for sucker addresses (fee-free cross-chain cash outs). Otherwise forwards to the stored data hook. Forwarded hook specifications preserve the inner hook's `noop` flag. |
|
|
45
|
-
| `CTDeployer.hasMintPermissionFor(projectId, ruleset, addr)` | Returns `true` if `addr` is a sucker for the project. |
|
|
46
|
-
|
|
47
|
-
### Burn-Lock Ownership
|
|
48
|
-
|
|
49
|
-
| Function | What it does |
|
|
50
|
-
|----------|-------------|
|
|
51
|
-
| `CTProjectOwner.onERC721Received(operator, from, tokenId, data)` | On receiving the project NFT, grants `CTPublisher` the `ADJUST_721_TIERS` permission for that project. Accepts both mints and transfers from `PROJECTS` (reverts if `msg.sender` is not `PROJECTS`). |
|
|
52
|
-
|
|
53
|
-
## Integration Points
|
|
3
|
+
## Use This File For
|
|
54
4
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
| `@bananapus/core-v6` | `IJBDirectory`, `IJBPermissions`, `IJBTerminal`, `IJBProjects`, `IJBController`, `JBConstants`, `JBMetadataResolver` | Project lookup, permission enforcement, payment routing, project creation, metadata encoding |
|
|
58
|
-
| `@bananapus/721-hook-v6` | `IJB721TiersHook`, `IJB721TiersHookDeployer`, `JB721TierConfig`, `JB721Tier` | Tier creation/adjustment, hook deployment, tier data resolution |
|
|
59
|
-
| `@bananapus/ownable-v6` | `JBOwnable` | Ownership checks and transfers for hooks |
|
|
60
|
-
| `@bananapus/suckers-v6` | `IJBSuckerRegistry`, `JBSuckerDeployerConfig` | Cross-chain sucker deployment and fee-free cash-out detection |
|
|
61
|
-
| `@bananapus/permission-ids-v6` | `JBPermissionIds` | Permission ID constants (`ADJUST_721_TIERS`, `DEPLOY_SUCKERS`, `MAP_SUCKER_TOKEN`, etc.) |
|
|
62
|
-
| `@openzeppelin/contracts` | `ERC2771Context`, `IERC721Receiver` | Meta-transaction support, safe project NFT receipt |
|
|
5
|
+
- Use this file when the task touches Croptop publishing, project deployment, data-hook forwarding, fee routing, or burn-locked ownership behavior.
|
|
6
|
+
- Start here, then jump into the publisher, deployer, or owner contract depending on which path the user is actually asking about.
|
|
63
7
|
|
|
64
|
-
##
|
|
8
|
+
## Read This Next
|
|
65
9
|
|
|
66
|
-
|
|
|
67
|
-
|
|
68
|
-
|
|
|
69
|
-
|
|
|
70
|
-
|
|
|
71
|
-
|
|
|
72
|
-
|
|
|
10
|
+
| If you need... | Open this next |
|
|
11
|
+
|---|---|
|
|
12
|
+
| Repo overview and expected flow | [`README.md`](./README.md), [`ARCHITECTURE.md`](./ARCHITECTURE.md) |
|
|
13
|
+
| Publishing and metadata behavior | [`src/CTPublisher.sol`](./src/CTPublisher.sol) |
|
|
14
|
+
| Deployment and fee-project wiring | [`src/CTDeployer.sol`](./src/CTDeployer.sol), [`script/Deploy.s.sol`](./script/Deploy.s.sol), [`script/ConfigureFeeProject.s.sol`](./script/ConfigureFeeProject.s.sol) |
|
|
15
|
+
| Ownership burn-lock behavior | [`src/CTProjectOwner.sol`](./src/CTProjectOwner.sol) |
|
|
16
|
+
| Regression, attack, or fork coverage | [`test/regression/`](./test/regression/), [`test/fork/`](./test/fork/), [`test/`](./test/) |
|
|
73
17
|
|
|
74
|
-
##
|
|
18
|
+
## Repo Map
|
|
75
19
|
|
|
76
|
-
|
|
|
77
|
-
|
|
78
|
-
|
|
|
79
|
-
| `
|
|
20
|
+
| Area | Where to look |
|
|
21
|
+
|---|---|
|
|
22
|
+
| Main contracts | [`src/`](./src/) |
|
|
23
|
+
| Types | [`src/structs/`](./src/structs/), [`src/interfaces/`](./src/interfaces/) |
|
|
24
|
+
| Scripts | [`script/`](./script/) |
|
|
25
|
+
| Tests | [`test/`](./test/) |
|
|
80
26
|
|
|
81
|
-
##
|
|
82
|
-
|
|
83
|
-
| Error | When |
|
|
84
|
-
|-------|------|
|
|
85
|
-
| `CTPublisher_EmptyEncodedIPFSUri` | Post has `encodedIPFSUri == bytes32(0)` |
|
|
86
|
-
| `CTPublisher_InsufficientEthSent` | `totalPrice + fee > msg.value` |
|
|
87
|
-
| `CTPublisher_MaxTotalSupplyLessThanMin` | `minimumTotalSupply > maximumTotalSupply` in config |
|
|
88
|
-
| `CTPublisher_NotInAllowList` | Caller not in allowlist (when allowlist is non-empty) |
|
|
89
|
-
| `CTPublisher_PriceTooSmall` | Post price below `minimumPrice` |
|
|
90
|
-
| `CTPublisher_SplitPercentExceedsMaximum` | Post `splitPercent > maximumSplitPercent` |
|
|
91
|
-
| `CTPublisher_TotalSupplyTooSmall` | Post `totalSupply < minimumTotalSupply` |
|
|
92
|
-
| `CTPublisher_TotalSupplyTooBig` | Post `totalSupply > maximumTotalSupply` (when max > 0) |
|
|
93
|
-
| `CTPublisher_UnauthorizedToPostInCategory` | Category unconfigured (`minSupply == 0`) |
|
|
94
|
-
| `CTPublisher_ZeroTotalSupply` | `configurePostingCriteriaFor` with `minimumTotalSupply == 0` |
|
|
95
|
-
| `CTPublisher_DuplicatePost(bytes32 encodedIPFSUri)` | Same `encodedIPFSUri` appears more than once within the same `mintFrom` batch |
|
|
96
|
-
| `CTDeployer_NotOwnerOfProject` | `claimCollectionOwnershipOf` called by non-owner |
|
|
97
|
-
|
|
98
|
-
## Constants
|
|
99
|
-
|
|
100
|
-
| Constant | Value | Purpose |
|
|
101
|
-
|----------|-------|---------|
|
|
102
|
-
| `FEE_DIVISOR` | 20 | 5% fee: `totalPrice / 20` |
|
|
103
|
-
| `FEE_PROJECT_ID` | immutable | Fees routed to this project. Fee skipped when `projectId == FEE_PROJECT_ID` |
|
|
104
|
-
|
|
105
|
-
## Storage
|
|
106
|
-
|
|
107
|
-
### CTPublisher
|
|
108
|
-
|
|
109
|
-
| Variable | Type | Purpose |
|
|
110
|
-
|----------|------|---------|
|
|
111
|
-
| `tierIdForEncodedIPFSUriOf` | `hook => encodedIPFSUri => uint256` | Maps IPFS URI to existing tier ID (prevents duplicates) |
|
|
112
|
-
| `_packedAllowanceFor` | `hook => category => uint256` | Bit-packed allowance: price (0-103), minSupply (104-135), maxSupply (136-167), maxSplitPercent (168-199) |
|
|
113
|
-
| `_allowedAddresses` | `hook => category => address[]` | Per-category address allowlist |
|
|
114
|
-
|
|
115
|
-
### CTDeployer
|
|
116
|
-
|
|
117
|
-
| Variable | Type | Purpose |
|
|
118
|
-
|----------|------|---------|
|
|
119
|
-
| `PROJECTS` | `IJBProjects` (immutable) | ERC-721 contract for Juicebox project ownership |
|
|
120
|
-
| `DEPLOYER` | `IJB721TiersHookDeployer` (immutable) | Factory for deploying 721 tiers hooks |
|
|
121
|
-
| `PUBLISHER` | `ICTPublisher` (immutable) | CTPublisher instance used for posting |
|
|
122
|
-
| `SUCKER_REGISTRY` | `IJBSuckerRegistry` (immutable) | Registry for cross-chain sucker deployment and lookup |
|
|
123
|
-
| `dataHookOf` | `projectId => IJBRulesetDataHook` | Stores original data hook per project (CTDeployer proxy pattern) |
|
|
124
|
-
|
|
125
|
-
## Gotchas
|
|
126
|
-
|
|
127
|
-
1. **Bit-packed allowances.** Allowances are packed into a single `uint256`: price in bits 0-103, min supply in 104-135, max supply in 136-167, max split percent in 168-199. Reading with wrong bit widths silently returns wrong values.
|
|
128
|
-
2. **Fee is 1/20, not a percentage.** `FEE_DIVISOR = 20` means fee = `totalPrice / 20` = 5%. Integer division truncates (rounding down favors payer).
|
|
129
|
-
3. **Fee skipped for fee project.** When `projectId == FEE_PROJECT_ID`, no fee is deducted. This prevents self-referential fee loops.
|
|
130
|
-
4. **Fee payment is pre-computed and try-catch wrapped.** After the main payment, `mintFrom` computes the fee as `msg.value - payValue` and sends it to the fee terminal via try-catch. If the fee terminal reverts, the fee falls back to `feeBeneficiary.call{value}`, then `msg.sender.call{value}`. A broken fee terminal never blocks mints.
|
|
131
|
-
5. **Tier reuse by IPFS URI.** If an encoded IPFS URI was already minted on the hook, the existing tier ID is reused instead of creating a new tier. The poster still gets a mint of the existing tier. The fee is calculated from the actual tier price stored on-chain (not from `post.price`), preventing fee evasion (H-19 fix).
|
|
132
|
-
6. **Stale tier mapping cleanup.** If a tier was removed externally via `adjustTiers()`, the `tierIdForEncodedIPFSUriOf` mapping is automatically cleared when the same IPFS URI is posted again, allowing a new tier to be created (L-52 fix).
|
|
133
|
-
7. **Array resizing via assembly.** `_setupPosts` resizes `tiersToAdd` via inline assembly when some posts reuse existing tiers. The `tierIdsToMint` array is NOT resized and may contain zeros for pre-existing tiers.
|
|
134
|
-
8. **CTProjectOwner only accepts from PROJECTS contract.** `onERC721Received` reverts if `msg.sender != address(PROJECTS)`. It does NOT check the `from` address, so both mints and transfers through `PROJECTS.safeTransferFrom` are accepted.
|
|
135
|
-
9. **CTDeployer rejects direct transfers.** `CTDeployer.onERC721Received` reverts if `from != address(0)`. It only accepts mints from `PROJECTS`.
|
|
136
|
-
10. **Temporary ownership during deployment.** `CTDeployer` owns the project NFT temporarily during `deployProjectFor` (to configure permissions and hooks), then transfers it to the specified `owner`. If the transfer reverts, the entire deployment fails.
|
|
137
|
-
11. **Data hook proxy pattern.** `CTDeployer` wraps itself as the data hook, forwarding to `dataHookOf[projectId]`. This is needed to intercept cash-out calls and grant fee-free cash outs to suckers. Both `useDataHookForPay` and `useDataHookForCashOut` are enabled (M-37 fix).
|
|
138
|
-
12. **Sucker registry trust.** `CTDeployer.beforeCashOutRecordedWith` trusts `SUCKER_REGISTRY.isSuckerOf` to determine fee exemption. If the registry is compromised, any address could cash out without tax.
|
|
139
|
-
13. **Allowlist uses linear scan.** `_isAllowed()` iterates the full allowlist array. Acceptable for <100 addresses; gas cost scales linearly with list size. This also affects `mintFrom` with large post batches: each post in the batch triggers a separate allowlist scan, so gas scales as `O(posts * allowlistSize)`. Keep batches under ~20 posts with allowlists under ~50 addresses to stay within block gas limits.
|
|
140
|
-
14. **Referral ID in metadata.** `FEE_PROJECT_ID` is stored in the first 32 bytes of mint metadata (via assembly `mstore`), allowing the fee terminal to track referrals.
|
|
141
|
-
15. **Deterministic deployment.** Hook salt is `keccak256(abi.encode(projectConfig.salt, msg.sender))` and sucker salt is `keccak256(abi.encode(suckerConfig.salt, msg.sender))`. Different callers with the same salt get different addresses.
|
|
142
|
-
16. **Default project weight.** `CTDeployer` deploys projects with `weight = 1_000_000 * 10^18`, ETH currency, and `maxCashOutTaxRate`. These defaults are hardcoded.
|
|
143
|
-
17. **ERC2771 meta-transaction support.** Both `CTPublisher` and `CTDeployer` support meta-transactions via `ERC2771Context` with a configurable trusted forwarder, allowing relayers to submit transactions on behalf of users.
|
|
144
|
-
|
|
145
|
-
## Example Integration
|
|
146
|
-
|
|
147
|
-
```solidity
|
|
148
|
-
import {ICTPublisher} from "@croptop/core-v6/src/interfaces/ICTPublisher.sol";
|
|
149
|
-
import {CTPost} from "@croptop/core-v6/src/structs/CTPost.sol";
|
|
150
|
-
import {IJB721TiersHook} from "@bananapus/721-hook-v6/src/interfaces/IJB721TiersHook.sol";
|
|
151
|
-
|
|
152
|
-
// --- Post content to a Croptop-enabled project ---
|
|
153
|
-
|
|
154
|
-
CTPost[] memory posts = new CTPost[](1);
|
|
155
|
-
posts[0] = CTPost({
|
|
156
|
-
encodedIPFSUri: 0x1234..., // encoded IPFS CID
|
|
157
|
-
totalSupply: 100,
|
|
158
|
-
price: 0.01 ether,
|
|
159
|
-
category: 1,
|
|
160
|
-
splitPercent: 0,
|
|
161
|
-
splits: new JBSplit[](0)
|
|
162
|
-
});
|
|
27
|
+
## Purpose
|
|
163
28
|
|
|
164
|
-
|
|
165
|
-
uint256 totalCost = 0.01 ether + (0.01 ether / 20);
|
|
29
|
+
Permissioned publishing layer for Juicebox 721 projects. Project owners define posting rules, publishers mint content as tiers through a 721 hook, Croptop routes fees, and the deployer can package the whole project shape in one transaction.
|
|
166
30
|
|
|
167
|
-
|
|
168
|
-
IJB721TiersHook(hookAddress),
|
|
169
|
-
posts,
|
|
170
|
-
msg.sender, // NFT beneficiary
|
|
171
|
-
msg.sender, // fee beneficiary
|
|
172
|
-
"", // additional pay metadata
|
|
173
|
-
"" // fee metadata
|
|
174
|
-
);
|
|
31
|
+
## Reference Files
|
|
175
32
|
|
|
176
|
-
|
|
33
|
+
- Open [`references/runtime.md`](./references/runtime.md) when you need publisher behavior, fee routing, data-hook forwarding, or the main invariants around posting criteria and tier reuse.
|
|
34
|
+
- Open [`references/operations.md`](./references/operations.md) when you need deployer behavior, burn-lock ownership implications, script breadcrumbs, or the common sources of stale assumptions.
|
|
177
35
|
|
|
178
|
-
|
|
179
|
-
owner: msg.sender,
|
|
180
|
-
projectConfig: CTProjectConfig({
|
|
181
|
-
terminalConfigurations: terminals,
|
|
182
|
-
projectUri: "ipfs://...",
|
|
183
|
-
allowedPosts: allowedPosts,
|
|
184
|
-
contractUri: "ipfs://...",
|
|
185
|
-
name: "My Collection",
|
|
186
|
-
symbol: "MYC",
|
|
187
|
-
salt: bytes32("my-project")
|
|
188
|
-
}),
|
|
189
|
-
suckerDeploymentConfiguration: CTSuckerDeploymentConfig({
|
|
190
|
-
deployerConfigurations: new JBSuckerDeployerConfig[](0),
|
|
191
|
-
salt: bytes32(0)
|
|
192
|
-
}),
|
|
193
|
-
controller: IJBController(controllerAddress)
|
|
194
|
-
});
|
|
36
|
+
## Working Rules
|
|
195
37
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
```
|
|
38
|
+
- Start in [`src/CTPublisher.sol`](./src/CTPublisher.sol) for posting-rule and fee behavior, but check [`src/CTDeployer.sol`](./src/CTDeployer.sol) when the bug might come from project shape or hook forwarding.
|
|
39
|
+
- Treat posting criteria, fee routing, and duplicate-content handling as treasury-sensitive and product-sensitive at the same time.
|
|
40
|
+
- If the task mentions project immutability or admin recovery, inspect [`src/CTProjectOwner.sol`](./src/CTProjectOwner.sol) before changing deployer or publisher code.
|
|
41
|
+
- When a bug looks like generic 721 issuance, confirm it is not actually in `nana-721-hook-v6`.
|