@croptop/core-v6 0.0.34 → 0.0.36

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/ADMINISTRATION.md CHANGED
@@ -7,7 +7,7 @@
7
7
  | Scope | Croptop deployment flow, publish-policy administration, and irreversible project owner sink behavior |
8
8
  | Control posture | Mixed deployer-managed and project-local control |
9
9
  | Highest-risk actions | Burn-locking a project into `CTProjectOwner`, misconfiguring posting criteria, and deploying suckers with the wrong authority assumptions |
10
- | Recovery posture | Posting policy can often be changed, but burn-lock and some deployer wiring choices require replacement flows |
10
+ | Recovery posture | Posting policy can often be changed, but burn-lock and some deployer wiring choices usually require replacement flows |
11
11
 
12
12
  ## Purpose
13
13
 
@@ -30,7 +30,7 @@
30
30
  | Hook owner | `JBOwnable(hook).owner()` | Per hook | Often resolves to the project owner after claim |
31
31
  | `CTDeployer` | Immutable singleton | Global | Launch helper and runtime wrapper |
32
32
  | `CTPublisher` | Immutable singleton | Global runtime surface | Needs `ADJUST_721_TIERS` authority on relevant hooks |
33
- | `CTProjectOwner` | Receives project NFT transfer | Per project | Burn-lock path; no return function |
33
+ | `CTProjectOwner` | Receives project NFT transfer | Per project | Burn-lock path with no return function |
34
34
  | `SUCKER_REGISTRY` | Immutable dependency | Global | Holds wildcard `MAP_SUCKER_TOKEN` from the deployer |
35
35
 
36
36
  ## Privileged Surfaces
@@ -44,10 +44,10 @@
44
44
  | `CTPublisher` | `mintFrom(...)` | Anyone subject to policy | Publishes posts, mints first copies, and routes the Croptop fee |
45
45
  | `CTProjectOwner` | `onERC721Received(...)` | Any project NFT transfer into it | Locks the project into the Croptop owner helper and grants `CTPublisher` tier-adjust authority |
46
46
 
47
- The important nuance is:
47
+ Important nuance:
48
48
 
49
49
  - after `deployProjectFor(...)`, the initial project owner can directly manage tiers, metadata, minting, and discount percent through permissions granted from `CTDeployer`
50
- - this means the owner can bypass the publisher path until ownership is claimed away from `CTDeployer`
50
+ - that means the owner can bypass the publisher path until ownership is claimed away from `CTDeployer`
51
51
 
52
52
  ## Immutable And One-Way
53
53
 
@@ -58,7 +58,7 @@ The important nuance is:
58
58
 
59
59
  ## Operational Notes
60
60
 
61
- - Validate posting criteria before broad publisher access; the publisher enforces those rules on every post.
61
+ - Validate posting criteria before broad publisher access.
62
62
  - Decide intentionally whether the project should keep the initial direct-management path or move to project-owned hook control with `claimCollectionOwnershipOf(...)`.
63
63
  - Use `claimCollectionOwnershipOf(...)` when the project should own the hook directly instead of relying on the deployer as the ownership bridge.
64
64
  - Treat the burn-lock path as governance finality, not convenience.
@@ -67,7 +67,7 @@ The important nuance is:
67
67
  ## Machine Notes
68
68
 
69
69
  - Do not treat `CTDeployer` as a passive script helper; it is also part of the live runtime path.
70
- - Treat `src/CTPublisher.sol`, `src/CTDeployer.sol`, and `src/CTProjectOwner.sol` as the minimum source set for control-plane crawling.
70
+ - Treat `src/CTPublisher.sol`, `src/CTDeployer.sol`, and `src/CTProjectOwner.sol` as the minimum source set for control-plane review.
71
71
  - If a project NFT has already been sent to `CTProjectOwner`, stop assuming the original owner can recover it.
72
72
 
73
73
  ## Recovery
@@ -78,7 +78,7 @@ The important nuance is:
78
78
 
79
79
  ## Admin Boundaries
80
80
 
81
- - Neither project owners nor Croptop can change the fixed Croptop fee divisor in `CTPublisher`.
81
+ - Neither project owners nor Croptop can change the fixed fee divisor in `CTPublisher`.
82
82
  - `CTPublisher` cannot trap fee ETH intentionally; failed fee-terminal payments refund `_msgSender()` or revert.
83
83
  - `CTProjectOwner` cannot return project ownership once it receives the NFT.
84
84
  - `CTDeployer` cannot later rewrite `dataHookOf[projectId]` through a setter.
package/ARCHITECTURE.md CHANGED
@@ -6,14 +6,14 @@
6
6
 
7
7
  ## System Overview
8
8
 
9
- `CTPublisher` is the runtime policy and fee-routing surface. `CTDeployer` is the launch-time wrapper that can package a project, its 721 hook configuration, posting rules, and optional omnichain setup in one transaction. `CTProjectOwner` is the irreversible ownership helper for projects that want Croptop-mediated administration instead of a plain owner EOA.
9
+ `CTPublisher` is the runtime policy and fee-routing surface. `CTDeployer` is the launch wrapper that can package a project, its 721 hook config, posting rules, and optional omnichain setup in one transaction. `CTProjectOwner` is the irreversible ownership helper for projects that want Croptop-mediated administration instead of a plain owner EOA.
10
10
 
11
11
  ## Core Invariants
12
12
 
13
13
  - A post can only be published if it satisfies the configured category, pricing, supply, split, and allowlist rules.
14
14
  - Publish fees must be computed from the call value, not from ambient contract balance.
