@croptop/core-v6 0.0.26 → 0.0.28

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.
@@ -1,508 +1,92 @@
1
- # croptop-core-v6 -- Audit Instructions
1
+ # Audit Instructions
2
2
 
3
- Audit preparation document for experienced Solidity auditors. This repo contains the Croptop content publishing system: three contracts that allow permissioned posting of NFT content as 721 tiers to Juicebox V6 projects, with fee accounting, an allowlist system, and a data hook proxy for cross-chain cash-out interception.
3
+ Croptop is a publishing layer on top of Juicebox projects and the 721 hook stack. Audit it as a permissions and fee-routing system, not just a content app.
4
4
 
5
- Compiler: `solc 0.8.28`. Framework: Foundry.
5
+ ## Objective
6
6
 
7
- ---
7
+ Find issues that:
8
+ - let publishers create or mint posts outside configured criteria
9
+ - let users evade Croptop fees or route them incorrectly
10
+ - grant fee-free or privileged cash-outs to the wrong actors
11
+ - create stale, duplicate, or abusive tier reuse across posts
12
+ - break ownership handoff or permanently lock a project in an unintended admin state
8
13
 
9
- ## 1. Architecture Overview
14
+ ## Scope
10
15
 
11
- Croptop is a thin orchestration layer on top of Juicebox V6 core and the 721 tiers hook system. It adds content-posting semantics (price floors, supply bounds, poster allowlists, split caps) and a 5% fee on every mint.
16
+ In scope:
17
+ - `src/CTPublisher.sol`
18
+ - `src/CTDeployer.sol`
19
+ - `src/CTProjectOwner.sol`
20
+ - `src/interfaces/`
21
+ - `src/structs/`
22
+ - deployment scripts in `script/`
12
23
 
13
- ### Contract Table
24
+ External integrations that matter:
25
+ - `nana-core-v6`
26
+ - `nana-721-hook-v6`
27
+ - `nana-ownable-v6`
28
+ - `nana-suckers-v6`
14
29
 
15
- | Contract | File | Lines | Role |
16
- |----------|------|-------|------|
17
- | **CTPublisher** | `src/CTPublisher.sol` | ~590 | Core publishing engine. Validates posts against bit-packed allowances, creates 721 tiers, mints first copies, routes fees. Inherits `JBPermissioned`, `ERC2771Context`. |
18
- | **CTDeployer** | `src/CTDeployer.sol` | ~433 | Factory that deploys a JB project + 721 hook + posting criteria in one transaction. Acts as `IJBRulesetDataHook` proxy: forwards pay/cash-out calls to the underlying hook while granting fee-free cash outs to suckers. Inherits `JBPermissioned`, `ERC2771Context`, `IERC721Receiver`. |
19
- | **CTProjectOwner** | `src/CTProjectOwner.sol` | ~82 | Receives project ownership NFT via `safeTransferFrom` and permanently grants `CTPublisher` the `ADJUST_721_TIERS` permission. Implements `IERC721Receiver`. |
30
+ ## System Model
20
31
 
21
- ### Struct Table
32
+ Croptop has three roles:
33
+ - `CTPublisher`: validates post configuration, creates or adjusts tiers, mints the first copy, and routes fees
34
+ - `CTDeployer`: launches a project, wires hook ownership and post criteria, and acts as a data-hook proxy where required
35
+ - `CTProjectOwner`: ownership helper for projects that want Croptop-controlled administration
22
36
 
23
- | Struct | File | Key Fields |
24
- |--------|------|------------|
25
- | `CTAllowedPost` | `src/structs/CTAllowedPost.sol` | `hook` (address), `category` (uint24), `minimumPrice` (uint104), `minimumTotalSupply` (uint32), `maximumTotalSupply` (uint32), `maximumSplitPercent` (uint32), `allowedAddresses` (address[]) |
26
- | `CTDeployerAllowedPost` | `src/structs/CTDeployerAllowedPost.sol` | Same as `CTAllowedPost` minus `hook` (inferred during deployment) |
27
- | `CTPost` | `src/structs/CTPost.sol` | `encodedIPFSUri` (bytes32), `totalSupply` (uint32), `price` (uint104), `category` (uint24), `splitPercent` (uint32), `splits` (JBSplit[]) |
28
- | `CTProjectConfig` | `src/structs/CTProjectConfig.sol` | `terminalConfigurations` (JBTerminalConfig[]), `projectUri` (string), `allowedPosts` (CTDeployerAllowedPost[]), `contractUri` (string), `name` (string), `symbol` (string), `salt` (bytes32) |
29
- | `CTSuckerDeploymentConfig` | `src/structs/CTSuckerDeploymentConfig.sol` | `deployerConfigurations` (JBSuckerDeployerConfig[]), `salt` (bytes32) |
37
+ The system relies on project-specific posting criteria such as:
38
+ - minimum price
39
+ - supply bounds
40
+ - category restrictions
41
+ - split limits
42
+ - optional address allowlists
30
43
 
