@croptop/core-v6 0.0.33 → 0.0.35

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,26 +1,27 @@
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>
7
- Architecture: [ARCHITECTURE.md](./ARCHITECTURE.md)
7
+ Architecture: [ARCHITECTURE.md](./ARCHITECTURE.md)
8
+ User journeys: [USER_JOURNEYS.md](./USER_JOURNEYS.md)
9
+ Skills: [SKILLS.md](./SKILLS.md)
10
+ Risks: [RISKS.md](./RISKS.md)
11
+ Administration: [ADMINISTRATION.md](./ADMINISTRATION.md)
12
+ Audit instructions: [AUDIT_INSTRUCTIONS.md](./AUDIT_INSTRUCTIONS.md)
8
13
 
9
14
  ## Overview
10
15
 
11
16
  Croptop is built around three ideas:
12
17
 
13
- - 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
14
19
  - 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
20
+ - a one-click deployer can create a full Juicebox project, its 721 hook config, and its posting rules in one transaction
16
21
 
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.
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.
20
23
 
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`.
22
-
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.
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.
24
25
 
25
26
  ## Key Contracts
26
27
 
@@ -28,16 +29,46 @@ If a bug looks like ordinary tier issuance or terminal accounting, start in the
28
29
  | --- | --- |
29
30
  | `CTPublisher` | Validates posts, adjusts 721 tiers, mints the first copy, and routes protocol and project payments. |
30
31
  | `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. |
32
+ | `CTProjectOwner` | Ownership sink that can permanently hold a project NFT while still delegating the posting permissions Croptop needs. |
32
33
 
33
34
  ## Mental Model
34
35
 
35
36
  There are two separate concerns here:
36
37
 
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
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
40
+
41
+ Many Croptop bugs are really deployment-shape bugs or posting-policy bugs, not generic 721 bugs.
42
+
43
+ ## Read These Files First
44
+
45
+ 1. `src/CTPublisher.sol`
46
+ 2. `src/CTDeployer.sol`
47
+ 3. `src/CTProjectOwner.sol`
48
+ 4. `test/CTPublisher.t.sol`
49
+ 5. `test/ClaimCollectionOwnership.t.sol`
50
+
51
+ ## High-Signal Tests
52
+
53
+ 1. `test/CTPublisher.t.sol`
54
+ 2. `test/CTDeployer.t.sol`
55
+ 3. `test/ClaimCollectionOwnership.t.sol`
56
+ 4. `test/audit/FeeFallbackBlackhole.t.sol`
57
+ 5. `test/regression/DuplicateUriFeeEvasion.t.sol`
39
58
 
40
- That distinction matters because many "Croptop bugs" are deployment-shape bugs rather than publishing-rule bugs.
59
+ ## Integration Traps
60
+
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
65
+
66
+ ## Where State Lives
67
+
68
+ - posting criteria and publish-side enforcement live in `CTPublisher`
69
+ - deployment-time project wiring lives in `CTDeployer`
70
+ - ownership-sink behavior lives in `CTProjectOwner`
71
+ - actual tier issuance and treasury accounting still live in sibling Juicebox repos
41
72
 
42
73
  ## Install
43
74
 
@@ -62,10 +93,7 @@ Useful scripts:
62
93
 
63
94
  ## Deployment Notes
64
95
 
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.
66
-
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.
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.
69
97
 
70
98
  ## Repository Layout
71
99
 
@@ -87,9 +115,13 @@ script/
87
115
  ## Risks And Notes
88
116
 
89
117
  - 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
- - after burn-locking into `CTProjectOwner`, the previous owner no longer holds the project NFT directly; control is
94
- intentionally mediated through Croptop's owner helper and hook-admin surface instead of remaining a plain owner EOA
95
- - 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
122
+
123
+ ## For AI Agents
124
+
125
+ - Do not describe Croptop as a generic 721 marketplace.
126
+ - Read `CTPublisher` before `CTDeployer` when the question is about publish eligibility or fee behavior.
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,88 +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
- - **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.
26
- - **JBDirectory / Terminal resolution.** `CTPublisher.mintFrom` resolves terminals via `DIRECTORY.primaryTerminalOf()`. A compromised directory could redirect payment and fee flows.
27
- - **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.
28
28
 