15
- - `CTPublisher` must not trap fee funds. If the fee-project payment fails, the fee is refunded to `_msgSender()`, and if that refund fails the publish reverts.
16
- - Tier creation and minting must continue to respect `nana-721-hook-v6` invariants.
15
+ - `CTPublisher` must not trap fee funds. If fee-project payment fails, the fee is refunded to `_msgSender()`, and if that refund fails the publish reverts.
16
+ - Tier creation and minting must still respect `nana-721-hook-v6` invariants.
17
17
  - `CTDeployer` intentionally creates a temporary owner-bypass period before collection ownership is claimed away from the deployer.
18
18
  - `CTProjectOwner` is a burn-lock primitive, not a flexible admin panel.
19
19
 
@@ -30,7 +30,7 @@
30
30
 
31
31
  - Tier storage and minting semantics live in `nana-721-hook-v6`.
32
32
  - Terminal accounting and project ownership live in `nana-core-v6`.
33
- - When omnichain setup is enabled, this repo composes deployer patterns from `nana-suckers-v6` and `nana-omnichain-deployers-v6` instead of reimplementing them.
33
+ - When omnichain setup is enabled, this repo composes patterns from `nana-suckers-v6` and `nana-omnichain-deployers-v6`.
34
34
 
35
35
  ## Critical Flows
36
36
 
@@ -42,7 +42,7 @@ poster
42
42
  -> publisher validates each post against project policy
43
43
  -> publisher creates or reuses 721 tiers
44
44
  -> project terminal receives the publish payment
45
- -> fee project receives the fixed fee slice, or `_msgSender()` is refunded if that fee payment fails
45
+ -> fee project receives the fixed fee slice, or _msgSender() is refunded if that fee payment fails
46
46
  -> first copy of each post tier is minted to the poster
47
47
  ```
48
48
 
@@ -58,16 +58,16 @@ creator
58
58
 
59
59
  ## Accounting Model
60
60
 
61
- This repo does not define treasury accounting. Its critical economic logic is publish-fee routing and the mapping from valid post data to 721 tier creation or reuse.
61
+ This repo does not define treasury accounting. Its critical economic logic is publish-fee routing and the mapping from valid post data to tier creation or reuse.
62
62
 
63
- `CTPublisher` also relies on duplicate-content and pricing checks to stop fee evasion through batch composition or tier reuse. Those checks are part of economic correctness, not just content hygiene.
63
+ `CTPublisher` also relies on duplicate-content and pricing checks to stop fee evasion through batch composition or tier reuse.
64
64
 
65
65
  ## Security Model
66
66
 
67
67
  - Fee routing is liveness-first but still value-sensitive; fallback refunds must stay correct.
68
68
  - `CTDeployer` has a larger review surface than a normal deployer because it can also participate at runtime.
69
69
  - Croptop's product boundary is partly social: until collection ownership is claimed away from `CTDeployer`, the project owner can interact through the granted permissions rather than only through the publisher surface.
70
- - Posting policy bugs are product-level authorization bugs, not just metadata bugs.
70
+ - Posting-policy bugs are product-level authorization bugs, not just metadata bugs.
71
71
 
72
72
  ## Safe Change Guide
73
73
 
@@ -5,6 +5,7 @@ Croptop is a publishing layer on top of Juicebox projects and the tiered 721 sta
5
5
  ## Audit Objective
6
6
 
7
7
  Find issues that:
8
+
8
9
  - let publishers create or mint posts outside configured criteria
9
10
  - let users evade Croptop fees or route them incorrectly
10
11
  - grant fee-free or privileged cash-outs to the wrong actors
@@ -14,6 +15,7 @@ Find issues that:
14
15
  ## Scope
15
16
 
16
17
  In scope:
18
+
17
19
  - `src/CTPublisher.sol`
18
20
  - `src/CTDeployer.sol`
19
21
  - `src/CTProjectOwner.sol`
@@ -30,14 +32,16 @@ In scope:
30
32
  ## Security Model
31
33
 
32
34
  Croptop composes several subsystems:
35
+
33
36
  - `CTPublisher` enforces posting criteria, creates or adjusts tiers, and routes fees
34
37
  - `CTDeployer` launches projects and wires hooks, criteria, and ownership helpers
35
38
  - `CTProjectOwner` lets a project follow Croptop-specific admin rules instead of a fixed EOA
36
39
 
37
40
  Trust boundaries that matter:
41
+
38
42
  - project owners choose policy, but should not be able to bypass the policy they configured
39
43
  - fee recipients and external hooks may revert or reenter
40
- - sucker-based privileges must be limited to genuine omnichain components
44
+ - sucker-based privileges must stay limited to genuine omnichain components
41
45
 
42
46
  ## Roles And Privileges
43
47
 
@@ -52,7 +56,7 @@ Trust boundaries that matter:
52
56
 
53
57
  | Dependency | Assumption | What breaks if wrong |
54
58
  |------------|------------|----------------------|
55
- | `nana-721-hook-v6` | Tier state and tier adjustments match Croptop policy checks | Posting criteria and tier reuse safety break |
59
+ | `nana-721-hook-v6` | Tier state and tier adjustments match Croptop policy checks | Posting criteria and tier-reuse safety break |
56
60
  | `nana-core-v6` | Terminal and project routing are authentic | Fee routing and publish settlement drift |
57
61
  | `nana-ownable-v6` | Ownership helper resolves the intended admin | Projects can end up misowned or stranded |
58
62
  | `nana-suckers-v6` | Registry identifies genuine omnichain actors | Fee-free or privileged paths widen incorrectly |
@@ -68,7 +72,7 @@ Trust boundaries that matter:
68
72
  ## Attack Surfaces
69
73
 
70
74
  - publish and mint entrypoints
71
- - fee computation from user input versus on-chain state
75
+ - fee computation from user input versus onchain state
72
76
  - tier creation, adjustment, and reuse logic
73
77
  - deployer-mediated pay or cash-out data-hook behavior
74
78
  - permission grants during deployment and ownership transfer
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Croptop Core
2
2
 
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.
3
+ Croptop turns a Juicebox project with a 721 hook into a permissioned publishing marketplace. Project owners define posting rules, 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
6
  Site: <https://croptop.eth.limo>
@@ -15,17 +15,13 @@ Audit instructions: [AUDIT_INSTRUCTIONS.md](./AUDIT_INSTRUCTIONS.md)
15
15
 
16
16
  Croptop is built around three ideas:
17
17
 
18
- - project owners set category-level posting criteria such as price floors, supply bounds, split limits, and optional allowlists
18
+ - project owners set category-level posting rules such as price floors, supply bounds, split limits, and optional allowlists
19
19
  - publishers call `mintFrom` to create or reuse 721 tiers that represent their post
20
- - a one-click deployer can create a full Juicebox project, its 721 hook configuration, and its posting rules in a single transaction
20
+ - a one-click deployer can create a full Juicebox project, its 721 hook config, and its posting rules in one transaction
21
21
 
22
- Every mint collects a 5% Croptop fee unless the target project is itself the fee project. If the configured fee
23
- terminal rejects that fee payment, Croptop refunds the fee portion to `_msgSender()` and still lets the publish
24
- continue. If `_msgSender()` cannot receive ETH, the mint reverts.
22
+ Every mint collects a 5% Croptop fee unless the target project is itself the fee project. If the fee terminal rejects that fee payment, Croptop refunds the fee portion to `_msgSender()` and still lets the publish continue. If `_msgSender()` cannot receive ETH, the mint reverts.
25
23
 
26
- 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`.
27
-
28
- 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.
24
+ Use this repo when the product is permissioned publishing on top of a Juicebox project. Do not use it for plain 721 tier sales.
29
25
 