31
- ### Dependency Map
44
+ ## Critical Invariants
32
45
 
33
- ```
34
- CTPublisher
35
- ├── JBPermissioned (access control)
36
- ├── ERC2771Context (meta-transactions)
37
- ├── IJBDirectory (terminal lookup)
38
- ├── IJBTerminal (payments)
39
- ├── IJB721TiersHook (tier adjustment, mint)
40
- ├── IJB721TiersHookStore (tier reads)
41
- ├── JBMetadataResolver (metadata encoding)
42
- └── JBOwnable (hook owner reads)
46
+ 1. Post criteria are binding
47
+ No publish path should bypass configured minimum price, total supply bounds, split caps, or allowlist restrictions.
43
48
 
44
- CTDeployer
45
- ├── JBPermissioned (access control)
46
- ├── ERC2771Context (meta-transactions)
47
- ├── IJBRulesetDataHook (pay/cashout interception)
48
- ├── IERC721Receiver (project NFT receipt)
49
- ├── IJBProjects (project NFT operations)
50
- ├── IJB721TiersHookDeployer (hook deployment)
51
- ├── IJBController (project launch)
52
- ├── IJBSuckerRegistry (sucker verification, deployment)
53
- ├── JBOwnable (hook ownership transfer)
54
- └── ICTPublisher (posting criteria delegation)
49
+ 2. Fee collection is complete
50
+ Each Croptop mint should either pay the configured fee or take the documented fallback path. Users must not be able to mint while underpaying Croptop.
55
51
 
56
- CTProjectOwner
57
- ├── IERC721Receiver (project NFT receipt)
58
- ├── IJBPermissions (permission grants)
59
- └── IJBProjects (sender validation)
60
- ```
52
+ 3. Tier reuse is safe
53
+ Existing tiers must not be reusable in a way that evades fees, stale criteria, or duplicate-content protections.
61
54
 
62
- ---
55
+ 4. Sucker privileges stay narrow
56
+ Any cash-out tax exemptions or mint permissions intended for legitimate suckers must not be reachable by arbitrary callers or spoofed registry state.
63
57
 
64
- ## Value Extraction Paths
58
+ 5. Ownership transitions are intentional
59
+ Burn-lock or project-owner helper flows must not grant broader privileges than intended or accidentally strand project administration.
65
60
 
66
- Quick reference for where the money is:
61
+ ## Threat Model
67
62
 
68
- | Path | Entry Point | Value at Risk | What to Verify |
69
- |------|------------|---------------|----------------|
70
- | Fee evasion | `CTPublisher.mintFrom()` | 5% fee on every mint | `totalPrice` computed from on-chain tier prices for existing tiers, not user-supplied `post.price` |
71
- | Fee-free cashout | `CTDeployer.beforeCashOutRecordedWith()` | Full treasury value | Only legitimate suckers (via registry) get 0% tax |
72
- | Unauthorized minting | `CTDeployer.hasMintPermissionFor()` | Arbitrary token minting | Only registered suckers get mint permission |
73
- | Tier spam | `CTPublisher._setupPosts()` | Gas griefing, hook degradation | Allowlist + price/supply floors gate tier creation |
74
- | Fee routing failure | `CTPublisher.mintFrom()` pre-computed fee | Fee project loses 5% | Pre-computed fee (`msg.value - payValue`) sent via try-catch to fee terminal, with fallback to `feeBeneficiary` then `msg.sender` |
63
+ Prioritize:
64
+ - malicious publishers choosing edge-case prices, split structures, or reused metadata
65
+ - malicious project owners misconfiguring rules and then trying to escape them
66
+ - fake or stale sucker registrations
67
+ - fee-recipient failures that alter control flow
68
+ - reentrancy through fee routing or tier-adjustment side effects
75
69
 
76
- ---
70
+ ## Hotspots
77
71
 
78
- ## 2. Content Posting Flow
72
+ - `CTPublisher.mintFrom` and its validation pipeline
73
+ - any code path that computes fees from user-provided versus on-chain values
74
+ - tier creation or adjustment against prior post state
75
+ - `CTDeployer` data-hook behavior for pay and cash-out flows
76
+ - permission grants made during deployment or project-owner handoff
77
+ - any one-way lock or burn-based ownership design
79
78
 
80
- The core flow is `CTPublisher.mintFrom()`. This is the primary entry point for all content publishing.
79
+ ## Build And Verification
81
80
 
82
- ### Step-by-step execution (CTPublisher.sol lines 310-430)
81
+ Standard workflow:
82
+ - `npm install`
83
+ - `forge build`
84
+ - `forge test`
83
85
 