29
- ## 2. Economic / Manipulation Risks
29
+ ## 2. Economic And Manipulation Risks
30
30
 
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.
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).
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.
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.
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.
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.
36
36
 
37
37
  ## 3. Access Control
38
38
 
39
- - **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.
40
- - **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.
41
- - **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.
42
- - **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.
43
- - **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.
44
44
 
45
45
  ## 4. DoS Vectors
46
46
 
47
- - **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.
48
- - **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.
49
- - **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.
50
- - **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.
51
51
 
52
52
  ## 5. Reentrancy Surface
53
53
 
54
- - **`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.
55
- - **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`.
56
- - **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.
57
56
 
58
57
  ## 6. Integration Risks
59
58
 
60
- - **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.
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.
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.
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.
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.
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.
65
65
 
66
66
  ## 7. Accepted Behaviors
67
67
 
68
- ### 7.1 O(n^2) duplicate detection in `_setupPosts` (bounded by practical limits)
69
-
70
- `_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.
71
-
72
- ### 7.2 Tier ID prediction assumes no concurrent transactions
68
+ ### 7.1 O(n^2) duplicate detection is accepted
73
69
 
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.
70
+ Duplicate detection within a batch is quadratic, but expected real-world batch sizes are small enough that this tradeoff is acceptable.
75
71
 
76
- ### 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
77
73
 
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`.
74
+ This is a known race. The mitigation is application-layer ordering and the fact that a bad prediction reverts the whole batch cleanly.
82
75
 
83
- ## 8. Invariants to Verify
76
+ ### 7.3 Project owners can bypass the publisher path while they still have direct hook permissions
84
77
 
