@croptop/core-v6 0.0.7 → 0.0.10

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.
@@ -0,0 +1,133 @@
1
+ # Administration
2
+
3
+ Admin privileges and their scope in croptop-core-v6.
4
+
5
+ ## Roles
6
+
7
+ ### 1. Project Owner
8
+
9
+ **How assigned:** Receives the JBProjects ERC-721 NFT for the project. Initially set by the `owner` parameter in `CTDeployer.deployProjectFor()` (line 325, `CTDeployer.sol`). Can be transferred via standard ERC-721 transfer.
10
+
11
+ **Scope:** Per-project. Controls posting rules and hook ownership for a single project.
12
+
13
+ ### 2. Hook Owner
14
+
15
+ **How assigned:** Determined by `JBOwnable(hook).owner()`. For CTDeployer-launched projects, the deployer initially owns the hook (via `DEPLOYER.deployHookFor()` at line 264). The project owner can later claim hook ownership via `claimCollectionOwnershipOf()` (line 223).
16
+
17
+ **Scope:** Per-hook. The hook owner (or anyone with `ADJUST_721_TIERS` permission for that hook's project) can configure posting criteria.
18
+
19
+ ### 3. CTDeployer (Contract)
20
+
21
+ **How assigned:** Immutable singleton deployed at construction. Acts as the `IJBRulesetDataHook` for all CTDeployer-launched projects (set at line 290 of `CTDeployer.sol`).
22
+
23
+ **Scope:** All Croptop-deployed projects. Proxies pay/cashout data hook calls, grants fee-free cashouts to suckers, and holds broad permissions on behalf of launched projects.
24
+
25
+ ### 4. CTPublisher (Contract)
26
+
27
+ **How assigned:** Immutable singleton deployed at construction. Receives `ADJUST_721_TIERS` permission from CTDeployer at construction (line 113-119, `CTDeployer.sol`).
28
+
29
+ **Scope:** All hooks for which it has `ADJUST_721_TIERS` permission. Creates NFT tiers and mints first copies.
30
+
31
+ ### 5. CTProjectOwner (Contract)
32
+
33
+ **How assigned:** Optional burn-lock proxy. Receives project ownership when a project NFT is `safeTransferFrom`'d to it.
34
+
35
+ **Scope:** Per-project. Grants `CTPublisher` permanent `ADJUST_721_TIERS` permission for the received project. Once the project is transferred here, human ownership is effectively burned.
36
+
37
+ ### 6. Sucker Registry
38
+
39
+ **How assigned:** Immutable dependency set at CTDeployer construction (line 98). Receives `MAP_SUCKER_TOKEN` permission at construction (line 101-110, `CTDeployer.sol`).
40
+
41
+ **Scope:** All projects deployed via CTDeployer. Can map tokens for cross-chain bridging. Determines which addresses get fee-free cashouts.
42
+
43
+ ### 7. Publishers (Poster Addresses)
44
+
45
+ **How assigned:** Either any address (when allowlist is empty) or explicitly added to a per-hook per-category allowlist via `configurePostingCriteriaFor()`.
46
+
47
+ **Scope:** Per-hook, per-category. Can create NFT tiers (posts) and mint first copies, subject to posting criteria.
48
+
49
+ ## Privileged Functions
50
+
51
+ ### CTDeployer
52
+
53
+ | Function | Required Role | Permission ID | Scope | What It Does |
54
+ |----------|--------------|---------------|-------|-------------|
55
+ | `deployProjectFor()` (line 243) | Anyone | None | Global | Deploys a new Juicebox project with 721 hook, configures posting rules, optionally deploys suckers, transfers ownership to `owner`. No access restriction -- anyone can deploy a project. |
56
+ | `claimCollectionOwnershipOf()` (line 223) | Project owner | None (direct `ownerOf` check) | Per-project | Transfers hook ownership to the project via `JBOwnable.transferOwnershipToProject()`. Caller must be `PROJECTS.ownerOf(projectId)`. |
57
+ | `deploySuckersFor()` (line 346) | Project owner or delegate | `JBPermissionIds.DEPLOY_SUCKERS` | Per-project | Deploys new cross-chain suckers for an existing project. Uses `_requirePermissionFrom()` against the project owner. |
58
+
59
+ ### CTPublisher
60
+
61
+ | Function | Required Role | Permission ID | Scope | What It Does |
62
+ |----------|--------------|---------------|-------|-------------|
63
+ | `configurePostingCriteriaFor()` (line 230) | Hook owner or delegate | `JBPermissionIds.ADJUST_721_TIERS` | Per-hook, per-category | Sets posting rules: minimum price, min/max supply, max split percent, address allowlist. Uses `_requirePermissionFrom()` against `JBOwnable(hook).owner()`. |
64
+ | `mintFrom()` (line 297) | Anyone (subject to allowlist) | None (enforced by allowlist in `_setupPosts`) | Per-hook | Publishes posts as 721 tiers, mints first copies, routes 5% fee to `FEE_PROJECT_ID`. Validates all posts against configured criteria. |
65
+
66
+ ### CTProjectOwner
67
+
68
+ | Function | Required Role | Permission ID | Scope | What It Does |
69
+ |----------|--------------|---------------|-------|-------------|
70
+ | `onERC721Received()` (line 47) | Anyone who transfers a JBProjects NFT | None | Per-project | On receiving a project NFT from `PROJECTS` (mint only, `from == address(0)` is NOT enforced here), grants `CTPublisher` the `ADJUST_721_TIERS` permission for that project. |
71
+
72
+ ### Permissions Granted at CTDeployer Construction
73
+
74
+ These permissions are set in the CTDeployer constructor and apply to all projects it will ever deploy (wildcard `projectId: 0`):
75
+
76
+ | Permission | Granted To | Line | Purpose |
77
+ |-----------|-----------|------|---------|
78
+ | `MAP_SUCKER_TOKEN` | `SUCKER_REGISTRY` | 101-110 | Allows the sucker registry to map tokens for cross-chain bridging on any project owned by CTDeployer. |
79
+ | `ADJUST_721_TIERS` | `PUBLISHER` (CTPublisher) | 113-119 | Allows CTPublisher to add tiers to any hook on any project owned by CTDeployer. |
80
+
81
+ ### Permissions Granted During `deployProjectFor()`
82
+
83
+ These permissions are set per-project during deployment (line 328-339, `CTDeployer.sol`):
84
+
85
+ | Permission | Granted To | Line | Purpose |
86
+ |-----------|-----------|------|---------|
87
+ | `ADJUST_721_TIERS` | `owner` | 329 | Allows the project owner to adjust 721 tiers. |
88
+ | `SET_721_METADATA` | `owner` | 330 | Allows the project owner to update 721 metadata. |
89
+ | `MINT_721` | `owner` | 331 | Allows the project owner to mint 721 tokens directly. |
90
+ | `SET_721_DISCOUNT_PERCENT` | `owner` | 332 | Allows the project owner to set tier discount percentages. |
91
+
92
+ ## Immutable Configuration
93
+
94
+ These values are set at deploy time and cannot be changed after deployment:
95
+
96
+ | Value | Contract | Set At | Description |
97
+ |-------|----------|--------|-------------|
98
+ | `FEE_DIVISOR` | CTPublisher | Compile time (constant = 20) | Fee percentage: 5% (1/20). Hardcoded, not configurable. |
99
+ | `FEE_PROJECT_ID` | CTPublisher | Constructor (immutable) | Project ID that receives all fees. Cannot be changed. |
100
+ | `DIRECTORY` | CTPublisher | Constructor (immutable) | JBDirectory for project/terminal lookups. |
101
+ | `PROJECTS` | CTDeployer | Constructor (immutable) | JBProjects NFT contract. |
102
+ | `DEPLOYER` | CTDeployer | Constructor (immutable) | JB721TiersHookDeployer for hook creation. |
103
+ | `PUBLISHER` | CTDeployer | Constructor (immutable) | CTPublisher contract reference. |
104
+ | `SUCKER_REGISTRY` | CTDeployer | Constructor (immutable) | Sucker registry for cross-chain bridging. |
105
+ | `PERMISSIONS` | CTDeployer, CTProjectOwner | Constructor (immutable) | JBPermissions contract for access control. |
106
+ | `trustedForwarder` | CTDeployer, CTPublisher | Constructor (immutable via ERC2771Context) | Meta-transaction trusted forwarder address. |
107
+ | `dataHookOf[projectId]` | CTDeployer | `deployProjectFor()` (line 307) | Once set during deployment, the data hook for a project can never be changed. Write-once storage. |
108
+ | Project weight | CTDeployer | `deployProjectFor()` (line 256) | Hardcoded at `1_000_000 * 10^18` with ETH base currency and max cashout tax rate. |
109
+ | Hook deploy salt | CTDeployer | `deployProjectFor()` (line 286) | `keccak256(abi.encode(salt, msg.sender))` -- deterministic but caller-specific. |
110
+
111
+ ## Admin Boundaries
112
+
113
+ What admins CANNOT do:
114
+
115
+ 1. **Project owners cannot change the fee rate.** `FEE_DIVISOR = 20` (5%) is a compile-time constant. No function exists to modify it.
116
+
117
+ 2. **Project owners cannot change the fee recipient.** `FEE_PROJECT_ID` is immutable. Fees always route to the same project.
118
+
119
+ 3. **Project owners cannot change the data hook.** `dataHookOf[projectId]` is write-once (set during `deployProjectFor`, no setter function). The data hook proxy pattern is permanent.
120
+
121
+ 4. **Project owners cannot disable Croptop posting entirely for a category.** `configurePostingCriteriaFor()` requires `minimumTotalSupply > 0` (line 250-252, `CTPublisher.sol`). The workaround is to set an astronomically high `minimumPrice` with `minimumTotalSupply = maximumTotalSupply = 1`. See finding NM-006.
122
+
123
+ 5. **Project owners cannot bypass posting criteria to mint directly through CTPublisher.** They must use `mintFrom()` like anyone else, which enforces all configured rules. However, owners can adjust tiers directly on the hook (bypassing CTPublisher) if they have `ADJUST_721_TIERS` permission.
124
+
125
+ 6. **CTPublisher cannot mint without paying.** `mintFrom()` requires `msg.value >= totalPrice + fee` (line 332-334). There is no free-mint path through CTPublisher.
126
+
127
+ 7. **CTProjectOwner cannot return project ownership.** Once a project NFT is transferred to CTProjectOwner, there is no function to transfer it back. Ownership is effectively burned.
128
+
129
+ 8. **No admin can modify existing tier prices.** Once a tier is created via `_setupPosts()`, the price is set in the `JB721TiersHookStore`. CTPublisher uses the stored price for fee calculation on subsequent mints (not `post.price`). See H-19 fix.
130
+
131
+ 9. **No admin can drain CTPublisher funds.** CTPublisher has no `withdraw()` function and no `receive()` / `fallback()`. The only ETH that enters the contract is during `mintFrom()` and it is fully routed to the project terminal and fee terminal within the same transaction.
132
+
133
+ 10. **Sucker registry trust is irrevocable.** The `MAP_SUCKER_TOKEN` permission is granted at CTDeployer construction with `projectId: 0` (wildcard). There is no function to revoke this permission from within CTDeployer.
@@ -0,0 +1,66 @@
1
+ # croptop-core-v6 — Architecture
2
+
3
+ ## Purpose
4
+
5
+ NFT publishing platform built on Juicebox V6. Allows permissioned posting of NFT content to Juicebox projects. CTDeployer creates projects pre-configured for content publishing, and CTPublisher manages the posting workflow with configurable rules (price floors, supply limits, category restrictions).
6
+
7
+ ## Contract Map
8
+
9
+ ```
10
+ src/
11
+ ├── CTDeployer.sol — Deploys Croptop projects with 721 hooks and publishing rules
12
+ ├── CTPublisher.sol — Manages permissioned NFT posting to projects
13
+ ├── CTProjectOwner.sol — Proxy owner for Croptop-deployed projects
14
+ ├── interfaces/
15
+ │ ├── ICTDeployer.sol
16
+ │ └── ICTPublisher.sol
17
+ └── structs/
18
+ ├── CTAllowedPost.sol — Rules for what can be posted
19
+ ├── CTDeployerAllowedPost.sol — Deployer-level post rules
20
+ ├── CTPost.sol — A post submission
21
+ ├── CTProjectConfig.sol — Project configuration
22
+ └── CTSuckerDeploymentConfig.sol — Cross-chain config
23
+ ```
24
+
25
+ ## Key Data Flows
26
+
27
+ ### Project Deployment
28
+ ```
29
+ Creator → CTDeployer.deployProjectFor()
30
+ → Launch JB project via JB721TiersHookProjectDeployer
31
+ → Configure CTDeployer as data hook
32
+ → Set allowed post rules (price floors, supply limits, categories)
33
+ → Transfer project ownership to CTProjectOwner proxy
34
+ ```
35
+
36
+ ### Content Publishing
37
+ ```
38
+ Publisher → CTPublisher.postFor(projectId, posts[])
39
+ → For each post:
40
+ → Validate against allowed post rules
41
+ → Check category permissions
42
+ → Check price >= minimum
43
+ → Check supply within bounds
44
+ → Check publisher in allowlist (if restricted)
45
+ → Add NFT tier to project's 721 hook
46
+ → Configure split for publisher (fee share)
47
+ → Pay project to mint NFTs
48
+ ```
49
+
50
+ ## Extension Points
51
+
52
+ | Point | Interface | Purpose |
53
+ |-------|-----------|---------|
54
+ | Data hook | `IJBRulesetDataHook` | CTDeployer acts as data hook |
55
+ | 721 hook | `IJB721TiersHook` | NFT tier management |
56
+ | Publisher | `ICTPublisher` | Content posting workflow |
57
+
58
+ ## Dependencies
59
+ - `@bananapus/core-v6` — Core protocol
60
+ - `@bananapus/721-hook-v6` — NFT tier system
61
+ - `@bananapus/ownable-v6` — JB-aware ownership
62
+ - `@bananapus/permission-ids-v6` — Permission constants
63
+ - `@bananapus/buyback-hook-v6` — Buyback integration
64
+ - `@bananapus/suckers-v6` — Cross-chain support
65
+ - `@bananapus/router-terminal-v6` — Payment routing
66
+ - `@openzeppelin/contracts` — ERC2771, ERC721Receiver
package/RISKS.md ADDED
@@ -0,0 +1,238 @@
1
+ # croptop-core-v6 -- Risks
2
+
3
+ Deep implementation-level risk analysis. References are to source files under `src/` and test files under `test/`.
4
+
5
+ ## Trust Assumptions
6
+
7
+ ### 1. CTDeployer as Data Hook (CRITICAL trust surface)
8
+
9
+ CTDeployer acts as `IJBRulesetDataHook` for every project it deploys (`CTDeployer.sol` line 290: `rulesetConfigurations[0].metadata.dataHook = address(this)`). This means:
10
+
11
+ - All pay and cashout calls for every CTDeployer-launched project are routed through CTDeployer's `beforePayRecordedWith()` (line 162) and `beforeCashOutRecordedWith()` (line 134).
12
+ - CTDeployer forwards these to `dataHookOf[projectId]` (line 152, 170), which is the JB721TiersHook.
13
+ - If CTDeployer has a bug, all Croptop projects are affected simultaneously.
14
+ - `dataHookOf` is write-once (no setter function), so a bug cannot be patched per-project.
15
+
16
+ ### 2. Sucker Registry (MEDIUM trust surface)
17
+
18
+ `CTDeployer.beforeCashOutRecordedWith()` at line 146 trusts `SUCKER_REGISTRY.isSuckerOf()` to determine whether a cashout should be tax-free. If the sucker registry is compromised or returns `true` for an attacker's address, that address can cash out with zero tax from any Croptop project.
19
+
20
+ ### 3. JBPermissions (HIGH trust surface)
21
+
22
+ Both CTDeployer and CTPublisher inherit `JBPermissioned` and rely on `_requirePermissionFrom()` for access control. The JBPermissions contract is a shared singleton. If compromised, all permission-gated functions across all projects lose access control.
23
+
24
+ ### 4. JB721TiersHookStore (MEDIUM trust surface)
25
+
26
+ CTPublisher reads tier data from the hook's store at `_setupPosts()` line 477 (`store.tierOf(address(hook), tierId, false).price`) and line 469 (`hook.STORE().isTierRemoved()`). If the store returns incorrect data, fee calculations and tier reuse logic break.
27
+
28
+ ### 5. Publisher-Set Splits (MEDIUM trust surface)
29
+
30
+ Publishers set their own `splitPercent` and `splits` array in `CTPost` (line 544, `CTPublisher.sol`). While `splitPercent` is bounded by `maximumSplitPercent` (line 518-519), the actual `splits` array contents are not validated against the percent. The JB721TiersHook and JBSplits contracts downstream are responsible for enforcing that splits sum correctly.
31
+
32
+ ### 6. ERC2771 Trusted Forwarder (LOW trust surface)
33
+
34
+ Both CTDeployer and CTPublisher use `ERC2771Context` with a trusted forwarder set at construction. If the trusted forwarder is compromised, any `_msgSender()` check can be spoofed, bypassing all permission checks. Mitigated by setting `trustedForwarder = address(0)` in deployments that don't need meta-transactions.
35
+
36
+ ## Audited Findings (Nemesis Audit)
37
+
38
+ ### Fixed: NM-001 -- Duplicate URI Fee Evasion (MEDIUM, FIXED)
39
+
40
+ **Location:** `CTPublisher._setupPosts()` lines 454-469
41
+ **Test:** `test/regression/M6_DuplicateUriFeeEvasion.t.sol`
42
+
43
+ **Root cause:** Two posts with the same `encodedIPFSUri` in a single `mintFrom()` batch caused a desync between `tierIdForEncodedIPFSUriOf` (written at line 552 during iteration) and the hook store (updated only after `adjustTiers()` is called at line 338). The second post found a non-zero tier ID, but `store.tierOf()` returned `price=0` because the tier hadn't been committed yet.
44
+
45
+ **Attack scenario:**
46
+ 1. Attacker calls `mintFrom()` with two identical `encodedIPFSUri` posts, each priced at 1 ETH.
47
+ 2. First post: `totalPrice += 1 ether`. Mapping written.
48
+ 3. Second post: Finds mapping, reads `store.tierOf().price = 0`. `totalPrice` stays at 1 ETH.
49
+ 4. Fee calculated on 1 ETH instead of 2 ETH. Attacker evades 50% of fees.
50
+
51
+ **Fix applied:** Explicit duplicate detection loop at lines 454-458. Reverts with `CTPublisher_DuplicatePost(encodedIPFSUri)` if any two posts in the batch share an `encodedIPFSUri`.
52
+
53
+ **Test coverage:** 5 tests covering adjacent duplicates, non-adjacent duplicates, distinct URIs, single posts, and fuzz over arbitrary URI pairs.
54
+
55
+ ### Fixed: H-19 -- Fee Evasion on Existing Tier Mints (HIGH, FIXED)
56
+
57
+ **Location:** `CTPublisher._setupPosts()` line 477
58
+ **Test:** `test/regression/H19_FeeEvasion.t.sol`
59
+
60
+ **Root cause:** When a post reused an existing tier (same `encodedIPFSUri`), the fee was calculated from `post.price` (attacker-controlled) instead of the actual tier price stored on-chain. An attacker could set `post.price = 0` for existing tiers to evade the 5% fee entirely.
61
+
62
+ **Fix applied:** For existing tiers, `totalPrice` accumulates `store.tierOf(address(hook), tierId, false).price` (line 477) -- the on-chain price -- not `post.price`.
63
+
64
+ **Test coverage:** 2 tests: one proving the attacker can't send 0 ETH for existing tiers, one proving the exact fee boundary (1.05 ETH required for a 1 ETH tier).
65
+
66
+ ### Fixed: L-52 -- Stale Tier ID Mapping After External Removal (LOW, FIXED)
67
+
68
+ **Location:** `CTPublisher._setupPosts()` lines 466-470
69
+ **Test:** `test/regression/L52_StaleTierIdMapping.t.sol`
70
+
71
+ **Root cause:** If a tier was removed externally via `adjustTiers()`, the `tierIdForEncodedIPFSUriOf` mapping still pointed to the removed tier ID. Subsequent posts with the same URI would try to mint from a removed tier.
72
+
73
+ **Fix applied:** Before reusing a tier, `isTierRemoved()` is checked (line 469). If true, the stale mapping is deleted (line 470), and the post falls through to create a new tier.
74
+
75
+ **Test coverage:** 2 tests: mapping cleared on removal, mapping preserved when tier still exists.
76
+
77
+ ### Open: NM-005 -- uint56 vs uint64 Project ID Cast (LOW, NOT FIXED)
78
+
79
+ **Location:** `CTProjectOwner.sol` line 72 vs `CTDeployer.sol` line 337
80
+
81
+ CTProjectOwner casts `tokenId` to `uint56`, while CTDeployer casts `projectId` to `uint64` in `JBPermissionsData`. No practical impact since project IDs are sequential and won't exceed 2^56, but the inconsistency should be harmonized.
82
+
83
+ ### Open: NM-006 -- Cannot Fully Disable Posting for a Category (LOW, NOT FIXED)
84
+
85
+ **Location:** `CTPublisher.configurePostingCriteriaFor()` line 250-252
86
+
87
+ `minimumTotalSupply` must be > 0, so once a category is enabled, there is no clean way to disable it. Workaround: set `minimumPrice` to `type(uint104).max` and `minimumTotalSupply = maximumTotalSupply = 1`.
88
+
89
+ ## Active Risk Analysis
90
+
91
+ ### R-1: Tier Spam via Permissionless Posting
92
+
93
+ **Severity:** MEDIUM
94
+ **Location:** `CTPublisher.mintFrom()` line 297, `_setupPosts()` line 419
95
+ **Tested:** Partially (CroptopAttacks.t.sol tests input validation, not volume)
96
+
97
+ **Description:** When `allowedAddresses` is empty (line 523 condition not met), anyone can call `mintFrom()` and create new tiers as long as they meet price/supply criteria. Each `mintFrom()` call can include multiple posts, each creating a new tier. There is no per-address rate limit or maximum tier count.
98
+
99
+ **Impact:** An attacker can create thousands of tiers on a project's hook, increasing gas costs for all tier-related operations (enumeration, minting, removal). The JB721TiersHookStore uses a linked list for tiers, making enumeration O(n).
100
+
101
+ **Mitigation:** Project owners should use allowlists for categories. The `minimumPrice` floor provides an economic barrier. Each new tier costs the poster the tier price + 5% fee.
102
+
103
+ ### R-2: Fee Rounding Loss (Dust)
104
+
105
+ **Severity:** INFORMATIONAL
106
+ **Location:** `CTPublisher.mintFrom()` line 328
107
+ **Tested:** No dedicated test
108
+
109
+ **Description:** Fee calculation `totalPrice / FEE_DIVISOR` uses integer division, which truncates. For `totalPrice = 39 wei`, the fee is `1 wei` instead of `1.95 wei`. This consistently rounds in the payer's favor (fee project receives slightly less).
110
+
111
+ **Quantification:** Maximum dust loss per transaction is `FEE_DIVISOR - 1 = 19 wei`. For practical ETH amounts (>= 0.001 ETH), this is < 0.000002% underpayment.
112
+
113
+ ### R-3: Allowlist Linear Scan Gas Scaling
114
+
115
+ **Severity:** LOW
116
+ **Location:** `CTPublisher._isAllowed()` lines 200-210
117
+ **Tested:** No gas benchmarks in test suite
118
+
119
+ **Description:** Allowlist checking is O(n) linear scan. For an allowlist of 1000 addresses, each `mintFrom()` call pays ~3000 gas per address checked (1000 * 3 gas/comparison) = ~3M additional gas. The EVM block gas limit (~30M on mainnet) imposes an effective cap of ~10,000 addresses before a `mintFrom()` transaction becomes infeasible.
120
+
121
+ **Mitigation:** Document the recommendation of < 100 addresses per allowlist (comment at line 197). A Merkle proof pattern would scale better but adds complexity.
122
+
123
+ ### R-4: Force-Sent ETH Routed to Fee Project
124
+
125
+ **Severity:** LOW (NM-004 from audit)
126
+ **Location:** `CTPublisher.mintFrom()` lines 389-404
127
+ **Tested:** No dedicated test
128
+
129
+ **Description:** If ETH is force-sent to CTPublisher via `selfdestruct` from another contract, it gets included in `address(this).balance` (line 397) and is sent to the fee project terminal on the next `mintFrom()` call. CTPublisher has no `receive()` or `fallback()`, so normal sends revert. Only `selfdestruct` (deprecated in future Ethereum hard forks) can force-send ETH.
130
+
131
+ **Impact:** The fee project receives a windfall. No funds are lost to attackers. The mint caller is not charged extra (fee is subtracted from `msg.value` before the main payment, and the residual balance goes to fees).
132
+
133
+ ### R-5: Data Hook Proxy Forwarding Failure
134
+
135
+ **Severity:** MEDIUM
136
+ **Location:** `CTDeployer.beforeCashOutRecordedWith()` line 152, `beforePayRecordedWith()` line 170
137
+ **Tested:** No unit test for forwarding failure
138
+
139
+ **Description:** CTDeployer forwards data hook calls to `dataHookOf[projectId]`, which is set to the JB721TiersHook. If the hook reverts (e.g., due to a bug or an upgrade in dependent contracts), all pay and cashout operations for the project will revert. There is no try-catch wrapping these forwards.
140
+
141
+ **Impact:** A broken or upgraded hook can DoS all payments and cashouts for a project. Since `dataHookOf` has no setter, the project is permanently bricked in this scenario.
142
+
143
+ **Mitigation:** The hook is a deterministic deployment (Create2) with no upgrade mechanism, reducing the likelihood of unexpected behavior changes.
144
+
145
+ ### R-6: CTProjectOwner Accepts Transfers (Not Just Mints)
146
+
147
+ **Severity:** LOW
148
+ **Location:** `CTProjectOwner.onERC721Received()` line 47-77
149
+ **Tested:** No negative test for transfer vs mint
150
+
151
+ **Description:** Unlike `CTDeployer.onERC721Received()` (which checks `from != address(0)` at line 201), `CTProjectOwner.onERC721Received()` only checks that `msg.sender == address(PROJECTS)` (line 62). It does NOT check `from == address(0)`. This means any holder can `safeTransferFrom` their project NFT to `CTProjectOwner`, effectively burning their project ownership. While this is the intended use case (burn-lock), there is no confirmation dialog or cooling period.
152
+
153
+ **Impact:** A project owner who accidentally transfers their project to CTProjectOwner loses ownership permanently with no recovery mechanism.
154
+
155
+ ### R-7: Sucker Fee-Free Cashout Trust Chain
156
+
157
+ **Severity:** MEDIUM
158
+ **Location:** `CTDeployer.beforeCashOutRecordedWith()` line 146
159
+ **Tested:** No adversarial test for sucker impersonation
160
+
161
+ **Description:** The fee-free cashout path relies entirely on `SUCKER_REGISTRY.isSuckerOf({projectId, addr: context.holder})`. The trust chain is:
162
+
163
+ 1. `SUCKER_REGISTRY` must correctly track deployed suckers.
164
+ 2. `allowSuckerDeployer()` on the registry must only be callable by the registry owner.
165
+ 3. Deployed suckers must not be compromisable.
166
+
167
+ If any link breaks, an attacker could register a malicious address as a "sucker" and cash out any Croptop project's treasury at 0% tax rate.
168
+
169
+ **Mitigation:** The sucker registry is operated by the protocol multisig. The `MAP_SUCKER_TOKEN` permission granted at CTDeployer construction (line 107) is wildcard (`projectId: 0`), which is necessary for cross-chain functionality but broadens the blast radius.
170
+
171
+ ### R-8: No Input Validation on `splits` Array Contents
172
+
173
+ **Severity:** LOW
174
+ **Location:** `CTPublisher._setupPosts()` line 544-545
175
+ **Tested:** Split percent bounds are tested (`CTPublisher.t.sol`, `CroptopAttacks.t.sol`), but split array contents are not
176
+
177
+ **Description:** While `splitPercent` is validated against `maximumSplitPercent` (line 518-519), the actual `splits` array (line 545) is passed through to the JB721TierConfig without validation. CTPublisher does not verify:
178
+ - That split beneficiaries are non-zero addresses.
179
+ - That split percentages sum to `SPLITS_TOTAL_PERCENT`.
180
+ - That split hooks are valid contracts.
181
+
182
+ **Mitigation:** The JB721TiersHook and JBSplits contracts downstream validate split configurations. CTPublisher relies on these downstream checks.
183
+
184
+ ### R-9: Project Deployment Front-Running
185
+
186
+ **Severity:** LOW
187
+ **Location:** `CTDeployer.deployProjectFor()` line 260
188
+ **Tested:** Fork test in `Fork.t.sol` but no front-running test
189
+
190
+ **Description:** `deployProjectFor()` calculates the expected project ID as `PROJECTS.count() + 1` (line 260) and asserts it matches the actual ID returned by `launchProjectFor()` (line 296). If another project is created between the `count()` read and the `launchProjectFor()` call (front-running), the assertion fails and the entire transaction reverts.
191
+
192
+ **Impact:** No fund loss. The deployment simply fails and must be retried. An attacker would pay gas to front-run with no economic benefit.
193
+
194
+ ### R-10: Metadata Assembly Correctness
195
+
196
+ **Severity:** INFORMATIONAL
197
+ **Location:** `CTPublisher.mintFrom()` lines 355-357
198
+ **Tested:** `test/Test_MetadataGeneration.t.sol`
199
+
200
+ **Description:** `FEE_PROJECT_ID` is written into the first 32 bytes of `mintMetadata` via inline assembly `mstore`. This overwrites whatever was previously in that position (the metadata length prefix is at offset 0, the first data word is at offset 32). The test file confirms this produces valid metadata that can be parsed by `JBMetadataResolver.getDataFor()`.
201
+
202
+ **Verified as correct:** False positive FF-002 from the Nemesis audit confirmed this is intentional per the JBMetadataResolver referral ID format.
203
+
204
+ ## Test Coverage Summary
205
+
206
+ | Risk Area | Test File | Coverage |
207
+ |-----------|-----------|----------|
208
+ | Posting criteria round-trip | `CTPublisher.t.sol` | 11 tests including fuzz |
209
+ | Permission enforcement | `CTPublisher.t.sol`, `CroptopAttacks.t.sol` | 3 tests |
210
+ | Input validation (price, supply, URI) | `CroptopAttacks.t.sol` | 6 tests |
211
+ | Split percent enforcement | `CTPublisher.t.sol`, `CroptopAttacks.t.sol` | 10 tests including fuzz |
212
+ | Fee evasion (existing tiers) | `H19_FeeEvasion.t.sol` | 2 regression tests |
213
+ | Fee evasion (duplicate URIs) | `M6_DuplicateUriFeeEvasion.t.sol` | 5 tests including fuzz |
214
+ | Stale tier mapping | `L52_StaleTierIdMapping.t.sol` | 2 regression tests |
215
+ | Metadata generation | `Test_MetadataGeneration.t.sol` | 1 test |
216
+ | Full deployment integration | `Fork.t.sol` | 2 fork tests |
217
+ | Data hook proxy forwarding | -- | **Not tested** |
218
+ | Force-sent ETH handling | -- | **Not tested** |
219
+ | Allowlist gas scaling | -- | **Not tested** |
220
+ | Sucker fee-free cashout abuse | -- | **Not tested** |
221
+ | CTProjectOwner transfer vs mint | -- | **Not tested** |
222
+ | Front-running `deployProjectFor` | -- | **Not tested** |
223
+
224
+ ## Invariants
225
+
226
+ These invariants should hold for all Croptop operations:
227
+
228
+ 1. **Fee invariant:** For any `mintFrom()` where `projectId != FEE_PROJECT_ID`, the fee project receives exactly `totalPrice / FEE_DIVISOR` (minus rounding dust of at most 19 wei).
229
+
230
+ 2. **Posting criteria invariant:** A `mintFrom()` call succeeds only if every post satisfies: `price >= minimumPrice`, `totalSupply >= minimumTotalSupply`, `totalSupply <= maximumTotalSupply`, `splitPercent <= maximumSplitPercent`, and (if allowlist non-empty) `msg.sender in allowedAddresses`.
231
+
232
+ 3. **Tier uniqueness invariant (post-fix):** Within a single `mintFrom()` batch, no two posts can have the same `encodedIPFSUri`.
233
+
234
+ 4. **Fee calculation invariant (post-fix):** For existing tiers, the fee is based on `store.tierOf().price` (on-chain price), not `post.price` (user-supplied).
235
+
236
+ 5. **Ownership invariant:** CTDeployer owns the project NFT only transiently during `deployProjectFor()`. By the end of the function, ownership is always transferred to the specified `owner`.
237
+
238
+ 6. **Data hook immutability invariant:** `dataHookOf[projectId]` is set exactly once (during `deployProjectFor`) and never modified afterward.