30
26
  ## Key Contracts
31
27
 
@@ -39,10 +35,10 @@ If a bug looks like ordinary tier issuance or terminal accounting, start in the
39
35
 
40
36
  There are two separate concerns here:
41
37
 
42
- 1. `CTPublisher` governs whether a post is allowed and how it becomes a tier
43
- 2. `CTDeployer` governs how a Croptop-flavored project is packaged and launched
38
+ 1. `CTPublisher` decides whether a post is allowed and how it becomes a tier
39
+ 2. `CTDeployer` decides how a Croptop-flavored project is packaged and launched
44
40
 
45
- That distinction matters because many "Croptop bugs" are deployment-shape bugs rather than publishing-rule bugs.
41
+ Many Croptop bugs are really deployment-shape bugs or posting-policy bugs, not generic 721 bugs.
46
42
 
47
43
  ## Read These Files First
48
44
 
@@ -62,10 +58,10 @@ That distinction matters because many "Croptop bugs" are deployment-shape bugs r
62
58
 
63
59
  ## Integration Traps
64
60
 
65
- - Croptop publishing policy is separate from ordinary 721 tier issuance, so readers often stop in the wrong repo
66
- - fee routing is part of the publish path and has fallback behavior that affects who must be able to receive ETH
67
- - `CTProjectOwner` intentionally changes the project's ownership shape and should be reviewed as part of the trust model
68
- - duplicate-content, stale-tier, and fee-evasion edge cases are first-class surfaces, not only UI concerns
61
+ - Croptop publishing policy is separate from ordinary 721 tier issuance
62
+ - fee routing is part of the publish path and its fallback behavior matters
63
+ - `CTProjectOwner` intentionally changes the ownership model and should be reviewed as part of the trust model
64
+ - duplicate-content, stale-tier, and fee-evasion edge cases are runtime behavior, not just UI concerns
69
65
 
70
66
  ## Where State Lives
71
67
 
@@ -97,10 +93,7 @@ Useful scripts:
97
93
 
98
94
  ## Deployment Notes
99
95
 
100
- Deployments are handled through Sphinx using the environments configured in the repo scripts. `CTDeployer` can also compose cross-chain sucker deployments when a nonzero sucker configuration is supplied for the target publishing project.
101
-
102
- The deploy script now expects an explicit nonzero `FEE_PROJECT_ID` for production-style deployments. It does not safely
103
- autodiscover a fee project by scanning existing project IDs.
96
+ Deployments are handled through Sphinx. `CTDeployer` can also compose cross-chain sucker deployments when a nonzero sucker configuration is supplied. The deploy script expects an explicit nonzero `FEE_PROJECT_ID` for production-style deployments.
104
97
 
105
98
  ## Repository Layout
106
99
 
@@ -122,15 +115,13 @@ script/
122
115
  ## Risks And Notes
123
116
 
124
117
  - posting criteria are only as safe as the project owner configures them
125
- - fee routing depends on the designated fee project remaining correctly configured; if its terminal rejects payments,
126
- Croptop refunds the fee to `_msgSender()` instead of trapping ETH in `CTPublisher`
127
- - parking a project in `CTProjectOwner` is intentionally irreversible in practice and should only be used when immutability is desired
128
- - after routing ownership into `CTProjectOwner`, the previous owner no longer holds the project NFT directly; control is
129
- intentionally mediated through Croptop's owner helper and hook-admin surface instead of remaining a plain owner EOA
130
- - duplicate-content and stale-tier edge cases are guarded by tests, but integrations should still treat metadata reuse carefully
118
+ - fee routing depends on the fee project staying correctly configured
119
+ - parking a project in `CTProjectOwner` is effectively irreversible
120
+ - after routing ownership into `CTProjectOwner`, the old owner no longer holds the project NFT directly
121
+ - duplicate-content and stale-tier edge cases are economically relevant, not cosmetic
131
122
 
