@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/README.md CHANGED
@@ -1,99 +1,43 @@
1
1
  # Croptop Core
2
2
 
3
- Permissioned NFT publishing for Juicebox projects -- anyone can post content as NFT tiers to a project's 721 hook, provided the posts meet criteria set by the project owner. A 5% fee is routed to a designated fee project on each mint.
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
- [Docs](https://docs.juicebox.money) | [Discord](https://discord.gg/juicebox) | [Croptop](https://croptop.eth.limo)
5
+ Docs: <https://docs.juicebox.money>
6
+ Site: <https://croptop.eth.limo>
7
+ Architecture: [ARCHITECTURE.md](./ARCHITECTURE.md)
6
8
 
7
- **Supported Chains:** Ethereum, Optimism, Base, Arbitrum (mainnets) and Ethereum Sepolia, Optimism Sepolia, Base Sepolia, Arbitrum Sepolia (testnets). See `script/Deploy.s.sol` for the Sphinx deployment configuration.
9
+ ## Overview
8
10
 
9
- ## Conceptual Overview
11
+ Croptop is built around three ideas:
10
12
 
11
- Croptop turns any Juicebox project with a 721 tiers hook into a permissioned content marketplace. Project owners define posting criteria -- minimum price, supply bounds, address allowlists -- and anyone who meets those criteria can publish new NFT tiers on the project. The poster's content becomes a mintable NFT tier, and the first copy is minted to them automatically.
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
- ### How It Works
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
- ### Burn-Lock Ownership
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
- `CTProjectOwner` provides an ownership burn-lock pattern. Transferring a project's NFT to this contract permanently locks ownership while granting `CTPublisher` tier-adjustment permissions -- making the project's configuration immutable except through Croptop posts.
25
+ ## Key Contracts
71
26
 
72
- ## Architecture
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
- | Contract | Description |
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
- ### Structs
35
+ There are two separate concerns here:
81
36
 
82
- | Struct | Purpose |
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
- ### Interfaces
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
- If using Forge directly:
48
+ ## Development
105
49
 
106
50
  ```bash
107
- forge install
51
+ npm install
52
+ forge build
53
+ forge test
108
54
  ```
109
55
 
110
- ## Develop
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
- | Command | Description |
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 # Core publishing engine (~590 lines)
123
- CTDeployer.sol # Project factory + data hook proxy (~433 lines)
124
- CTProjectOwner.sol # Burn-lock ownership helper (~84 lines)
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
- CTPublisher.t.sol # Unit tests (~865 lines, ~26 cases)
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 # Sphinx multi-chain deployment
152
- ConfigureFeeProject.s.sol # Fee project configuration
82
+ Deploy.s.sol
83
+ ConfigureFeeProject.s.sol
153
84
  helpers/
154
- CroptopDeploymentLib.sol # Deployment artifact reader
155
85
  ```
156
86
 
157
- ## Permissions
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
- - **Sucker registry compromise:** `CTDeployer.beforeCashOutRecordedWith` checks `SUCKER_REGISTRY.isSuckerOf` to grant fee-free cash outs. If the sucker registry is compromised, any address could cash out without tax.
173
- - **Fee skipping:** When `projectId == FEE_PROJECT_ID`, no fee is collected. This is intentional but means the fee project itself never pays Croptop fees.
174
- - **Allowlist scaling:** `_isAllowed()` uses linear scan over the address allowlist. Large allowlists (100+ addresses) increase gas costs proportionally.
175
- - **Tier reuse via IPFS URI:** If the same encoded IPFS URI has already been minted, the existing tier is reused rather than creating a new one. This prevents duplicate content but means a poster cannot create a second tier with the same content.
176
- - **Fee terminal failure:** The fee terminal payment is wrapped in try-catch. If the fee terminal reverts, the fee is sent to `feeBeneficiary` via low-level call, then to `msg.sender` if that also fails. A broken fee terminal never blocks mints, but the fee project loses revenue during the outage.
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
- # RISKS.md -- croptop-core-v6
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
- - **Try-catch fee payment.** The fee terminal payment is wrapped in try-catch. If the fee terminal reverts, the fee is sent to `feeBeneficiary` via low-level call. If that also fails, the fee is sent to `msg.sender`. This means a broken fee terminal does not block mints, but the fee project may lose fee revenue during the outage.
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, fee payments will fail. However, the fee terminal payment is wrapped in try-catch: on failure, the fee is sent to `feeBeneficiary` via low-level call, then to `msg.sender` if that also fails. Minting is never blocked by a broken fee terminal, but the fee project loses revenue during the outage.
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
- ## Purpose
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
- | Dependency | Import | Used For |
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
- ## Key Types
8
+ ## Read This Next
65
9
 
66
- | Struct | Key Fields | Used In |
67
- |--------|------------|---------|
68
- | `CTAllowedPost` | `hook`, `category` (uint24), `minimumPrice` (uint104), `minimumTotalSupply` (uint32), `maximumTotalSupply` (uint32), `maximumSplitPercent` (uint32), `allowedAddresses[]` | `configurePostingCriteriaFor` -- used when calling `CTPublisher` directly on an existing hook |
69
- | `CTDeployerAllowedPost` | Same fields as `CTAllowedPost` minus `hook` | `CTProjectConfig.allowedPosts` -- used during `deployProjectFor` because the hook address is not yet known; `CTDeployer._configurePostingCriteriaFor` fills in the `hook` field automatically after deploying the hook |
70
- | `CTPost` | `encodedIPFSUri` (bytes32), `totalSupply` (uint32), `price` (uint104), `category` (uint24), `splitPercent` (uint32), `splits[]` (JBSplit[]) | `mintFrom` |
71
- | `CTProjectConfig` | `terminalConfigurations`, `projectUri`, `allowedPosts` (CTDeployerAllowedPost[]), `contractUri`, `name`, `symbol`, `salt` | `deployProjectFor` |
72
- | `CTSuckerDeploymentConfig` | `deployerConfigurations` (JBSuckerDeployerConfig[]), `salt` | `deployProjectFor`, `deploySuckersFor` |
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
- ## Events
18
+ ## Repo Map
75
19
 
76
- | Event | When |
77
- |-------|------|
78
- | `ConfigurePostingCriteria(hook, allowedPost, caller)` | Posting criteria set or updated for a hook/category |
79
- | `Mint(projectId, hook, nftBeneficiary, feeBeneficiary, posts, postValue, txValue, caller)` | Posts published and first copies minted |
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
- ## Errors
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
- // Price + 5% fee
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
- publisher.mintFrom{value: totalCost}(
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
- // --- Deploy a new Croptop project ---
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
- (uint256 projectId, IJB721TiersHook hook) = deployer.deployProjectFor({
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
- // --- Lock ownership via CTProjectOwner ---
197
- // Transfer the project NFT to CTProjectOwner to burn-lock ownership
198
- // while keeping Croptop posting enabled.
199
- IERC721(projects).safeTransferFrom(msg.sender, address(projectOwner), projectId);
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`.