84
- ```
85
- Poster calls mintFrom(hook, posts[], nftBeneficiary, feeBeneficiary, additionalPayMetadata, feeMetadata)
86
- with msg.value = sum(post prices) + 5% fee
86
+ Current tests emphasize:
87
+ - fee evasion
88
+ - stale tier mappings
89
+ - reentrancy and attacker-controlled publish flows
90
+ - fork and omnichain composition
87
91
 
88
- 1. Read projectId from hook.PROJECT_ID() [line 329]
89
- 2. _setupPosts(hook, posts) returns: [line 333-334]
90
- (tiersToAdd[], tierIdsToMint[], totalPrice)
91
-
92
- For each post in the batch:
93
- a. Revert if encodedIPFSUri == bytes32("") [line 473-474]
94
- b. O(i) duplicate check against all prior posts in batch [line 478-482]
95
- c. Look up tierIdForEncodedIPFSUriOf[hook][encodedIPFSUri] [line 487]
96
- - If tier exists and NOT removed: reuse tier ID, [line 496]
97
- accumulate store.tierOf().price (on-chain price) [line 501]
98
- - If tier exists but removed: delete stale mapping, [line 494]
99
- fall through to new tier creation
100
- - If no tier exists: validate against allowance: [line 510-549]
101
- * Category must be configured (minimumTotalSupply > 0)
102
- * price >= minimumPrice
103
- * totalSupply >= minimumTotalSupply
104
- * totalSupply <= maximumTotalSupply
105
- * splitPercent <= maximumSplitPercent
106
- * caller in allowedAddresses (if non-empty)
107
- Then build JB721TierConfig and accumulate post.price [line 553-579]
108
- d. Store tierIdForEncodedIPFSUriOf mapping for new tiers [line 576]
109
-
110
- Assembly-resize tiersToAdd if some posts reused existing tiers [line 584-588]
111
-
112
- 3. If projectId != FEE_PROJECT_ID: [line 336]
113
- payValue = msg.value - (totalPrice / FEE_DIVISOR) [line 341/348]
114
- Else: payValue = msg.value (no fee for fee project)
115
-
116
- 4. Revert if totalPrice > payValue [line 352-354]
117
-
118
- 5. hook.adjustTiers(tiersToAdd, []) [line 358]
119
-
120
- 6. Build mint metadata: [line 366-370]
121
- - JBMetadataResolver.addToMetadata with tier IDs
122
- - Assembly: write FEE_PROJECT_ID into first 32 bytes (referral)
123
-
124
- 7. Emit Mint event [line 380-389]
125
-
126
- 8. Look up project's primary ETH terminal via DIRECTORY [line 393-394]
127
- terminal.pay{value: payValue}(...) [line 398-406]
128
-
129
- 9. payValue = msg.value - payValue (pre-computed fee) [line 411]
130
- If payValue != 0: [line 414]
131
- Look up fee project's primary ETH terminal [line 416-417]
132
- try feeTerminal.pay{value: payValue}(...) {} [line 421-429]
133
- catch { feeBeneficiary.call{value}; fallback msg.sender.call } [line 430-437]
134
- ```
135
-
136
- ### Fee Calculation
137
-
138
- - `FEE_DIVISOR = 20` (5% fee)
139
- - Fee = `totalPrice / 20` (integer division, truncates)
140
- - Maximum rounding loss: 19 wei per transaction
141
- - Fee is deducted from `msg.value` before the project payment
142
- - Pre-computed fee (`msg.value - payValue`) goes to fee terminal via try-catch, with fallback to `feeBeneficiary` then `msg.sender`
143
- - Fee is skipped entirely when `projectId == FEE_PROJECT_ID`
144
-
145
- ---
146
-
147
- ## 3. Tier Creation Mechanics
148
-
149
- ### New Tier Path
150
-
151
- When a post's `encodedIPFSUri` has no existing mapping (or the mapped tier was removed), a new tier is created:
152
-
153
- 1. Posting criteria are read from bit-packed `_packedAllowanceFor[hook][category]` (CTPublisher.sol lines 177-190)
154
- 2. Each field is validated against the post's parameters
155
- 3. A `JB721TierConfig` is constructed with the post's values (lines 553-570)
156
- 4. The tier ID is computed as `startingTierId + numberOfTiersBeingAdded++` (line 573)
157
- 5. The mapping `tierIdForEncodedIPFSUriOf[hook][encodedIPFSUri]` is set (line 576)
158
- 6. All new tiers are committed to the hook via `hook.adjustTiers()` after the loop (line 358)
159
-
160
- ### Existing Tier Path
161
-
162
- When a post's `encodedIPFSUri` already has a mapping to a live (non-removed) tier:
163
-
164
- 1. The tier ID is reused (line 496)
165
- 2. The fee is calculated from `store.tierOf().price` -- the on-chain price, not `post.price` (line 501)
166
- 3. No new `JB721TierConfig` is added to `tiersToAdd`
167
- 4. The poster still gets a mint of the existing tier
168
-
169
- ### Stale Tier Cleanup
170
-
171
- If a tier was removed externally via `adjustTiers()`, the publisher detects this via `hook.STORE().isTierRemoved()` (line 493) and deletes the stale mapping (line 494), allowing the URI to be posted as a new tier.
172
-
173
- ---
174
-
175
- ## 4. Bit-Packed Allowance Storage
176
-
177
- Posting criteria are packed into a single `uint256` per hook/category:
178
-
179
- ```
180
- Bits 0-103 (104 bits): minimumPrice (uint104)
181
- Bits 104-135 ( 32 bits): minimumTotalSupply (uint32)
182
- Bits 136-167 ( 32 bits): maximumTotalSupply (uint32)
183
- Bits 168-199 ( 32 bits): maximumSplitPercent(uint32)
184
- Bits 200-255 ( 56 bits): unused
185
- ```
186
-
187
- Packing logic: CTPublisher.sol lines 274-282
188
- Unpacking logic: CTPublisher.sol lines 177-190
189
-
190
- The address allowlist is stored separately in `_allowedAddresses[hook][category]` (a dynamic array).
191
-
192
- ---
193
-
194
- ## 5. Allowlist System
195
-
196
- ### Configuration
197
-
198
- `configurePostingCriteriaFor()` (line 243) accepts an array of `CTAllowedPost` structs. For each:
199
-
200
- 1. Emits `ConfigurePostingCriteria` event (line 252)
201
- 2. Checks `ADJUST_721_TIERS` permission from `JBOwnable(hook).owner()` (lines 256-260)
202
- 3. Validates `minimumTotalSupply > 0` (line 263)
203
- 4. Validates `minimumTotalSupply <= maximumTotalSupply` (line 268)
204
- 5. Packs numeric fields into `_packedAllowanceFor` (lines 274-284)
205
- 6. Replaces the entire `_allowedAddresses` array (delete + push loop, lines 289-296)
206
-
207
- ### Enforcement
208
-
209
- In `_setupPosts()` at line 547:
210
-
211
- ```solidity
212
- if (addresses.length != 0 && !_isAllowed({addrs: _msgSender(), addresses: addresses})) {
213
- revert CTPublisher_NotInAllowList(_msgSender(), addresses);
214
- }
215
- ```
216
-
217
- `_isAllowed()` (lines 210-220) is a linear scan: O(n) where n = allowlist size.
218
-
219
- ### Key Behavior
220
-
221
- - Empty `allowedAddresses` means anyone can post (permissionless)
222
- - Reconfiguring a category fully replaces the previous criteria and allowlist
223
- - Categories with `minimumTotalSupply == 0` are treated as unconfigured (posting reverts)
224
- - There is no mechanism to fully disable a configured category (NM-006, documented as won't-fix)
225
-
226
- ---
227
-
228
- ## 6. Data Hook Proxy (CTDeployer)
229
-
230
- ### Architecture
231
-
232
- CTDeployer registers itself as the `dataHook` for every project it deploys (CTDeployer.sol line 289). It implements `IJBRulesetDataHook` and proxies calls:
233
-
234
- - **`beforePayRecordedWith(context)`** (line 160): Forwards directly to `dataHookOf[context.projectId]` (the JB721TiersHook).
235
- - **`beforeCashOutRecordedWith(context)`** (line 132): Checks `SUCKER_REGISTRY.isSuckerOf()` first. If the holder is a sucker, returns `cashOutTaxRate = 0` (fee-free). Otherwise forwards to the data hook.
236
- - **`hasMintPermissionFor(projectId, ruleset, addr)`** (line 176): Returns `true` if `addr` is a registered sucker.
237
-
238
- ### Failure Scenarios
239
-
240
- **Critical: Data hook forwarding has no try-catch.** If `dataHookOf[projectId]` reverts for any reason:
241
-
242
- - All `pay()` calls to the project will revert (line 168)
243
- - All `cashOut()` calls (for non-sucker holders) will revert (line 150)
244
- - Since `dataHookOf` is write-once (set at line 306, no setter), the project is permanently bricked
245
-
246
- **Scenarios that could trigger this:**
247
-
248
- 1. The 721 hook has a bug in `beforePayRecordedWith()` or `beforeCashOutRecordedWith()`
249
- 2. The hook's dependencies (store, prices, rulesets) revert due to bad state
250
- 3. An upgrade to a dependency contract breaks ABI compatibility
251
-
252
- **Mitigations:**
253
-
254
- - The hook is deployed via Create2 with deterministic bytecode (no proxy, no upgrade)
255
- - The hook's logic is well-tested in the `nana-721-hook-v6` repo
256
- - Sucker cash-outs bypass the hook entirely (they return before the forwarding call)
257
-
258
- ---
259
-
260
- ## 7. Sucker Impersonation Risks
261
-
262
- ### Trust Chain
263
-
264
- ```
265
- CTDeployer.beforeCashOutRecordedWith()
266
- └── SUCKER_REGISTRY.isSuckerOf(projectId, context.holder)
267
- └── Registry tracks suckers deployed by allowed deployers
268
- └── allowSuckerDeployer() restricted to registry owner (multisig)
269
- ```
270
-
271
- ### Attack Surface
272
-
273
- If an attacker can make `SUCKER_REGISTRY.isSuckerOf()` return `true` for their address:
274
-
275
- 1. They call `cashOut()` on any Croptop project
276
- 2. CTDeployer intercepts the cash-out, sees the attacker as a "sucker"
277
- 3. Returns `cashOutTaxRate = 0` instead of forwarding to the hook
278
- 4. The attacker receives full treasury value without paying the project's cash-out tax
279
-
280
- ### Risk Factors
281
-
282
- - `MAP_SUCKER_TOKEN` permission is granted as wildcard (`projectId: 0`) at CTDeployer construction (line 105)
283
- - The sucker registry is a shared singleton controlled by the protocol multisig
284
- - Once a sucker deployer is allowed, it can deploy suckers for any project
285
- - Compromising the multisig or a sucker deployer would affect all Croptop projects
286
-
287
- ### What to Verify
288
-
289
- - That `isSuckerOf()` cannot be manipulated without multisig action
290
- - That the wildcard `MAP_SUCKER_TOKEN` permission cannot be abused to register arbitrary addresses
291
- - That the `hasMintPermissionFor()` function (which also trusts the sucker registry) cannot be exploited to mint tokens without payment
292
-
293
- ---
294
-
295
- ## 8. Allowlist Gas Scaling
296
-
297
- ### Current Implementation
298
-
299
- `_isAllowed()` at CTPublisher.sol lines 210-220:
300
-
301
- ```solidity
302
- function _isAllowed(address addrs, address[] memory addresses) internal pure returns (bool) {
303
- uint256 numberOfAddresses = addresses.length;
304
- for (uint256 i; i < numberOfAddresses; i++) {
305
- if (addrs == addresses[i]) return true;
306
- }
307
- return false;
308
- }
309
- ```
310
-
311
- ### Gas Analysis
312
-
313
- - Each comparison: ~3 gas (MLOAD + EQ)
314
- - Per-address overhead: ~100 gas (loop counter, bounds check, memory access)
315
- - 100 addresses: ~10,000 gas additional
316
- - 1,000 addresses: ~100,000 gas additional
317
- - 10,000 addresses: ~1,000,000 gas additional
318
- - Block gas limit (~30M mainnet): effective cap of ~300,000 addresses before tx becomes infeasible
319
-
320
- ### Storage Scaling
321
-
322
- The `_allowedAddresses` array is also written during `configurePostingCriteriaFor()` via a push loop (lines 293-295). For large allowlists, the configuration transaction gas cost could also become prohibitive.
323
-
324
- ### Recommendation for Auditors
325
-
326
- Check that no realistic usage pattern could cause a revert due to gas limits. The recommended practical cap is 100 addresses per category. A Merkle proof pattern would scale to millions of addresses but was not implemented (complexity vs. expected usage tradeoff).
327
-
328
- ---
329
-
330
- ## 9. Priority Audit Areas
331
-
332
- ### P0 -- Critical (fund loss or permanent DoS)
333
-
334
- 1. **Fee accounting correctness in `_setupPosts()`** (CTPublisher.sol lines 442-589). Verify:
335
- - `totalPrice` is always computed from on-chain tier prices for existing tiers (not user-supplied `post.price`)
336
- - `totalPrice` is always computed from `post.price` for new tiers
337
- - No path exists where `totalPrice` can be manipulated to be less than the actual value of tiers being minted
338
- - The duplicate URI check (lines 478-482) covers all batch orderings
339
-
340
- 2. **Fee deduction and routing** (CTPublisher.sol lines 336-428). Verify:
341
- - `payValue = msg.value - (totalPrice / FEE_DIVISOR)` cannot underflow
342
- - The check `totalPrice > payValue` correctly prevents underpayment
343
- - The pre-computed fee (`msg.value - payValue`) equals exactly the intended fee amount, independent of `address(this).balance`
344
- - The fee terminal payment is wrapped in try-catch with fallback to `feeBeneficiary` then `msg.sender` — verify the fallback chain cannot lose ETH
345
-
346
- 3. **Data hook proxy forwarding** (CTDeployer.sol lines 132-169). Verify:
347
- - `dataHookOf[projectId]` is always set before any pay/cashout can occur for that project
348
- - No path exists where `dataHookOf[projectId]` is `address(0)` and a forwarding call is made
349
- - The sucker check correctly short-circuits before the forwarding call
350
-
351
- ### P1 -- High (access control bypass, permission escalation)
352
-
353
- 4. **Sucker fee-free cash-out** (CTDeployer.sol lines 143-146). Verify:
354
- - Only legitimate suckers can trigger the zero-tax path
355
- - The `hasMintPermissionFor()` function cannot be abused for unauthorized minting
356
-
357
- 5. **Permission enforcement in `configurePostingCriteriaFor()`** (CTPublisher.sol lines 256-260). Verify:
358
- - The permission check uses `JBOwnable(hook).owner()` and `IJB721TiersHook(hook).PROJECT_ID()` correctly
359
- - No one besides the hook owner (or permissioned delegate) can modify posting criteria
360
-
361
- 6. **CTProjectOwner permission grant** (CTProjectOwner.sol lines 47-80). Verify:
362
- - The permission granted is scoped to the correct project ID
363
- - The `uint64(tokenId)` cast does not truncate for realistic project IDs
364
- - Any address can transfer a project NFT to CTProjectOwner (no `from == address(0)` check), effectively burning ownership permanently
365
-
366
- ### P2 -- Medium (economic manipulation, griefing)
367
-
368
- 7. **Tier spam / unbounded tier creation** (R-1 in RISKS.md). When allowlist is empty, anyone meeting price/supply floors can create unlimited tiers. Assess impact on hook gas costs.
369
-
370
- 8. **Bit-packing correctness** in `_packedAllowanceFor` storage (CTPublisher.sol lines 274-282 write, lines 177-190 read). Verify no field overlap or silent truncation.
371
-
372
- 9. **Assembly metadata injection** (CTPublisher.sol lines 375-377). Verify the `mstore` correctly places `FEE_PROJECT_ID` in the referral position without corrupting the JBMetadataResolver lookup table.
373
-
374
- 10. **Project deployment front-running** (CTDeployer.sol lines 261, 294-303). Verify the `assert(projectId == ...)` check correctly prevents ID mismatch without permanent fund loss.
375
-
376
- ### P3 -- Low (informational, code quality)
377
-
378
- 11. **`uint64` project ID cast** in CTProjectOwner (line 77) and CTDeployer (line 344). Both now use `uint64`. Confirm no truncation risk for realistic project IDs.
379
-
380
- 12. **Force-sent ETH stranding** (CTPublisher.sol lines 409-438). Fee is now pre-computed from `msg.value`, not `address(this).balance`. Force-sent ETH remains stranded. Confirm this is acceptable and the try-catch fallback chain cannot lose ETH from `msg.value`.
381
-
382
- 13. **Allowlist overwrite behavior** (CTPublisher.sol lines 289-296). Verify that `delete` followed by `push` loop correctly replaces the array with no residual state.
383
-
384
- ---
385
-
386
- ## 10. Invariants
387
-
388
- These properties should hold across all operations. They are suitable targets for fuzz testing and formal verification.
389
-
390
- ### Fee Invariants
391
-
392
- 1. **Fee correctness:** For any `mintFrom()` where `projectId != FEE_PROJECT_ID`, the fee project receives at least `totalPrice / FEE_DIVISOR - 19 wei` and at most `totalPrice / FEE_DIVISOR` ETH.
393
-
394
- 2. **No fee for fee project:** When `projectId == FEE_PROJECT_ID`, the full `msg.value` is sent to the project terminal (zero deducted for fees).
395
-
396
- 3. **ETH conservation:** For every `mintFrom()` call, `msg.value == payValue + feeAmount + dust`, where `dust <= 19 wei`. The fee amount is pre-computed as `msg.value - payValue` and routed via try-catch to the fee terminal, then `feeBeneficiary`, then `msg.sender`. No ETH from `msg.value` is lost. Force-sent ETH (via `selfdestruct`) is not routed and remains stranded in the contract.
397
-
398
- ### Posting Invariants
399
-
400
- 4. **Allowance enforcement:** A `mintFrom()` call succeeds for a new tier only if every post satisfies: `price >= minimumPrice`, `totalSupply >= minimumTotalSupply`, `totalSupply <= maximumTotalSupply`, `splitPercent <= maximumSplitPercent`, and (if allowlist non-empty) `_msgSender()` is in `allowedAddresses`.
401
-
402
- 5. **Duplicate rejection:** Within a single `mintFrom()` batch, no two posts can have the same `encodedIPFSUri`.
403
-
404
- 6. **Existing tier price integrity:** For existing tiers, `totalPrice` accumulates `store.tierOf().price` (the on-chain price), never `post.price`.
405
-
406
- 7. **Tier uniqueness:** After `_setupPosts()` completes, every `encodedIPFSUri` in the batch maps to a unique tier ID via `tierIdForEncodedIPFSUriOf`.
407
-
408
- ### Ownership Invariants
409
-
410
- 8. **Transient deployer ownership:** CTDeployer owns a project NFT only during `deployProjectFor()` execution. By function return, ownership has been transferred to the specified `owner`.
411
-
412
- 9. **Data hook immutability:** `dataHookOf[projectId]` is set exactly once (during `deployProjectFor`) and never modified afterward. There is no setter function.
413
-
414
- 10. **Permission scoping:** CTProjectOwner grants `ADJUST_721_TIERS` permission scoped to the specific `tokenId` (project ID) received, not globally.
415
-
416
- ---
417
-
418
- ## 11. Testing Setup
419
-
420
- ### Running Tests
421
-
422
- ```bash
423
- cd croptop-core-v6
424
- forge install
425
- forge test
426
- ```
427
-
428
- For fork tests (requires RPC URL):
429
- ```bash
430
- ETHEREUM_RPC_URL=<your-rpc> forge test --match-contract ForkTest --fork-url $ETHEREUM_RPC_URL
431
- ```
432
-
433
- ### Test File Overview
434
-
435
- | Test File | Focus | Tests |
436
- |-----------|-------|-------|
437
- | `test/CTPublisher.t.sol` | Allowance round-trip, bit packing fuzz, permission checks, split validation | 26 tests including fuzz |
438
- | `test/CTDeployer.t.sol` | Deploy flow, data hook proxy, sucker permissions, onERC721Received, supportsInterface | 21 tests |
439
- | `test/CTProjectOwner.t.sol` | Permission grants on NFT receipt, rejection of non-project NFTs, rejection of transfers | 7 tests |
440
- | `test/ClaimCollectionOwnership.t.sol` | NM-002 scenario: ownership transfer, permission breakage, recovery path | 6 tests |
441
- | `test/TestAuditGaps.sol` | Data hook proxy forwarding failure, sucker impersonation, allowlist gas scaling, force-sent ETH | 19 tests |
442
- | `test/CroptopAttacks.t.sol` | Adversarial input validation, allowlist bypass, split percent enforcement | 12 tests |
443
- | `test/Fork.t.sol` | Full deployment integration with real JB infrastructure | 2 fork tests |
444
- | `test/fork/PublishFork.t.sol` | End-to-end mint flow, fee distribution, duplicate post reuse on mainnet fork | 4 fork tests |
445
- | `test/Test_MetadataGeneration.t.sol` | Metadata assembly correctness | 1 test |
446
- | `test/regression/DuplicateUriFeeEvasion.t.sol` | NM-001 fix: duplicate URI detection | 5 tests including fuzz |
447
- | `test/regression/FeeEvasion.t.sol` | H-19 fix: existing tier price used for fees | 2 tests |
448
- | `test/regression/StaleTierIdMapping.t.sol` | L-52 fix: stale mapping cleanup | 2 tests |
449
-
450
- ### Coverage Gaps (no existing tests)
451
-
452
- - Force-sent ETH handling via selfdestruct (fee is now pre-computed from `msg.value`, not `address(this).balance`, so force-sent ETH remains stranded)
453
- - `deployProjectFor` front-running race condition
454
- - Multiple hooks sharing the same CTPublisher instance
455
- - Cross-category posting in a single batch (different categories, different allowlists)
456
- - `configurePostingCriteriaFor()` called with a very large allowlist (storage gas)
457
- - Edge case: `totalPrice == 0` when all posts reuse existing free (price=0) tiers
458
-
459
- ### Testing Approach Used
460
-
461
- Tests use Foundry's `vm.mockCall()` to isolate CTPublisher from its dependencies (hook, store, permissions, directory, terminal). The fork test (`Fork.t.sol`) deploys all JB infrastructure fresh within a mainnet fork for integration testing. Regression tests target specific audit findings with dedicated attack reproductions.
462
-
463
- ---
464
-
465
- ## 12. Previous Audit Findings
466
-
467
- Six Nemesis findings plus two regression-test findings. See `.audit/findings/nemesis-verified.md` for full Nemesis details and `RISKS.md` for context.
468
-
469
- | ID | Severity | Status | Description |
470
- |----|----------|--------|-------------|
471
- | NM-001 | MEDIUM | FALSE POSITIVE | `dataHookOf` write-once = permanent project bricking -- project owner can queue new ruleset to escape (`useDataHookForPay = false`) |
472
- | NM-002 | MEDIUM | OPEN | `claimCollectionOwnershipOf` breaks all `CTPublisher.mintFrom` calls -- hook ownership transfer does not update CTPublisher permissions |
473
- | NM-003 | LOW | OPEN | Permission grants to initial owner stale after project NFT transfer -- old owner retains 4 permissions |
474
- | NM-004 | LOW | OPEN | Stale `tierIdForEncodedIPFSUriOf` in `tiersFor()` view -- removed tiers still returned to off-chain consumers |
475
- | NM-005 | LOW | FIXED | Fee underflow gives generic panic (`0x11`) instead of custom `CTPublisher_InsufficientEthSent` error -- the `if (payValue < fee)` check now guards the subtraction |
476
- | NM-006 | LOW | OPEN | Cannot fully disable posting for a configured category |
477
- | H-19 | HIGH | FIXED | Fee evasion on existing tier mints via `post.price = 0` *(regression test naming, not from Nemesis audit)* |
478
- | L-52 | LOW | FIXED | Stale tier ID mapping after external tier removal *(regression test naming, not from Nemesis audit)* |
479
-
480
- ---
481
-
482
- ## Compiler and Version Info
483
-
484
- - **Solidity**: 0.8.28
485
- - **EVM target**: Cancun
486
- - **Optimizer**: 200 runs
487
- - **Dependencies**: OpenZeppelin 5.x, nana-core-v6, nana-721-hook-v6, nana-suckers-v6
488
- - **Build**: `forge build` (Foundry)
489
-
490
- ---
491
-
492
- ## How to Report Findings
493
-
494
- For each finding:
495
-
496
- 1. **Title** -- one line, starts with severity (CRITICAL/HIGH/MEDIUM/LOW)
497
- 2. **Affected contract(s)** -- exact file path and line numbers
498
- 3. **Description** -- what is wrong, in plain language
499
- 4. **Trigger sequence** -- step-by-step
500
- 5. **Impact** -- what an attacker gains, what the project/fee project loses
501
- 6. **Proof** -- code trace or Foundry test
502
- 7. **Fix** -- minimal code change
503
-
504
- **Severity guide:**
505
- - **CRITICAL**: Fee evasion at scale, unauthorized treasury drain, permanent project DoS.
506
- - **HIGH**: Conditional fee bypass, sucker impersonation, broken posting criteria.
507
- - **MEDIUM**: Tier spam, gas griefing, rounding errors in fee calculation.
508
- - **LOW**: Informational, cosmetic, testnet-only.
92
+ Strong findings usually show either fee loss, unauthorized publishing power, or a project entering a control configuration it cannot safely escape.
package/CHANGELOG.md ADDED
@@ -0,0 +1,57 @@
1
+ # Changelog
2
+
3
+ ## Scope
4
+
5
+ This file describes the verified change from `croptop-core-v5` to the current `croptop-core-v6` repo.
6
+
7
+ ## Current v6 surface
8
+
9
+ - `CTDeployer`
10
+ - `CTProjectOwner`
11
+ - `CTPublisher`
12
+ - `CTAllowedPost`
13
+ - `CTDeployerAllowedPost`
14
+ - `CTPost`
15
+
16
+ ## Summary
17
+
18
+ - `CTPost` and the related allowlist structs now carry split-routing data, so a post can route part of its payment through `JBSplit[]` recipients.
19
+ - The deployer now acts as the data-hook entry point instead of wiring the 721 hook directly, which is what enables the intended omnichain and sucker-aware cash-out behavior.
20
+ - v6 closes several correctness gaps that were easy to miss in v5: duplicate posts in a batch are rejected, existing tiers use on-chain pricing instead of caller-supplied pricing, and stale tier mappings are recreated when tiers were removed externally.
21
+ - The repo was moved to the v6 dependency set and Solidity `0.8.28`.
22
+
23
+ ## Verified deltas
24
+
25
+ - `CTPost` gained `splitPercent` and `JBSplit[] splits`.
26
+ - `CTAllowedPost` and `CTDeployerAllowedPost` gained `maximumSplitPercent`.
27
+ - `ICTPublisher.allowanceFor(...)` now returns five values instead of four because `maximumSplitPercent` is part of the result.
28
+ - `CTDeployer` now points project metadata to itself as the data hook instead of pointing directly at the 721 hook.
29
+ - The repo carries dedicated regression tests for duplicate-URI fee evasion, stale tier mappings, and existing-tier pricing.
30
+
31
+ ## Breaking ABI changes
32
+
33
+ - `CTPost` is not v5-compatible because it now includes `splitPercent` and `splits`.
34
+ - `CTAllowedPost` and `CTDeployerAllowedPost` are not v5-compatible because they now include `maximumSplitPercent`.
35
+ - `ICTPublisher.allowanceFor(...)` return decoding changed because of the added field.
36
+
37
+ ## Indexer impact
38
+
39
+ - Any event or log decoding path that embeds `CTPost` or `CTAllowedPost` must be updated for the new struct layouts.
40
+ - Post-publishing integrations should not assume the old "all payment goes to treasury" model once split-bearing posts are live.
41
+
42
+ ## Migration notes
43
+
44
+ - Rebuild any ABI or indexer code that decodes `CTPost` or `CTAllowedPost`. Their layouts are not v5-compatible.
45
+ - If you integrated the deployer as if the 721 hook were the direct data hook, update that assumption. The deployer is now part of the routing path.
46
+ - Re-check any fee logic that trusted caller-supplied prices for existing tiers. That is not the v6 behavior.
47
+
48
+ ## ABI appendix
49
+
50
+ - Changed structs
51
+ - `CTPost`
52
+ - `CTAllowedPost`
53
+ - `CTDeployerAllowedPost`
54
+ - Changed decoding expectations
55
+ - `ICTPublisher.allowanceFor(...)`
56
+ - Behaviorally important surface shift
57
+ - deployer acts as the data-hook entrypoint