132
123
  ## For AI Agents
133
124
 
134
- - Do not describe Croptop as a generic 721 marketplace; it is a rules-driven publishing layer on top of Juicebox.
125
+ - Do not describe Croptop as a generic 721 marketplace.
135
126
  - Read `CTPublisher` before `CTDeployer` when the question is about publish eligibility or fee behavior.
136
127
  - If the issue is basic tier minting or accounting, move to `nana-721-hook-v6` or `nana-core-v6`.
package/RISKS.md CHANGED
@@ -4,90 +4,75 @@ This file focuses on the publishing, fee-routing, and hook-composition risks tha
4
4
 
5
5
  ## How to use this file
6
6
 
7
- - Read `Priority risks` first to understand the failure modes with the highest user or treasury impact.
7
+ - Read `Priority risks` first.
8
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.
9
+ - Treat `Accepted Behaviors` and `Invariants to Verify` as the boundary between intentional tradeoffs and defects.
10
10
 
11
11
  ## Priority risks
12
12
 
13
13
  | Priority | Risk | Why it matters | Primary controls |
14
14
  |----------|------|----------------|------------------|
15
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
-
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. |
17
+ | P1 | Fee-path degradation without mint failure | The fee terminal is fail-open via try/catch, so publishing continues even if the fee project temporarily stops receiving revenue. | Terminal health monitoring, fallback-beneficiary handling, and explicit fee-routing checks. |
19
18
 
20
19
  ## 1. Trust Assumptions
21
20
 
22
- - **Trusted forwarder.** ERC-2771 `_msgSender()` is trusted in both CTPublisher and CTDeployer for permission checks, allowlist validation, and payment routing. A compromised forwarder can post as any allowed address, deploy projects as any owner, and redirect payments.
23
- - **CTDeployer as permanent data hook proxy.** `CTDeployer` sets itself as the data hook for projects it deploys. `dataHookOf[projectId]` is set once during `deployProjectFor` and has no setter to update it. If the underlying data hook needs to change, there is no mechanism to do so without redeploying.
24
- - **Sucker registry.** `CTDeployer.beforeCashOutRecordedWith` trusts `SUCKER_REGISTRY.isSuckerOf()` for 0% tax cashouts, same risk as the omnichain deployer.
25
- - **Sucker deployment is intended to be fail-open at launch time.** `deployProjectFor` calls `SUCKER_REGISTRY.deploySuckersFor` only when a non-zero deployment salt is configured, and the current deployment path is documented as allowing launch to continue on chains where the configured sucker deployer cascade cannot complete. Operators should not assume a successful Croptop launch implies omnichain support is live.
26
- - **CTProjectOwner as burn target.** Projects transferred to `CTProjectOwner` grant `ADJUST_721_TIERS` to `PUBLISHER`. The project NFT cannot be recovered -- this is intentional but irreversible.
27
- - **JBDirectory / Terminal resolution.** `CTPublisher.mintFrom` resolves terminals via `DIRECTORY.primaryTerminalOf()`. A compromised directory could redirect payment and fee flows.
28
- - **721 hook store.** `_setupPosts` calls `hook.STORE().tierOf()` and `hook.STORE().isTierRemoved()`. The store is trusted to return accurate tier data. A malicious hook returning a fake store can report manipulated prices, supply limits, and removal status, causing `_setupPosts` to miscalculate `totalPrice` or skip duplicate detection.
21
+ - **Trusted forwarder.** ERC-2771 `_msgSender()` is trusted in both publisher and deployer for permission checks, allowlists, and payment routing.
22
+ - **CTDeployer as permanent data-hook proxy.** `CTDeployer` sets itself as the data hook for projects it deploys. `dataHookOf[projectId]` is set once and has no setter.
23
+ - **Sucker registry.** `CTDeployer.beforeCashOutRecordedWith` trusts `SUCKER_REGISTRY.isSuckerOf()` for 0% tax cash outs.
24
+ - **Sucker deployment is fail-open at launch time.** Launch can continue on chains where the configured sucker deployer cascade cannot complete.
25
+ - **CTProjectOwner as burn target.** Projects transferred to `CTProjectOwner` cannot be recovered.
26
+ - **JBDirectory / terminal resolution.** `CTPublisher.mintFrom` trusts `DIRECTORY.primaryTerminalOf()`.
27
+ - **721 hook store.** `_setupPosts` trusts the hook store for tier state, removal checks, and prices.
29
28
 
30
- ## 2. Economic / Manipulation Risks
29
+ ## 2. Economic And Manipulation Risks
31
30
 
32
- - **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.
33
- - **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).
34
- - **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.
35
- - **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.
36
- - **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.
31
+ - **Fee evasion via duplicate posts across hooks.** Duplicate-content checks are keyed per hook, so the same URI can be reused across different hooks.
32
+ - **Fee calculation rounding.** Fee is `totalPrice / 20`, so integer division truncates small amounts.
33
+ - **Fee is computed from `msg.value`.** Force-sent ETH does not affect the fee calculation.
34
+ - **Fee terminal fallback refunds the caller.** If the fee project cannot accept the fee, Croptop refunds `_msgSender()`. Relayers or contracts that cannot receive ETH will make the mint revert.
35
+ - **Split percent manipulation.** Posters can direct large shares of tier revenue away from the project if `maximumSplitPercent` is configured high.
37
36
 
38
37
  ## 3. Access Control
39
38
 