85
- - `tierIdForEncodedIPFSUriOf[hook][encodedIPFSUri]` is set exactly once per (hook, encodedIPFSUri) pair and points to a valid, non-removed tier.
86
- - `totalPrice` accumulated in `_setupPosts` equals the sum of prices for all posts (new tier price for new posts, existing tier price for existing posts).
87
- - Fee amount: `msg.value - payValue == totalPrice / FEE_DIVISOR` (within 19 wei rounding).
88
- - For every configured category, `minimumTotalSupply <= maximumTotalSupply` and `minimumTotalSupply > 0`.
89
- - Packed allowance encoding/decoding round-trips correctly for all valid input ranges.
90
- - After `CTDeployer.deployProjectFor`, the project NFT is owned by `owner`, and `dataHookOf[projectId]` is the deployed 721 hook.
91
- - `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 jump into the publisher, deployer, or owner contract depending on which path the user is actually asking about.
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
 
@@ -13,7 +13,9 @@
13
13
  | Publishing and metadata behavior | [`src/CTPublisher.sol`](./src/CTPublisher.sol) |
14
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
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/) |
16
+ | Runtime and operational invariants | [`references/runtime.md`](./references/runtime.md), [`references/operations.md`](./references/operations.md) |
17
+ | Publishing, metadata, and attack coverage | [`test/CTPublisher.t.sol`](./test/CTPublisher.t.sol), [`test/Test_MetadataGeneration.t.sol`](./test/Test_MetadataGeneration.t.sol), [`test/CroptopAttacks.t.sol`](./test/CroptopAttacks.t.sol) |
18
+ | Deployment, ownership, and fork coverage | [`test/CTDeployer.t.sol`](./test/CTDeployer.t.sol), [`test/CTProjectOwner.t.sol`](./test/CTProjectOwner.t.sol), [`test/ClaimCollectionOwnership.t.sol`](./test/ClaimCollectionOwnership.t.sol), [`test/Fork.t.sol`](./test/Fork.t.sol), [`test/TestAuditGaps.sol`](./test/TestAuditGaps.sol) |
17
19
 
18
20
  ## Repo Map
19
21
 
@@ -30,12 +32,15 @@ Permissioned publishing layer for Juicebox 721 projects. Project owners define p
30
32
 
31
33
  ## Reference Files
32
34
 
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.
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.
35
37
 
36
38
  ## Working Rules
37
39
 
38
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.
39
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 allowlists, supply bounds, or split caps change what can be published.
40
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 simple formatting issue.
45
+ - Duplicate-post and tier-reuse behavior are runtime semantics, not convenience logic.
41
46
  - When a bug looks like generic 721 issuance, confirm it is not actually in `nana-721-hook-v6`.
package/USER_JOURNEYS.md CHANGED
@@ -1,56 +1,132 @@
1
1
  # User Journeys
2
2
 
3
- ## Who This Repo Serves
3
+ ## Repo Purpose
4
4
 
5
- - project owners turning a Juicebox 721 project into a publishing marketplace
6
- - publishers creating or reusing NFT tiers as posts
7
- - operators locking project administration into Croptop-specific ownership patterns
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.
6
+
7
+ ## Primary Actors
8
+
9
+ - project owners creating a Croptop publishing surface
10
+ - publishers minting posts into an existing Croptop project
11
+ - auditors reviewing fee routing, posting policy, and owner-lock semantics
12
+
13
+ ## Key Surfaces
14
+
15
+ - `CTPublisher`: validates posts, adjusts tiers, mints the first copy, and routes Croptop fees
16
+ - `CTDeployer`: launches a Croptop-shaped project and can compose omnichain deployment
17
+ - `CTProjectOwner`: owner helper that can burn-lock administration into Croptop
18
+ - `mintFrom(...)`: main publishing entrypoint
8
19
 
9
20
  ## Journey 1: Turn A Project Into A Croptop Publisher
10
21
 
11
- **Starting state:** a project already exists or is about to launch, and the owner wants category-level posting rules.
22
+ **Actor:** project owner.
23
+
24
+ **Intent:** install Croptop publishing policy on a project.
25
+
26
+ **Preconditions**
27
+
28
+ - the project already exists or will be launched through `CTDeployer`
29
+ - the owner has chosen category rules and the expected 721 hook shape
30
+
31
+ **Main Flow**
32
+
33
+ 1. Configure category-level constraints such as price floor, supply, splits, and allowlists.
34
+ 2. Install or verify the expected 721 hook setup.
35
+ 3. Route publishing through Croptop so future posts are policy-checked instead of free-form tier edits.
12
36
 
13
- **Success:** Croptop posting criteria are installed and future posts must satisfy them.
37
+ **Failure Modes**
14
38
 
15
- **Flow**
16
- 1. Configure category-level posting constraints such as price floor, supply bounds, split limits, and optional allowlists.
17
- 2. Install or verify the 721 hook shape the project expects.
18
- 3. Route the project through Croptop's publisher logic so future post creation is policy-checked instead of free-form tier editing.
39
+ - category rules do not match the intended publishing product
40
+ - teams assume Croptop replaces the need to audit the underlying 721 hook
41
+
42
+ **Postconditions**
43
+
44
+ - the project now routes publishing through Croptop policy instead of direct free-form tier creation
19
45
 
20
46
  ## Journey 2: Publish Content Into An Existing Croptop Project
21
47
 
22
- **Starting state:** a publisher has a post that satisfies the target project's posting rules.
48
+ **Actor:** publisher.
49
+
50
+ **Intent:** publish one post into a Croptop project and mint the first copy.
51
+
52
+ **Preconditions**
53
+
54
+ - the post satisfies the target project's category policy
55
+ - the caller can receive ETH if the fee refund fallback is needed
56
+ - duplicate-content and stale-tier implications are understood
57
+
58
+ **Main Flow**
59
+
60
+ 1. Call `mintFrom(...)` with the content URI and pricing data.
61
+ 2. `CTPublisher` validates the post against category and fee policy.
62
+ 3. It creates or reuses the underlying tier, mints the first copy, and routes project revenue plus the Croptop fee.
63
+
64
+ **Failure Modes**
23
65
 
24
- **Success:** the post becomes a valid 721 tier and the first mint settles correctly.
66
+ - duplicate URIs or stale tier mappings
67
+ - publisher inputs satisfy the base 721 hook but violate Croptop's stricter rules
68
+ - the fee terminal rejects the fee payment and `_msgSender()` cannot receive the refund
25
69
 
26
- **Flow**
27
- 1. The publisher calls `mintFrom(...)` or the equivalent publishing surface with the content URI and pricing data.
28
- 2. `CTPublisher` checks the post against category rules and fee policy.
29
- 3. It creates or reuses the underlying 721 tier, mints the first copy, and routes both project revenue and the Croptop fee. If the fee terminal is unavailable, the fee is refunded to `_msgSender()` instead.
70
+ **Postconditions**
30
71
 
31
- **Failure cases that matter:** duplicate URIs, split configurations that evade fees, stale tier mappings, publisher inputs that satisfy the 721 hook but violate Croptop's stricter publishing rules, and callers that cannot receive ETH when a fee refund fallback is needed.
72
+ - the post is minted or reused as a tier under Croptop policy and the fee path is accounted for
32
73
 
33
74
  ## Journey 3: Launch A New Croptop Project End To End
34
75
 
35
- **Starting state:** the product wants a fresh project that already has Croptop deployment choices baked in.
76
+ **Actor:** product team or deployer.
36
77
 
37
- **Success:** one deployment flow launches the project, wires the 721 hook, and installs the initial posting rules.
78
+ **Intent:** launch a project already wired for Croptop publishing.
38
79
 
39
- **Flow**
40
- 1. Use `CTDeployer` with project config, posting rules, and any omnichain deployment config.
41
- 2. The deployer launches the Juicebox project, configures the Croptop-specific owner model, and wires in publisher behavior.
42
- 3. The project is ready for publishers without a manual post-launch setup phase.
80
+ **Preconditions**
81
+
82
+ - the team has project config, posting rules, and any omnichain requirements ready
83
+ - the correct `FEE_PROJECT_ID` is known
84
+
85
+ **Main Flow**
86
+
87
+ 1. Use `CTDeployer` with project config, posting rules, and optional omnichain config.
88
+ 2. The deployer launches the project, configures Croptop ownership assumptions, and wires publisher behavior.
89
+ 3. The resulting project is ready for publishers without a manual post-launch setup gap.
90
+
91
+ **Failure Modes**
92
+
93
+ - the fee project is misconfigured or omitted
94
+ - teams treat `CTDeployer` as packaging only and miss its policy implications
95
+
96
+ **Postconditions**
97
+
98
+ - the project is ready for Croptop publishers without a post-launch wiring gap
43
99
 
44
100
  ## Journey 4: Lock Administration Into Croptop's Owner Surface
45
101
 
46
- **Starting state:** the project should continue operating through Croptop's policy surface instead of ordinary project-owner discretion.
102
+ **Actor:** project owner.
103
+
104
+ **Intent:** keep governance inside Croptop's constrained owner surface.
47
105
 
48
- **Success:** the project's admin path is burn-locked or otherwise routed through `CTProjectOwner`.
106
+ **Preconditions**
49
107
 
50
- **Flow**
51
- 1. Transfer or configure ownership so Croptop's owner helper controls the relevant admin surface.
108
+ - the owner wants irreversible product-shaping constraints, not ordinary owner flexibility
109
+
110
+ **Main Flow**
111
+
112
+ 1. Transfer or configure ownership so `CTProjectOwner` controls the relevant admin surface.
52
113
  2. Restrict future edits to the paths Croptop intentionally exposes.
53
- 3. Accept that this is a product-shaping choice, not a cosmetic deployment detail.
114
+ 3. Accept that this is an ownership-model decision, not cosmetic packaging.
115
+
116
+ **Failure Modes**
117
+
118
+ - teams burn-lock before validating the publishing policy in production-like conditions
119
+ - reviewers miss that prior owner discretion no longer exists directly
120
+
121
+ **Postconditions**
122
+
123
+ - future administration is constrained to the Croptop owner surface instead of ordinary owner discretion
124
+
125
+ ## Trust Boundaries
126
+
127
+ - this repo is trusted for publishing policy and fee routing
128
+ - the underlying 721 hook remains trusted for tier issuance and lower-level NFT accounting
129
+ - Croptop fee behavior depends on the fee project and its terminal remaining correctly configured
54
130
 
55
131
  ## Hand-Offs
56
132
 
package/foundry.toml CHANGED
@@ -16,6 +16,8 @@ fail_on_revert = false
16
16
  [rpc_endpoints]
17
17
  ethereum = "${RPC_ETHEREUM_MAINNET}"
18
18
 
19
+ [lint]
20
+ exclude_lints = ["pascal-case-struct", "mixed-case-variable"]
19
21
  [fmt]
20
22
  number_underscore = "thousands"
21
23
  multiline_func_header = "all"
package/package.json CHANGED
@@ -1,33 +1,33 @@
1
1
  {
2
- "name": "@croptop/core-v6",
3
- "version": "0.0.33",
4
- "license": "MIT",
5
- "repository": {
6
- "type": "git",
7
- "url": "git+https://github.com/mejango/croptop-core-v6"
8
- },
9
- "scripts": {
10
- "test": "forge test",
11
- "coverage:integration": "forge coverage --match-path \"./src/*.sol\" --report lcov --report summary",
12
- "deploy:mainnets": "source ./.env && npx sphinx propose ./script/Deploy.s.sol --networks mainnets",
13
- "deploy:mainnets:project": "source ./.env && export START_TIME=$(date +%s) && npx sphinx propose ./script/ConfigureFeeProject.s.sol --networks mainnets",
14
- "deploy:testnets": "source ./.env && npx sphinx propose ./script/Deploy.s.sol --networks testnets",
15
- "deploy:testnets:project": "source ./.env && export START_TIME=$(date +%s) && npx sphinx propose ./script/ConfigureFeeProject.s.sol --networks testnets",
16
- "artifacts": "source ./.env && npx sphinx artifacts --org-id 'ea165b21-7cdc-4d7b-be59-ecdd4c26bee4' --project-name 'croptop-core-v5'"
17
- },
18
- "dependencies": {
19
- "@bananapus/721-hook-v6": "^0.0.33",
20
- "@bananapus/buyback-hook-v6": "^0.0.27",
21
- "@bananapus/core-v6": "^0.0.34",
22
- "@bananapus/ownable-v6": "^0.0.17",
23
- "@bananapus/permission-ids-v6": "^0.0.17",
24
- "@bananapus/router-terminal-v6": "^0.0.26",
25
- "@bananapus/suckers-v6": "^0.0.25",
26
- "@openzeppelin/contracts": "^5.6.1"
27
- },
28
- "devDependencies": {
29
- "@bananapus/address-registry-v6": "^0.0.17",
30
- "@rev-net/core-v6": "^0.0.31",
31
- "@sphinx-labs/plugins": "^0.33.3"
32
- }
2
+ "name": "@croptop/core-v6",
3
+ "version": "0.0.35",
4
+ "license": "MIT",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/mejango/croptop-core-v6"
8
+ },
9
+ "scripts": {
10
+ "test": "forge test",
11
+ "coverage:integration": "forge coverage --match-path \"./src/*.sol\" --report lcov --report summary",
12
+ "deploy:mainnets": "source ./.env && npx sphinx propose ./script/Deploy.s.sol --networks mainnets",
13
+ "deploy:mainnets:project": "source ./.env && export START_TIME=$(date +%s) && npx sphinx propose ./script/ConfigureFeeProject.s.sol --networks mainnets",
14
+ "deploy:testnets": "source ./.env && npx sphinx propose ./script/Deploy.s.sol --networks testnets",
15
+ "deploy:testnets:project": "source ./.env && export START_TIME=$(date +%s) && npx sphinx propose ./script/ConfigureFeeProject.s.sol --networks testnets",
16
+ "artifacts": "source ./.env && npx sphinx artifacts --org-id 'ea165b21-7cdc-4d7b-be59-ecdd4c26bee4' --project-name 'croptop-core-v5'"
17
+ },
18
+ "dependencies": {
19
+ "@bananapus/721-hook-v6": "^0.0.35",
20
+ "@bananapus/buyback-hook-v6": "^0.0.27",
21
+ "@bananapus/core-v6": "^0.0.34",
22
+ "@bananapus/ownable-v6": "^0.0.17",
23
+ "@bananapus/permission-ids-v6": "^0.0.17",
24
+ "@bananapus/router-terminal-v6": "^0.0.26",
25
+ "@bananapus/suckers-v6": "^0.0.25",
26
+ "@openzeppelin/contracts": "^5.6.1"
27
+ },
28
+ "devDependencies": {
29
+ "@bananapus/address-registry-v6": "^0.0.17",
30
+ "@rev-net/core-v6": "^0.0.32",
31
+ "@sphinx-labs/plugins": "^0.33.3"
32
+ }
33
33
  }
@@ -21,5 +21,5 @@
21
21
 
22
22
  ## Useful Proof Points
23
23
 
24
- - [`test/fork/`](../test/fork/) when deployment shape matters.
25
- - [`script/helpers/`](../script/helpers/) if the issue is really script/config assembly.
24
+ - [`test/Fork.t.sol`](../test/Fork.t.sol) when deployment shape matters.
25
+ - [`script/ConfigureFeeProject.s.sol`](../script/ConfigureFeeProject.s.sol) if the issue is really script/config assembly.
@@ -22,6 +22,6 @@
22
22
 
23
23
  ## Tests To Trust First
24
24
 
25
- - [`test/regression/`](../test/regression/) for pinned content and tier edge cases.
26
- - [`test/fork/`](../test/fork/) for live integration assumptions.
27
- - [`test/`](../test/) broadly when the issue could be in publisher or deployer behavior rather than one isolated function.
25
+ - [`test/CTPublisher.t.sol`](../test/CTPublisher.t.sol) and [`test/Test_MetadataGeneration.t.sol`](../test/Test_MetadataGeneration.t.sol) for content and metadata behavior.
26
+ - [`test/CTDeployer.t.sol`](../test/CTDeployer.t.sol) and [`test/Fork.t.sol`](../test/Fork.t.sol) for live deployment assumptions.
27
+ - [`test/CroptopAttacks.t.sol`](../test/CroptopAttacks.t.sol) and [`test/TestAuditGaps.sol`](../test/TestAuditGaps.sol) when the issue could be in publisher or deployer behavior rather than one isolated function.
@@ -27,7 +27,6 @@ contract CTPublisher is JBPermissioned, ERC2771Context, ICTPublisher {
27
27
  // --------------------------- custom errors ------------------------- //
28
28
  //*********************************************************************//
29
29
 
30
- // forge-lint: disable-next-line(mixed-case-variable)
31
30
  error CTPublisher_DuplicatePost(bytes32 encodedIPFSUri);
32
31
  error CTPublisher_EmptyEncodedIPFSUri();
33
32
  error CTPublisher_InsufficientEthSent(uint256 expected, uint256 sent);
@@ -66,7 +65,6 @@ contract CTPublisher is JBPermissioned, ERC2771Context, ICTPublisher {
66
65
  /// @notice The ID of the tier that an IPFS metadata has been saved to.
67
66
  /// @custom:param hook The hook for which the tier ID applies.
68
67
  /// @custom:param encodedIPFSUri The IPFS URI.
69
- // forge-lint: disable-next-line(mixed-case-variable)
70
68
  mapping(address hook => mapping(bytes32 encodedIPFSUri => uint256)) public override tierIdForEncodedIPFSUriOf;
71
69
 
72
70
  //*********************************************************************//
@@ -324,7 +322,6 @@ contract CTPublisher is JBPermissioned, ERC2771Context, ICTPublisher {
324
322
  /// is returned.
325
323
  function tiersFor(
326
324
  address hook,
327
- // forge-lint: disable-next-line(mixed-case-variable)
328
325
  bytes32[] memory encodedIPFSUris
329
326
  )
330
327
  external
@@ -332,7 +329,6 @@ contract CTPublisher is JBPermissioned, ERC2771Context, ICTPublisher {
332
329
  override
333
330
  returns (JB721Tier[] memory tiers)
334
331
  {
335
- // forge-lint: disable-next-line(mixed-case-variable)
336
332
  uint256 numberOfEncodedIPFSUris = encodedIPFSUris.length;
337
333
 
338
334
  // Initialize the tier array being returned.