40
- - **Allowlist is O(n) linear scan.** `_isAllowed` iterates the entire allowlist array. Acceptable for small lists but gas-expensive for large allowlists. No Merkle proof alternative.
41
- - **Categories cannot be disabled.** Once `configurePostingCriteriaFor` is called for a category, it can only be restricted by setting very high `minimumPrice` or `minimumTotalSupply`, but never fully removed.
42
- - **CTDeployer grants broad permissions.** Constructor grants `MAP_SUCKER_TOKEN` (wildcard, projectId=0) to sucker registry and `ADJUST_721_TIERS` (wildcard, projectId=0) to publisher. These permissions apply to ALL projects deployed by this CTDeployer instance.
43
- - **CTDeployer.deployProjectFor permission gap.** No explicit permission check -- anyone can call `deployProjectFor` and create a project. A griefer could deploy many projects with arbitrary owners.
44
- - **CTDeployer.claimCollectionOwnershipOf.** Only checks `PROJECTS.ownerOf(projectId) == _msgSender()`. No Juicebox permission check. If the project NFT is transferred, the new owner can claim collection ownership. After claiming, the project owner must grant CTPublisher the `ADJUST_721_TIERS` permission for the project so that `mintFrom()` continues to work — without this, all subsequent posts revert.
39
+ - **Allowlist is O(n).** `_isAllowed` linearly scans the full allowlist.
40
+ - **Categories cannot be disabled cleanly.** Once configured, a category can only be made impractical through stricter bounds.
41
+ - **CTDeployer grants broad permissions.** Wildcard permissions to the sucker registry and publisher apply to all projects deployed by that deployer instance.
42
+ - **`deployProjectFor` is permissionless for new projects.** Anyone can create a project with arbitrary owners.
43
+ - **`claimCollectionOwnershipOf` only checks current NFT ownership.** After claiming, the project owner must still grant `CTPublisher` the needed tier-adjust permission or publishing stops working.
45
44
 
46
45
  ## 4. DoS Vectors
47
46
 
48
- - **Large batch posts.** `_setupPosts` iterates all posts with O(n^2) duplicate detection (inner loop `j < i`). A batch of 100+ posts has quadratic gas growth.
49
- - **External hook calls in loops.** `_setupPosts` calls `hook.STORE().tierOf()` and `hook.STORE().isTierRemoved()` inside the post loop. A reverting or gas-expensive store blocks the entire mint.
50
- - **Terminal resolution failure.** If `DIRECTORY.primaryTerminalOf()` returns `address(0)` for the project or fee project, the `pay()` call will revert with a low-level error.
51
- - **adjustTiers revert.** `hook.adjustTiers()` can revert if tiers violate category ordering constraints or other hook-level rules. This blocks the entire `mintFrom` call.
47
+ - **Large batch posts.** `_setupPosts` does O(n^2) duplicate detection within a batch.
48
+ - **External hook calls in loops.** Tier-store calls inside the post loop can revert or become gas-heavy.
49
+ - **Terminal resolution failure.** If `DIRECTORY.primaryTerminalOf()` returns `address(0)`, payment calls revert.
50
+ - **`adjustTiers` revert.** Hook-level tier rules can block the whole `mintFrom` call.
52
51
 
53
52
  ## 5. Reentrancy Surface
54
53
 
55
- - **`mintFrom` external call chain.** `mintFrom` makes three categories of external calls: (1) `hook.adjustTiers()` to create new tiers, (2) `terminal.pay{value}()` to pay the project, (3) `feeTerminal.pay{value}()` to pay the fee project (wrapped in try-catch, with fallback to `feeBeneficiary.call` then `msg.sender.call`). The first `terminal.pay` can trigger pay hooks on the target project, which could call back into `CTPublisher`. However, `mintFrom` has no mutable state between the tier adjustment and the payment — `totalPrice` and `payValue` are computed from local variables before the external calls. A re-entrant `mintFrom` call would process independently.
56
- - **Fee payment ordering.** The fee is sent AFTER the main payment (line ordering in `mintFrom`). If the main payment's pay hook re-enters and calls `mintFrom` again, the fee for the first call has not yet been sent. This is safe because the fee is pre-computed from `msg.value` before the external call (`msg.value - payValue`), and each call independently computes its own fee from its own `msg.value`. Force-sent ETH (via selfdestruct) does not affect fee calculation since the fee is derived from `msg.value`, not `address(this).balance`. The fee terminal payment is wrapped in try-catch, so a reverting fee terminal does not block the mint — the fee falls back to `feeBeneficiary` then `msg.sender`.
57
- - **No `ReentrancyGuard`.** The publisher relies on independent local state per call. This is safe for the current implementation but fragile if mutable contract storage is added in future versions.
54
+ - **`mintFrom` external call chain.** The function calls into the hook and terminals. It currently relies on local-call state isolation rather than a `ReentrancyGuard`.
55
+ - **Fee payment ordering.** The fee is sent after the main payment. This is safe under the current `msg.value`-based accounting model, but future mutable storage in the publisher would make the surface riskier.
58
56
 
59
57
  ## 6. Integration Risks
60
58
 
61
- - **CTDeployer forwards pay/cashout calls to `dataHookOf` with null check.** `beforePayRecordedWith` and `beforeCashOutRecordedWith` check for a null `dataHookOf` and return defaults (context weight, empty specs) instead of reverting. If a non-null data hook reverts, payments/cashouts for the project are still blocked.
62
- - **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.
63
- - **Sucker support can be absent even when deployment requested it.** Launch does not treat successful sucker setup as part of the Croptop app's core publish flow. A project can come online with the publisher, hook, and main terminal flow active while no suckers were actually deployed yet. Monitoring and deployment tooling should verify the returned sucker set explicitly instead of inferring success from project launch.
64
- - **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.
65
- - **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.
66
- - **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.
59
+ - **Null data-hook forwarding in deployer.** `beforePayRecordedWith` and `beforeCashOutRecordedWith` return defaults when `dataHookOf` is null.
60
+ - **No hook migration path.** `dataHookOf` is written once and never updated.
61
+ - **Sucker support can be absent even when requested.** A launch can complete while omnichain support is still missing.
62
+ - **Tier ID prediction.** `_setupPosts` predicts new tier IDs ahead of the actual `adjustTiers` call.
63
+ - **CTProjectOwner accepts any project NFT.** Accidentally transferring a non-Croptop project there still grants publisher permissions.
64
+ - **Fee payment destination.** If the fee project changes terminal behavior incompatibly, mints fall back to refund or revert.
67
65
 
68
66
  ## 7. Accepted Behaviors
69
67
 
70
- ### 7.1 O(n^2) duplicate detection in `_setupPosts` (bounded by practical limits)
71
-
72
- `_setupPosts` uses an inner loop (`j < i`) to detect duplicate `encodedIPFSUri` values within a single batch. This is O(n^2) in the number of posts. For typical batch sizes (1-20 posts), gas cost is negligible (~2k gas per comparison). At 100 posts, the quadratic cost adds ~10M gas. The practical limit is ~150 posts per batch before approaching block gas limits. No mitigation is needed because: (1) the quadratic detection prevents duplicate NFT tiers which would corrupt tier ID tracking, (2) real-world posting batches are small (marketplace UX limits), and (3) the gas cost is borne by the poster, not the protocol.
73
-
74
- ### 7.2 Tier ID prediction assumes no concurrent transactions
68
+ ### 7.1 O(n^2) duplicate detection is accepted
75
69
 
76
- `_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.
70
+ Duplicate detection within a batch is quadratic, but expected real-world batch sizes are small enough that this tradeoff is acceptable.
77
71
 
78
- ### 7.3 Project owners can bypass the publisher surface while they retain direct hook permissions
72
+ ### 7.2 Tier ID prediction assumes no concurrent tier writes
79
73
 
80
- `CTDeployer.deployProjectFor` intentionally grants the initial owner/operator enough hook permissions to manage the
81
- collection directly. That means the owner can bypass `CTPublisher`'s policy and fee path until ownership is moved into
82
- another authority surface or those permissions are narrowed. This is an accepted product tradeoff and should be treated
83
- as part of the trust model, not as a hidden invariant enforced by `CTPublisher`.
74
+ This is a known race. The mitigation is application-layer ordering and the fact that a bad prediction reverts the whole batch cleanly.
84
75
 
85
- ## 8. Invariants to Verify
76
+ ### 7.3 Project owners can bypass the publisher path while they still have direct hook permissions
86
77
 
87
- - `tierIdForEncodedIPFSUriOf[hook][encodedIPFSUri]` is set exactly once per (hook, encodedIPFSUri) pair and points to a valid, non-removed tier.
88
- - `totalPrice` accumulated in `_setupPosts` equals the sum of prices for all posts (new tier price for new posts, existing tier price for existing posts).
89
- - Fee amount: `msg.value - payValue == totalPrice / FEE_DIVISOR` (within 19 wei rounding).
90
- - For every configured category, `minimumTotalSupply <= maximumTotalSupply` and `minimumTotalSupply > 0`.
91
- - Packed allowance encoding/decoding round-trips correctly for all valid input ranges.
92
- - After `CTDeployer.deployProjectFor`, the project NFT is owned by `owner`, and `dataHookOf[projectId]` is the deployed 721 hook.
93
- - `CTProjectOwner` only grants `ADJUST_721_TIERS` permission, never broader permissions.
78
+ `CTDeployer.deployProjectFor` intentionally grants the initial owner enough hook permissions to manage the collection directly. That is part of the trust model until ownership is moved into a narrower surface.
package/SKILLS.md CHANGED
@@ -2,8 +2,8 @@
2
2
 
3
3
  ## Use This File For
4
4
 
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 decide whether the issue is posting-policy validation, tier reuse/content identity, deployer-packaged project shape, or burn-locked ownership. Those concerns interact, but they are not the same subsystem.
5
+ - Use this file when the task touches Croptop publishing, project deployment, data-hook forwarding, fee routing, or burn-locked ownership.
6
+ - Start here, then decide whether the issue is posting-policy validation, tier reuse and content identity, deployer-packaged project shape, or burn-locked ownership.
7
7
 
8
8
  ## Read This Next
9
9
 
@@ -32,15 +32,15 @@ Permissioned publishing layer for Juicebox 721 projects. Project owners define p
32
32
 
33
33
  ## Reference Files
34
34
 
35
- - 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.
36
- - 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.
35
+ - Open [`references/runtime.md`](./references/runtime.md) for publisher behavior, fee routing, data-hook forwarding, and the main invariants around posting criteria and tier reuse.
36
+ - Open [`references/operations.md`](./references/operations.md) for deployer behavior, burn-lock implications, script breadcrumbs, and common stale assumptions.
37
37
 
38
38
  ## Working Rules
39
39
 
40
40
  - 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.
41
41
  - Treat posting criteria, fee routing, and duplicate-content handling as treasury-sensitive and product-sensitive at the same time.
42
- - Category policy is part of the product surface. Changes to allowed addresses, supply bounds, or split caps alter what can be published, not just how it is paid for.
42
+ - Category policy is part of the product surface. Changes to allowlists, supply bounds, or split caps change what can be published.
43
43
  - If the task mentions project immutability or admin recovery, inspect [`src/CTProjectOwner.sol`](./src/CTProjectOwner.sol) before changing deployer or publisher code.
44
- - Metadata bugs can be publishing bugs, resolver-shape bugs, or duplicate-content bugs. Check all three before assuming a string-formatting issue.
45
- - Duplicate-post and tier-reuse behavior are first-class runtime semantics. Do not treat them like cacheable convenience logic.
44
+ - Metadata bugs can be publishing bugs, resolver-shape bugs, or duplicate-content bugs. Check all three before assuming a simple formatting issue.
45
+ - Duplicate-post and tier-reuse behavior are runtime semantics, not convenience logic.
46
46
  - When a bug looks like generic 721 issuance, confirm it is not actually in `nana-721-hook-v6`.
package/USER_JOURNEYS.md CHANGED
@@ -2,9 +2,7 @@
2
2
 
3
3
  ## Repo Purpose
4
4
 
5
- This repo turns a Juicebox 721 project into a permissioned publishing system.
6
- It owns post validation, Croptop fee routing, and the deployment packaging that turns a project into a Croptop-managed
7
- publisher. It does not own the base terminal accounting or the underlying 721 tier mechanics it wraps.
5
+ This repo turns a Juicebox 721 project into a permissioned publishing system. It owns post validation, Croptop fee routing, and the deployment packaging that turns a project into a Croptop-managed publisher. It does not own base terminal accounting or the underlying 721 tier mechanics.
8
6
 
9
7
  ## Primary Actors
10
8
 
@@ -17,7 +15,7 @@ publisher. It does not own the base terminal accounting or the underlying 721 ti
17
15
  - `CTPublisher`: validates posts, adjusts tiers, mints the first copy, and routes Croptop fees
18
16
  - `CTDeployer`: launches a Croptop-shaped project and can compose omnichain deployment
19
17
  - `CTProjectOwner`: owner helper that can burn-lock administration into Croptop
20
- - `mintFrom(...)`: main publishing entrypoint for new content
18
+ - `mintFrom(...)`: main publishing entrypoint
21
19
 
22
20
  ## Journey 1: Turn A Project Into A Croptop Publisher
23
21
 
@@ -26,20 +24,24 @@ publisher. It does not own the base terminal accounting or the underlying 721 ti
26
24
  **Intent:** install Croptop publishing policy on a project.
27
25
 
28
26
  **Preconditions**
27
+
29
28
  - the project already exists or will be launched through `CTDeployer`
30
29
  - the owner has chosen category rules and the expected 721 hook shape
31
30
 
32
31
  **Main Flow**
32
+
33
33
  1. Configure category-level constraints such as price floor, supply, splits, and allowlists.
34
34
  2. Install or verify the expected 721 hook setup.
35
35
  3. Route publishing through Croptop so future posts are policy-checked instead of free-form tier edits.
36
36
 
37
37
  **Failure Modes**
38
+
38
39
  - category rules do not match the intended publishing product
39
40
  - teams assume Croptop replaces the need to audit the underlying 721 hook
40
41
 
41
42
  **Postconditions**
42
- - the project now routes publishing through Croptop policy rather than direct free-form tier creation
43
+
44
+ - the project now routes publishing through Croptop policy instead of direct free-form tier creation
43
45
 
44
46
  ## Journey 2: Publish Content Into An Existing Croptop Project
45
47
 
@@ -48,21 +50,25 @@ publisher. It does not own the base terminal accounting or the underlying 721 ti
48
50
  **Intent:** publish one post into a Croptop project and mint the first copy.
49
51
 
50
52
  **Preconditions**
53
+
51
54
  - the post satisfies the target project's category policy
52
55
  - the caller can receive ETH if the fee refund fallback is needed
53
56
  - duplicate-content and stale-tier implications are understood
54
57
 
55
58
  **Main Flow**
59
+
56
60
  1. Call `mintFrom(...)` with the content URI and pricing data.
57
61
  2. `CTPublisher` validates the post against category and fee policy.
58
62
  3. It creates or reuses the underlying tier, mints the first copy, and routes project revenue plus the Croptop fee.
59
63
 
60
64
  **Failure Modes**
65
+
61
66
  - duplicate URIs or stale tier mappings
62
67
  - publisher inputs satisfy the base 721 hook but violate Croptop's stricter rules
63
68
  - the fee terminal rejects the fee payment and `_msgSender()` cannot receive the refund
64
69
 
65
70
  **Postconditions**
71
+
66
72
  - the post is minted or reused as a tier under Croptop policy and the fee path is accounted for
67
73
 
68
74
  ## Journey 3: Launch A New Croptop Project End To End
@@ -72,20 +78,24 @@ publisher. It does not own the base terminal accounting or the underlying 721 ti
72
78
  **Intent:** launch a project already wired for Croptop publishing.
73
79
 
74
80
  **Preconditions**
81
+
75
82
  - the team has project config, posting rules, and any omnichain requirements ready
76
- - the correct `FEE_PROJECT_ID` is known for deployment
83
+ - the correct `FEE_PROJECT_ID` is known
77
84
 
78
85
  **Main Flow**
86
+
79
87
  1. Use `CTDeployer` with project config, posting rules, and optional omnichain config.
80
88
  2. The deployer launches the project, configures Croptop ownership assumptions, and wires publisher behavior.
81
89
  3. The resulting project is ready for publishers without a manual post-launch setup gap.
82
90
 
83
91
  **Failure Modes**
92
+
84
93
  - the fee project is misconfigured or omitted
85
94
  - teams treat `CTDeployer` as packaging only and miss its policy implications
86
95
 
87
96
  **Postconditions**
88
- - the resulting project is ready for Croptop publishers without a post-launch wiring gap
97
+
98
+ - the project is ready for Croptop publishers without a post-launch wiring gap
89
99
 
90
100
  ## Journey 4: Lock Administration Into Croptop's Owner Surface
91
101
 
@@ -94,18 +104,22 @@ publisher. It does not own the base terminal accounting or the underlying 721 ti
94
104
  **Intent:** keep governance inside Croptop's constrained owner surface.
95
105
 
96
106
  **Preconditions**
107
+
97
108
  - the owner wants irreversible product-shaping constraints, not ordinary owner flexibility
98
109
 
99
110
  **Main Flow**
111
+
100
112
  1. Transfer or configure ownership so `CTProjectOwner` controls the relevant admin surface.
101
113
  2. Restrict future edits to the paths Croptop intentionally exposes.
102
114
  3. Accept that this is an ownership-model decision, not cosmetic packaging.
103
115
 
104
116
  **Failure Modes**
117
+
105
118
  - teams burn-lock before validating the publishing policy in production-like conditions
106
119
  - reviewers miss that prior owner discretion no longer exists directly
107
120
 
108
121
  **Postconditions**
122
+
109
123
  - future administration is constrained to the Croptop owner surface instead of ordinary owner discretion
110
124
 
111
125
  ## Trust Boundaries
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@croptop/core-v6",
3
- "version": "0.0.34",
3
+ "version": "0.0.36",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -37,6 +37,7 @@ contract CTPublisher is JBPermissioned, ERC2771Context, ICTPublisher {
37
37
  error CTPublisher_SplitPercentExceedsMaximum(uint256 splitPercent, uint256 maximumSplitPercent);
38
38
  error CTPublisher_TotalSupplyTooBig(uint256 totalSupply, uint256 maximumTotalSupply);
39
39
  error CTPublisher_TotalSupplyTooSmall(uint256 totalSupply, uint256 minimumTotalSupply);
40
+ error CTPublisher_NoPosts();
40
41
  error CTPublisher_UnauthorizedToPostInCategory();
41
42
  error CTPublisher_ZeroTotalSupply();
42
43
 
@@ -191,6 +192,9 @@ contract CTPublisher is JBPermissioned, ERC2771Context, ICTPublisher {
191
192
  payable
192
193
  override
193
194
  {
195
+ // Reject empty posts to prevent fee-free metadata shadowing.
196
+ if (posts.length == 0) revert CTPublisher_NoPosts();
197
+
194
198
  // Keep a reference to the amount being paid, which is msg.value minus the fee.
195
199
  uint256 payValue = msg.value;
196
200
 
@@ -0,0 +1,53 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity 0.8.28;
3
+
4
+ // forge-lint: disable-next-line(unaliased-plain-import)
5
+ import "forge-std/Test.sol";
6
+
7
+ import {IJBPermissions} from "@bananapus/core-v6/src/interfaces/IJBPermissions.sol";
8
+ import {IJBDirectory} from "@bananapus/core-v6/src/interfaces/IJBDirectory.sol";
9
+ import {IJB721TiersHook} from "@bananapus/721-hook-v6/src/interfaces/IJB721TiersHook.sol";
10
+
11
+ import {CTPublisher} from "../../src/CTPublisher.sol";
12
+ import {CTPost} from "../../src/structs/CTPost.sol";
13
+
14
+ /// @title M24_EmptyPostFeeBypass
15
+ /// @notice Verifies that calling mintFrom with an empty posts array reverts,
16
+ /// preventing fee-free metadata shadowing via additionalPayMetadata.
17
+ contract M24_EmptyPostFeeBypass is Test {
18
+ CTPublisher publisher;
19
+
20
+ IJBPermissions permissions = IJBPermissions(makeAddr("permissions"));
21
+ IJBDirectory directory = IJBDirectory(makeAddr("directory"));
22
+ address hookAddr = makeAddr("hook");
23
+ address poster = makeAddr("poster");
24
+
25
+ uint256 feeProjectId = 1;
26
+
27
+ function setUp() public {
28
+ publisher = new CTPublisher(directory, permissions, feeProjectId, address(0));
29
+ vm.deal(poster, 10 ether);
30
+ }
31
+
32
+ /// @notice mintFrom with empty posts should revert with CTPublisher_NoPosts.
33
+ function test_revert_emptyPostsArray() public {
34
+ CTPost[] memory emptyPosts = new CTPost[](0);
35
+
36
+ vm.prank(poster);
37
+ vm.expectRevert(CTPublisher.CTPublisher_NoPosts.selector);
38
+ publisher.mintFrom{value: 1 ether}(IJB721TiersHook(hookAddr), emptyPosts, poster, poster, "", "");
39
+ }
40
+
41
+ /// @notice mintFrom with empty posts and crafted additionalPayMetadata should still revert.
42
+ function test_revert_emptyPostsWithMetadata() public {
43
+ CTPost[] memory emptyPosts = new CTPost[](0);
44
+
45
+ // Attacker preloads additionalPayMetadata with hook mint metadata.
46
+ bytes memory craftedMetadata =
47
+ abi.encodePacked(bytes32(uint256(1)), bytes4(0xdeadbeef), uint256(32), uint256(1));
48
+
49
+ vm.prank(poster);
50
+ vm.expectRevert(CTPublisher.CTPublisher_NoPosts.selector);
51
+ publisher.mintFrom{value: 1 ether}(IJB721TiersHook(hookAddr), emptyPosts, poster, poster, craftedMetadata, "");
52
+ }
53
+ }